vendor tsgo
This commit is contained in:
26
tools/tsgo/internal/format/README.md
Normal file
26
tools/tsgo/internal/format/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# How does TypeScript formatting work?
|
||||
|
||||
To format code you need to have a formatting context and a `SourceFile`. The formatting context contains
|
||||
all user settings like tab size, newline character, etc.
|
||||
|
||||
The end result of formatting is represented by TextChange objects which hold the new string content, and
|
||||
the text to replace it with.
|
||||
|
||||
## Internals
|
||||
|
||||
Most of the exposed APIs internally are `Format*` and they all set up and configure `FormatSpan` which could be considered the root call for formatting. Span in this case refers to the range of
|
||||
the sourcefile which should be formatted.
|
||||
|
||||
The formatSpan then uses a scanner (either with or without JSX support) which starts at the highest
|
||||
node the covers the span of text and recurses down through the node's children.
|
||||
|
||||
As it recurses, `processNode` is called on the children setting the indentation is decided and passed
|
||||
through into each of that node's children.
|
||||
|
||||
The meat of formatting decisions is made via `processPair`, the pair here being the current node and the previous node. `processPair` which mutates the formatting context to represent the current place in the scanner and requests a set of rules which can be applied to the items via `createRulesMap`.
|
||||
|
||||
There are a lot of rules, which you can find in [rules.ts](./rules.ts) each one has a left and right reference to nodes or token ranges and note of what action should be applied by the formatter.
|
||||
|
||||
### Where is this used?
|
||||
|
||||
The formatter is used mainly from any language service operation that inserts or modifies code. The formatter is not exported publicly, and so all usage can only come through the language server.
|
||||
189
tools/tsgo/internal/format/api.go
Normal file
189
tools/tsgo/internal/format/api.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"context"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
type FormatRequestKind int
|
||||
|
||||
const (
|
||||
FormatRequestKindFormatDocument FormatRequestKind = iota
|
||||
FormatRequestKindFormatSelection
|
||||
FormatRequestKindFormatOnEnter
|
||||
FormatRequestKindFormatOnSemicolon
|
||||
FormatRequestKindFormatOnOpeningCurlyBrace
|
||||
FormatRequestKindFormatOnClosingCurlyBrace
|
||||
)
|
||||
|
||||
type formatContextKey int
|
||||
|
||||
const (
|
||||
formatOptionsKey formatContextKey = iota
|
||||
formatNewlineKey
|
||||
)
|
||||
|
||||
func WithFormatCodeSettings(ctx context.Context, options lsutil.FormatCodeSettings, newLine string) context.Context {
|
||||
ctx = context.WithValue(ctx, formatOptionsKey, options)
|
||||
ctx = context.WithValue(ctx, formatNewlineKey, newLine)
|
||||
// In strada, the rules map was both globally cached *and* cached into the context, for some reason. We skip that here and just use the global one.
|
||||
return ctx
|
||||
}
|
||||
|
||||
func GetFormatCodeSettingsFromContext(ctx context.Context) lsutil.FormatCodeSettings {
|
||||
if opt := ctx.Value(formatOptionsKey); opt != nil {
|
||||
return opt.(lsutil.FormatCodeSettings)
|
||||
}
|
||||
return lsutil.GetDefaultFormatCodeSettings()
|
||||
}
|
||||
|
||||
func GetNewLineOrDefaultFromContext(ctx context.Context) string { // TODO: Move into broader LS - more than just the formatter uses the newline editor setting/host new line
|
||||
opt := GetFormatCodeSettingsFromContext(ctx)
|
||||
if len(opt.NewLineCharacter) > 0 {
|
||||
return opt.NewLineCharacter
|
||||
}
|
||||
host := ctx.Value(formatNewlineKey).(string)
|
||||
if len(host) > 0 {
|
||||
return host
|
||||
}
|
||||
return "\n"
|
||||
}
|
||||
|
||||
func FormatSpan(ctx context.Context, span core.TextRange, file *ast.SourceFile, kind FormatRequestKind) []core.TextChange {
|
||||
// find the smallest node that fully wraps the range and compute the initial indentation for the node
|
||||
enclosingNode := findEnclosingNode(span, file)
|
||||
opts := GetFormatCodeSettingsFromContext(ctx)
|
||||
|
||||
return newFormattingScanner(
|
||||
file.Text(),
|
||||
file.LanguageVariant,
|
||||
getScanStartPosition(enclosingNode, span, file),
|
||||
span.End(),
|
||||
newFormatSpanWorker(
|
||||
ctx,
|
||||
span,
|
||||
enclosingNode,
|
||||
GetIndentationForNode(enclosingNode, &span, file, opts),
|
||||
getOwnOrInheritedDelta(enclosingNode, opts, file),
|
||||
kind,
|
||||
prepareRangeContainsErrorFunction(file.Diagnostics(), span),
|
||||
file,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func FormatNodeGivenIndentation(ctx context.Context, node *ast.Node, file *ast.SourceFile, languageVariant core.LanguageVariant, initialIndentation int, delta int) []core.TextChange {
|
||||
textRange := core.NewTextRange(node.Pos(), node.End())
|
||||
return newFormattingScanner(
|
||||
file.Text(),
|
||||
languageVariant,
|
||||
textRange.Pos(),
|
||||
textRange.End(),
|
||||
newFormatSpanWorker(
|
||||
ctx,
|
||||
textRange,
|
||||
node,
|
||||
initialIndentation,
|
||||
delta,
|
||||
FormatRequestKindFormatSelection,
|
||||
func(core.TextRange) bool { return false }, // assume that node does not have any errors
|
||||
file,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func formatNodeLines(ctx context.Context, sourceFile *ast.SourceFile, node *ast.Node, requestKind FormatRequestKind) []core.TextChange {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
tokenStart := scanner.GetTokenPosOfNode(node, sourceFile, false)
|
||||
lineStart := GetLineStartPositionForPosition(tokenStart, sourceFile)
|
||||
span := core.NewTextRange(lineStart, node.End())
|
||||
return FormatSpan(ctx, span, sourceFile, requestKind)
|
||||
}
|
||||
|
||||
func FormatDocument(ctx context.Context, sourceFile *ast.SourceFile) []core.TextChange {
|
||||
return FormatSpan(ctx, core.NewTextRange(0, sourceFile.End()), sourceFile, FormatRequestKindFormatDocument)
|
||||
}
|
||||
|
||||
func FormatSelection(ctx context.Context, sourceFile *ast.SourceFile, start int, end int) []core.TextChange {
|
||||
return FormatSpan(ctx, core.NewTextRange(GetLineStartPositionForPosition(start, sourceFile), end), sourceFile, FormatRequestKindFormatSelection)
|
||||
}
|
||||
|
||||
func FormatOnOpeningCurly(ctx context.Context, sourceFile *ast.SourceFile, position int) []core.TextChange {
|
||||
openingCurly := findImmediatelyPrecedingTokenOfKind(position, ast.KindOpenBraceToken, sourceFile)
|
||||
if openingCurly == nil {
|
||||
return nil
|
||||
}
|
||||
curlyBraceRange := openingCurly.Parent
|
||||
outermostNode := findOutermostNodeWithinListLevel(curlyBraceRange)
|
||||
/**
|
||||
* We limit the span to end at the opening curly to handle the case where
|
||||
* the brace matched to that just typed will be incorrect after further edits.
|
||||
* For example, we could type the opening curly for the following method
|
||||
* body without brace-matching activated:
|
||||
* ```
|
||||
* class C {
|
||||
* foo()
|
||||
* }
|
||||
* ```
|
||||
* and we wouldn't want to move the closing brace.
|
||||
*/
|
||||
textRange := core.NewTextRange(GetLineStartPositionForPosition(scanner.GetTokenPosOfNode(outermostNode, sourceFile, false), sourceFile), position)
|
||||
return FormatSpan(ctx, textRange, sourceFile, FormatRequestKindFormatOnOpeningCurlyBrace)
|
||||
}
|
||||
|
||||
func FormatOnClosingCurly(ctx context.Context, sourceFile *ast.SourceFile, position int) []core.TextChange {
|
||||
precedingToken := findImmediatelyPrecedingTokenOfKind(position, ast.KindCloseBraceToken, sourceFile)
|
||||
return formatNodeLines(ctx, sourceFile, findOutermostNodeWithinListLevel(precedingToken), FormatRequestKindFormatOnClosingCurlyBrace)
|
||||
}
|
||||
|
||||
func FormatOnSemicolon(ctx context.Context, sourceFile *ast.SourceFile, position int) []core.TextChange {
|
||||
semicolon := findImmediatelyPrecedingTokenOfKind(position, ast.KindSemicolonToken, sourceFile)
|
||||
return formatNodeLines(ctx, sourceFile, findOutermostNodeWithinListLevel(semicolon), FormatRequestKindFormatOnSemicolon)
|
||||
}
|
||||
|
||||
func FormatOnEnter(ctx context.Context, sourceFile *ast.SourceFile, position int) []core.TextChange {
|
||||
line := scanner.GetECMALineOfPosition(sourceFile, position)
|
||||
if line == 0 {
|
||||
return nil
|
||||
}
|
||||
// get start position for the previous line
|
||||
startPos := int(scanner.GetECMALineStarts(sourceFile)[line-1])
|
||||
// After the enter key, the cursor is now at a new line. The new line may or may not contain non-whitespace characters.
|
||||
// If the new line has only whitespaces, we won't want to format this line, because that would remove the indentation as
|
||||
// trailing whitespaces. So the end of the formatting span should be the later one between:
|
||||
// 1. the end of the previous line
|
||||
// 2. the last non-whitespace character in the current line
|
||||
endOfFormatSpan := scanner.GetECMAEndLinePosition(sourceFile, line)
|
||||
for endOfFormatSpan > startPos {
|
||||
ch, s := utf8.DecodeRuneInString(sourceFile.Text()[endOfFormatSpan:])
|
||||
if s == 0 || stringutil.IsWhiteSpaceSingleLine(ch) { // on multibyte character keep backing up
|
||||
endOfFormatSpan--
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// if the character at the end of the span is a line break, we shouldn't include it, because it indicates we don't want to
|
||||
// touch the current line at all. Also, on some OSes the line break consists of two characters (\r\n), we should test if the
|
||||
// previous character before the end of format span is line break character as well.
|
||||
ch, _ := utf8.DecodeRuneInString(sourceFile.Text()[endOfFormatSpan:])
|
||||
if stringutil.IsLineBreak(ch) {
|
||||
endOfFormatSpan--
|
||||
}
|
||||
|
||||
span := core.NewTextRange(
|
||||
startPos,
|
||||
// end value is exclusive so add 1 to the result
|
||||
endOfFormatSpan+1,
|
||||
)
|
||||
|
||||
return FormatSpan(ctx, span, sourceFile, FormatRequestKindFormatOnEnter)
|
||||
}
|
||||
115
tools/tsgo/internal/format/api_test.go
Normal file
115
tools/tsgo/internal/format/api_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package format_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/format"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/repo"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func applyBulkEdits(text string, edits []core.TextChange) string {
|
||||
b := strings.Builder{}
|
||||
b.Grow(len(text))
|
||||
lastEnd := 0
|
||||
for _, e := range edits {
|
||||
start := e.TextRange.Pos()
|
||||
if start != lastEnd {
|
||||
b.WriteString(text[lastEnd:e.TextRange.Pos()])
|
||||
}
|
||||
b.WriteString(e.NewText)
|
||||
|
||||
lastEnd = e.TextRange.End()
|
||||
}
|
||||
b.WriteString(text[lastEnd:])
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("format checker.ts", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 4,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSTrue,
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
InsertSpaceBeforeTypeAnnotation: core.TSTrue,
|
||||
}, "\n")
|
||||
repo.SkipIfNoTypeScriptSubmodule(t)
|
||||
filePath := filepath.Join(repo.TypeScriptSubmodulePath(), "src/compiler/checker.ts")
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
assert.NilError(t, err)
|
||||
text := string(fileContent)
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/checker.ts",
|
||||
Path: "/checker.ts",
|
||||
}, text, core.ScriptKindTS)
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
newText := applyBulkEdits(text, edits)
|
||||
assert.Assert(t, len(newText) > 0)
|
||||
assert.Assert(t, text != newText)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkFormat(b *testing.B) {
|
||||
ctx := format.WithFormatCodeSettings(b.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 4,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSTrue,
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
InsertSpaceBeforeTypeAnnotation: core.TSTrue,
|
||||
}, "\n")
|
||||
repo.SkipIfNoTypeScriptSubmodule(b)
|
||||
filePath := filepath.Join(repo.TypeScriptSubmodulePath(), "src/compiler/checker.ts")
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
assert.NilError(b, err)
|
||||
text := string(fileContent)
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/checker.ts",
|
||||
Path: "/checker.ts",
|
||||
}, text, core.ScriptKindTS)
|
||||
|
||||
b.Run("format checker.ts", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
newText := applyBulkEdits(text, edits)
|
||||
assert.Assert(b, len(newText) > 0)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("format checker.ts (no edit application)", func(b *testing.B) { // for comparison (how long does applying many edits take?)
|
||||
for b.Loop() {
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
assert.Assert(b, len(edits) > 0)
|
||||
}
|
||||
})
|
||||
|
||||
p := printer.NewPrinter(printer.PrinterOptions{}, printer.PrintHandlers{}, printer.NewEmitContext())
|
||||
b.Run("pretty print checker.ts", func(b *testing.B) { // for comparison
|
||||
for b.Loop() {
|
||||
newText := p.EmitSourceFile(sourceFile)
|
||||
assert.Assert(b, len(newText) > 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
402
tools/tsgo/internal/format/comment_test.go
Normal file
402
tools/tsgo/internal/format/comment_test.go
Normal file
@@ -0,0 +1,402 @@
|
||||
package format_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/format"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestCommentFormatting(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("format comment issue reproduction", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 4,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSTrue,
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
InsertSpaceBeforeTypeAnnotation: core.TSTrue,
|
||||
}, "\n")
|
||||
|
||||
// Original code that causes the bug
|
||||
originalText := `class C {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async x() {}
|
||||
}`
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, originalText, core.ScriptKindTS)
|
||||
|
||||
// Apply formatting once
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
firstFormatted := applyBulkEdits(originalText, edits)
|
||||
|
||||
// Check that the asterisk is not corrupted
|
||||
assert.Check(t, !strings.Contains(firstFormatted, "*/\n /"), "should not corrupt */ to /")
|
||||
assert.Check(t, strings.Contains(firstFormatted, "*/"), "should preserve */ token")
|
||||
assert.Check(t, strings.Contains(firstFormatted, "async"), "should preserve async keyword")
|
||||
|
||||
// Apply formatting a second time to test stability
|
||||
sourceFile2 := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, firstFormatted, core.ScriptKindTS)
|
||||
|
||||
edits2 := format.FormatDocument(ctx, sourceFile2)
|
||||
secondFormatted := applyBulkEdits(firstFormatted, edits2)
|
||||
|
||||
// Check that second formatting doesn't introduce corruption
|
||||
assert.Check(t, !strings.Contains(secondFormatted, " sync x()"), "should not corrupt async to sync")
|
||||
assert.Check(t, strings.Contains(secondFormatted, "async"), "should preserve async keyword on second pass")
|
||||
})
|
||||
|
||||
t.Run("format JSDoc with tab indentation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 0,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSFalse, // Use tabs
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
InsertSpaceBeforeTypeAnnotation: core.TSTrue,
|
||||
}, "\n")
|
||||
|
||||
// Original code with tab indentation (tabs represented as \t)
|
||||
originalText := "class Foo {\n\t/**\n\t * @param {string} argument - This is a param description.\n\t */\n\texample(argument) {\nconsole.log(argument);\n\t}\n}"
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, originalText, core.ScriptKindTS)
|
||||
|
||||
// Apply formatting
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
formatted := applyBulkEdits(originalText, edits)
|
||||
|
||||
// Check that tabs come before spaces (not spaces before tabs)
|
||||
// The comment lines should have format: tab followed by space and asterisk
|
||||
// NOT: space followed by tab and asterisk
|
||||
assert.Check(t, !strings.Contains(formatted, " \t*"), "should not have space before tab before asterisk")
|
||||
assert.Check(t, strings.Contains(formatted, "\t *"), "should have tab before space before asterisk")
|
||||
|
||||
// Verify console.log is properly indented with tabs
|
||||
assert.Check(t, strings.Contains(formatted, "\t\tconsole.log"), "console.log should be indented with two tabs")
|
||||
})
|
||||
|
||||
t.Run("format comment inside multi-line argument list", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 0,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSFalse, // Use tabs
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
InsertSpaceBeforeTypeAnnotation: core.TSTrue,
|
||||
}, "\n")
|
||||
|
||||
// Original code with proper indentation
|
||||
originalText := "console.log(\n\t\"a\",\n\t// the second arg\n\t\"b\"\n);"
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, originalText, core.ScriptKindTS)
|
||||
|
||||
// Apply formatting
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
formatted := applyBulkEdits(originalText, edits)
|
||||
|
||||
// The comment should remain indented with a tab
|
||||
assert.Check(t, strings.Contains(formatted, "\t// the second arg"), "comment should be indented with tab")
|
||||
// The comment should not lose its indentation
|
||||
assert.Check(t, !strings.Contains(formatted, "\n// the second arg"), "comment should not lose indentation")
|
||||
})
|
||||
|
||||
t.Run("format comment in chained method calls", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 0,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSFalse, // Use tabs
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
InsertSpaceBeforeTypeAnnotation: core.TSTrue,
|
||||
}, "\n")
|
||||
|
||||
// Original code with proper indentation
|
||||
originalText := "foo\n\t.bar()\n\t// A second call\n\t.baz();"
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, originalText, core.ScriptKindTS)
|
||||
|
||||
// Apply formatting
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
formatted := applyBulkEdits(originalText, edits)
|
||||
|
||||
// The comment should remain indented
|
||||
assert.Check(t, strings.Contains(formatted, "\t// A second call") || strings.Contains(formatted, " // A second call"), "comment should be indented")
|
||||
// The comment should not lose its indentation
|
||||
assert.Check(t, !strings.Contains(formatted, "\n// A second call"), "comment should not lose indentation")
|
||||
})
|
||||
|
||||
// Regression test for issue #1928 - panic when formatting chained method call with comment
|
||||
t.Run("format chained method call with comment (issue #1928)", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 0,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSFalse, // Use tabs
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
InsertSpaceBeforeTypeAnnotation: core.TSTrue,
|
||||
}, "\n")
|
||||
|
||||
// This code previously caused a panic with "strings: negative Repeat count"
|
||||
// because tokenIndentation was -1 and was being used directly for indentation
|
||||
originalText := "foo\n\t.bar()\n\t// A second call\n\t.baz();"
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, originalText, core.ScriptKindTS)
|
||||
|
||||
// Apply formatting - should not panic
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
formatted := applyBulkEdits(originalText, edits)
|
||||
|
||||
// Verify the comment maintains proper indentation and doesn't lose it
|
||||
assert.Check(t, strings.Contains(formatted, "\t// A second call") || strings.Contains(formatted, " // A second call"), "comment should be indented")
|
||||
assert.Check(t, !strings.Contains(formatted, "\n// A second call"), "comment should not be at column 0")
|
||||
})
|
||||
|
||||
t.Run("multiline comment inside block that opens on first line (issue #2649)", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 0,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSFalse,
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
}, "\n")
|
||||
|
||||
originalText := `document.addEventListener('DOMContentLoaded', () => {
|
||||
/** @type {NodeListOf<HTMLSpanElement>} */
|
||||
const elements = document.querySelectorAll('.test')
|
||||
});`
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.js",
|
||||
Path: "/test.js",
|
||||
}, originalText, core.ScriptKindJS)
|
||||
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
formatted := applyBulkEdits(originalText, edits)
|
||||
assert.Check(t, len(formatted) > 0, "formatted text should not be empty")
|
||||
})
|
||||
|
||||
t.Run("single-line comment inside block that opens on first line (issue #2649)", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 0,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSFalse,
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
}, "\n")
|
||||
|
||||
originalText := `document.addEventListener('DOMContentLoaded', () => {
|
||||
// a comment
|
||||
const x = 1
|
||||
});`
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, originalText, core.ScriptKindTS)
|
||||
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
formatted := applyBulkEdits(originalText, edits)
|
||||
assert.Check(t, len(formatted) > 0, "formatted text should not be empty")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatSelectionPreservesComments(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("format selection should not delete block comment when selection ends inside comment", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 0,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSTrue,
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
}, "\n")
|
||||
|
||||
// Reproduce: const test/* comment */=5;
|
||||
// When selecting a range that ends inside the comment (before */), format selection should not delete the comment.
|
||||
originalText := `const test/* comment */=5;`
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, originalText, core.ScriptKindTS)
|
||||
|
||||
// Select a range that starts at the beginning of the line and ends inside the block comment.
|
||||
// This covers `const test/* comment`, stopping before the closing `*/`.
|
||||
commentStart := strings.Index(originalText, "/*")
|
||||
selectionEnd := commentStart + len("/* comment") // ends inside the comment, before the closing `*/`
|
||||
|
||||
edits := format.FormatSelection(ctx, sourceFile, 0, selectionEnd)
|
||||
formatted := applyBulkEdits(originalText, edits)
|
||||
|
||||
// The entire statement should be preserved unchanged
|
||||
assert.Equal(t, formatted, originalText, "format selection should not delete the block comment or alter the statement")
|
||||
})
|
||||
|
||||
t.Run("format selection should not delete block comment when selection starts inside comment", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 0,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSTrue,
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
}, "\n")
|
||||
|
||||
originalText := `const test/* comment */=5;`
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, originalText, core.ScriptKindTS)
|
||||
|
||||
// Select from inside the comment to the end
|
||||
commentStart := strings.Index(originalText, "/*")
|
||||
selectionStart := commentStart + 3 // inside the comment
|
||||
|
||||
edits := format.FormatSelection(ctx, sourceFile, selectionStart, len(originalText))
|
||||
formatted := applyBulkEdits(originalText, edits)
|
||||
|
||||
// The entire statement should be preserved unchanged
|
||||
assert.Equal(t, formatted, originalText, "format selection should not delete the block comment or alter the statement")
|
||||
})
|
||||
|
||||
t.Run("full document format should preserve block comment and add spaces", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 0,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSTrue,
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
InsertSpaceBeforeAndAfterBinaryOperators: core.TSTrue,
|
||||
}, "\n")
|
||||
|
||||
originalText := `const test/* comment */=5;`
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, originalText, core.ScriptKindTS)
|
||||
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
formatted := applyBulkEdits(originalText, edits)
|
||||
|
||||
// Full document format should preserve the comment and add spaces around `=`
|
||||
assert.Equal(t, "const test/* comment */ = 5;", formatted, "full format should preserve the block comment and add spaces")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSliceBoundsPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("format code with trailing semicolon should not panic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
BaseIndentSize: 4,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSTrue,
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
InsertSpaceBeforeTypeAnnotation: core.TSTrue,
|
||||
}, "\n")
|
||||
|
||||
// Code from the issue that causes slice bounds panic
|
||||
originalText := `const _enableDisposeWithListenerWarning = false
|
||||
// || Boolean("TRUE") // causes a linter warning so that it cannot be pushed
|
||||
;
|
||||
`
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, originalText, core.ScriptKindTS)
|
||||
|
||||
// This should not panic
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
formatted := applyBulkEdits(originalText, edits)
|
||||
|
||||
// Basic sanity checks
|
||||
assert.Check(t, len(formatted) > 0, "formatted text should not be empty")
|
||||
assert.Check(t, strings.Contains(formatted, "_enableDisposeWithListenerWarning"), "should preserve variable name")
|
||||
})
|
||||
}
|
||||
121
tools/tsgo/internal/format/context.go
Normal file
121
tools/tsgo/internal/format/context.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
type FormattingContext struct {
|
||||
currentTokenSpan TextRangeWithKind
|
||||
nextTokenSpan TextRangeWithKind
|
||||
contextNode *ast.Node
|
||||
currentTokenParent *ast.Node
|
||||
nextTokenParent *ast.Node
|
||||
|
||||
contextNodeAllOnSameLine core.Tristate
|
||||
nextNodeAllOnSameLine core.Tristate
|
||||
tokensAreOnSameLine core.Tristate
|
||||
contextNodeBlockIsOnOneLine core.Tristate
|
||||
nextNodeBlockIsOnOneLine core.Tristate
|
||||
|
||||
SourceFile *ast.SourceFile
|
||||
FormattingRequestKind FormatRequestKind
|
||||
Options lsutil.FormatCodeSettings
|
||||
}
|
||||
|
||||
func NewFormattingContext(file *ast.SourceFile, kind FormatRequestKind, options lsutil.FormatCodeSettings) *FormattingContext {
|
||||
res := &FormattingContext{
|
||||
SourceFile: file,
|
||||
FormattingRequestKind: kind,
|
||||
Options: options,
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (this *FormattingContext) UpdateContext(cur TextRangeWithKind, curParent *ast.Node, next TextRangeWithKind, nextParent *ast.Node, commonParent *ast.Node) {
|
||||
if curParent == nil {
|
||||
panic("nil current range node parent in update context")
|
||||
}
|
||||
if nextParent == nil {
|
||||
panic("nil next range node parent in update context")
|
||||
}
|
||||
if commonParent == nil {
|
||||
panic("nil common parent node in update context")
|
||||
}
|
||||
this.currentTokenSpan = cur
|
||||
this.currentTokenParent = curParent
|
||||
this.nextTokenSpan = next
|
||||
this.nextTokenParent = nextParent
|
||||
this.contextNode = commonParent
|
||||
|
||||
// drop cached results
|
||||
this.contextNodeAllOnSameLine = core.TSUnknown
|
||||
this.nextNodeAllOnSameLine = core.TSUnknown
|
||||
this.tokensAreOnSameLine = core.TSUnknown
|
||||
this.contextNodeBlockIsOnOneLine = core.TSUnknown
|
||||
this.nextNodeBlockIsOnOneLine = core.TSUnknown
|
||||
}
|
||||
|
||||
func (this *FormattingContext) rangeIsOnOneLine(node core.TextRange) core.Tristate {
|
||||
if rangeIsOnOneLine(node, this.SourceFile) {
|
||||
return core.TSTrue
|
||||
}
|
||||
return core.TSFalse
|
||||
}
|
||||
|
||||
func (this *FormattingContext) nodeIsOnOneLine(node *ast.Node) core.Tristate {
|
||||
return this.rangeIsOnOneLine(withTokenStart(node, this.SourceFile))
|
||||
}
|
||||
|
||||
func withTokenStart(loc *ast.Node, file *ast.SourceFile) core.TextRange {
|
||||
startPos := scanner.GetTokenPosOfNode(loc, file, false)
|
||||
return core.NewTextRange(startPos, loc.End())
|
||||
}
|
||||
|
||||
func (this *FormattingContext) blockIsOnOneLine(node *ast.Node) core.Tristate {
|
||||
openBrace := astnav.FindChildOfKind(node, ast.KindOpenBraceToken, this.SourceFile)
|
||||
closeBrace := astnav.FindChildOfKind(node, ast.KindCloseBraceToken, this.SourceFile)
|
||||
if openBrace != nil && closeBrace != nil {
|
||||
closeBraceStart := scanner.GetTokenPosOfNode(closeBrace, this.SourceFile, false)
|
||||
return this.rangeIsOnOneLine(core.NewTextRange(openBrace.End(), closeBraceStart))
|
||||
}
|
||||
return core.TSFalse
|
||||
}
|
||||
|
||||
func (this *FormattingContext) ContextNodeAllOnSameLine() bool {
|
||||
if this.contextNodeAllOnSameLine == core.TSUnknown {
|
||||
this.contextNodeAllOnSameLine = this.nodeIsOnOneLine(this.contextNode)
|
||||
}
|
||||
return this.contextNodeAllOnSameLine == core.TSTrue
|
||||
}
|
||||
|
||||
func (this *FormattingContext) NextNodeAllOnSameLine() bool {
|
||||
if this.nextNodeAllOnSameLine == core.TSUnknown {
|
||||
this.nextNodeAllOnSameLine = this.nodeIsOnOneLine(this.nextTokenParent)
|
||||
}
|
||||
return this.nextNodeAllOnSameLine == core.TSTrue
|
||||
}
|
||||
|
||||
func (this *FormattingContext) TokensAreOnSameLine() bool {
|
||||
if this.tokensAreOnSameLine == core.TSUnknown {
|
||||
this.tokensAreOnSameLine = this.rangeIsOnOneLine(core.NewTextRange(this.currentTokenSpan.Loc.Pos(), this.nextTokenSpan.Loc.End()))
|
||||
}
|
||||
return this.tokensAreOnSameLine == core.TSTrue
|
||||
}
|
||||
|
||||
func (this *FormattingContext) ContextNodeBlockIsOnOneLine() bool {
|
||||
if this.contextNodeBlockIsOnOneLine == core.TSUnknown {
|
||||
this.contextNodeBlockIsOnOneLine = this.blockIsOnOneLine(this.contextNode)
|
||||
}
|
||||
return this.contextNodeBlockIsOnOneLine == core.TSTrue
|
||||
}
|
||||
|
||||
func (this *FormattingContext) NextNodeBlockIsOnOneLine() bool {
|
||||
if this.nextNodeBlockIsOnOneLine == core.TSUnknown {
|
||||
this.nextNodeBlockIsOnOneLine = this.blockIsOnOneLine(this.nextTokenParent)
|
||||
}
|
||||
return this.nextNodeBlockIsOnOneLine == core.TSTrue
|
||||
}
|
||||
58
tools/tsgo/internal/format/format_test.go
Normal file
58
tools/tsgo/internal/format/format_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package format_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/format"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestFormatNoTrailingSpace(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
text string
|
||||
}{
|
||||
{"simple statement without trailing newline", "1;"},
|
||||
{"function call without trailing newline", "console.log('hello');"},
|
||||
{"if block on single line", "if (true) { }"},
|
||||
{"class declaration", "class A {\n // Class Contents Go Here\n}"},
|
||||
{"class declaration with trailing newline", "class A {\n // Class Contents Go Here\n}\n"},
|
||||
{"empty block", "if (true) {}"},
|
||||
{"module declaration", "module M { }"},
|
||||
{"enum declaration", "enum E { A, B }"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
|
||||
EditorSettings: lsutil.EditorSettings{
|
||||
TabSize: 4,
|
||||
IndentSize: 4,
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSTrue,
|
||||
IndentStyle: lsutil.IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
}, "\n")
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, tc.text, core.ScriptKindTS)
|
||||
edits := format.FormatDocument(ctx, sourceFile)
|
||||
newText := applyBulkEdits(tc.text, edits)
|
||||
// Formatting should not add trailing whitespace at end of file
|
||||
for i, line := range strings.Split(newText, "\n") {
|
||||
trimmed := strings.TrimRight(line, " \t")
|
||||
assert.Equal(t, line, trimmed, "Formatter should not add trailing whitespace on line %d", i+1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
821
tools/tsgo/internal/format/indent.go
Normal file
821
tools/tsgo/internal/format/indent.go
Normal file
@@ -0,0 +1,821 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"slices"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/debug"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
func GetIndentationForNode(n *ast.Node, ignoreActualIndentationRange *core.TextRange, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
startline, startpos := scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false))
|
||||
return getIndentationForNodeWorker(n, startline, startpos, ignoreActualIndentationRange /*indentationDelta*/, 0, sourceFile /*isNextChild*/, false, options)
|
||||
}
|
||||
|
||||
// GetIndentation computes the expected indentation for a position in a source file.
|
||||
// This is the Go port of SmartIndenter.getIndentation from TypeScript.
|
||||
func GetIndentation(position int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings, assumeNewLineBeforeCloseBrace bool) int {
|
||||
if position > len(sourceFile.Text()) {
|
||||
return options.BaseIndentSize // past EOF
|
||||
}
|
||||
|
||||
// no indentation when the indent style is set to none,
|
||||
// so we can return fast
|
||||
if options.IndentStyle == lsutil.IndentStyleNone {
|
||||
return 0
|
||||
}
|
||||
|
||||
precedingToken := astnav.FindPrecedingTokenEx(sourceFile, position, nil /*startNode*/, true /*excludeJSDoc*/)
|
||||
|
||||
enclosingCommentRange := getRangeOfEnclosingComment(sourceFile, position, precedingToken)
|
||||
if enclosingCommentRange != nil && enclosingCommentRange.Kind == ast.KindMultiLineCommentTrivia {
|
||||
return getCommentIndent(sourceFile, position, options, enclosingCommentRange)
|
||||
}
|
||||
|
||||
if precedingToken == nil {
|
||||
return options.BaseIndentSize
|
||||
}
|
||||
|
||||
// no indentation in string/regex/template literals
|
||||
if isStringOrRegularExpressionOrTemplateLiteral(precedingToken.Kind) {
|
||||
tokenStart := scanner.GetTokenPosOfNode(precedingToken, sourceFile, false)
|
||||
if tokenStart <= position && position < precedingToken.End() {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
lineAtPosition := scanner.GetECMALineOfPosition(sourceFile, position)
|
||||
|
||||
// indentation is first non-whitespace character in a previous line
|
||||
// for block indentation, we should look for a line which contains something that's not
|
||||
// whitespace.
|
||||
currentToken := astnav.GetTokenAtPosition(sourceFile, position)
|
||||
// For object literals, we want indentation to work just like with blocks.
|
||||
// If the `{` starts in any position (even in the middle of a line), then
|
||||
// the following indentation should treat `{` as the start of that line (including leading whitespace).
|
||||
// ```
|
||||
// const a: { x: undefined, y: undefined } = {} // leading 4 whitespaces and { starts in the middle of line
|
||||
// ->
|
||||
// const a: { x: undefined, y: undefined } = {
|
||||
// x: undefined,
|
||||
// y: undefined,
|
||||
// }
|
||||
// ---------------------
|
||||
// const a: {x : undefined, y: undefined } =
|
||||
// {}
|
||||
// ->
|
||||
// const a: { x: undefined, y: undefined } =
|
||||
// { // leading 5 whitespaces and { starts at 6 column
|
||||
// x: undefined,
|
||||
// y: undefined,
|
||||
// }
|
||||
// ```
|
||||
isObjectLiteral := currentToken.Kind == ast.KindOpenBraceToken && currentToken.Parent != nil && currentToken.Parent.Kind == ast.KindObjectLiteralExpression
|
||||
if options.IndentStyle == lsutil.IndentStyleBlock || isObjectLiteral {
|
||||
return getBlockIndent(sourceFile, position, options)
|
||||
}
|
||||
|
||||
if precedingToken.Kind == ast.KindCommaToken && precedingToken.Parent != nil && precedingToken.Parent.Kind != ast.KindBinaryExpression {
|
||||
// previous token is comma that separates items in list - find the previous item and try to derive indentation from it
|
||||
actualIndentation := getActualIndentationForListItemBeforeComma(precedingToken, sourceFile, options)
|
||||
if actualIndentation != -1 {
|
||||
return actualIndentation
|
||||
}
|
||||
}
|
||||
|
||||
containerList := getListByPosition(position, precedingToken.Parent, sourceFile)
|
||||
// use list position if the preceding token is before any list items
|
||||
if containerList != nil && !precedingToken.Loc.ContainedBy(containerList.Loc) {
|
||||
useTheSameBaseIndentation := currentToken.Parent != nil && (currentToken.Parent.Kind == ast.KindFunctionExpression || currentToken.Parent.Kind == ast.KindArrowFunction)
|
||||
indentSize := 0
|
||||
if !useTheSameBaseIndentation {
|
||||
indentSize = options.IndentSize
|
||||
}
|
||||
res := getActualIndentationForListStartLine(containerList, sourceFile, options)
|
||||
if res == -1 {
|
||||
return indentSize
|
||||
}
|
||||
return res + indentSize
|
||||
}
|
||||
|
||||
return getSmartIndent(sourceFile, position, precedingToken, lineAtPosition, assumeNewLineBeforeCloseBrace, options)
|
||||
}
|
||||
|
||||
func getCommentIndent(sourceFile *ast.SourceFile, position int, options lsutil.FormatCodeSettings, enclosingCommentRange *ast.CommentRange) int {
|
||||
previousLine := scanner.GetECMALineOfPosition(sourceFile, position) - 1
|
||||
commentStartLine := scanner.GetECMALineOfPosition(sourceFile, enclosingCommentRange.Pos())
|
||||
|
||||
debug.Assert(commentStartLine >= 0, "commentStartLine >= 0")
|
||||
|
||||
if previousLine <= commentStartLine {
|
||||
lineStarts := scanner.GetECMALineStarts(sourceFile)
|
||||
return FindFirstNonWhitespaceColumn(int(lineStarts[commentStartLine]), position, sourceFile, options)
|
||||
}
|
||||
|
||||
lineStarts := scanner.GetECMALineStarts(sourceFile)
|
||||
startPositionOfLine := int(lineStarts[previousLine])
|
||||
character, column := findFirstNonWhitespaceCharacterAndColumn(startPositionOfLine, position, sourceFile, options)
|
||||
|
||||
if column == 0 {
|
||||
return column
|
||||
}
|
||||
|
||||
firstNonWhitespaceCharacterCode := sourceFile.Text()[startPositionOfLine+character]
|
||||
if firstNonWhitespaceCharacterCode == '*' {
|
||||
return column - 1
|
||||
}
|
||||
return column
|
||||
}
|
||||
|
||||
func getLeadingCommentRangesOfNode(node *ast.Node, file *ast.SourceFile) iter.Seq[ast.CommentRange] {
|
||||
if node.Kind == ast.KindJsxText {
|
||||
return nil
|
||||
}
|
||||
return scanner.GetLeadingCommentRanges(&ast.NodeFactory{}, file.Text(), node.Pos())
|
||||
}
|
||||
|
||||
func getRangeOfEnclosingComment(
|
||||
sourceFile *ast.SourceFile,
|
||||
position int,
|
||||
precedingToken *ast.Node,
|
||||
) *ast.CommentRange {
|
||||
tokenAtPosition := astnav.GetTokenAtPosition(sourceFile, position)
|
||||
jsdoc := ast.FindAncestor(tokenAtPosition, (*ast.Node).IsJSDoc)
|
||||
if jsdoc != nil {
|
||||
tokenAtPosition = jsdoc.Parent
|
||||
}
|
||||
tokenStart := astnav.GetStartOfNode(tokenAtPosition, sourceFile, false /*includeJSDoc*/)
|
||||
if tokenStart <= position && position < tokenAtPosition.End() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Between two consecutive tokens, all comments are either trailing on the former
|
||||
// or leading on the latter (and none are in both lists).
|
||||
var trailingRangesOfPreviousToken iter.Seq[ast.CommentRange]
|
||||
if precedingToken != nil {
|
||||
trailingRangesOfPreviousToken = scanner.GetTrailingCommentRanges(&ast.NodeFactory{}, sourceFile.Text(), precedingToken.End())
|
||||
}
|
||||
leadingRangesOfNextToken := getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile)
|
||||
commentRanges := core.ConcatenateSeq(trailingRangesOfPreviousToken, leadingRangesOfNextToken)
|
||||
for commentRange := range commentRanges {
|
||||
if commentRange.ContainsExclusive(position) ||
|
||||
position == commentRange.End() &&
|
||||
(commentRange.Kind == ast.KindSingleLineCommentTrivia || position == len(sourceFile.Text())) {
|
||||
return &commentRange
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBlockIndent(sourceFile *ast.SourceFile, position int, options lsutil.FormatCodeSettings) int {
|
||||
// move backwards until we find a line with a non-whitespace character,
|
||||
// then find the first non-whitespace character for that line.
|
||||
current := position
|
||||
for current > 0 {
|
||||
ch, size := utf8.DecodeRuneInString(sourceFile.Text()[current:])
|
||||
if !stringutil.IsWhiteSpaceLike(ch) {
|
||||
break
|
||||
}
|
||||
current -= size
|
||||
}
|
||||
|
||||
lineStart := GetLineStartPositionForPosition(current, sourceFile)
|
||||
return FindFirstNonWhitespaceColumn(lineStart, current, sourceFile, options)
|
||||
}
|
||||
|
||||
func getActualIndentationForListItemBeforeComma(commaToken *ast.Node, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
// previous token is comma that separates items in list - find the previous item and try to derive indentation from it
|
||||
if commaToken.Parent == nil {
|
||||
return -1
|
||||
}
|
||||
containingList := GetContainingList(commaToken, sourceFile)
|
||||
if containingList == nil {
|
||||
return -1
|
||||
}
|
||||
commaIndex := core.FindIndex(containingList.Nodes, func(n *ast.Node) bool { return n == commaToken })
|
||||
if commaIndex > 0 {
|
||||
return deriveActualIndentationFromList(containingList, commaIndex-1, sourceFile, options)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type nextTokenKind int
|
||||
|
||||
const (
|
||||
nextTokenKindUnknown nextTokenKind = 0
|
||||
nextTokenKindOpenBrace nextTokenKind = 1
|
||||
nextTokenKindCloseBrace nextTokenKind = 2
|
||||
)
|
||||
|
||||
func nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken *ast.Node, current *ast.Node, lineAtPosition int, sourceFile *ast.SourceFile) nextTokenKind {
|
||||
nextToken := astnav.FindNextToken(precedingToken, current, sourceFile)
|
||||
if nextToken == nil {
|
||||
return nextTokenKindUnknown
|
||||
}
|
||||
|
||||
if nextToken.Kind == ast.KindOpenBraceToken {
|
||||
// open braces are always indented at the parent level
|
||||
return nextTokenKindOpenBrace
|
||||
} else if nextToken.Kind == ast.KindCloseBraceToken {
|
||||
// close braces are indented at the parent level if they are located on the same line with cursor
|
||||
nextTokenStartLine := getStartLineForNode(nextToken, sourceFile)
|
||||
if lineAtPosition == nextTokenStartLine {
|
||||
return nextTokenKindCloseBrace
|
||||
}
|
||||
return nextTokenKindUnknown
|
||||
}
|
||||
|
||||
return nextTokenKindUnknown
|
||||
}
|
||||
|
||||
func getSmartIndent(sourceFile *ast.SourceFile, position int, precedingToken *ast.Node, lineAtPosition int, assumeNewLineBeforeCloseBrace bool, options lsutil.FormatCodeSettings) int {
|
||||
// try to find node that can contribute to indentation and includes 'position' starting from 'precedingToken'
|
||||
// if such node is found - compute initial indentation for 'position' inside this node
|
||||
var previous *ast.Node
|
||||
current := precedingToken
|
||||
|
||||
for current != nil {
|
||||
if lsutil.PositionBelongsToNode(current, position, sourceFile) && ShouldIndentChildNode(options, current, previous, sourceFile, true) {
|
||||
currentStartLine, currentStartChar := getStartLineAndCharacterForNode(current, sourceFile)
|
||||
ntk := nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken, current, lineAtPosition, sourceFile)
|
||||
var indentationDelta int
|
||||
if ntk != nextTokenKindUnknown {
|
||||
// handle cases when codefix is about to be inserted before the close brace
|
||||
if assumeNewLineBeforeCloseBrace && ntk == nextTokenKindCloseBrace {
|
||||
indentationDelta = options.IndentSize
|
||||
}
|
||||
// else 0
|
||||
} else {
|
||||
if lineAtPosition != currentStartLine {
|
||||
indentationDelta = options.IndentSize
|
||||
}
|
||||
}
|
||||
return getIndentationForNodeWorker(current, currentStartLine, currentStartChar, nil, indentationDelta, sourceFile, true, options)
|
||||
}
|
||||
|
||||
// check if current node is a list item - if yes, take indentation from it
|
||||
// do not consider parent-child line sharing yet:
|
||||
// function foo(a
|
||||
// | preceding node 'a' does share line with its parent but indentation is expected
|
||||
actualIndentation := getActualIndentationForListItem(current, sourceFile, options, true /*listIndentsChild*/)
|
||||
if actualIndentation != -1 {
|
||||
return actualIndentation
|
||||
}
|
||||
|
||||
previous = current
|
||||
current = current.Parent
|
||||
}
|
||||
// no parent was found - return the base indentation of the SourceFile
|
||||
return options.BaseIndentSize
|
||||
}
|
||||
|
||||
func getIndentationForNodeWorker(
|
||||
current *ast.Node,
|
||||
currentStartLine int,
|
||||
currentStartCharacter int,
|
||||
ignoreActualIndentationRange *core.TextRange,
|
||||
indentationDelta int,
|
||||
sourceFile *ast.SourceFile,
|
||||
isNextChild bool,
|
||||
options lsutil.FormatCodeSettings,
|
||||
) int {
|
||||
parent := current.Parent
|
||||
|
||||
// Walk up the tree and collect indentation for parent-child node pairs. Indentation is not added if
|
||||
// * parent and child nodes start on the same line, or
|
||||
// * parent is an IfStatement and child starts on the same line as an 'else clause'.
|
||||
for parent != nil {
|
||||
useActualIndentation := true
|
||||
if ignoreActualIndentationRange != nil {
|
||||
start := scanner.GetTokenPosOfNode(current, sourceFile, false)
|
||||
useActualIndentation = start < ignoreActualIndentationRange.Pos() || start > ignoreActualIndentationRange.End()
|
||||
}
|
||||
|
||||
containingListOrParentStartLine, containingListOrParentStartCharacter := getContainingListOrParentStart(parent, current, sourceFile)
|
||||
parentAndChildShareLine := containingListOrParentStartLine == currentStartLine ||
|
||||
childStartsOnTheSameLineWithElseInIfStatement(parent, current, currentStartLine, sourceFile)
|
||||
|
||||
if useActualIndentation {
|
||||
// check if current node is a list item - if yes, take indentation from it
|
||||
var firstListChild *ast.Node
|
||||
containerList := GetContainingList(current, sourceFile)
|
||||
if containerList != nil {
|
||||
firstListChild = core.FirstOrNil(containerList.Nodes)
|
||||
}
|
||||
// A list indents its children if the children begin on a later line than the list itself:
|
||||
//
|
||||
// f1( L0 - List start
|
||||
// { L1 - First child start: indented, along with all other children
|
||||
// prop: 0
|
||||
// },
|
||||
// {
|
||||
// prop: 1
|
||||
// }
|
||||
// )
|
||||
//
|
||||
// f2({ L0 - List start and first child start: children are not indented.
|
||||
// prop: 0 Object properties are indented only one level, because the list
|
||||
// }, { itself contributes nothing.
|
||||
// prop: 1 L3 - The indentation of the second object literal is best understood by
|
||||
// }) looking at the relationship between the list and *first* list item.
|
||||
var listIndentsChild bool
|
||||
if firstListChild != nil {
|
||||
listLine := getStartLineForNode(firstListChild, sourceFile)
|
||||
listIndentsChild = listLine > containingListOrParentStartLine
|
||||
}
|
||||
actualIndentation := getActualIndentationForListItem(current, sourceFile, options, listIndentsChild)
|
||||
if actualIndentation != -1 {
|
||||
return actualIndentation + indentationDelta
|
||||
}
|
||||
|
||||
// try to fetch actual indentation for current node from source text
|
||||
actualIndentation = getActualIndentationForNode(current, parent, currentStartLine, currentStartCharacter, parentAndChildShareLine, sourceFile, options)
|
||||
if actualIndentation != -1 {
|
||||
return actualIndentation + indentationDelta
|
||||
}
|
||||
}
|
||||
|
||||
// increase indentation if parent node wants its content to be indented and parent and child nodes don't start on the same line
|
||||
if ShouldIndentChildNode(options, parent, current, sourceFile, isNextChild) && !parentAndChildShareLine {
|
||||
indentationDelta += options.IndentSize
|
||||
}
|
||||
|
||||
// In our AST, a call argument's `parent` is the call-expression, not the argument list.
|
||||
// We would like to increase indentation based on the relationship between an argument and its argument-list,
|
||||
// so we spoof the starting position of the (parent) call-expression to match the (non-parent) argument-list.
|
||||
// But, the spoofed start-value could then cause a problem when comparing the start position of the call-expression
|
||||
// to *its* parent (in the case of an iife, an expression statement), adding an extra level of indentation.
|
||||
//
|
||||
// Instead, when at an argument, we unspoof the starting position of the enclosing call expression
|
||||
// *after* applying indentation for the argument.
|
||||
|
||||
useTrueStart := isArgumentAndStartLineOverlapsExpressionBeingCalled(parent, current, currentStartLine, sourceFile)
|
||||
|
||||
current = parent
|
||||
parent = current.Parent
|
||||
|
||||
if useTrueStart {
|
||||
currentStartLine, currentStartCharacter = scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, scanner.GetTokenPosOfNode(current, sourceFile, false))
|
||||
} else {
|
||||
currentStartLine = containingListOrParentStartLine
|
||||
currentStartCharacter = containingListOrParentStartCharacter
|
||||
}
|
||||
}
|
||||
|
||||
return indentationDelta + options.BaseIndentSize
|
||||
}
|
||||
|
||||
/*
|
||||
* Function returns -1 if actual indentation for node should not be used (i.e because node is nested expression)
|
||||
*/
|
||||
func getActualIndentationForNode(current *ast.Node, parent *ast.Node, cuurentLine int, currentChar int, parentAndChildShareLine bool, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
// actual indentation is used for statements\declarations if one of cases below is true:
|
||||
// - parent is SourceFile - by default immediate children of SourceFile are not indented except when user indents them manually
|
||||
// - parent and child are not on the same line
|
||||
useActualIndentation := (ast.IsDeclaration(current) || ast.IsStatementButNotDeclaration(current)) && (parent.Kind == ast.KindSourceFile || !parentAndChildShareLine)
|
||||
|
||||
if !useActualIndentation {
|
||||
return -1
|
||||
}
|
||||
|
||||
return findColumnForFirstNonWhitespaceCharacterInLine(cuurentLine, currentChar, sourceFile, options)
|
||||
}
|
||||
|
||||
func isArgumentAndStartLineOverlapsExpressionBeingCalled(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
|
||||
if !(ast.IsCallExpression(parent) && slices.Contains(parent.Arguments(), child)) {
|
||||
return false
|
||||
}
|
||||
expressionOfCallExpressionEnd := parent.Expression().End()
|
||||
expressionOfCallExpressionEndLine := scanner.GetECMALineOfPosition(sourceFile, expressionOfCallExpressionEnd)
|
||||
return expressionOfCallExpressionEndLine == childStartLine
|
||||
}
|
||||
|
||||
func getActualIndentationForListItem(node *ast.Node, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings, listIndentsChild bool) int {
|
||||
if node.Parent != nil && node.Parent.Kind == ast.KindVariableDeclarationList {
|
||||
// VariableDeclarationList has no wrapping tokens
|
||||
return -1
|
||||
}
|
||||
containingList := GetContainingList(node, sourceFile)
|
||||
if containingList != nil {
|
||||
index := core.FindIndex(containingList.Nodes, func(e *ast.Node) bool { return e == node })
|
||||
if index != -1 {
|
||||
result := deriveActualIndentationFromList(containingList, index, sourceFile, options)
|
||||
if result != -1 {
|
||||
return result
|
||||
}
|
||||
}
|
||||
delta := 0
|
||||
if listIndentsChild {
|
||||
delta = options.IndentSize
|
||||
}
|
||||
res := getActualIndentationForListStartLine(containingList, sourceFile, options)
|
||||
if res == -1 {
|
||||
return delta
|
||||
}
|
||||
return res + delta
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func getActualIndentationForListStartLine(list *ast.NodeList, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
if list == nil {
|
||||
return -1
|
||||
}
|
||||
line, char := scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, list.Loc.Pos())
|
||||
return findColumnForFirstNonWhitespaceCharacterInLine(line, char, sourceFile, options)
|
||||
}
|
||||
|
||||
func deriveActualIndentationFromList(list *ast.NodeList, index int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
debug.Assert(list != nil && index >= 0 && index < len(list.Nodes))
|
||||
|
||||
node := list.Nodes[index]
|
||||
|
||||
// walk toward the start of the list starting from current node and check if the line is the same for all items.
|
||||
// if end line for item [i - 1] differs from the start line for item [i] - find column of the first non-whitespace character on the line of item [i]
|
||||
|
||||
line, char := getStartLineAndCharacterForNode(node, sourceFile)
|
||||
|
||||
for i := index; i >= 0; i-- {
|
||||
if list.Nodes[i].Kind == ast.KindCommaToken {
|
||||
continue
|
||||
}
|
||||
// skip list items that ends on the same line with the current list element
|
||||
prevEndLine := scanner.GetECMALineOfPosition(sourceFile, list.Nodes[i].End())
|
||||
if prevEndLine != line {
|
||||
return findColumnForFirstNonWhitespaceCharacterInLine(line, char, sourceFile, options)
|
||||
}
|
||||
|
||||
line, char = getStartLineAndCharacterForNode(list.Nodes[i], sourceFile)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func findColumnForFirstNonWhitespaceCharacterInLine(line int, char int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
lineStart := scanner.GetECMAPositionOfLineAndByteOffset(sourceFile, line, 0)
|
||||
return FindFirstNonWhitespaceColumn(lineStart, lineStart+char, sourceFile, options)
|
||||
}
|
||||
|
||||
func FindFirstNonWhitespaceColumn(startPos int, endPos int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
_, col := findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options)
|
||||
return col
|
||||
}
|
||||
|
||||
/**
|
||||
* Character is the actual index of the character since the beginning of the line.
|
||||
* Column - position of the character after expanding tabs to spaces.
|
||||
* "0\t2$"
|
||||
* value of 'character' for '$' is 3
|
||||
* value of 'column' for '$' is 6 (assuming that tab size is 4)
|
||||
*/
|
||||
func findFirstNonWhitespaceCharacterAndColumn(startPos int, endPos int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) (character int, column int) {
|
||||
column = 0
|
||||
text := sourceFile.Text()
|
||||
pos := startPos
|
||||
for pos < endPos {
|
||||
ch, size := utf8.DecodeRuneInString(text[pos:])
|
||||
if !stringutil.IsWhiteSpaceSingleLine(ch) {
|
||||
break
|
||||
}
|
||||
|
||||
if ch == '\t' {
|
||||
if options.TabSize > 0 {
|
||||
column += options.TabSize + (column % options.TabSize)
|
||||
}
|
||||
} else {
|
||||
column++
|
||||
}
|
||||
|
||||
pos += size
|
||||
}
|
||||
return pos - startPos, column
|
||||
}
|
||||
|
||||
func childStartsOnTheSameLineWithElseInIfStatement(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
|
||||
if parent.Kind == ast.KindIfStatement && parent.AsIfStatement().ElseStatement == child {
|
||||
elseKeyword := astnav.FindPrecedingToken(sourceFile, child.Pos())
|
||||
debug.Assert(elseKeyword != nil)
|
||||
elseKeywordStartLine := getStartLineForNode(elseKeyword, sourceFile)
|
||||
return elseKeywordStartLine == childStartLine
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getStartLineAndCharacterForNode(n *ast.Node, sourceFile *ast.SourceFile) (line int, character int) {
|
||||
return scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false))
|
||||
}
|
||||
|
||||
func getStartLineForNode(n *ast.Node, sourceFile *ast.SourceFile) int {
|
||||
return scanner.GetECMALineOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false))
|
||||
}
|
||||
|
||||
func GetContainingList(node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
|
||||
if node.Parent == nil {
|
||||
return nil
|
||||
}
|
||||
return getListByRange(scanner.GetTokenPosOfNode(node, sourceFile, false), node.End(), node.Parent, sourceFile)
|
||||
}
|
||||
|
||||
func getListByPosition(pos int, node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return getListByRange(pos, pos, node, sourceFile)
|
||||
}
|
||||
|
||||
func getListByRange(start int, end int, node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
|
||||
r := core.NewTextRange(start, end)
|
||||
switch node.Kind {
|
||||
case ast.KindTypeReference:
|
||||
return getList(node.TypeArgumentList(), r, node, sourceFile)
|
||||
case ast.KindObjectLiteralExpression:
|
||||
return getList(node.PropertyList(), r, node, sourceFile)
|
||||
case ast.KindArrayLiteralExpression:
|
||||
return getList(node.ElementList(), r, node, sourceFile)
|
||||
case ast.KindTypeLiteral:
|
||||
return getList(node.MemberList(), r, node, sourceFile)
|
||||
case ast.KindFunctionDeclaration,
|
||||
ast.KindFunctionExpression,
|
||||
ast.KindArrowFunction,
|
||||
ast.KindMethodDeclaration,
|
||||
ast.KindMethodSignature,
|
||||
ast.KindCallSignature,
|
||||
ast.KindConstructor,
|
||||
ast.KindConstructorType,
|
||||
ast.KindConstructSignature:
|
||||
tpl := getList(node.TypeParameterList(), r, node, sourceFile)
|
||||
if tpl != nil {
|
||||
return tpl
|
||||
}
|
||||
return getList(node.ParameterList(), r, node, sourceFile)
|
||||
case ast.KindGetAccessor:
|
||||
return getList(node.ParameterList(), r, node, sourceFile)
|
||||
case ast.KindClassDeclaration,
|
||||
ast.KindClassExpression,
|
||||
ast.KindInterfaceDeclaration,
|
||||
ast.KindTypeAliasDeclaration,
|
||||
ast.KindJSDocTemplateTag:
|
||||
return getList(node.TypeParameterList(), r, node, sourceFile)
|
||||
case ast.KindNewExpression, ast.KindCallExpression:
|
||||
l := getList(node.TypeArgumentList(), r, node, sourceFile)
|
||||
if l != nil {
|
||||
return l
|
||||
}
|
||||
return getList(node.ArgumentList(), r, node, sourceFile)
|
||||
case ast.KindVariableDeclarationList:
|
||||
return getList(node.AsVariableDeclarationList().Declarations, r, node, sourceFile)
|
||||
case ast.KindObjectBindingPattern, ast.KindArrayBindingPattern, ast.KindNamedImports, ast.KindNamedExports:
|
||||
return getList(node.ElementList(), r, node, sourceFile)
|
||||
}
|
||||
return nil // TODO: should this be a panic? It isn't in strada.
|
||||
}
|
||||
|
||||
func getList(list *ast.NodeList, r core.TextRange, node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
if r.ContainedBy(getVisualListRange(node, list.Loc, sourceFile)) {
|
||||
return list
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getVisualListRange(node *ast.Node, list core.TextRange, sourceFile *ast.SourceFile) core.TextRange {
|
||||
// In strada, this relied on the services .getChildren method, which manifested synthetic token nodes
|
||||
// _however_, the logic boils down to "find the child with the matching span and adjust its start to the
|
||||
// previous (possibly token) child's end and its end to the token start of the following element" - basically
|
||||
// expanding the range to encompass all the neighboring non-token trivia
|
||||
// Now, we perform that logic with the scanner instead
|
||||
prior := astnav.FindPrecedingToken(sourceFile, list.Pos())
|
||||
var priorEnd int
|
||||
if prior == nil {
|
||||
priorEnd = list.Pos()
|
||||
} else {
|
||||
priorEnd = prior.End()
|
||||
}
|
||||
// Find the token that starts at or after list.End() using the scanner
|
||||
scan := scanner.GetScannerForSourceFile(sourceFile, list.End())
|
||||
var nextStart int
|
||||
if scan.Token() == ast.KindEndOfFile {
|
||||
nextStart = list.End()
|
||||
} else {
|
||||
nextStart = scan.TokenStart()
|
||||
}
|
||||
return core.NewTextRange(priorEnd, nextStart)
|
||||
}
|
||||
|
||||
func getContainingListOrParentStart(parent *ast.Node, child *ast.Node, sourceFile *ast.SourceFile) (line int, character int) {
|
||||
containingList := GetContainingList(child, sourceFile)
|
||||
var startPos int
|
||||
if containingList != nil {
|
||||
startPos = containingList.Loc.Pos()
|
||||
} else {
|
||||
startPos = scanner.GetTokenPosOfNode(parent, sourceFile, false)
|
||||
}
|
||||
return scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, startPos)
|
||||
}
|
||||
|
||||
func isControlFlowEndingStatement(kind ast.Kind, parentKind ast.Kind) bool {
|
||||
switch kind {
|
||||
case ast.KindReturnStatement, ast.KindThrowStatement, ast.KindContinueStatement, ast.KindBreakStatement:
|
||||
return parentKind != ast.KindBlock
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the parent node should indent the given child by an explicit rule.
|
||||
* @param isNextChild If true, we are judging indent of a hypothetical child *after* this one, not the current child.
|
||||
*/
|
||||
func ShouldIndentChildNode(settings lsutil.FormatCodeSettings, parent *ast.Node, child *ast.Node, sourceFile *ast.SourceFile, isNextChildArg ...bool) bool {
|
||||
isNextChild := false
|
||||
if len(isNextChildArg) > 0 {
|
||||
isNextChild = isNextChildArg[0]
|
||||
}
|
||||
|
||||
return NodeWillIndentChild(settings, parent, child, sourceFile, false) && !(isNextChild && child != nil && isControlFlowEndingStatement(child.Kind, parent.Kind))
|
||||
}
|
||||
|
||||
func NodeWillIndentChild(settings lsutil.FormatCodeSettings, parent *ast.Node, child *ast.Node, sourceFile *ast.SourceFile, indentByDefault bool) bool {
|
||||
childKind := ast.KindUnknown
|
||||
if child != nil {
|
||||
childKind = child.Kind
|
||||
}
|
||||
|
||||
switch parent.Kind {
|
||||
case ast.KindExpressionStatement,
|
||||
ast.KindClassDeclaration,
|
||||
ast.KindClassExpression,
|
||||
ast.KindInterfaceDeclaration,
|
||||
ast.KindEnumDeclaration,
|
||||
ast.KindTypeAliasDeclaration,
|
||||
ast.KindArrayLiteralExpression,
|
||||
ast.KindBlock,
|
||||
ast.KindModuleBlock,
|
||||
ast.KindObjectLiteralExpression,
|
||||
ast.KindTypeLiteral,
|
||||
ast.KindMappedType,
|
||||
ast.KindTupleType,
|
||||
ast.KindParenthesizedExpression,
|
||||
ast.KindPropertyAccessExpression,
|
||||
ast.KindCallExpression,
|
||||
ast.KindNewExpression,
|
||||
ast.KindVariableStatement,
|
||||
ast.KindExportAssignment,
|
||||
ast.KindReturnStatement,
|
||||
ast.KindConditionalExpression,
|
||||
ast.KindArrayBindingPattern,
|
||||
ast.KindObjectBindingPattern,
|
||||
ast.KindJsxOpeningElement,
|
||||
ast.KindJsxOpeningFragment,
|
||||
ast.KindJsxSelfClosingElement,
|
||||
ast.KindJsxExpression,
|
||||
ast.KindMethodSignature,
|
||||
ast.KindCallSignature,
|
||||
ast.KindConstructSignature,
|
||||
ast.KindParameter,
|
||||
ast.KindFunctionType,
|
||||
ast.KindConstructorType,
|
||||
ast.KindParenthesizedType,
|
||||
ast.KindTaggedTemplateExpression,
|
||||
ast.KindAwaitExpression,
|
||||
ast.KindNamedExports,
|
||||
ast.KindNamedImports,
|
||||
ast.KindExportSpecifier,
|
||||
ast.KindImportSpecifier,
|
||||
ast.KindPropertyDeclaration,
|
||||
ast.KindCaseClause,
|
||||
ast.KindDefaultClause:
|
||||
return true
|
||||
case ast.KindCaseBlock:
|
||||
return settings.IndentSwitchCase.IsTrueOrUnknown()
|
||||
case ast.KindVariableDeclaration, ast.KindPropertyAssignment, ast.KindBinaryExpression:
|
||||
if settings.IndentMultiLineObjectLiteralBeginningOnBlankLine.IsFalseOrUnknown() && sourceFile != nil && childKind == ast.KindObjectLiteralExpression {
|
||||
return rangeIsOnOneLine(child.Loc, sourceFile)
|
||||
}
|
||||
if parent.Kind == ast.KindBinaryExpression && sourceFile != nil && childKind == ast.KindJsxElement {
|
||||
parentStartLine := scanner.GetECMALineOfPosition(sourceFile, scanner.SkipTrivia(sourceFile.Text(), parent.Pos()))
|
||||
childStartLine := scanner.GetECMALineOfPosition(sourceFile, scanner.SkipTrivia(sourceFile.Text(), child.Pos()))
|
||||
return parentStartLine != childStartLine
|
||||
}
|
||||
if parent.Kind != ast.KindBinaryExpression {
|
||||
return true
|
||||
}
|
||||
return indentByDefault
|
||||
case ast.KindDoStatement,
|
||||
ast.KindWhileStatement,
|
||||
ast.KindForInStatement,
|
||||
ast.KindForOfStatement,
|
||||
ast.KindForStatement,
|
||||
ast.KindIfStatement,
|
||||
ast.KindFunctionDeclaration,
|
||||
ast.KindFunctionExpression,
|
||||
ast.KindMethodDeclaration,
|
||||
ast.KindConstructor,
|
||||
ast.KindGetAccessor,
|
||||
ast.KindSetAccessor:
|
||||
return childKind != ast.KindBlock
|
||||
case ast.KindArrowFunction:
|
||||
if sourceFile != nil && childKind == ast.KindParenthesizedExpression {
|
||||
return rangeIsOnOneLine(child.Loc, sourceFile)
|
||||
}
|
||||
return childKind != ast.KindBlock
|
||||
case ast.KindExportDeclaration:
|
||||
return childKind != ast.KindNamedExports
|
||||
case ast.KindImportDeclaration:
|
||||
return childKind != ast.KindImportClause || (child.AsImportClause().NamedBindings != nil && child.AsImportClause().NamedBindings.Kind != ast.KindNamedImports)
|
||||
case ast.KindJsxElement:
|
||||
return childKind != ast.KindJsxClosingElement
|
||||
case ast.KindJsxFragment:
|
||||
return childKind != ast.KindJsxClosingFragment
|
||||
case ast.KindIntersectionType, ast.KindUnionType, ast.KindSatisfiesExpression:
|
||||
if childKind == ast.KindTypeLiteral || childKind == ast.KindTupleType || childKind == ast.KindMappedType {
|
||||
return false
|
||||
}
|
||||
return indentByDefault
|
||||
case ast.KindTryStatement:
|
||||
if childKind == ast.KindBlock {
|
||||
return false
|
||||
}
|
||||
return indentByDefault
|
||||
}
|
||||
|
||||
// No explicit rule for given nodes so the result will follow the default value argument
|
||||
return indentByDefault
|
||||
}
|
||||
|
||||
// A multiline conditional typically increases the indentation of its whenTrue and whenFalse children:
|
||||
//
|
||||
// condition
|
||||
//
|
||||
// ? whenTrue
|
||||
// : whenFalse;
|
||||
//
|
||||
// However, that indentation does not apply if the subexpressions themselves span multiple lines,
|
||||
// applying their own indentation:
|
||||
//
|
||||
// (() => {
|
||||
// return complexCalculationForCondition();
|
||||
// })() ? {
|
||||
//
|
||||
// whenTrue: 'multiline object literal'
|
||||
// } : (
|
||||
//
|
||||
// whenFalse('multiline parenthesized expression')
|
||||
//
|
||||
// );
|
||||
//
|
||||
// In these cases, we must discard the indentation increase that would otherwise be applied to the
|
||||
// whenTrue and whenFalse children to avoid double-indenting their contents. To identify this scenario,
|
||||
// we check for the whenTrue branch beginning on the line that the condition ends, and the whenFalse
|
||||
// branch beginning on the line that the whenTrue branch ends.
|
||||
func childIsUnindentedBranchOfConditionalExpression(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
|
||||
if parent.Kind == ast.KindConditionalExpression && (child == parent.AsConditionalExpression().WhenTrue || child == parent.AsConditionalExpression().WhenFalse) {
|
||||
conditionEndLine := scanner.GetECMALineOfPosition(sourceFile, parent.AsConditionalExpression().Condition.End())
|
||||
if child == parent.AsConditionalExpression().WhenTrue {
|
||||
return childStartLine == conditionEndLine
|
||||
} else {
|
||||
// On the whenFalse side, we have to look at the whenTrue side, because if that one was
|
||||
// indented, whenFalse must also be indented:
|
||||
//
|
||||
// const y = true
|
||||
// ? 1 : ( L1: whenTrue indented because it's on a new line
|
||||
// 0 L2: indented two stops, one because whenTrue was indented
|
||||
// ); and one because of the parentheses spanning multiple lines
|
||||
trueStartLine := getStartLineForNode(parent.AsConditionalExpression().WhenTrue, sourceFile)
|
||||
trueEndLine := scanner.GetECMALineOfPosition(sourceFile, parent.AsConditionalExpression().WhenTrue.End())
|
||||
return conditionEndLine == trueStartLine && trueEndLine == childStartLine
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func argumentStartsOnSameLineAsPreviousArgument(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
|
||||
if ast.IsCallExpression(parent) || ast.IsNewExpression(parent) {
|
||||
if len(parent.Arguments()) == 0 {
|
||||
return false
|
||||
}
|
||||
currentIndex := core.FindIndex(parent.Arguments(), func(n *ast.Node) bool { return n == child })
|
||||
if currentIndex == -1 {
|
||||
// If it's not one of the arguments, don't look past this
|
||||
return false
|
||||
}
|
||||
if currentIndex == 0 {
|
||||
return false // Can't look at previous node if first
|
||||
}
|
||||
|
||||
previousNode := parent.Arguments()[currentIndex-1]
|
||||
lineOfPreviousNode := scanner.GetECMALineOfPosition(sourceFile, previousNode.End())
|
||||
if childStartLine == lineOfPreviousNode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
39
tools/tsgo/internal/format/indent_getindentation_test.go
Normal file
39
tools/tsgo/internal/format/indent_getindentation_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package format_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/format"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
)
|
||||
|
||||
func TestGetIndentationForNamedImportsPosition(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
text := "import {\n type SomeInterface,\n} from \"./exports.js\";"
|
||||
// Position 9: \n
|
||||
// Position 10: first space of " type SomeInterface"
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, text, core.ScriptKindTS)
|
||||
|
||||
options := lsutil.GetDefaultFormatCodeSettings()
|
||||
|
||||
// The line that contains " type SomeInterface" starts at position 9 (the \n).
|
||||
// The getAdjustedStartPosition with LeadingTriviaOptionNone returns line start.
|
||||
// Let's test at position 9 (start of line containing the specifier)
|
||||
lineStart := format.GetLineStartPositionForPosition(14, sourceFile) // 14 is somewhere in " type"
|
||||
|
||||
indent := format.GetIndentation(lineStart, sourceFile, options, true)
|
||||
t.Logf("lineStart=%d, text[lineStart:]=%q", lineStart, text[lineStart:lineStart+10])
|
||||
t.Logf("GetIndentation at lineStart %d = %d", lineStart, indent)
|
||||
|
||||
if indent != 4 {
|
||||
t.Errorf("Expected indentation 4, got %d", indent)
|
||||
}
|
||||
}
|
||||
50
tools/tsgo/internal/format/indent_test.go
Normal file
50
tools/tsgo/internal/format/indent_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package format_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/format"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestGetContainingList_NamedImports(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
text := `import type {
|
||||
AAA,
|
||||
BBB,
|
||||
} from "./bar";`
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, text, core.ScriptKindTS)
|
||||
|
||||
// Find ImportSpecifier nodes (AAA and BBB)
|
||||
var importSpecifiers []*ast.Node
|
||||
forEachDescendantOfKind(sourceFile.AsNode(), ast.KindImportSpecifier, func(node *ast.Node) {
|
||||
importSpecifiers = append(importSpecifiers, node)
|
||||
})
|
||||
|
||||
assert.Assert(t, len(importSpecifiers) == 2, "Expected 2 import specifiers, got %d", len(importSpecifiers))
|
||||
|
||||
// Test GetContainingList for each import specifier
|
||||
for _, specifier := range importSpecifiers {
|
||||
list := format.GetContainingList(specifier, sourceFile)
|
||||
assert.Assert(t, list != nil, "GetContainingList should return non-nil for import specifier")
|
||||
assert.Assert(t, len(list.Nodes) == 2, "Expected list with 2 elements, got %d", len(list.Nodes))
|
||||
}
|
||||
}
|
||||
|
||||
func forEachDescendantOfKind(node *ast.Node, kind ast.Kind, action func(*ast.Node)) {
|
||||
node.ForEachChild(func(child *ast.Node) bool {
|
||||
if child.Kind == kind {
|
||||
action(child)
|
||||
}
|
||||
forEachDescendantOfKind(child, kind, action)
|
||||
return false
|
||||
})
|
||||
}
|
||||
109
tools/tsgo/internal/format/rule.go
Normal file
109
tools/tsgo/internal/format/rule.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package format
|
||||
|
||||
import "github.com/microsoft/typescript-go/internal/ast"
|
||||
|
||||
type ruleImpl struct {
|
||||
debugName string
|
||||
context []contextPredicate
|
||||
action ruleAction
|
||||
flags ruleFlags
|
||||
}
|
||||
|
||||
func (r ruleImpl) Action() ruleAction {
|
||||
return r.action
|
||||
}
|
||||
|
||||
func (r ruleImpl) Context() []contextPredicate {
|
||||
return r.context
|
||||
}
|
||||
|
||||
func (r ruleImpl) Flags() ruleFlags {
|
||||
return r.flags
|
||||
}
|
||||
|
||||
func (r ruleImpl) String() string {
|
||||
return r.debugName
|
||||
}
|
||||
|
||||
type tokenRange struct {
|
||||
tokens []ast.Kind
|
||||
isSpecific bool
|
||||
}
|
||||
|
||||
type ruleSpec struct {
|
||||
leftTokenRange tokenRange
|
||||
rightTokenRange tokenRange
|
||||
rule *ruleImpl
|
||||
}
|
||||
|
||||
/**
|
||||
* A rule takes a two tokens (left/right) and a particular context
|
||||
* for which you're meant to look at them. You then declare what should the
|
||||
* whitespace annotation be between these tokens via the action param.
|
||||
*
|
||||
* @param debugName Name to print
|
||||
* @param left The left side of the comparison
|
||||
* @param right The right side of the comparison
|
||||
* @param context A set of filters to narrow down the space in which this formatter rule applies
|
||||
* @param action a declaration of the expected whitespace
|
||||
* @param flags whether the rule deletes a line or not, defaults to no-op
|
||||
*/
|
||||
func rule(debugName string, left any, right any, context []contextPredicate, action ruleAction, flags ...ruleFlags) ruleSpec {
|
||||
flag := ruleFlagsNone
|
||||
if len(flags) > 0 {
|
||||
flag = flags[0]
|
||||
}
|
||||
leftRange := toTokenRange(left)
|
||||
rightRange := toTokenRange(right)
|
||||
rule := &ruleImpl{
|
||||
debugName: debugName,
|
||||
context: context,
|
||||
action: action,
|
||||
flags: flag,
|
||||
}
|
||||
return ruleSpec{
|
||||
leftTokenRange: leftRange,
|
||||
rightTokenRange: rightRange,
|
||||
rule: rule,
|
||||
}
|
||||
}
|
||||
|
||||
func toTokenRange(e any) tokenRange {
|
||||
switch t := e.(type) {
|
||||
case ast.Kind:
|
||||
return tokenRange{isSpecific: true, tokens: []ast.Kind{t}}
|
||||
case []ast.Kind:
|
||||
return tokenRange{isSpecific: true, tokens: t}
|
||||
case tokenRange:
|
||||
return t
|
||||
}
|
||||
panic("Unknown argument type passed to toTokenRange - only ast.Kind, []ast.Kind, and tokenRange supported")
|
||||
}
|
||||
|
||||
type contextPredicate = func(ctx *FormattingContext) bool
|
||||
|
||||
var anyContext = []contextPredicate{}
|
||||
|
||||
type ruleAction int
|
||||
|
||||
const (
|
||||
ruleActionNone ruleAction = 0
|
||||
ruleActionStopProcessingSpaceActions ruleAction = 1 << 0
|
||||
ruleActionStopProcessingTokenActions ruleAction = 1 << 1
|
||||
ruleActionInsertSpace ruleAction = 1 << 2
|
||||
ruleActionInsertNewLine ruleAction = 1 << 3
|
||||
ruleActionDeleteSpace ruleAction = 1 << 4
|
||||
ruleActionDeleteToken ruleAction = 1 << 5
|
||||
ruleActionInsertTrailingSemicolon ruleAction = 1 << 6
|
||||
|
||||
ruleActionStopAction ruleAction = ruleActionStopProcessingSpaceActions | ruleActionStopProcessingTokenActions
|
||||
ruleActionModifySpaceAction ruleAction = ruleActionInsertSpace | ruleActionInsertNewLine | ruleActionDeleteSpace
|
||||
ruleActionModifyTokenAction ruleAction = ruleActionDeleteToken | ruleActionInsertTrailingSemicolon
|
||||
)
|
||||
|
||||
type ruleFlags int
|
||||
|
||||
const (
|
||||
ruleFlagsNone ruleFlags = iota
|
||||
ruleFlagsCanDeleteNewLines
|
||||
)
|
||||
629
tools/tsgo/internal/format/rulecontext.go
Normal file
629
tools/tsgo/internal/format/rulecontext.go
Normal file
@@ -0,0 +1,629 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
///
|
||||
/// Contexts
|
||||
///
|
||||
|
||||
type (
|
||||
optionSelector = func(options lsutil.FormatCodeSettings) core.Tristate
|
||||
anyOptionSelector[T comparable] = func(options lsutil.FormatCodeSettings) T
|
||||
)
|
||||
|
||||
func semicolonOption(options lsutil.FormatCodeSettings) lsutil.SemicolonPreference {
|
||||
return options.Semicolons
|
||||
}
|
||||
|
||||
func insertSpaceAfterCommaDelimiterOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterCommaDelimiter
|
||||
}
|
||||
|
||||
func insertSpaceAfterSemicolonInForStatementsOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterSemicolonInForStatements
|
||||
}
|
||||
|
||||
func insertSpaceBeforeAndAfterBinaryOperatorsOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceBeforeAndAfterBinaryOperators
|
||||
}
|
||||
|
||||
func insertSpaceAfterConstructorOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterConstructor
|
||||
}
|
||||
|
||||
func insertSpaceAfterKeywordsInControlFlowStatementsOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterKeywordsInControlFlowStatements
|
||||
}
|
||||
|
||||
func insertSpaceAfterFunctionKeywordForAnonymousFunctionsOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterFunctionKeywordForAnonymousFunctions
|
||||
}
|
||||
|
||||
func insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesisOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis
|
||||
}
|
||||
|
||||
func insertSpaceAfterOpeningAndBeforeClosingNonemptyBracketsOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets
|
||||
}
|
||||
|
||||
func insertSpaceAfterOpeningAndBeforeClosingNonemptyBracesOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces
|
||||
}
|
||||
|
||||
func insertSpaceAfterOpeningAndBeforeClosingEmptyBracesOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces
|
||||
}
|
||||
|
||||
func insertSpaceAfterOpeningAndBeforeClosingTemplateStringBracesOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces
|
||||
}
|
||||
|
||||
func insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBracesOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces
|
||||
}
|
||||
|
||||
func insertSpaceAfterTypeAssertionOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceAfterTypeAssertion
|
||||
}
|
||||
|
||||
func insertSpaceBeforeFunctionParenthesisOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceBeforeFunctionParenthesis
|
||||
}
|
||||
|
||||
func placeOpenBraceOnNewLineForFunctionsOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.PlaceOpenBraceOnNewLineForFunctions
|
||||
}
|
||||
|
||||
func placeOpenBraceOnNewLineForControlBlocksOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.PlaceOpenBraceOnNewLineForControlBlocks
|
||||
}
|
||||
|
||||
func insertSpaceBeforeTypeAnnotationOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.InsertSpaceBeforeTypeAnnotation
|
||||
}
|
||||
|
||||
func indentMultiLineObjectLiteralBeginningOnBlankLineOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.IndentMultiLineObjectLiteralBeginningOnBlankLine
|
||||
}
|
||||
|
||||
func indentSwitchCaseOption(options lsutil.FormatCodeSettings) core.Tristate {
|
||||
return options.IndentSwitchCase
|
||||
}
|
||||
|
||||
func optionEquals[T comparable](optionName anyOptionSelector[T], optionValue T) contextPredicate {
|
||||
return func(context *FormattingContext) bool {
|
||||
return optionName(context.Options) == optionValue
|
||||
}
|
||||
}
|
||||
|
||||
func isOptionEnabled(optionName optionSelector) contextPredicate {
|
||||
return func(context *FormattingContext) bool {
|
||||
return optionName(context.Options).IsTrue()
|
||||
}
|
||||
}
|
||||
|
||||
func isOptionDisabled(optionName optionSelector) contextPredicate {
|
||||
return func(context *FormattingContext) bool {
|
||||
return optionName(context.Options).IsFalse()
|
||||
}
|
||||
}
|
||||
|
||||
func isOptionDisabledOrUndefined(optionName optionSelector) contextPredicate {
|
||||
return func(context *FormattingContext) bool {
|
||||
return optionName(context.Options).IsFalseOrUnknown()
|
||||
}
|
||||
}
|
||||
|
||||
func isOptionDisabledOrUndefinedOrTokensOnSameLine(optionName optionSelector) contextPredicate {
|
||||
return func(context *FormattingContext) bool {
|
||||
return optionName(context.Options).IsFalseOrUnknown() || context.TokensAreOnSameLine()
|
||||
}
|
||||
}
|
||||
|
||||
func isOptionEnabledOrUndefined(optionName optionSelector) contextPredicate {
|
||||
return func(context *FormattingContext) bool {
|
||||
return optionName(context.Options).IsTrueOrUnknown()
|
||||
}
|
||||
}
|
||||
|
||||
func isForContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindForStatement
|
||||
}
|
||||
|
||||
func isNotForContext(context *FormattingContext) bool {
|
||||
return !isForContext(context)
|
||||
}
|
||||
|
||||
func isBinaryOpContext(context *FormattingContext) bool {
|
||||
switch context.contextNode.Kind {
|
||||
case ast.KindBinaryExpression:
|
||||
return context.contextNode.AsBinaryExpression().OperatorToken.Kind != ast.KindCommaToken
|
||||
case ast.KindConditionalExpression,
|
||||
ast.KindConditionalType,
|
||||
ast.KindAsExpression,
|
||||
ast.KindExportSpecifier,
|
||||
ast.KindImportSpecifier,
|
||||
ast.KindTypePredicate,
|
||||
ast.KindUnionType,
|
||||
ast.KindIntersectionType,
|
||||
ast.KindSatisfiesExpression:
|
||||
return true
|
||||
|
||||
// equals in binding elements func foo([[x, y] = [1, 2]])
|
||||
case ast.KindBindingElement:
|
||||
// equals in type X = ...
|
||||
fallthrough
|
||||
case ast.KindTypeAliasDeclaration:
|
||||
// equal in import a = module('a');
|
||||
fallthrough
|
||||
case ast.KindImportEqualsDeclaration:
|
||||
// equal in export = 1
|
||||
fallthrough
|
||||
case ast.KindExportAssignment:
|
||||
// equal in let a = 0
|
||||
fallthrough
|
||||
case ast.KindVariableDeclaration:
|
||||
// equal in p = 0
|
||||
fallthrough
|
||||
case ast.KindParameter,
|
||||
ast.KindEnumMember,
|
||||
ast.KindPropertyDeclaration,
|
||||
ast.KindPropertySignature:
|
||||
return context.currentTokenSpan.Kind == ast.KindEqualsToken || context.nextTokenSpan.Kind == ast.KindEqualsToken
|
||||
// "in" keyword in for (let x in []) { }
|
||||
case ast.KindForInStatement:
|
||||
// "in" keyword in [P in keyof T] T[P]
|
||||
fallthrough
|
||||
case ast.KindTypeParameter:
|
||||
return context.currentTokenSpan.Kind == ast.KindInKeyword || context.nextTokenSpan.Kind == ast.KindInKeyword || context.currentTokenSpan.Kind == ast.KindEqualsToken || context.nextTokenSpan.Kind == ast.KindEqualsToken
|
||||
// Technically, "of" is not a binary operator, but format it the same way as "in"
|
||||
case ast.KindForOfStatement:
|
||||
return context.currentTokenSpan.Kind == ast.KindOfKeyword || context.nextTokenSpan.Kind == ast.KindOfKeyword
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isNotBinaryOpContext(context *FormattingContext) bool {
|
||||
return !isBinaryOpContext(context)
|
||||
}
|
||||
|
||||
func isNotTypeAnnotationContext(context *FormattingContext) bool {
|
||||
return !isTypeAnnotationContext(context)
|
||||
}
|
||||
|
||||
func isTypeAnnotationContext(context *FormattingContext) bool {
|
||||
contextKind := context.contextNode.Kind
|
||||
return contextKind == ast.KindPropertyDeclaration ||
|
||||
contextKind == ast.KindPropertySignature ||
|
||||
contextKind == ast.KindParameter ||
|
||||
contextKind == ast.KindVariableDeclaration ||
|
||||
ast.IsFunctionLikeKind(contextKind)
|
||||
}
|
||||
|
||||
func isOptionalPropertyContext(context *FormattingContext) bool {
|
||||
return ast.IsPropertyDeclaration(context.contextNode) && ast.HasQuestionToken(context.contextNode)
|
||||
}
|
||||
|
||||
func isNonOptionalPropertyContext(context *FormattingContext) bool {
|
||||
return !isOptionalPropertyContext(context)
|
||||
}
|
||||
|
||||
func isConditionalOperatorContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindConditionalExpression ||
|
||||
context.contextNode.Kind == ast.KindConditionalType
|
||||
}
|
||||
|
||||
func isSameLineTokenOrBeforeBlockContext(context *FormattingContext) bool {
|
||||
return context.TokensAreOnSameLine() || isBeforeBlockContext(context)
|
||||
}
|
||||
|
||||
func isBraceWrappedContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindObjectBindingPattern ||
|
||||
context.contextNode.Kind == ast.KindMappedType ||
|
||||
isSingleLineBlockContext(context)
|
||||
}
|
||||
|
||||
// This check is done before an open brace in a control construct, a function, or a typescript block declaration
|
||||
func isBeforeMultilineBlockContext(context *FormattingContext) bool {
|
||||
return isBeforeBlockContext(context) && !(context.NextNodeAllOnSameLine() || context.NextNodeBlockIsOnOneLine())
|
||||
}
|
||||
|
||||
func isMultilineBlockContext(context *FormattingContext) bool {
|
||||
return isBlockContext(context) && !(context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine())
|
||||
}
|
||||
|
||||
func isSingleLineBlockContext(context *FormattingContext) bool {
|
||||
return isBlockContext(context) && (context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine())
|
||||
}
|
||||
|
||||
func isBlockContext(context *FormattingContext) bool {
|
||||
return nodeIsBlockContext(context.contextNode)
|
||||
}
|
||||
|
||||
func isBeforeBlockContext(context *FormattingContext) bool {
|
||||
return nodeIsBlockContext(context.nextTokenParent)
|
||||
}
|
||||
|
||||
// IMPORTANT!!! This method must return true ONLY for nodes with open and close braces as immediate children
|
||||
func nodeIsBlockContext(node *ast.Node) bool {
|
||||
if nodeIsTypeScriptDeclWithBlockContext(node) {
|
||||
// This means we are in a context that looks like a block to the user, but in the grammar is actually not a node (it's a class, module, enum, object type literal, etc).
|
||||
return true
|
||||
}
|
||||
|
||||
switch node.Kind {
|
||||
case ast.KindBlock,
|
||||
ast.KindCaseBlock,
|
||||
ast.KindObjectLiteralExpression,
|
||||
ast.KindModuleBlock:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isFunctionDeclContext(context *FormattingContext) bool {
|
||||
switch context.contextNode.Kind {
|
||||
case ast.KindFunctionDeclaration,
|
||||
ast.KindMethodDeclaration,
|
||||
ast.KindMethodSignature:
|
||||
// case ast.KindMemberFunctionDeclaration:
|
||||
fallthrough
|
||||
case ast.KindGetAccessor,
|
||||
ast.KindSetAccessor:
|
||||
// case ast.KindMethodSignature:
|
||||
fallthrough
|
||||
case ast.KindCallSignature,
|
||||
ast.KindFunctionExpression,
|
||||
ast.KindConstructor,
|
||||
ast.KindArrowFunction:
|
||||
// case ast.KindConstructorDeclaration:
|
||||
// case ast.KindSimpleArrowFunctionExpression:
|
||||
// case ast.KindParenthesizedArrowFunctionExpression:
|
||||
fallthrough
|
||||
case ast.KindInterfaceDeclaration: // This one is not truly a function, but for formatting purposes, it acts just like one
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isNotFunctionDeclContext(context *FormattingContext) bool {
|
||||
return !isFunctionDeclContext(context)
|
||||
}
|
||||
|
||||
func isFunctionDeclarationOrFunctionExpressionContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindFunctionDeclaration || context.contextNode.Kind == ast.KindFunctionExpression
|
||||
}
|
||||
|
||||
func isTypeScriptDeclWithBlockContext(context *FormattingContext) bool {
|
||||
return nodeIsTypeScriptDeclWithBlockContext(context.contextNode)
|
||||
}
|
||||
|
||||
func nodeIsTypeScriptDeclWithBlockContext(node *ast.Node) bool {
|
||||
switch node.Kind {
|
||||
case ast.KindClassDeclaration,
|
||||
ast.KindClassExpression,
|
||||
ast.KindInterfaceDeclaration,
|
||||
ast.KindEnumDeclaration,
|
||||
ast.KindTypeLiteral,
|
||||
ast.KindModuleDeclaration,
|
||||
ast.KindExportDeclaration,
|
||||
ast.KindNamedExports,
|
||||
ast.KindImportDeclaration,
|
||||
ast.KindNamedImports:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isAfterCodeBlockContext(context *FormattingContext) bool {
|
||||
switch context.currentTokenParent.Kind {
|
||||
case ast.KindClassDeclaration,
|
||||
ast.KindModuleDeclaration,
|
||||
ast.KindEnumDeclaration,
|
||||
ast.KindCatchClause,
|
||||
ast.KindModuleBlock,
|
||||
ast.KindSwitchStatement:
|
||||
return true
|
||||
case ast.KindBlock:
|
||||
blockParent := context.currentTokenParent.Parent
|
||||
// In a codefix scenario, we can't rely on parents being set. So just always return true.
|
||||
if blockParent == nil || blockParent.Kind != ast.KindArrowFunction && blockParent.Kind != ast.KindFunctionExpression {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isControlDeclContext(context *FormattingContext) bool {
|
||||
switch context.contextNode.Kind {
|
||||
case ast.KindIfStatement,
|
||||
ast.KindSwitchStatement,
|
||||
ast.KindForStatement,
|
||||
ast.KindForInStatement,
|
||||
ast.KindForOfStatement,
|
||||
ast.KindWhileStatement,
|
||||
ast.KindTryStatement,
|
||||
ast.KindDoStatement,
|
||||
ast.KindWithStatement:
|
||||
// TODO
|
||||
// case ast.KindElseClause:
|
||||
fallthrough
|
||||
case ast.KindCatchClause:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isObjectContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindObjectLiteralExpression
|
||||
}
|
||||
|
||||
func isFunctionCallContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindCallExpression
|
||||
}
|
||||
|
||||
func isNewContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindNewExpression
|
||||
}
|
||||
|
||||
func isFunctionCallOrNewContext(context *FormattingContext) bool {
|
||||
return isFunctionCallContext(context) || isNewContext(context)
|
||||
}
|
||||
|
||||
func isPreviousTokenNotComma(context *FormattingContext) bool {
|
||||
return context.currentTokenSpan.Kind != ast.KindCommaToken
|
||||
}
|
||||
|
||||
func isNextTokenNotCloseBracket(context *FormattingContext) bool {
|
||||
return context.nextTokenSpan.Kind != ast.KindCloseBracketToken
|
||||
}
|
||||
|
||||
func isNextTokenNotCloseParen(context *FormattingContext) bool {
|
||||
return context.nextTokenSpan.Kind != ast.KindCloseParenToken
|
||||
}
|
||||
|
||||
func isArrowFunctionContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindArrowFunction
|
||||
}
|
||||
|
||||
func isImportTypeContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindImportType
|
||||
}
|
||||
|
||||
func isNonJsxSameLineTokenContext(context *FormattingContext) bool {
|
||||
return context.TokensAreOnSameLine() && context.contextNode.Kind != ast.KindJsxText
|
||||
}
|
||||
|
||||
func isNonJsxTextContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind != ast.KindJsxText
|
||||
}
|
||||
|
||||
func isNonJsxElementOrFragmentContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind != ast.KindJsxElement && context.contextNode.Kind != ast.KindJsxFragment
|
||||
}
|
||||
|
||||
func isJsxExpressionContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindJsxExpression || context.contextNode.Kind == ast.KindJsxSpreadAttribute
|
||||
}
|
||||
|
||||
func isNextTokenParentJsxAttribute(context *FormattingContext) bool {
|
||||
return context.nextTokenParent.Kind == ast.KindJsxAttribute || (context.nextTokenParent.Kind == ast.KindJsxNamespacedName && context.nextTokenParent.Parent.Kind == ast.KindJsxAttribute)
|
||||
}
|
||||
|
||||
func isJsxAttributeContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindJsxAttribute
|
||||
}
|
||||
|
||||
func isNextTokenParentNotJsxNamespacedName(context *FormattingContext) bool {
|
||||
return context.nextTokenParent.Kind != ast.KindJsxNamespacedName
|
||||
}
|
||||
|
||||
func isNextTokenParentJsxNamespacedName(context *FormattingContext) bool {
|
||||
return context.nextTokenParent.Kind == ast.KindJsxNamespacedName
|
||||
}
|
||||
|
||||
func isJsxSelfClosingElementContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindJsxSelfClosingElement
|
||||
}
|
||||
|
||||
func isNotBeforeBlockInFunctionDeclarationContext(context *FormattingContext) bool {
|
||||
return !isFunctionDeclContext(context) && !isBeforeBlockContext(context)
|
||||
}
|
||||
|
||||
func isEndOfDecoratorContextOnSameLine(context *FormattingContext) bool {
|
||||
return context.TokensAreOnSameLine() &&
|
||||
ast.HasDecorators(context.contextNode) &&
|
||||
nodeIsInDecoratorContext(context.currentTokenParent) &&
|
||||
!nodeIsInDecoratorContext(context.nextTokenParent)
|
||||
}
|
||||
|
||||
func nodeIsInDecoratorContext(node *ast.Node) bool {
|
||||
for node != nil && ast.IsExpression(node) {
|
||||
node = node.Parent
|
||||
}
|
||||
return node != nil && node.Kind == ast.KindDecorator
|
||||
}
|
||||
|
||||
func isStartOfVariableDeclarationList(context *FormattingContext) bool {
|
||||
return context.currentTokenParent.Kind == ast.KindVariableDeclarationList &&
|
||||
scanner.GetTokenPosOfNode(context.currentTokenParent, context.SourceFile, false) == context.currentTokenSpan.Loc.Pos()
|
||||
}
|
||||
|
||||
func isNotFormatOnEnter(context *FormattingContext) bool {
|
||||
return context.FormattingRequestKind != FormatRequestKindFormatOnEnter
|
||||
}
|
||||
|
||||
func isModuleDeclContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindModuleDeclaration
|
||||
}
|
||||
|
||||
func isObjectTypeContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindTypeLiteral // && context.contextNode.parent.Kind != ast.KindInterfaceDeclaration;
|
||||
}
|
||||
|
||||
func isConstructorSignatureContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindConstructSignature
|
||||
}
|
||||
|
||||
func isTypeArgumentOrParameterOrAssertion(token TextRangeWithKind, parent *ast.Node) bool {
|
||||
if token.Kind != ast.KindLessThanToken && token.Kind != ast.KindGreaterThanToken {
|
||||
return false
|
||||
}
|
||||
switch parent.Kind {
|
||||
case ast.KindTypeReference,
|
||||
ast.KindTypeAssertionExpression,
|
||||
ast.KindTypeAliasDeclaration,
|
||||
ast.KindClassDeclaration,
|
||||
ast.KindClassExpression,
|
||||
ast.KindInterfaceDeclaration,
|
||||
ast.KindFunctionDeclaration,
|
||||
ast.KindFunctionExpression,
|
||||
ast.KindArrowFunction,
|
||||
ast.KindMethodDeclaration,
|
||||
ast.KindMethodSignature,
|
||||
ast.KindCallSignature,
|
||||
ast.KindConstructSignature,
|
||||
ast.KindCallExpression,
|
||||
ast.KindNewExpression,
|
||||
ast.KindExpressionWithTypeArguments:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isTypeArgumentOrParameterOrAssertionContext(context *FormattingContext) bool {
|
||||
return isTypeArgumentOrParameterOrAssertion(context.currentTokenSpan, context.currentTokenParent) ||
|
||||
isTypeArgumentOrParameterOrAssertion(context.nextTokenSpan, context.nextTokenParent)
|
||||
}
|
||||
|
||||
func isTypeAssertionContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindTypeAssertionExpression
|
||||
}
|
||||
|
||||
func isNonTypeAssertionContext(context *FormattingContext) bool {
|
||||
return !isTypeAssertionContext(context)
|
||||
}
|
||||
|
||||
func isVoidOpContext(context *FormattingContext) bool {
|
||||
return context.currentTokenSpan.Kind == ast.KindVoidKeyword && context.currentTokenParent.Kind == ast.KindVoidExpression
|
||||
}
|
||||
|
||||
func isYieldOrYieldStarWithOperand(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindYieldExpression && context.contextNode.Expression() != nil
|
||||
}
|
||||
|
||||
func isNonNullAssertionContext(context *FormattingContext) bool {
|
||||
return context.contextNode.Kind == ast.KindNonNullExpression
|
||||
}
|
||||
|
||||
func isNotStatementConditionContext(context *FormattingContext) bool {
|
||||
return !isStatementConditionContext(context)
|
||||
}
|
||||
|
||||
func isStatementConditionContext(context *FormattingContext) bool {
|
||||
switch context.contextNode.Kind {
|
||||
case ast.KindIfStatement,
|
||||
ast.KindForStatement,
|
||||
ast.KindForInStatement,
|
||||
ast.KindForOfStatement,
|
||||
ast.KindDoStatement,
|
||||
ast.KindWhileStatement:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isSemicolonDeletionContext(context *FormattingContext) bool {
|
||||
nextTokenKind := context.nextTokenSpan.Kind
|
||||
nextTokenStart := context.nextTokenSpan.Loc.Pos()
|
||||
if ast.IsTrivia(nextTokenKind) {
|
||||
var nextRealToken *ast.Node
|
||||
if context.nextTokenParent == context.currentTokenParent {
|
||||
// !!! TODO: very different from strada, but strada's logic here is wonky - find the first ancestor without a parent? that's just the source file.
|
||||
nextRealToken = astnav.FindNextToken(context.nextTokenParent, context.SourceFile.AsNode(), context.SourceFile)
|
||||
} else {
|
||||
nextRealToken = lsutil.GetFirstToken(context.nextTokenParent, context.SourceFile)
|
||||
}
|
||||
|
||||
if nextRealToken == nil {
|
||||
return true
|
||||
}
|
||||
nextTokenKind = nextRealToken.Kind
|
||||
nextTokenStart = scanner.GetTokenPosOfNode(nextRealToken, context.SourceFile, false)
|
||||
}
|
||||
|
||||
startLine := scanner.GetECMALineOfPosition(context.SourceFile, context.currentTokenSpan.Loc.Pos())
|
||||
endLine := scanner.GetECMALineOfPosition(context.SourceFile, nextTokenStart)
|
||||
if startLine == endLine {
|
||||
return nextTokenKind == ast.KindCloseBraceToken || nextTokenKind == ast.KindEndOfFile
|
||||
}
|
||||
|
||||
if nextTokenKind == ast.KindSemicolonToken &&
|
||||
context.currentTokenSpan.Kind == ast.KindSemicolonToken {
|
||||
return true
|
||||
}
|
||||
|
||||
if nextTokenKind == ast.KindSemicolonClassElement ||
|
||||
nextTokenKind == ast.KindSemicolonToken {
|
||||
return false
|
||||
}
|
||||
|
||||
if context.contextNode.Kind == ast.KindInterfaceDeclaration ||
|
||||
context.contextNode.Kind == ast.KindTypeAliasDeclaration {
|
||||
// Can't remove semicolon after `foo`; it would parse as a method declaration:
|
||||
//
|
||||
// interface I {
|
||||
// foo;
|
||||
// () void
|
||||
// }
|
||||
return context.currentTokenParent.Kind != ast.KindPropertySignature ||
|
||||
context.currentTokenParent.Type() != nil ||
|
||||
nextTokenKind != ast.KindOpenParenToken
|
||||
}
|
||||
|
||||
if ast.IsPropertyDeclaration(context.currentTokenParent) {
|
||||
return context.currentTokenParent.Initializer() == nil
|
||||
}
|
||||
|
||||
return context.currentTokenParent.Kind != ast.KindForStatement &&
|
||||
context.currentTokenParent.Kind != ast.KindEmptyStatement &&
|
||||
context.currentTokenParent.Kind != ast.KindSemicolonClassElement &&
|
||||
nextTokenKind != ast.KindOpenBracketToken &&
|
||||
nextTokenKind != ast.KindOpenParenToken &&
|
||||
nextTokenKind != ast.KindPlusToken &&
|
||||
nextTokenKind != ast.KindMinusToken &&
|
||||
nextTokenKind != ast.KindSlashToken &&
|
||||
nextTokenKind != ast.KindRegularExpressionLiteral &&
|
||||
nextTokenKind != ast.KindCommaToken &&
|
||||
nextTokenKind != ast.KindTemplateExpression &&
|
||||
nextTokenKind != ast.KindTemplateHead &&
|
||||
nextTokenKind != ast.KindNoSubstitutionTemplateLiteral &&
|
||||
nextTokenKind != ast.KindDotToken
|
||||
}
|
||||
|
||||
func isSemicolonInsertionContext(context *FormattingContext) bool {
|
||||
return lsutil.PositionIsASICandidate(context.currentTokenSpan.Loc.End(), context.currentTokenParent, context.SourceFile)
|
||||
}
|
||||
|
||||
func isNotPropertyAccessOnIntegerLiteral(context *FormattingContext) bool {
|
||||
return !ast.IsPropertyAccessExpression(context.contextNode) ||
|
||||
!ast.IsNumericLiteral(context.contextNode.Expression()) ||
|
||||
strings.Contains(context.contextNode.Expression().Text(), ".")
|
||||
}
|
||||
450
tools/tsgo/internal/format/rules.go
Normal file
450
tools/tsgo/internal/format/rules.go
Normal file
@@ -0,0 +1,450 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
)
|
||||
|
||||
func getAllRules() []ruleSpec {
|
||||
allTokens := make([]ast.Kind, 0, ast.KindLastToken-ast.KindFirstToken+1)
|
||||
for token := ast.KindFirstToken; token <= ast.KindLastToken; token++ {
|
||||
if token != ast.KindEndOfFile {
|
||||
allTokens = append(allTokens, token)
|
||||
}
|
||||
}
|
||||
|
||||
anyTokenExcept := func(tokens ...ast.Kind) tokenRange {
|
||||
newTokens := make([]ast.Kind, 0, len(allTokens))
|
||||
for _, token := range allTokens {
|
||||
if slices.Contains(tokens, token) {
|
||||
continue
|
||||
}
|
||||
newTokens = append(newTokens, token)
|
||||
}
|
||||
return tokenRange{
|
||||
isSpecific: false,
|
||||
tokens: newTokens,
|
||||
}
|
||||
}
|
||||
|
||||
anyToken := tokenRange{
|
||||
isSpecific: false,
|
||||
tokens: allTokens,
|
||||
}
|
||||
|
||||
anyTokenIncludingMultilineComments := tokenRangeFromEx(allTokens, ast.KindMultiLineCommentTrivia)
|
||||
anyTokenIncludingEOF := tokenRangeFromEx(allTokens, ast.KindEndOfFile)
|
||||
keywords := tokenRangeFromRange(ast.KindFirstKeyword, ast.KindLastKeyword)
|
||||
binaryOperators := tokenRangeFromRange(ast.KindFirstBinaryOperator, ast.KindLastBinaryOperator)
|
||||
binaryKeywordOperators := []ast.Kind{
|
||||
ast.KindInKeyword,
|
||||
ast.KindInstanceOfKeyword,
|
||||
ast.KindOfKeyword,
|
||||
ast.KindAsKeyword,
|
||||
ast.KindIsKeyword,
|
||||
ast.KindSatisfiesKeyword,
|
||||
}
|
||||
unaryPrefixOperators := []ast.Kind{ast.KindPlusPlusToken, ast.KindMinusToken, ast.KindTildeToken, ast.KindExclamationToken}
|
||||
unaryPrefixExpressions := []ast.Kind{
|
||||
ast.KindNumericLiteral,
|
||||
ast.KindBigIntLiteral,
|
||||
ast.KindIdentifier,
|
||||
ast.KindOpenParenToken,
|
||||
ast.KindOpenBracketToken,
|
||||
ast.KindOpenBraceToken,
|
||||
ast.KindThisKeyword,
|
||||
ast.KindNewKeyword,
|
||||
}
|
||||
unaryPreincrementExpressions := []ast.Kind{ast.KindIdentifier, ast.KindOpenParenToken, ast.KindThisKeyword, ast.KindNewKeyword}
|
||||
unaryPostincrementExpressions := []ast.Kind{ast.KindIdentifier, ast.KindCloseParenToken, ast.KindCloseBracketToken, ast.KindNewKeyword}
|
||||
unaryPredecrementExpressions := []ast.Kind{ast.KindIdentifier, ast.KindOpenParenToken, ast.KindThisKeyword, ast.KindNewKeyword}
|
||||
unaryPostdecrementExpressions := []ast.Kind{ast.KindIdentifier, ast.KindCloseParenToken, ast.KindCloseBracketToken, ast.KindNewKeyword}
|
||||
comments := []ast.Kind{ast.KindSingleLineCommentTrivia, ast.KindMultiLineCommentTrivia}
|
||||
typeKeywords := []ast.Kind{
|
||||
ast.KindAnyKeyword,
|
||||
ast.KindAssertsKeyword,
|
||||
ast.KindBigIntKeyword,
|
||||
ast.KindBooleanKeyword,
|
||||
ast.KindFalseKeyword,
|
||||
ast.KindInferKeyword,
|
||||
ast.KindKeyOfKeyword,
|
||||
ast.KindNeverKeyword,
|
||||
ast.KindNullKeyword,
|
||||
ast.KindNumberKeyword,
|
||||
ast.KindObjectKeyword,
|
||||
ast.KindReadonlyKeyword,
|
||||
ast.KindStringKeyword,
|
||||
ast.KindSymbolKeyword,
|
||||
ast.KindTypeOfKeyword,
|
||||
ast.KindTrueKeyword,
|
||||
ast.KindVoidKeyword,
|
||||
ast.KindUndefinedKeyword,
|
||||
ast.KindUniqueKeyword,
|
||||
ast.KindUnknownKeyword,
|
||||
}
|
||||
typeNames := append([]ast.Kind{ast.KindIdentifier}, typeKeywords...)
|
||||
|
||||
// Place a space before open brace in a function declaration
|
||||
// TypeScript: Function can have return types, which can be made of tons of different token kinds
|
||||
functionOpenBraceLeftTokenRange := anyTokenIncludingMultilineComments
|
||||
|
||||
// Place a space before open brace in a TypeScript declaration that has braces as children (class, module, enum, etc)
|
||||
typeScriptOpenBraceLeftTokenRange := tokenRangeFrom(ast.KindIdentifier, ast.KindGreaterThanToken, ast.KindMultiLineCommentTrivia, ast.KindClassKeyword, ast.KindExportKeyword, ast.KindImportKeyword)
|
||||
|
||||
// Place a space before open brace in a control flow construct
|
||||
controlOpenBraceLeftTokenRange := tokenRangeFrom(ast.KindCloseParenToken, ast.KindMultiLineCommentTrivia, ast.KindDoKeyword, ast.KindTryKeyword, ast.KindFinallyKeyword, ast.KindElseKeyword, ast.KindCatchKeyword)
|
||||
|
||||
// These rules are higher in priority than user-configurable
|
||||
highPriorityCommonRules := []ruleSpec{
|
||||
// Leave comments alone
|
||||
rule("IgnoreBeforeComment", anyToken, comments, anyContext, ruleActionStopProcessingSpaceActions),
|
||||
rule("IgnoreAfterLineComment", ast.KindSingleLineCommentTrivia, anyToken, anyContext, ruleActionStopProcessingSpaceActions),
|
||||
|
||||
rule("NotSpaceBeforeColon", anyToken, ast.KindColonToken, []contextPredicate{isNonJsxSameLineTokenContext, isNotBinaryOpContext, isNotTypeAnnotationContext}, ruleActionDeleteSpace),
|
||||
rule("SpaceAfterColon", ast.KindColonToken, anyToken, []contextPredicate{isNonJsxSameLineTokenContext, isNotBinaryOpContext, isNextTokenParentNotJsxNamespacedName}, ruleActionInsertSpace),
|
||||
rule("NoSpaceBeforeQuestionMark", anyToken, ast.KindQuestionToken, []contextPredicate{isNonJsxSameLineTokenContext, isNotBinaryOpContext, isNotTypeAnnotationContext}, ruleActionDeleteSpace),
|
||||
// insert space after '?' only when it is used in conditional operator
|
||||
rule("SpaceAfterQuestionMarkInConditionalOperator", ast.KindQuestionToken, anyToken, []contextPredicate{isNonJsxSameLineTokenContext, isConditionalOperatorContext}, ruleActionInsertSpace),
|
||||
|
||||
// in other cases there should be no space between '?' and next token
|
||||
rule("NoSpaceAfterQuestionMark", ast.KindQuestionToken, anyToken, []contextPredicate{isNonJsxSameLineTokenContext, isNonOptionalPropertyContext}, ruleActionDeleteSpace),
|
||||
|
||||
rule("NoSpaceBeforeDot", anyToken, []ast.Kind{ast.KindDotToken, ast.KindQuestionDotToken}, []contextPredicate{isNonJsxSameLineTokenContext, isNotPropertyAccessOnIntegerLiteral}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterDot", []ast.Kind{ast.KindDotToken, ast.KindQuestionDotToken}, anyToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
|
||||
rule("NoSpaceBetweenImportParenInImportType", ast.KindImportKeyword, ast.KindOpenParenToken, []contextPredicate{isNonJsxSameLineTokenContext, isImportTypeContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Special handling of unary operators.
|
||||
// Prefix operators generally shouldn't have a space between
|
||||
// them and their target unary expression.
|
||||
rule("NoSpaceAfterUnaryPrefixOperator", unaryPrefixOperators, unaryPrefixExpressions, []contextPredicate{isNonJsxSameLineTokenContext, isNotBinaryOpContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterUnaryPreincrementOperator", ast.KindPlusPlusToken, unaryPreincrementExpressions, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterUnaryPredecrementOperator", ast.KindMinusMinusToken, unaryPredecrementExpressions, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceBeforeUnaryPostincrementOperator", unaryPostincrementExpressions, ast.KindPlusPlusToken, []contextPredicate{isNonJsxSameLineTokenContext, isNotStatementConditionContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceBeforeUnaryPostdecrementOperator", unaryPostdecrementExpressions, ast.KindMinusMinusToken, []contextPredicate{isNonJsxSameLineTokenContext, isNotStatementConditionContext}, ruleActionDeleteSpace),
|
||||
|
||||
// More unary operator special-casing.
|
||||
// DevDiv 181814: Be careful when removing leading whitespace
|
||||
// around unary operators. Examples:
|
||||
// 1 - -2 --X--> 1--2
|
||||
// a + ++b --X--> a+++b
|
||||
rule("SpaceAfterPostincrementWhenFollowedByAdd", ast.KindPlusPlusToken, ast.KindPlusToken, []contextPredicate{isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionInsertSpace),
|
||||
rule("SpaceAfterAddWhenFollowedByUnaryPlus", ast.KindPlusToken, ast.KindPlusToken, []contextPredicate{isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionInsertSpace),
|
||||
rule("SpaceAfterAddWhenFollowedByPreincrement", ast.KindPlusToken, ast.KindPlusPlusToken, []contextPredicate{isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionInsertSpace),
|
||||
rule("SpaceAfterPostdecrementWhenFollowedBySubtract", ast.KindMinusMinusToken, ast.KindMinusToken, []contextPredicate{isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionInsertSpace),
|
||||
rule("SpaceAfterSubtractWhenFollowedByUnaryMinus", ast.KindMinusToken, ast.KindMinusToken, []contextPredicate{isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionInsertSpace),
|
||||
rule("SpaceAfterSubtractWhenFollowedByPredecrement", ast.KindMinusToken, ast.KindMinusMinusToken, []contextPredicate{isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionInsertSpace),
|
||||
|
||||
rule("NoSpaceAfterCloseBrace", ast.KindCloseBraceToken, []ast.Kind{ast.KindCommaToken, ast.KindSemicolonToken}, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
// For functions and control block place } on a new line []ast.Kind{multi-line rule}
|
||||
rule("NewLineBeforeCloseBraceInBlockContext", anyTokenIncludingMultilineComments, ast.KindCloseBraceToken, []contextPredicate{isMultilineBlockContext}, ruleActionInsertNewLine),
|
||||
|
||||
// Space/new line after }.
|
||||
rule("SpaceAfterCloseBrace", ast.KindCloseBraceToken, anyTokenExcept(ast.KindCloseParenToken), []contextPredicate{isNonJsxSameLineTokenContext, isAfterCodeBlockContext}, ruleActionInsertSpace),
|
||||
// Special case for (}, else) and (}, while) since else & while tokens are not part of the tree which makes SpaceAfterCloseBrace rule not applied
|
||||
// Also should not apply to })
|
||||
rule("SpaceBetweenCloseBraceAndElse", ast.KindCloseBraceToken, ast.KindElseKeyword, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("SpaceBetweenCloseBraceAndWhile", ast.KindCloseBraceToken, ast.KindWhileKeyword, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceBetweenEmptyBraceBrackets", ast.KindOpenBraceToken, ast.KindCloseBraceToken, []contextPredicate{isNonJsxSameLineTokenContext, isObjectContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Add a space after control dec context if the next character is an open bracket ex: 'if (false)[]ast.Kind{a, b} = []ast.Kind{1, 2};' -> 'if (false) []ast.Kind{a, b} = []ast.Kind{1, 2};'
|
||||
rule("SpaceAfterConditionalClosingParen", ast.KindCloseParenToken, ast.KindOpenBracketToken, []contextPredicate{isControlDeclContext}, ruleActionInsertSpace),
|
||||
|
||||
rule("NoSpaceBetweenFunctionKeywordAndStar", ast.KindFunctionKeyword, ast.KindAsteriskToken, []contextPredicate{isFunctionDeclarationOrFunctionExpressionContext}, ruleActionDeleteSpace),
|
||||
rule("SpaceAfterStarInGeneratorDeclaration", ast.KindAsteriskToken, ast.KindIdentifier, []contextPredicate{isFunctionDeclarationOrFunctionExpressionContext}, ruleActionInsertSpace),
|
||||
|
||||
rule("SpaceAfterFunctionInFuncDecl", ast.KindFunctionKeyword, anyToken, []contextPredicate{isFunctionDeclContext}, ruleActionInsertSpace),
|
||||
// Insert new line after { and before } in multi-line contexts.
|
||||
rule("NewLineAfterOpenBraceInBlockContext", ast.KindOpenBraceToken, anyToken, []contextPredicate{isMultilineBlockContext}, ruleActionInsertNewLine),
|
||||
|
||||
// For get/set members, we check for (identifier,identifier) since get/set don't have tokens and they are represented as just an identifier token.
|
||||
// Though, we do extra check on the context to make sure we are dealing with get/set node. Example:
|
||||
// get x() {}
|
||||
// set x(val) {}
|
||||
rule("SpaceAfterGetSetInMember", []ast.Kind{ast.KindGetKeyword, ast.KindSetKeyword}, ast.KindIdentifier, []contextPredicate{isFunctionDeclContext}, ruleActionInsertSpace),
|
||||
|
||||
rule("NoSpaceBetweenYieldKeywordAndStar", ast.KindYieldKeyword, ast.KindAsteriskToken, []contextPredicate{isNonJsxSameLineTokenContext, isYieldOrYieldStarWithOperand}, ruleActionDeleteSpace),
|
||||
rule("SpaceBetweenYieldOrYieldStarAndOperand", []ast.Kind{ast.KindYieldKeyword, ast.KindAsteriskToken}, anyToken, []contextPredicate{isNonJsxSameLineTokenContext, isYieldOrYieldStarWithOperand}, ruleActionInsertSpace),
|
||||
|
||||
rule("NoSpaceBetweenReturnAndSemicolon", ast.KindReturnKeyword, ast.KindSemicolonToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("SpaceAfterCertainKeywords", []ast.Kind{ast.KindVarKeyword, ast.KindThrowKeyword, ast.KindNewKeyword, ast.KindDeleteKeyword, ast.KindReturnKeyword, ast.KindTypeOfKeyword, ast.KindAwaitKeyword}, anyToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("SpaceAfterLetConstInVariableDeclaration", []ast.Kind{ast.KindLetKeyword, ast.KindConstKeyword}, anyToken, []contextPredicate{isNonJsxSameLineTokenContext, isStartOfVariableDeclarationList}, ruleActionInsertSpace),
|
||||
rule("NoSpaceBeforeOpenParenInFuncCall", anyToken, ast.KindOpenParenToken, []contextPredicate{isNonJsxSameLineTokenContext, isFunctionCallOrNewContext, isPreviousTokenNotComma}, ruleActionDeleteSpace),
|
||||
|
||||
// Special case for binary operators (that are keywords). For these we have to add a space and shouldn't follow any user options.
|
||||
rule("SpaceBeforeBinaryKeywordOperator", anyToken, binaryKeywordOperators, []contextPredicate{isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionInsertSpace),
|
||||
rule("SpaceAfterBinaryKeywordOperator", binaryKeywordOperators, anyToken, []contextPredicate{isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionInsertSpace),
|
||||
|
||||
rule("SpaceAfterVoidOperator", ast.KindVoidKeyword, anyToken, []contextPredicate{isNonJsxSameLineTokenContext, isVoidOpContext}, ruleActionInsertSpace),
|
||||
|
||||
// Async-await
|
||||
rule("SpaceBetweenAsyncAndOpenParen", ast.KindAsyncKeyword, ast.KindOpenParenToken, []contextPredicate{isArrowFunctionContext, isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("SpaceBetweenAsyncAndFunctionKeyword", ast.KindAsyncKeyword, []ast.Kind{ast.KindFunctionKeyword, ast.KindIdentifier}, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
|
||||
// Template string
|
||||
rule("NoSpaceBetweenTagAndTemplateString", []ast.Kind{ast.KindIdentifier, ast.KindCloseParenToken}, []ast.Kind{ast.KindNoSubstitutionTemplateLiteral, ast.KindTemplateHead}, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
|
||||
// JSX opening elements
|
||||
rule("SpaceBeforeJsxAttribute", anyToken, ast.KindIdentifier, []contextPredicate{isNextTokenParentJsxAttribute, isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("SpaceBeforeSlashInJsxOpeningElement", anyToken, ast.KindSlashToken, []contextPredicate{isJsxSelfClosingElementContext, isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceBeforeGreaterThanTokenInJsxOpeningElement", ast.KindSlashToken, ast.KindGreaterThanToken, []contextPredicate{isJsxSelfClosingElementContext, isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceBeforeEqualInJsxAttribute", anyToken, ast.KindEqualsToken, []contextPredicate{isJsxAttributeContext, isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterEqualInJsxAttribute", ast.KindEqualsToken, anyToken, []contextPredicate{isJsxAttributeContext, isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceBeforeJsxNamespaceColon", ast.KindIdentifier, ast.KindColonToken, []contextPredicate{isNextTokenParentJsxNamespacedName}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterJsxNamespaceColon", ast.KindColonToken, ast.KindIdentifier, []contextPredicate{isNextTokenParentJsxNamespacedName}, ruleActionDeleteSpace),
|
||||
|
||||
// TypeScript-specific rules
|
||||
// Use of module as a function call. e.g.: import m2 = module("m2");
|
||||
rule("NoSpaceAfterModuleImport", []ast.Kind{ast.KindModuleKeyword, ast.KindRequireKeyword}, ast.KindOpenParenToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
// Add a space around certain TypeScript keywords
|
||||
rule(
|
||||
"SpaceAfterCertainTypeScriptKeywords",
|
||||
[]ast.Kind{
|
||||
ast.KindAbstractKeyword,
|
||||
ast.KindAccessorKeyword,
|
||||
ast.KindClassKeyword,
|
||||
ast.KindDeclareKeyword,
|
||||
ast.KindDefaultKeyword,
|
||||
ast.KindEnumKeyword,
|
||||
ast.KindExportKeyword,
|
||||
ast.KindExtendsKeyword,
|
||||
ast.KindGetKeyword,
|
||||
ast.KindImplementsKeyword,
|
||||
ast.KindImportKeyword,
|
||||
ast.KindInterfaceKeyword,
|
||||
ast.KindModuleKeyword,
|
||||
ast.KindNamespaceKeyword,
|
||||
ast.KindOverrideKeyword,
|
||||
ast.KindPrivateKeyword,
|
||||
ast.KindPublicKeyword,
|
||||
ast.KindProtectedKeyword,
|
||||
ast.KindReadonlyKeyword,
|
||||
ast.KindSetKeyword,
|
||||
ast.KindStaticKeyword,
|
||||
ast.KindTypeKeyword,
|
||||
ast.KindFromKeyword,
|
||||
ast.KindKeyOfKeyword,
|
||||
ast.KindInferKeyword,
|
||||
},
|
||||
anyToken,
|
||||
[]contextPredicate{isNonJsxSameLineTokenContext},
|
||||
ruleActionInsertSpace,
|
||||
),
|
||||
rule(
|
||||
"SpaceBeforeCertainTypeScriptKeywords",
|
||||
anyToken,
|
||||
[]ast.Kind{ast.KindExtendsKeyword, ast.KindImplementsKeyword, ast.KindFromKeyword},
|
||||
[]contextPredicate{isNonJsxSameLineTokenContext},
|
||||
ruleActionInsertSpace,
|
||||
),
|
||||
// Treat string literals in module names as identifiers, and add a space between the literal and the opening Brace braces, e.g.: module "m2" {
|
||||
rule("SpaceAfterModuleName", ast.KindStringLiteral, ast.KindOpenBraceToken, []contextPredicate{isModuleDeclContext}, ruleActionInsertSpace),
|
||||
|
||||
// Lambda expressions
|
||||
rule("SpaceBeforeArrow", anyToken, ast.KindEqualsGreaterThanToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("SpaceAfterArrow", ast.KindEqualsGreaterThanToken, anyToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
|
||||
// Optional parameters and let args
|
||||
rule("NoSpaceAfterEllipsis", ast.KindDotDotDotToken, ast.KindIdentifier, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterOptionalParameters", ast.KindQuestionToken, []ast.Kind{ast.KindCloseParenToken, ast.KindCommaToken}, []contextPredicate{isNonJsxSameLineTokenContext, isNotBinaryOpContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Remove spaces in empty interface literals. e.g.: x: {}
|
||||
rule("NoSpaceBetweenEmptyInterfaceBraceBrackets", ast.KindOpenBraceToken, ast.KindCloseBraceToken, []contextPredicate{isNonJsxSameLineTokenContext, isObjectTypeContext}, ruleActionDeleteSpace),
|
||||
|
||||
// generics and type assertions
|
||||
rule("NoSpaceBeforeOpenAngularBracket", typeNames, ast.KindLessThanToken, []contextPredicate{isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceBetweenCloseParenAndAngularBracket", ast.KindCloseParenToken, ast.KindLessThanToken, []contextPredicate{isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterOpenAngularBracket", ast.KindLessThanToken, anyToken, []contextPredicate{isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceBeforeCloseAngularBracket", anyToken, ast.KindGreaterThanToken, []contextPredicate{isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterCloseAngularBracket", ast.KindGreaterThanToken, []ast.Kind{ast.KindOpenParenToken, ast.KindOpenBracketToken, ast.KindGreaterThanToken, ast.KindCommaToken}, []contextPredicate{
|
||||
isNonJsxSameLineTokenContext,
|
||||
isTypeArgumentOrParameterOrAssertionContext,
|
||||
isNotFunctionDeclContext, /*To prevent an interference with the SpaceBeforeOpenParenInFuncDecl rule*/
|
||||
isNonTypeAssertionContext,
|
||||
}, ruleActionDeleteSpace),
|
||||
|
||||
// decorators
|
||||
rule("SpaceBeforeAt", []ast.Kind{ast.KindCloseParenToken, ast.KindIdentifier}, ast.KindAtToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceAfterAt", ast.KindAtToken, anyToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
// Insert space after @ in decorator
|
||||
rule(
|
||||
"SpaceAfterDecorator",
|
||||
anyToken,
|
||||
[]ast.Kind{
|
||||
ast.KindAbstractKeyword,
|
||||
ast.KindIdentifier,
|
||||
ast.KindExportKeyword,
|
||||
ast.KindDefaultKeyword,
|
||||
ast.KindClassKeyword,
|
||||
ast.KindStaticKeyword,
|
||||
ast.KindPublicKeyword,
|
||||
ast.KindPrivateKeyword,
|
||||
ast.KindProtectedKeyword,
|
||||
ast.KindGetKeyword,
|
||||
ast.KindSetKeyword,
|
||||
ast.KindOpenBracketToken,
|
||||
ast.KindAsteriskToken,
|
||||
},
|
||||
[]contextPredicate{isEndOfDecoratorContextOnSameLine},
|
||||
ruleActionInsertSpace,
|
||||
),
|
||||
|
||||
rule("NoSpaceBeforeNonNullAssertionOperator", anyToken, ast.KindExclamationToken, []contextPredicate{isNonJsxSameLineTokenContext, isNonNullAssertionContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterNewKeywordOnConstructorSignature", ast.KindNewKeyword, ast.KindOpenParenToken, []contextPredicate{isNonJsxSameLineTokenContext, isConstructorSignatureContext}, ruleActionDeleteSpace),
|
||||
rule("SpaceLessThanAndNonJSXTypeAnnotation", ast.KindLessThanToken, ast.KindLessThanToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
}
|
||||
|
||||
// These rules are applied after high priority
|
||||
userConfigurableRules := []ruleSpec{
|
||||
// Treat constructor as an identifier in a function declaration, and remove spaces between constructor and following left parentheses
|
||||
rule("SpaceAfterConstructor", ast.KindConstructorKeyword, ast.KindOpenParenToken, []contextPredicate{isOptionEnabled(insertSpaceAfterConstructorOption), isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceAfterConstructor", ast.KindConstructorKeyword, ast.KindOpenParenToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterConstructorOption), isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
|
||||
rule("SpaceAfterComma", ast.KindCommaToken, anyToken, []contextPredicate{isOptionEnabled(insertSpaceAfterCommaDelimiterOption), isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext, isNextTokenNotCloseBracket, isNextTokenNotCloseParen}, ruleActionInsertSpace),
|
||||
rule("NoSpaceAfterComma", ast.KindCommaToken, anyToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterCommaDelimiterOption), isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Insert space after function keyword for anonymous functions
|
||||
rule("SpaceAfterAnonymousFunctionKeyword", []ast.Kind{ast.KindFunctionKeyword, ast.KindAsteriskToken}, ast.KindOpenParenToken, []contextPredicate{isOptionEnabled(insertSpaceAfterFunctionKeywordForAnonymousFunctionsOption), isFunctionDeclContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceAfterAnonymousFunctionKeyword", []ast.Kind{ast.KindFunctionKeyword, ast.KindAsteriskToken}, ast.KindOpenParenToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterFunctionKeywordForAnonymousFunctionsOption), isFunctionDeclContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Insert space after keywords in control flow statements
|
||||
rule("SpaceAfterKeywordInControl", keywords, ast.KindOpenParenToken, []contextPredicate{isOptionEnabled(insertSpaceAfterKeywordsInControlFlowStatementsOption), isControlDeclContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceAfterKeywordInControl", keywords, ast.KindOpenParenToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterKeywordsInControlFlowStatementsOption), isControlDeclContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Insert space after opening and before closing nonempty parenthesis
|
||||
rule("SpaceAfterOpenParen", ast.KindOpenParenToken, anyToken, []contextPredicate{isOptionEnabled(insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesisOption), isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("SpaceBeforeCloseParen", anyToken, ast.KindCloseParenToken, []contextPredicate{isOptionEnabled(insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesisOption), isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("SpaceBetweenOpenParens", ast.KindOpenParenToken, ast.KindOpenParenToken, []contextPredicate{isOptionEnabled(insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesisOption), isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceBetweenParens", ast.KindOpenParenToken, ast.KindCloseParenToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterOpenParen", ast.KindOpenParenToken, anyToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesisOption), isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceBeforeCloseParen", anyToken, ast.KindCloseParenToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesisOption), isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Insert space after opening and before closing nonempty brackets
|
||||
rule("SpaceAfterOpenBracket", ast.KindOpenBracketToken, anyToken, []contextPredicate{isOptionEnabled(insertSpaceAfterOpeningAndBeforeClosingNonemptyBracketsOption), isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("SpaceBeforeCloseBracket", anyToken, ast.KindCloseBracketToken, []contextPredicate{isOptionEnabled(insertSpaceAfterOpeningAndBeforeClosingNonemptyBracketsOption), isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceBetweenBrackets", ast.KindOpenBracketToken, ast.KindCloseBracketToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterOpenBracket", ast.KindOpenBracketToken, anyToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterOpeningAndBeforeClosingNonemptyBracketsOption), isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceBeforeCloseBracket", anyToken, ast.KindCloseBracketToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterOpeningAndBeforeClosingNonemptyBracketsOption), isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Insert a space after { and before } in single-line contexts, but remove space from empty object literals {}.
|
||||
rule("SpaceAfterOpenBrace", ast.KindOpenBraceToken, anyToken, []contextPredicate{isOptionEnabledOrUndefined(insertSpaceAfterOpeningAndBeforeClosingNonemptyBracesOption), isBraceWrappedContext}, ruleActionInsertSpace),
|
||||
rule("SpaceBeforeCloseBrace", anyToken, ast.KindCloseBraceToken, []contextPredicate{isOptionEnabledOrUndefined(insertSpaceAfterOpeningAndBeforeClosingNonemptyBracesOption), isBraceWrappedContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceBetweenEmptyBraceBrackets", ast.KindOpenBraceToken, ast.KindCloseBraceToken, []contextPredicate{isNonJsxSameLineTokenContext, isObjectContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterOpenBrace", ast.KindOpenBraceToken, anyToken, []contextPredicate{isOptionDisabled(insertSpaceAfterOpeningAndBeforeClosingNonemptyBracesOption), isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceBeforeCloseBrace", anyToken, ast.KindCloseBraceToken, []contextPredicate{isOptionDisabled(insertSpaceAfterOpeningAndBeforeClosingNonemptyBracesOption), isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Insert a space after opening and before closing empty brace brackets
|
||||
rule("SpaceBetweenEmptyBraceBrackets", ast.KindOpenBraceToken, ast.KindCloseBraceToken, []contextPredicate{isOptionEnabled(insertSpaceAfterOpeningAndBeforeClosingEmptyBracesOption)}, ruleActionInsertSpace),
|
||||
rule("NoSpaceBetweenEmptyBraceBrackets", ast.KindOpenBraceToken, ast.KindCloseBraceToken, []contextPredicate{isOptionDisabled(insertSpaceAfterOpeningAndBeforeClosingEmptyBracesOption), isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Insert space after opening and before closing template string braces
|
||||
rule("SpaceAfterTemplateHeadAndMiddle", []ast.Kind{ast.KindTemplateHead, ast.KindTemplateMiddle}, anyToken, []contextPredicate{isOptionEnabled(insertSpaceAfterOpeningAndBeforeClosingTemplateStringBracesOption), isNonJsxTextContext}, ruleActionInsertSpace, ruleFlagsCanDeleteNewLines),
|
||||
rule("SpaceBeforeTemplateMiddleAndTail", anyToken, []ast.Kind{ast.KindTemplateMiddle, ast.KindTemplateTail}, []contextPredicate{isOptionEnabled(insertSpaceAfterOpeningAndBeforeClosingTemplateStringBracesOption), isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceAfterTemplateHeadAndMiddle", []ast.Kind{ast.KindTemplateHead, ast.KindTemplateMiddle}, anyToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterOpeningAndBeforeClosingTemplateStringBracesOption), isNonJsxTextContext}, ruleActionDeleteSpace, ruleFlagsCanDeleteNewLines),
|
||||
rule("NoSpaceBeforeTemplateMiddleAndTail", anyToken, []ast.Kind{ast.KindTemplateMiddle, ast.KindTemplateTail}, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterOpeningAndBeforeClosingTemplateStringBracesOption), isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
|
||||
// No space after { and before } in JSX expression
|
||||
rule("SpaceAfterOpenBraceInJsxExpression", ast.KindOpenBraceToken, anyToken, []contextPredicate{isOptionEnabled(insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBracesOption), isNonJsxSameLineTokenContext, isJsxExpressionContext}, ruleActionInsertSpace),
|
||||
rule("SpaceBeforeCloseBraceInJsxExpression", anyToken, ast.KindCloseBraceToken, []contextPredicate{isOptionEnabled(insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBracesOption), isNonJsxSameLineTokenContext, isJsxExpressionContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceAfterOpenBraceInJsxExpression", ast.KindOpenBraceToken, anyToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBracesOption), isNonJsxSameLineTokenContext, isJsxExpressionContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceBeforeCloseBraceInJsxExpression", anyToken, ast.KindCloseBraceToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBracesOption), isNonJsxSameLineTokenContext, isJsxExpressionContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Insert space after semicolon in for statement
|
||||
rule("SpaceAfterSemicolonInFor", ast.KindSemicolonToken, anyToken, []contextPredicate{isOptionEnabled(insertSpaceAfterSemicolonInForStatementsOption), isNonJsxSameLineTokenContext, isForContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceAfterSemicolonInFor", ast.KindSemicolonToken, anyToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterSemicolonInForStatementsOption), isNonJsxSameLineTokenContext, isForContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Insert space before and after binary operators
|
||||
rule("SpaceBeforeBinaryOperator", anyToken, binaryOperators, []contextPredicate{isOptionEnabled(insertSpaceBeforeAndAfterBinaryOperatorsOption), isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionInsertSpace),
|
||||
rule("SpaceAfterBinaryOperator", binaryOperators, anyToken, []contextPredicate{isOptionEnabled(insertSpaceBeforeAndAfterBinaryOperatorsOption), isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceBeforeBinaryOperator", anyToken, binaryOperators, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceBeforeAndAfterBinaryOperatorsOption), isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterBinaryOperator", binaryOperators, anyToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceBeforeAndAfterBinaryOperatorsOption), isNonJsxSameLineTokenContext, isBinaryOpContext}, ruleActionDeleteSpace),
|
||||
|
||||
rule("SpaceBeforeOpenParenInFuncDecl", anyToken, ast.KindOpenParenToken, []contextPredicate{isOptionEnabled(insertSpaceBeforeFunctionParenthesisOption), isNonJsxSameLineTokenContext, isFunctionDeclContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceBeforeOpenParenInFuncDecl", anyToken, ast.KindOpenParenToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceBeforeFunctionParenthesisOption), isNonJsxSameLineTokenContext, isFunctionDeclContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Open Brace braces after control block
|
||||
rule("NewLineBeforeOpenBraceInControl", controlOpenBraceLeftTokenRange, ast.KindOpenBraceToken, []contextPredicate{isOptionEnabled(placeOpenBraceOnNewLineForControlBlocksOption), isControlDeclContext, isBeforeMultilineBlockContext}, ruleActionInsertNewLine, ruleFlagsCanDeleteNewLines),
|
||||
|
||||
// Open Brace braces after function
|
||||
// TypeScript: Function can have return types, which can be made of tons of different token kinds
|
||||
rule("NewLineBeforeOpenBraceInFunction", functionOpenBraceLeftTokenRange, ast.KindOpenBraceToken, []contextPredicate{isOptionEnabled(placeOpenBraceOnNewLineForFunctionsOption), isFunctionDeclContext, isBeforeMultilineBlockContext}, ruleActionInsertNewLine, ruleFlagsCanDeleteNewLines),
|
||||
// Open Brace braces after TypeScript module/class/interface
|
||||
rule("NewLineBeforeOpenBraceInTypeScriptDeclWithBlock", typeScriptOpenBraceLeftTokenRange, ast.KindOpenBraceToken, []contextPredicate{isOptionEnabled(placeOpenBraceOnNewLineForFunctionsOption), isTypeScriptDeclWithBlockContext, isBeforeMultilineBlockContext}, ruleActionInsertNewLine, ruleFlagsCanDeleteNewLines),
|
||||
|
||||
rule("SpaceAfterTypeAssertion", ast.KindGreaterThanToken, anyToken, []contextPredicate{isOptionEnabled(insertSpaceAfterTypeAssertionOption), isNonJsxSameLineTokenContext, isTypeAssertionContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceAfterTypeAssertion", ast.KindGreaterThanToken, anyToken, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceAfterTypeAssertionOption), isNonJsxSameLineTokenContext, isTypeAssertionContext}, ruleActionDeleteSpace),
|
||||
|
||||
rule("SpaceBeforeTypeAnnotation", anyToken, []ast.Kind{ast.KindQuestionToken, ast.KindColonToken}, []contextPredicate{isOptionEnabled(insertSpaceBeforeTypeAnnotationOption), isNonJsxSameLineTokenContext, isTypeAnnotationContext}, ruleActionInsertSpace),
|
||||
rule("NoSpaceBeforeTypeAnnotation", anyToken, []ast.Kind{ast.KindQuestionToken, ast.KindColonToken}, []contextPredicate{isOptionDisabledOrUndefined(insertSpaceBeforeTypeAnnotationOption), isNonJsxSameLineTokenContext, isTypeAnnotationContext}, ruleActionDeleteSpace),
|
||||
|
||||
rule("NoOptionalSemicolon", ast.KindSemicolonToken, anyTokenIncludingEOF, []contextPredicate{optionEquals(semicolonOption, lsutil.SemicolonPreferenceRemove), isSemicolonDeletionContext}, ruleActionDeleteToken),
|
||||
rule("OptionalSemicolon", anyToken, anyTokenIncludingEOF, []contextPredicate{optionEquals(semicolonOption, lsutil.SemicolonPreferenceInsert), isSemicolonInsertionContext}, ruleActionInsertTrailingSemicolon),
|
||||
}
|
||||
|
||||
// These rules are lower in priority than user-configurable. Rules earlier in this list have priority over rules later in the list.
|
||||
lowPriorityCommonRules := []ruleSpec{
|
||||
// Space after keyword but not before ; or : or ?
|
||||
rule("NoSpaceBeforeSemicolon", anyToken, ast.KindSemicolonToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
|
||||
rule("SpaceBeforeOpenBraceInControl", controlOpenBraceLeftTokenRange, ast.KindOpenBraceToken, []contextPredicate{isOptionDisabledOrUndefinedOrTokensOnSameLine(placeOpenBraceOnNewLineForControlBlocksOption), isControlDeclContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext}, ruleActionInsertSpace, ruleFlagsCanDeleteNewLines),
|
||||
rule("SpaceBeforeOpenBraceInFunction", functionOpenBraceLeftTokenRange, ast.KindOpenBraceToken, []contextPredicate{isOptionDisabledOrUndefinedOrTokensOnSameLine(placeOpenBraceOnNewLineForFunctionsOption), isFunctionDeclContext, isBeforeBlockContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext}, ruleActionInsertSpace, ruleFlagsCanDeleteNewLines),
|
||||
rule("SpaceBeforeOpenBraceInTypeScriptDeclWithBlock", typeScriptOpenBraceLeftTokenRange, ast.KindOpenBraceToken, []contextPredicate{isOptionDisabledOrUndefinedOrTokensOnSameLine(placeOpenBraceOnNewLineForFunctionsOption), isTypeScriptDeclWithBlockContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext}, ruleActionInsertSpace, ruleFlagsCanDeleteNewLines),
|
||||
|
||||
rule("NoSpaceBeforeComma", anyToken, ast.KindCommaToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
|
||||
// No space before and after indexer `x[]ast.Kind{}`
|
||||
rule("NoSpaceBeforeOpenBracket", anyTokenExcept(ast.KindAsyncKeyword, ast.KindCaseKeyword), ast.KindOpenBracketToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
rule("NoSpaceAfterCloseBracket", ast.KindCloseBracketToken, anyToken, []contextPredicate{isNonJsxSameLineTokenContext, isNotBeforeBlockInFunctionDeclarationContext}, ruleActionDeleteSpace),
|
||||
rule("SpaceAfterSemicolon", ast.KindSemicolonToken, anyToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
|
||||
// Remove extra space between for and await
|
||||
rule("SpaceBetweenForAndAwaitKeyword", ast.KindForKeyword, ast.KindAwaitKeyword, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
|
||||
// Remove extra spaces between ... and type name in tuple spread
|
||||
rule("SpaceBetweenDotDotDotAndTypeName", ast.KindDotDotDotToken, typeNames, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionDeleteSpace),
|
||||
|
||||
// Add a space between statements. All keywords except (do,else,case) has open/close parens after them.
|
||||
// So, we have a rule to add a space for []ast.Kind{),Any}, []ast.Kind{do,Any}, []ast.Kind{else,Any}, and []ast.Kind{case,Any}
|
||||
rule(
|
||||
"SpaceBetweenStatements",
|
||||
[]ast.Kind{ast.KindCloseParenToken, ast.KindDoKeyword, ast.KindElseKeyword, ast.KindCaseKeyword},
|
||||
anyToken,
|
||||
[]contextPredicate{isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext, isNotForContext},
|
||||
ruleActionInsertSpace,
|
||||
),
|
||||
// This low-pri rule takes care of "try {", "catch {" and "finally {" in case the rule SpaceBeforeOpenBraceInControl didn't execute on FormatOnEnter.
|
||||
rule("SpaceAfterTryCatchFinally", []ast.Kind{ast.KindTryKeyword, ast.KindCatchKeyword, ast.KindFinallyKeyword}, ast.KindOpenBraceToken, []contextPredicate{isNonJsxSameLineTokenContext}, ruleActionInsertSpace),
|
||||
}
|
||||
|
||||
result := make([]ruleSpec, 0, len(highPriorityCommonRules)+len(userConfigurableRules)+len(lowPriorityCommonRules))
|
||||
result = append(result, highPriorityCommonRules...)
|
||||
result = append(result, userConfigurableRules...)
|
||||
result = append(result, lowPriorityCommonRules...)
|
||||
return result
|
||||
}
|
||||
|
||||
func tokenRangeFrom(tokens ...ast.Kind) tokenRange {
|
||||
return tokenRange{
|
||||
isSpecific: true,
|
||||
tokens: tokens,
|
||||
}
|
||||
}
|
||||
|
||||
func tokenRangeFromEx(prefix []ast.Kind, tokens ...ast.Kind) tokenRange {
|
||||
tokens = append(prefix, tokens...)
|
||||
return tokenRange{
|
||||
isSpecific: true,
|
||||
tokens: tokens,
|
||||
}
|
||||
}
|
||||
|
||||
func tokenRangeFromRange(start ast.Kind, end ast.Kind) tokenRange {
|
||||
tokens := make([]ast.Kind, 0, end-start+1)
|
||||
for token := start; token <= end; token++ {
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
|
||||
return tokenRangeFrom(tokens...)
|
||||
}
|
||||
156
tools/tsgo/internal/format/rulesmap.go
Normal file
156
tools/tsgo/internal/format/rulesmap.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/debug"
|
||||
)
|
||||
|
||||
func getRules(context *FormattingContext, rules []*ruleImpl) []*ruleImpl {
|
||||
bucket := getRulesMap()[getRuleBucketIndex(context.currentTokenSpan.Kind, context.nextTokenSpan.Kind)]
|
||||
if len(bucket) > 0 {
|
||||
ruleActionMask := ruleActionNone
|
||||
outer:
|
||||
for _, rule := range bucket {
|
||||
acceptRuleActions := ^getRuleActionExclusion(ruleActionMask)
|
||||
if rule.Action()&acceptRuleActions != 0 {
|
||||
preds := rule.Context()
|
||||
for _, p := range preds {
|
||||
if !p(context) {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
ruleActionMask |= rule.Action()
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
func getRuleBucketIndex(row ast.Kind, column ast.Kind) int {
|
||||
debug.Assert(row <= ast.KindLastKeyword && column <= ast.KindLastKeyword, "Must compute formatting context from tokens")
|
||||
return (int(row) * mapRowLength) + int(column)
|
||||
}
|
||||
|
||||
const (
|
||||
maskBitSize = 5
|
||||
mask = 0b11111 // MaskBitSize bits
|
||||
mapRowLength = int(ast.KindLastToken) + 1
|
||||
)
|
||||
|
||||
/**
|
||||
* For a given rule action, gets a mask of other rule actions that
|
||||
* cannot be applied at the same position.
|
||||
*/
|
||||
func getRuleActionExclusion(ruleAction ruleAction) ruleAction {
|
||||
mask := ruleActionNone
|
||||
if ruleAction&ruleActionStopProcessingSpaceActions != 0 {
|
||||
mask |= ruleActionModifySpaceAction
|
||||
}
|
||||
if ruleAction&ruleActionStopProcessingTokenActions != 0 {
|
||||
mask |= ruleActionModifyTokenAction
|
||||
}
|
||||
if ruleAction&ruleActionModifySpaceAction != 0 {
|
||||
mask |= ruleActionModifySpaceAction
|
||||
}
|
||||
if ruleAction&ruleActionModifyTokenAction != 0 {
|
||||
mask |= ruleActionModifyTokenAction
|
||||
}
|
||||
return mask
|
||||
}
|
||||
|
||||
var getRulesMap = sync.OnceValue(buildRulesMap)
|
||||
|
||||
func buildRulesMap() [][]*ruleImpl {
|
||||
rules := getAllRules()
|
||||
// Map from bucket index to array of rules
|
||||
m := make([][]*ruleImpl, mapRowLength*mapRowLength)
|
||||
// This array is used only during construction of the rulesbucket in the map
|
||||
rulesBucketConstructionStateList := make([]int, len(m))
|
||||
for _, rule := range rules {
|
||||
specificRule := rule.leftTokenRange.isSpecific && rule.rightTokenRange.isSpecific
|
||||
|
||||
for _, left := range rule.leftTokenRange.tokens {
|
||||
for _, right := range rule.rightTokenRange.tokens {
|
||||
index := getRuleBucketIndex(left, right)
|
||||
m[index] = addRule(m[index], rule.rule, specificRule, rulesBucketConstructionStateList, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
type RulesPosition int
|
||||
|
||||
const (
|
||||
RulesPositionStopRulesSpecific RulesPosition = 0
|
||||
RulesPositionStopRulesAny RulesPosition = maskBitSize * 1
|
||||
RulesPositionContextRulesSpecific RulesPosition = maskBitSize * 2
|
||||
RulesPositionContextRulesAny RulesPosition = maskBitSize * 3
|
||||
RulesPositionNoContextRulesSpecific RulesPosition = maskBitSize * 4
|
||||
RulesPositionNoContextRulesAny RulesPosition = maskBitSize * 5
|
||||
)
|
||||
|
||||
// The Rules list contains all the inserted rules into a rulebucket in the following order:
|
||||
//
|
||||
// 1- Ignore rules with specific token combination
|
||||
// 2- Ignore rules with any token combination
|
||||
// 3- Context rules with specific token combination
|
||||
// 4- Context rules with any token combination
|
||||
// 5- Non-context rules with specific token combination
|
||||
// 6- Non-context rules with any token combination
|
||||
//
|
||||
// The member rulesInsertionIndexBitmap is used to describe the number of rules
|
||||
// in each sub-bucket (above) hence can be used to know the index of where to insert
|
||||
// the next rule. It's a bitmap which contains 6 different sections each is given 5 bits.
|
||||
//
|
||||
// Example:
|
||||
// In order to insert a rule to the end of sub-bucket (3), we get the index by adding
|
||||
// the values in the bitmap segments 3rd, 2nd, and 1st.
|
||||
func addRule(rules []*ruleImpl, rule *ruleImpl, specificTokens bool, constructionState []int, rulesBucketIndex int) []*ruleImpl {
|
||||
var position RulesPosition
|
||||
if rule.Action()&ruleActionStopAction != 0 {
|
||||
if specificTokens {
|
||||
position = RulesPositionStopRulesSpecific
|
||||
} else {
|
||||
position = RulesPositionStopRulesAny
|
||||
}
|
||||
} else if len(rule.Context()) != 0 {
|
||||
if specificTokens {
|
||||
position = RulesPositionContextRulesSpecific
|
||||
} else {
|
||||
position = RulesPositionContextRulesAny
|
||||
}
|
||||
} else {
|
||||
if specificTokens {
|
||||
position = RulesPositionNoContextRulesSpecific
|
||||
} else {
|
||||
position = RulesPositionNoContextRulesAny
|
||||
}
|
||||
}
|
||||
|
||||
state := constructionState[rulesBucketIndex]
|
||||
|
||||
rules = slices.Insert(rules, getRuleInsertionIndex(state, position), rule)
|
||||
constructionState[rulesBucketIndex] = increaseInsertionIndex(state, position)
|
||||
return rules
|
||||
}
|
||||
|
||||
func getRuleInsertionIndex(indexBitmap int, maskPosition RulesPosition) int {
|
||||
index := 0
|
||||
for pos := 0; pos <= int(maskPosition); pos += maskBitSize {
|
||||
index += indexBitmap & mask
|
||||
indexBitmap >>= maskBitSize
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
func increaseInsertionIndex(indexBitmap int, maskPosition RulesPosition) int {
|
||||
value := ((indexBitmap >> maskPosition) & mask) + 1
|
||||
debug.Assert((value&mask) == value, "Adding more rules into the sub-bucket than allowed. Maximum allowed is 32 rules.")
|
||||
return (indexBitmap & ^(mask << maskPosition)) | (value << maskPosition)
|
||||
}
|
||||
374
tools/tsgo/internal/format/scanner.go
Normal file
374
tools/tsgo/internal/format/scanner.go
Normal file
@@ -0,0 +1,374 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/debug"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
type TextRangeWithKind struct {
|
||||
Loc core.TextRange
|
||||
Kind ast.Kind
|
||||
}
|
||||
|
||||
func NewTextRangeWithKind(pos int, end int, kind ast.Kind) TextRangeWithKind {
|
||||
return TextRangeWithKind{
|
||||
Loc: core.NewTextRange(pos, end),
|
||||
Kind: kind,
|
||||
}
|
||||
}
|
||||
|
||||
type tokenInfo struct {
|
||||
leadingTrivia []TextRangeWithKind
|
||||
token TextRangeWithKind
|
||||
trailingTrivia []TextRangeWithKind
|
||||
}
|
||||
|
||||
type formattingScanner struct {
|
||||
s *scanner.Scanner
|
||||
startPos int
|
||||
endPos int
|
||||
savedPos int
|
||||
hasLastTokenInfo bool
|
||||
lastTokenInfo tokenInfo
|
||||
lastScanAction scanAction
|
||||
leadingTrivia []TextRangeWithKind
|
||||
trailingTrivia []TextRangeWithKind
|
||||
wasNewLine bool
|
||||
}
|
||||
|
||||
func newFormattingScanner(text string, languageVariant core.LanguageVariant, startPos int, endPos int, worker *formatSpanWorker) []core.TextChange {
|
||||
scan := scanner.NewScanner()
|
||||
scan.SetSkipTrivia(false)
|
||||
scan.SetLanguageVariant(languageVariant)
|
||||
scan.SetText(text)
|
||||
scan.ResetTokenState(startPos)
|
||||
|
||||
fmtScn := &formattingScanner{
|
||||
s: scan,
|
||||
startPos: startPos,
|
||||
endPos: endPos,
|
||||
wasNewLine: true,
|
||||
}
|
||||
|
||||
res := worker.execute(fmtScn)
|
||||
|
||||
fmtScn.hasLastTokenInfo = false
|
||||
scan.Reset()
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *formattingScanner) advance() {
|
||||
s.hasLastTokenInfo = false
|
||||
isStarted := s.s.TokenFullStart() != s.startPos
|
||||
|
||||
if isStarted {
|
||||
s.wasNewLine = len(s.trailingTrivia) > 0 && core.LastOrNil(s.trailingTrivia).Kind == ast.KindNewLineTrivia
|
||||
} else {
|
||||
s.s.Scan()
|
||||
}
|
||||
|
||||
s.leadingTrivia = nil
|
||||
s.trailingTrivia = nil
|
||||
|
||||
pos := s.s.TokenFullStart()
|
||||
|
||||
// Read leading trivia and token
|
||||
for pos < s.endPos {
|
||||
t := s.s.Token()
|
||||
if !ast.IsTrivia(t) {
|
||||
break
|
||||
}
|
||||
|
||||
// consume leading trivia
|
||||
s.s.Scan()
|
||||
item := NewTextRangeWithKind(pos, s.s.TokenFullStart(), t)
|
||||
|
||||
pos = s.s.TokenFullStart()
|
||||
|
||||
s.leadingTrivia = append(s.leadingTrivia, item)
|
||||
}
|
||||
|
||||
s.savedPos = s.s.TokenFullStart()
|
||||
}
|
||||
|
||||
func shouldRescanGreaterThanToken(node *ast.Node) bool {
|
||||
switch node.Kind {
|
||||
case ast.KindGreaterThanEqualsToken,
|
||||
ast.KindGreaterThanGreaterThanEqualsToken,
|
||||
ast.KindGreaterThanGreaterThanGreaterThanEqualsToken,
|
||||
ast.KindGreaterThanGreaterThanGreaterThanToken,
|
||||
ast.KindGreaterThanGreaterThanToken:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldRescanJsxIdentifier(node *ast.Node) bool {
|
||||
if node.Parent != nil {
|
||||
switch node.Parent.Kind {
|
||||
case ast.KindJsxAttribute,
|
||||
ast.KindJsxOpeningElement,
|
||||
ast.KindJsxClosingElement,
|
||||
ast.KindJsxSelfClosingElement,
|
||||
ast.KindJsxNamespacedName:
|
||||
// May parse an identifier like `module-layout`; that will be scanned as a keyword at first, but we should parse the whole thing to get an identifier.
|
||||
return ast.IsKeywordKind(node.Kind) || node.Kind == ast.KindIdentifier
|
||||
case ast.KindPropertyAccessExpression:
|
||||
// The leftmost name of a dotted JSX tag name (e.g. `a-b` in `<a-b.c>`) may contain hyphens, so rescan it as a JSX identifier.
|
||||
return (ast.IsKeywordKind(node.Kind) || node.Kind == ast.KindIdentifier) && isLeftmostJsxTagName(node)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isLeftmostJsxTagName(node *ast.Node) bool {
|
||||
return ast.FindAncestorOrQuit(node, func(n *ast.Node) ast.FindAncestorResult {
|
||||
switch {
|
||||
case n.Parent == nil:
|
||||
return ast.FindAncestorQuit
|
||||
case ast.IsJsxTagName(n):
|
||||
return ast.FindAncestorTrue
|
||||
case ast.IsPropertyAccessExpression(n.Parent) && n.Parent.Expression() == n:
|
||||
return ast.FindAncestorFalse
|
||||
default:
|
||||
return ast.FindAncestorQuit
|
||||
}
|
||||
}) != nil
|
||||
}
|
||||
|
||||
func (s *formattingScanner) shouldRescanJsxText(node *ast.Node) bool {
|
||||
if ast.IsJsxText(node) {
|
||||
return true
|
||||
}
|
||||
if !ast.IsJsxElement(node) || s.hasLastTokenInfo == false {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.lastTokenInfo.token.Kind == ast.KindJsxText
|
||||
}
|
||||
|
||||
func shouldRescanSlashToken(container *ast.Node) bool {
|
||||
return container.Kind == ast.KindRegularExpressionLiteral
|
||||
}
|
||||
|
||||
func shouldRescanTemplateToken(container *ast.Node) bool {
|
||||
return container.Kind == ast.KindTemplateMiddle ||
|
||||
container.Kind == ast.KindTemplateTail
|
||||
}
|
||||
|
||||
func shouldRescanJsxAttributeValue(node *ast.Node) bool {
|
||||
return node.Parent != nil && ast.IsJsxAttribute(node.Parent) && node.Parent.Initializer() == node
|
||||
}
|
||||
|
||||
func startsWithSlashToken(t ast.Kind) bool {
|
||||
return t == ast.KindSlashToken || t == ast.KindSlashEqualsToken
|
||||
}
|
||||
|
||||
type scanAction int
|
||||
|
||||
const (
|
||||
actionScan scanAction = iota
|
||||
actionRescanGreaterThanToken
|
||||
actionRescanSlashToken
|
||||
actionRescanTemplateToken
|
||||
actionRescanJsxIdentifier
|
||||
actionRescanJsxText
|
||||
actionRescanJsxAttributeValue
|
||||
)
|
||||
|
||||
func fixTokenKind(tokenInfo tokenInfo, container *ast.Node) tokenInfo {
|
||||
if ast.IsTokenKind(container.Kind) && tokenInfo.token.Kind != container.Kind {
|
||||
tokenInfo.token.Kind = container.Kind
|
||||
}
|
||||
return tokenInfo
|
||||
}
|
||||
|
||||
func (s *formattingScanner) readTokenInfo(n *ast.Node) tokenInfo {
|
||||
debug.Assert(s.isOnToken())
|
||||
|
||||
// normally scanner returns the smallest available token
|
||||
// check the kind of context node to determine if scanner should have more greedy behavior and consume more text.
|
||||
|
||||
var expectedScanAction scanAction
|
||||
if shouldRescanGreaterThanToken(n) {
|
||||
expectedScanAction = actionRescanGreaterThanToken
|
||||
} else if shouldRescanSlashToken(n) {
|
||||
expectedScanAction = actionRescanSlashToken
|
||||
} else if shouldRescanTemplateToken(n) {
|
||||
expectedScanAction = actionRescanTemplateToken
|
||||
} else if shouldRescanJsxIdentifier(n) {
|
||||
expectedScanAction = actionRescanJsxIdentifier
|
||||
} else if s.shouldRescanJsxText(n) {
|
||||
expectedScanAction = actionRescanJsxText
|
||||
} else if shouldRescanJsxAttributeValue(n) {
|
||||
expectedScanAction = actionRescanJsxAttributeValue
|
||||
} else {
|
||||
expectedScanAction = actionScan
|
||||
}
|
||||
|
||||
if s.hasLastTokenInfo && expectedScanAction == s.lastScanAction {
|
||||
// readTokenInfo was called before with the same expected scan action.
|
||||
// No need to re-scan text, return existing 'lastTokenInfo'
|
||||
// it is ok to call fixTokenKind here since it does not affect
|
||||
// what portion of text is consumed. In contrast rescanning can change it,
|
||||
// i.e. for '>=' when originally scanner eats just one character
|
||||
// and rescanning forces it to consume more.
|
||||
s.lastTokenInfo = fixTokenKind(s.lastTokenInfo, n)
|
||||
return s.lastTokenInfo
|
||||
}
|
||||
|
||||
if s.s.TokenFullStart() != s.savedPos {
|
||||
// readTokenInfo was called before but scan action differs - rescan text
|
||||
s.s.ResetTokenState(s.savedPos)
|
||||
s.s.Scan()
|
||||
}
|
||||
|
||||
currentToken := s.getNextToken(n, expectedScanAction)
|
||||
|
||||
token := NewTextRangeWithKind(
|
||||
s.s.TokenFullStart(),
|
||||
s.s.TokenEnd(),
|
||||
currentToken,
|
||||
)
|
||||
|
||||
// consume trailing trivia
|
||||
s.trailingTrivia = nil
|
||||
for s.s.TokenFullStart() < s.endPos {
|
||||
currentToken = s.s.Scan()
|
||||
if !ast.IsTrivia(currentToken) {
|
||||
break
|
||||
}
|
||||
trivia := NewTextRangeWithKind(
|
||||
s.s.TokenFullStart(),
|
||||
s.s.TokenEnd(),
|
||||
currentToken,
|
||||
)
|
||||
|
||||
s.trailingTrivia = append(s.trailingTrivia, trivia)
|
||||
|
||||
if currentToken == ast.KindNewLineTrivia {
|
||||
// move past new line
|
||||
s.s.Scan()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
s.hasLastTokenInfo = true
|
||||
s.lastTokenInfo = tokenInfo{
|
||||
leadingTrivia: slices.Clone(s.leadingTrivia),
|
||||
token: token,
|
||||
trailingTrivia: slices.Clone(s.trailingTrivia),
|
||||
}
|
||||
s.lastTokenInfo = fixTokenKind(s.lastTokenInfo, n)
|
||||
|
||||
return s.lastTokenInfo
|
||||
}
|
||||
|
||||
func (s *formattingScanner) getNextToken(n *ast.Node, expectedScanAction scanAction) ast.Kind {
|
||||
token := s.s.Token()
|
||||
s.lastScanAction = actionScan
|
||||
switch expectedScanAction {
|
||||
case actionRescanGreaterThanToken:
|
||||
if token == ast.KindGreaterThanToken {
|
||||
s.lastScanAction = actionRescanGreaterThanToken
|
||||
newToken := s.s.ReScanGreaterThanToken()
|
||||
debug.Assert(n.Kind == newToken)
|
||||
return newToken
|
||||
}
|
||||
case actionRescanSlashToken:
|
||||
if startsWithSlashToken(token) {
|
||||
s.lastScanAction = actionRescanSlashToken
|
||||
newToken := s.s.ReScanSlashToken()
|
||||
debug.Assert(n.Kind == newToken)
|
||||
return newToken
|
||||
}
|
||||
case actionRescanTemplateToken:
|
||||
if token == ast.KindCloseBraceToken {
|
||||
s.lastScanAction = actionRescanTemplateToken
|
||||
return s.s.ReScanTemplateToken( /*isTaggedTemplate*/ false)
|
||||
}
|
||||
case actionRescanJsxIdentifier:
|
||||
s.lastScanAction = actionRescanJsxIdentifier
|
||||
return s.s.ScanJsxIdentifier()
|
||||
case actionRescanJsxText:
|
||||
s.lastScanAction = actionRescanJsxText
|
||||
return s.s.ReScanJsxToken( /*allowMultilineJsxText*/ false)
|
||||
case actionRescanJsxAttributeValue:
|
||||
s.lastScanAction = actionRescanJsxAttributeValue
|
||||
return s.s.ReScanJsxAttributeValue()
|
||||
case actionScan:
|
||||
break
|
||||
default:
|
||||
debug.AssertNever(expectedScanAction, "unhandled scan action kind")
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func (s *formattingScanner) readEOFTokenRange() TextRangeWithKind {
|
||||
debug.Assert(s.isOnEOF())
|
||||
return NewTextRangeWithKind(
|
||||
s.s.TokenFullStart(),
|
||||
s.s.TokenEnd(),
|
||||
ast.KindEndOfFile,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *formattingScanner) isOnToken() bool {
|
||||
current := s.s.Token()
|
||||
if s.hasLastTokenInfo {
|
||||
current = s.lastTokenInfo.token.Kind
|
||||
}
|
||||
return current != ast.KindEndOfFile && !ast.IsTrivia(current)
|
||||
}
|
||||
|
||||
func (s *formattingScanner) isOnEOF() bool {
|
||||
current := s.s.Token()
|
||||
if s.hasLastTokenInfo {
|
||||
current = s.lastTokenInfo.token.Kind
|
||||
}
|
||||
return current == ast.KindEndOfFile
|
||||
}
|
||||
|
||||
func (s *formattingScanner) skipToEndOf(r *core.TextRange) {
|
||||
s.s.ResetTokenState(r.End())
|
||||
s.savedPos = s.s.TokenFullStart()
|
||||
s.lastScanAction = actionScan
|
||||
s.hasLastTokenInfo = false
|
||||
s.wasNewLine = false
|
||||
s.leadingTrivia = nil
|
||||
s.trailingTrivia = nil
|
||||
}
|
||||
|
||||
func (s *formattingScanner) skipToStartOf(r *core.TextRange) {
|
||||
s.s.ResetTokenState(r.Pos())
|
||||
s.savedPos = s.s.TokenFullStart()
|
||||
s.lastScanAction = actionScan
|
||||
s.hasLastTokenInfo = false
|
||||
s.wasNewLine = false
|
||||
s.leadingTrivia = nil
|
||||
s.trailingTrivia = nil
|
||||
}
|
||||
|
||||
func (s *formattingScanner) getCurrentLeadingTrivia() []TextRangeWithKind {
|
||||
return s.leadingTrivia
|
||||
}
|
||||
|
||||
func (s *formattingScanner) lastTrailingTriviaWasNewLine() bool {
|
||||
return s.wasNewLine
|
||||
}
|
||||
|
||||
func (s *formattingScanner) getTokenFullStart() int {
|
||||
if s.hasLastTokenInfo {
|
||||
return s.lastTokenInfo.token.Loc.Pos()
|
||||
}
|
||||
return s.s.TokenFullStart()
|
||||
}
|
||||
|
||||
func (s *formattingScanner) getStartPos() int { // TODO: redundant?
|
||||
return s.getTokenFullStart()
|
||||
}
|
||||
1252
tools/tsgo/internal/format/span.go
Normal file
1252
tools/tsgo/internal/format/span.go
Normal file
File diff suppressed because it is too large
Load Diff
190
tools/tsgo/internal/format/util.go
Normal file
190
tools/tsgo/internal/format/util.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
func rangeIsOnOneLine(node core.TextRange, file *ast.SourceFile) bool {
|
||||
startLine := scanner.GetECMALineOfPosition(file, node.Pos())
|
||||
endLine := scanner.GetECMALineOfPosition(file, node.End())
|
||||
return startLine == endLine
|
||||
}
|
||||
|
||||
func getOpenTokenForList(node *ast.Node, list *ast.NodeList) ast.Kind {
|
||||
switch node.Kind {
|
||||
case ast.KindConstructor,
|
||||
ast.KindFunctionDeclaration,
|
||||
ast.KindFunctionExpression,
|
||||
ast.KindMethodDeclaration,
|
||||
ast.KindMethodSignature,
|
||||
ast.KindArrowFunction,
|
||||
ast.KindCallSignature,
|
||||
ast.KindConstructSignature,
|
||||
ast.KindFunctionType,
|
||||
ast.KindConstructorType,
|
||||
ast.KindGetAccessor,
|
||||
ast.KindSetAccessor:
|
||||
if node.TypeParameterList() == list {
|
||||
return ast.KindLessThanToken
|
||||
} else if node.ParameterList() == list {
|
||||
return ast.KindOpenParenToken
|
||||
}
|
||||
case ast.KindCallExpression, ast.KindNewExpression:
|
||||
if node.TypeArgumentList() == list {
|
||||
return ast.KindLessThanToken
|
||||
} else if node.ArgumentList() == list {
|
||||
return ast.KindOpenParenToken
|
||||
}
|
||||
case ast.KindClassDeclaration,
|
||||
ast.KindClassExpression,
|
||||
ast.KindInterfaceDeclaration,
|
||||
ast.KindTypeAliasDeclaration:
|
||||
if node.TypeParameterList() == list {
|
||||
return ast.KindLessThanToken
|
||||
}
|
||||
case ast.KindTypeReference,
|
||||
ast.KindTaggedTemplateExpression,
|
||||
ast.KindTypeQuery,
|
||||
ast.KindExpressionWithTypeArguments,
|
||||
ast.KindImportType:
|
||||
if node.TypeArgumentList() == list {
|
||||
return ast.KindLessThanToken
|
||||
}
|
||||
case ast.KindTypeLiteral:
|
||||
return ast.KindOpenBraceToken
|
||||
}
|
||||
|
||||
return ast.KindUnknown
|
||||
}
|
||||
|
||||
func getCloseTokenForOpenToken(kind ast.Kind) ast.Kind {
|
||||
// TODO: matches strada - seems like it could handle more pairs of braces, though? [] notably missing
|
||||
switch kind {
|
||||
case ast.KindOpenParenToken:
|
||||
return ast.KindCloseParenToken
|
||||
case ast.KindLessThanToken:
|
||||
return ast.KindGreaterThanToken
|
||||
case ast.KindOpenBraceToken:
|
||||
return ast.KindCloseBraceToken
|
||||
}
|
||||
return ast.KindUnknown
|
||||
}
|
||||
|
||||
func GetLineStartPositionForPosition(position int, sourceFile *ast.SourceFile) int {
|
||||
lineStarts := scanner.GetECMALineStarts(sourceFile)
|
||||
line := scanner.GetECMALineOfPosition(sourceFile, position)
|
||||
return int(lineStarts[line])
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether `child` is a grammar error on `parent`.
|
||||
* In strada, this also checked node arrays, but it is never actually called with one in practice.
|
||||
*/
|
||||
func isGrammarError(parent *ast.Node, child *ast.Node) bool {
|
||||
if ast.IsTypeParameterDeclaration(parent) {
|
||||
return child == parent.AsTypeParameterDeclaration().Expression
|
||||
}
|
||||
if ast.IsPropertySignatureDeclaration(parent) {
|
||||
return child == parent.Initializer()
|
||||
}
|
||||
if ast.IsPropertyDeclaration(parent) {
|
||||
return ast.IsAutoAccessorPropertyDeclaration(parent) && child == parent.PostfixToken() && child.Kind == ast.KindQuestionToken
|
||||
}
|
||||
if ast.IsPropertyAssignment(parent) {
|
||||
pa := parent.AsPropertyAssignment()
|
||||
mods := pa.Modifiers()
|
||||
return child == pa.PostfixToken || (mods != nil && isGrammarErrorElement(&mods.NodeList, child, ast.IsModifierLike))
|
||||
}
|
||||
if ast.IsShorthandPropertyAssignment(parent) {
|
||||
sp := parent.AsShorthandPropertyAssignment()
|
||||
mods := sp.Modifiers()
|
||||
return child == sp.EqualsToken || child == sp.PostfixToken || (mods != nil && isGrammarErrorElement(&mods.NodeList, child, ast.IsModifierLike))
|
||||
}
|
||||
if ast.IsMethodDeclaration(parent) {
|
||||
return child == parent.PostfixToken() && child.Kind == ast.KindExclamationToken
|
||||
}
|
||||
if ast.IsConstructorDeclaration(parent) {
|
||||
return child == parent.AsConstructorDeclaration().Type || isGrammarErrorElement(parent.TypeParameterList(), child, ast.IsTypeParameterDeclaration)
|
||||
}
|
||||
if ast.IsGetAccessorDeclaration(parent) {
|
||||
return isGrammarErrorElement(parent.TypeParameterList(), child, ast.IsTypeParameterDeclaration)
|
||||
}
|
||||
if ast.IsSetAccessorDeclaration(parent) {
|
||||
return child == parent.AsSetAccessorDeclaration().Type || isGrammarErrorElement(parent.TypeParameterList(), child, ast.IsTypeParameterDeclaration)
|
||||
}
|
||||
if ast.IsNamespaceExportDeclaration(parent) {
|
||||
mods := parent.AsNamespaceExportDeclaration().Modifiers()
|
||||
return mods != nil && isGrammarErrorElement(&mods.NodeList, child, ast.IsModifierLike)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isGrammarErrorElement(list *ast.NodeList, child *ast.Node, isPossibleElement func(node *ast.Node) bool) bool {
|
||||
if list == nil || len(list.Nodes) == 0 {
|
||||
return false
|
||||
}
|
||||
if !isPossibleElement(child) {
|
||||
return false
|
||||
}
|
||||
return slices.Contains(list.Nodes, child)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validating `expectedTokenKind` ensures the token was typed in the context we expect (eg: not a comment).
|
||||
* @param expectedTokenKind The kind of the last token constituting the desired parent node.
|
||||
*/
|
||||
func findImmediatelyPrecedingTokenOfKind(end int, expectedTokenKind ast.Kind, sourceFile *ast.SourceFile) *ast.Node {
|
||||
precedingToken := astnav.FindPrecedingToken(sourceFile, end)
|
||||
if precedingToken == nil || precedingToken.Kind != expectedTokenKind || precedingToken.End() != end {
|
||||
return nil
|
||||
}
|
||||
return precedingToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the highest node enclosing `node` at the same list level as `node`
|
||||
* and whose end does not exceed `node.end`.
|
||||
*
|
||||
* Consider typing the following
|
||||
* ```
|
||||
* let x = 1;
|
||||
* while (true) {
|
||||
* }
|
||||
* ```
|
||||
* Upon typing the closing curly, we want to format the entire `while`-statement, but not the preceding
|
||||
* variable declaration.
|
||||
*/
|
||||
func findOutermostNodeWithinListLevel(node *ast.Node) *ast.Node {
|
||||
current := node
|
||||
for current != nil &&
|
||||
current.Parent != nil &&
|
||||
current.Parent.End() == node.End() &&
|
||||
!isListElement(current.Parent, current) {
|
||||
current = current.Parent
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
// Returns true if node is a element in some list in parent
|
||||
// i.e. parent is class declaration with the list of members and node is one of members.
|
||||
func isListElement(parent *ast.Node, node *ast.Node) bool {
|
||||
switch parent.Kind {
|
||||
case ast.KindClassDeclaration, ast.KindInterfaceDeclaration:
|
||||
return node.Loc.ContainedBy(parent.MemberList().Loc)
|
||||
case ast.KindModuleDeclaration:
|
||||
body := parent.Body()
|
||||
return body != nil && body.Kind == ast.KindModuleBlock && node.Loc.ContainedBy(body.StatementList().Loc)
|
||||
case ast.KindSourceFile, ast.KindBlock, ast.KindModuleBlock:
|
||||
return node.Loc.ContainedBy(parent.StatementList().Loc)
|
||||
case ast.KindCatchClause:
|
||||
return node.Loc.ContainedBy(parent.AsCatchClause().Block.StatementList().Loc)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user