From bd6cc28a07fa17ae59c22339651715e561513a22 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 6 Jun 2025 10:08:40 -0700 Subject: [PATCH 01/11] single file format cli for testing --- internal/execute/tsc.go | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/internal/execute/tsc.go b/internal/execute/tsc.go index bf694fd235..0fad4937ac 100644 --- a/internal/execute/tsc.go +++ b/internal/execute/tsc.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "os" "runtime" "slices" "strings" @@ -13,13 +14,34 @@ import ( "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnostics" + "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/parser" "github.com/microsoft/typescript-go/internal/pprof" + "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) type cbType = func(p any) any +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 CommandLine(sys System, cb cbType, commandLineArgs []string) ExitStatus { if len(commandLineArgs) > 0 { // !!! build mode @@ -28,6 +50,32 @@ func CommandLine(sys System, cb cbType, commandLineArgs []string) ExitStatus { fmt.Fprint(sys.Writer(), "Build mode is currently unsupported."+sys.NewLine()) sys.EndWrite() return ExitStatusNotImplemented + case "-f": + path := commandLineArgs[1] + ctx := format.WithFormatCodeSettings(context.Background(), format.GetDefaultFormatCodeSettings("\n"), "\n") + fileContent, err := os.ReadFile(path) + if err != nil { + fmt.Fprint(sys.Writer(), err.Error()+sys.NewLine()) + return ExitStatusNotImplemented + } + text := string(fileContent) + pathified := tspath.ToPath(path, sys.GetCurrentDirectory(), true) + sourceFile := parser.ParseSourceFile( + string(pathified), + pathified, + text, + core.ScriptTargetESNext, + scanner.JSDocParsingModeParseAll, + ) + ast.SetParentInChildren(sourceFile.AsNode()) + edits := format.FormatDocument(ctx, sourceFile) + newText := applyBulkEdits(text, edits) + err = os.WriteFile(path, []byte(newText), 0o644) + if err != nil { + fmt.Fprint(sys.Writer(), err.Error()+sys.NewLine()) + return ExitStatusNotImplemented + } + return ExitStatusSuccess } } From c0f152ae035b5ba723ab335b326afa72bcec520d Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 6 Jun 2025 11:03:49 -0700 Subject: [PATCH 02/11] Fix unguarded addition bug --- internal/format/indent.go | 6 +++++- internal/format/span.go | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/format/indent.go b/internal/format/indent.go index 8763c46eb6..f7eab25fea 100644 --- a/internal/format/indent.go +++ b/internal/format/indent.go @@ -152,7 +152,11 @@ func getActualIndentationForListItem(node *ast.Node, sourceFile *ast.SourceFile, if listIndentsChild { delta = options.IndentSize } - return getActualIndentationForListStartLine(containingList, sourceFile, options) + delta + res := getActualIndentationForListStartLine(containingList, sourceFile, options) + if res == -1 { + return delta + } + return res + delta } return -1 } diff --git a/internal/format/span.go b/internal/format/span.go index c3a90590fe..88f18b42fe 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -548,7 +548,11 @@ func (w *formatSpanWorker) computeIndentation(node *ast.Node, startLine int, inh argumentStartsOnSameLineAsPreviousArgument(parent, node, startLine, w.sourceFile) { return parentDynamicIndentation.getIndentation(), delta } else { - return parentDynamicIndentation.getIndentation() + parentDynamicIndentation.getDelta(node), delta + i := parentDynamicIndentation.getIndentation() + if i == -1 { + return parentDynamicIndentation.getIndentation(), delta + } + return i + parentDynamicIndentation.getDelta(node), delta } } From 7c376dda89bfaa99172fd629f39e23b8d47b1799 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 6 Jun 2025 16:21:51 -0700 Subject: [PATCH 03/11] Add cli bench --- internal/execute/fmt_test.go | 95 ++++++++++++++++++++++++++++++++++++ internal/execute/tsc.go | 56 +++++++++++---------- 2 files changed, 125 insertions(+), 26 deletions(-) create mode 100644 internal/execute/fmt_test.go diff --git a/internal/execute/fmt_test.go b/internal/execute/fmt_test.go new file mode 100644 index 0000000000..d248bb9463 --- /dev/null +++ b/internal/execute/fmt_test.go @@ -0,0 +1,95 @@ +package execute + +import ( + "fmt" + "io" + "os" + "runtime" + "testing" + "time" + + "github.com/microsoft/typescript-go/internal/bundled" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/repo" + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/osvfs" +) + +func BenchmarkFormat(b *testing.B) { + checkerPath := tspath.CombinePaths(repo.TypeScriptSubmodulePath, "src", "compiler", "checker.ts") + + tmp := b.TempDir() + sys := newSystem() + + var count int + + b.ReportAllocs() + time.Sleep(5 * time.Second) + for b.Loop() { + out := tspath.CombinePaths(tmp, fmt.Sprintf("out%d.ts", count)) + code := fmtMain(sys, checkerPath, out) + if code != 0 { + b.Fatalf("Unexpected exit code: %d", code) + } + } +} + +type osSys struct { + writer io.Writer + fs vfs.FS + defaultLibraryPath string + newLine string + cwd string + start time.Time +} + +func (s *osSys) SinceStart() time.Duration { + return time.Since(s.start) +} + +func (s *osSys) Now() time.Time { + return time.Now() +} + +func (s *osSys) FS() vfs.FS { + return s.fs +} + +func (s *osSys) DefaultLibraryPath() string { + return s.defaultLibraryPath +} + +func (s *osSys) GetCurrentDirectory() string { + return s.cwd +} + +func (s *osSys) NewLine() string { + return s.newLine +} + +func (s *osSys) Writer() io.Writer { + return s.writer +} + +func (s *osSys) EndWrite() { + // do nothing, this is needed in the interface for testing + // todo: revisit if improving tsc/build/watch unittest baselines +} + +func newSystem() *osSys { + cwd, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting current directory: %v\n", err) + os.Exit(int(ExitStatusInvalidProject_OutputsSkipped)) + } + + return &osSys{ + cwd: tspath.NormalizePath(cwd), + fs: bundled.WrapFS(osvfs.FS()), + defaultLibraryPath: bundled.LibPath(), + writer: os.Stdout, + newLine: core.IfElse(runtime.GOOS == "windows", "\r\n", "\n"), + start: time.Now(), + } +} diff --git a/internal/execute/tsc.go b/internal/execute/tsc.go index 0fad4937ac..4ba7f868c6 100644 --- a/internal/execute/tsc.go +++ b/internal/execute/tsc.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "os" "runtime" "slices" "strings" @@ -51,31 +50,7 @@ func CommandLine(sys System, cb cbType, commandLineArgs []string) ExitStatus { sys.EndWrite() return ExitStatusNotImplemented case "-f": - path := commandLineArgs[1] - ctx := format.WithFormatCodeSettings(context.Background(), format.GetDefaultFormatCodeSettings("\n"), "\n") - fileContent, err := os.ReadFile(path) - if err != nil { - fmt.Fprint(sys.Writer(), err.Error()+sys.NewLine()) - return ExitStatusNotImplemented - } - text := string(fileContent) - pathified := tspath.ToPath(path, sys.GetCurrentDirectory(), true) - sourceFile := parser.ParseSourceFile( - string(pathified), - pathified, - text, - core.ScriptTargetESNext, - scanner.JSDocParsingModeParseAll, - ) - ast.SetParentInChildren(sourceFile.AsNode()) - edits := format.FormatDocument(ctx, sourceFile) - newText := applyBulkEdits(text, edits) - err = os.WriteFile(path, []byte(newText), 0o644) - if err != nil { - fmt.Fprint(sys.Writer(), err.Error()+sys.NewLine()) - return ExitStatusNotImplemented - } - return ExitStatusSuccess + return fmtMain(sys, commandLineArgs[1], commandLineArgs[1]) } } @@ -87,6 +62,35 @@ func CommandLine(sys System, cb cbType, commandLineArgs []string) ExitStatus { return start(watcher) } +func fmtMain(sys System, input, output string) ExitStatus { + ctx := format.WithFormatCodeSettings(context.Background(), format.GetDefaultFormatCodeSettings(sys.NewLine()), sys.NewLine()) + input = string(tspath.ToPath(input, sys.GetCurrentDirectory(), sys.FS().UseCaseSensitiveFileNames())) + output = string(tspath.ToPath(output, sys.GetCurrentDirectory(), sys.FS().UseCaseSensitiveFileNames())) + fileContent, ok := sys.FS().ReadFile(input) + if !ok { + fmt.Fprint(sys.Writer(), "File not found: "+input+sys.NewLine()) + return ExitStatusNotImplemented + } + text := string(fileContent) + pathified := tspath.ToPath(input, sys.GetCurrentDirectory(), true) + sourceFile := parser.ParseSourceFile( + string(pathified), + pathified, + text, + core.ScriptTargetESNext, + scanner.JSDocParsingModeParseAll, + ) + ast.SetParentInChildren(sourceFile.AsNode()) + edits := format.FormatDocument(ctx, sourceFile) + newText := applyBulkEdits(text, edits) + + if err := sys.FS().WriteFile(output, newText, false); err != nil { + fmt.Fprint(sys.Writer(), err.Error()+sys.NewLine()) + return ExitStatusNotImplemented + } + return ExitStatusSuccess +} + func executeCommandLineWorker(sys System, cb cbType, commandLine *tsoptions.ParsedCommandLine) (ExitStatus, *watcher) { configFileName := "" reportDiagnostic := createDiagnosticReporter(sys, commandLine.CompilerOptions()) From c681b93f2842c4c7f5477ac043c05c2d264018d8 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 6 Jun 2025 16:23:11 -0700 Subject: [PATCH 04/11] Make generic to avoid boxing --- internal/format/rulecontext.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/format/rulecontext.go b/internal/format/rulecontext.go index 365d70db1c..9930447e75 100644 --- a/internal/format/rulecontext.go +++ b/internal/format/rulecontext.go @@ -15,11 +15,11 @@ import ( /// type ( - optionSelector = func(options *FormatCodeSettings) core.Tristate - anyOptionSelector = func(options *FormatCodeSettings) any + optionSelector = func(options *FormatCodeSettings) core.Tristate + anyOptionSelector[T comparable] = func(options *FormatCodeSettings) T ) -func semicolonOption(options *FormatCodeSettings) any { return options.Semicolons } +func semicolonOption(options *FormatCodeSettings) SemicolonPreference { return options.Semicolons } func insertSpaceAfterCommaDelimiterOption(options *FormatCodeSettings) core.Tristate { return options.InsertSpaceAfterCommaDelimiter } @@ -96,7 +96,7 @@ func indentSwitchCaseOption(options *FormatCodeSettings) core.Tristate { return options.IndentSwitchCase } -func optionEquals(optionName anyOptionSelector, optionValue any) contextPredicate { +func optionEquals[T comparable](optionName anyOptionSelector[T], optionValue T) contextPredicate { return func(context *formattingContext) bool { if context.Options == nil { return false From 668cfc34f3611a9b7fa800a59ea34dd8ac0a00dd Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 6 Jun 2025 16:36:32 -0700 Subject: [PATCH 05/11] Depointer TextRangeWithKind --- internal/format/context.go | 12 +++-------- internal/format/rulecontext.go | 2 +- internal/format/scanner.go | 18 ++++++++-------- internal/format/span.go | 38 +++++++++++++++++----------------- 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/internal/format/context.go b/internal/format/context.go index aeb864addd..aae5b935bf 100644 --- a/internal/format/context.go +++ b/internal/format/context.go @@ -86,8 +86,8 @@ func GetDefaultFormatCodeSettings(newLineCharacter string) *FormatCodeSettings { } type formattingContext struct { - currentTokenSpan *TextRangeWithKind - nextTokenSpan *TextRangeWithKind + currentTokenSpan TextRangeWithKind + nextTokenSpan TextRangeWithKind contextNode *ast.Node currentTokenParent *ast.Node nextTokenParent *ast.Node @@ -117,16 +117,10 @@ func NewFormattingContext(file *ast.SourceFile, kind FormatRequestKind, options return res } -func (this *formattingContext) UpdateContext(cur *TextRangeWithKind, curParent *ast.Node, next *TextRangeWithKind, nextParent *ast.Node, commonParent *ast.Node) { - if cur == nil { - panic("nil current range in update context") - } +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 next == nil { - panic("nil next range in update context") - } if nextParent == nil { panic("nil next range node parent in update context") } diff --git a/internal/format/rulecontext.go b/internal/format/rulecontext.go index 9930447e75..0e57e24d38 100644 --- a/internal/format/rulecontext.go +++ b/internal/format/rulecontext.go @@ -494,7 +494,7 @@ func isConstructorSignatureContext(context *formattingContext) bool { return context.contextNode.Kind == ast.KindConstructSignature } -func isTypeArgumentOrParameterOrAssertion(token *TextRangeWithKind, parent *ast.Node) bool { +func isTypeArgumentOrParameterOrAssertion(token TextRangeWithKind, parent *ast.Node) bool { if token.Kind != ast.KindLessThanToken && token.Kind != ast.KindGreaterThanToken { return false } diff --git a/internal/format/scanner.go b/internal/format/scanner.go index 0658fb3646..d71635e518 100644 --- a/internal/format/scanner.go +++ b/internal/format/scanner.go @@ -13,17 +13,17 @@ type TextRangeWithKind struct { Kind ast.Kind } -func NewTextRangeWithKind(pos int, end int, kind ast.Kind) *TextRangeWithKind { - return &TextRangeWithKind{ +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 + leadingTrivia []TextRangeWithKind + token TextRangeWithKind + trailingTrivia []TextRangeWithKind } type formattingScanner struct { @@ -33,8 +33,8 @@ type formattingScanner struct { savedPos int lastTokenInfo *tokenInfo lastScanAction scanAction - leadingTrivia []*TextRangeWithKind - trailingTrivia []*TextRangeWithKind + leadingTrivia []TextRangeWithKind + trailingTrivia []TextRangeWithKind wasNewLine bool } @@ -287,7 +287,7 @@ func (s *formattingScanner) getNextToken(n *ast.Node, expectedScanAction scanAct return token } -func (s *formattingScanner) readEOFTokenRange() *TextRangeWithKind { +func (s *formattingScanner) readEOFTokenRange() TextRangeWithKind { // Debug.assert(isOnEOF()); // !!! return NewTextRangeWithKind( s.s.TokenFullStart(), @@ -332,7 +332,7 @@ func (s *formattingScanner) skipToStartOf(r *core.TextRange) { s.trailingTrivia = nil } -func (s *formattingScanner) getCurrentLeadingTrivia() []*TextRangeWithKind { +func (s *formattingScanner) getCurrentLeadingTrivia() []TextRangeWithKind { return s.leadingTrivia } diff --git a/internal/format/span.go b/internal/format/span.go index 88f18b42fe..1f16f649b6 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -161,7 +161,7 @@ type formatSpanWorker struct { formattingContext *formattingContext edits []core.TextChange - previousRange *TextRangeWithKind + previousRange TextRangeWithKind previousRangeTriviaEnd int previousParent *ast.Node previousRangeStartLine int @@ -257,7 +257,7 @@ func (w *formatSpanWorker) execute(s *formattingScanner) []core.TextChange { indentation += opt.IndentSize // !!! TODO: nil check??? } - w.indentTriviaItems(remainingTrivia, indentation, true, func(item *TextRangeWithKind) { + w.indentTriviaItems(remainingTrivia, indentation, true, func(item TextRangeWithKind) { startLine, startChar := scanner.GetLineAndCharacterOfPosition(w.sourceFile, item.Loc.Pos()) w.processRange(item, startLine, startChar, w.enclosingNode, w.enclosingNode, nil) w.insertIndentation(item.Loc.Pos(), indentation, false) @@ -268,7 +268,7 @@ func (w *formatSpanWorker) execute(s *formattingScanner) []core.TextChange { } } - if w.previousRange != nil && w.formattingScanner.getTokenFullStart() >= w.originalRange.End() { + if w.previousRange != NewTextRangeWithKind(0, 0, 0) && w.formattingScanner.getTokenFullStart() >= w.originalRange.End() { // Formatting edits happen by looking at pairs of contiguous tokens (see `processPair`), // typically inserting or deleting whitespace between them. The recursive `processNode` // logic above bails out as soon as it encounters a token that is beyond the end of the @@ -279,14 +279,14 @@ func (w *formatSpanWorker) execute(s *formattingScanner) []core.TextChange { // inclusive. We would expect a format-selection would delete the space (if rules apply), // but in order to do that, we need to process the pair ["{", "}"], but we stopped processing // just before getting there. This block handles this trailing edit. - var tokenInfo *TextRangeWithKind + var tokenInfo TextRangeWithKind if w.formattingScanner.isOnEOF() { tokenInfo = w.formattingScanner.readEOFTokenRange() } else if w.formattingScanner.isOnToken() { tokenInfo = w.formattingScanner.readTokenInfo(w.enclosingNode).token } - if tokenInfo != nil && tokenInfo.Loc.Pos() == w.previousRangeTriviaEnd { + if tokenInfo.Loc.Pos() == w.previousRangeTriviaEnd { // We need to check that tokenInfo and previousRange are contiguous: the `originalRange` // may have ended in the middle of a token, which means we will have stopped formatting // on that token, leaving `previousRange` pointing to the token before it, but already @@ -624,7 +624,7 @@ func (w *formatSpanWorker) processNode(node *ast.Node, contextNode *ast.Node, no } } -func (w *formatSpanWorker) processPair(currentItem *TextRangeWithKind, currentStartLine int, currentParent *ast.Node, previousItem *TextRangeWithKind, previousStartLine int, previousParent *ast.Node, contextNode *ast.Node, dynamicIndentation *dynamicIndenter) LineAction { +func (w *formatSpanWorker) processPair(currentItem TextRangeWithKind, currentStartLine int, currentParent *ast.Node, previousItem TextRangeWithKind, previousStartLine int, previousParent *ast.Node, contextNode *ast.Node, dynamicIndentation *dynamicIndenter) LineAction { w.formattingContext.UpdateContext(previousItem, previousParent, currentItem, currentParent, contextNode) rules := getRules(w.formattingContext) @@ -673,7 +673,7 @@ func (w *formatSpanWorker) processPair(currentItem *TextRangeWithKind, currentSt return lineAction } -func (w *formatSpanWorker) applyRuleEdits(rule *ruleImpl, previousRange *TextRangeWithKind, previousStartLine int, currentRange *TextRangeWithKind, currentStartLine int) LineAction { +func (w *formatSpanWorker) applyRuleEdits(rule *ruleImpl, previousRange TextRangeWithKind, previousStartLine int, currentRange TextRangeWithKind, currentStartLine int) LineAction { onLaterLine := currentStartLine != previousStartLine switch rule.Action() { case ruleActionStopProcessingSpaceActions: @@ -735,14 +735,14 @@ const ( LineActionLineRemoved ) -func (w *formatSpanWorker) processRange(r *TextRangeWithKind, rangeStartLine int, rangeStartCharacter int, parent *ast.Node, contextNode *ast.Node, dynamicIndentation *dynamicIndenter) LineAction { +func (w *formatSpanWorker) processRange(r TextRangeWithKind, rangeStartLine int, rangeStartCharacter int, parent *ast.Node, contextNode *ast.Node, dynamicIndentation *dynamicIndenter) LineAction { rangeHasError := w.rangeContainsError(r.Loc) lineAction := LineActionNone if !rangeHasError { - if w.previousRange == nil { + if w.previousRange == NewTextRangeWithKind(0, 0, 0) { // trim whitespaces starting from the beginning of the span up to the current line originalStartLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, w.originalRange.Pos()) - w.trimTrailingWhitespacesForLines(originalStartLine, rangeStartLine, nil) + w.trimTrailingWhitespacesForLines(originalStartLine, rangeStartLine, NewTextRangeWithKind(0, 0, 0)) } else { lineAction = w.processPair(r, rangeStartLine, parent, w.previousRange, w.previousRangeStartLine, w.previousParent, contextNode, dynamicIndentation) } @@ -756,7 +756,7 @@ func (w *formatSpanWorker) processRange(r *TextRangeWithKind, rangeStartLine int return lineAction } -func (w *formatSpanWorker) processTrivia(trivia []*TextRangeWithKind, parent *ast.Node, contextNode *ast.Node, dynamicIndentation *dynamicIndenter) { +func (w *formatSpanWorker) processTrivia(trivia []TextRangeWithKind, parent *ast.Node, contextNode *ast.Node, dynamicIndentation *dynamicIndenter) { for _, triviaItem := range trivia { if isComment(triviaItem.Kind) && triviaItem.Loc.ContainedBy(w.originalRange) { triviaItemStartLine, triviaItemStartCharacter := scanner.GetLineAndCharacterOfPosition(w.sourceFile, triviaItem.Loc.Pos()) @@ -769,9 +769,9 @@ func (w *formatSpanWorker) processTrivia(trivia []*TextRangeWithKind, parent *as * Trimming will be done for lines after the previous range. * Exclude comments as they had been previously processed. */ -func (w *formatSpanWorker) trimTrailingWhitespacesForRemainingRange(trivias []*TextRangeWithKind) { +func (w *formatSpanWorker) trimTrailingWhitespacesForRemainingRange(trivias []TextRangeWithKind) { startPos := w.originalRange.Pos() - if w.previousRange != nil { + if w.previousRange != NewTextRangeWithKind(0, 0, 0) { startPos = w.previousRange.Loc.End() } @@ -790,21 +790,21 @@ func (w *formatSpanWorker) trimTrailingWhitespacesForRemainingRange(trivias []*T } } -func (w *formatSpanWorker) trimTrailingWitespacesForPositions(startPos int, endPos int, previousRange *TextRangeWithKind) { +func (w *formatSpanWorker) trimTrailingWitespacesForPositions(startPos int, endPos int, previousRange TextRangeWithKind) { startLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, startPos) endLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, endPos) w.trimTrailingWhitespacesForLines(startLine, endLine+1, previousRange) } -func (w *formatSpanWorker) trimTrailingWhitespacesForLines(line1 int, line2 int, r *TextRangeWithKind) { +func (w *formatSpanWorker) trimTrailingWhitespacesForLines(line1 int, line2 int, r TextRangeWithKind) { lineStarts := scanner.GetLineStarts(w.sourceFile) for line := line1; line < line2; line++ { lineStartPosition := int(lineStarts[line]) lineEndPosition := scanner.GetEndLinePosition(w.sourceFile, line) // do not trim whitespaces in comments or template expression - if r != nil && (isComment(r.Kind) || isStringOrRegularExpressionOrTemplateLiteral(r.Kind)) && r.Loc.Pos() <= lineEndPosition && r.Loc.End() > lineEndPosition { + if r != NewTextRangeWithKind(0, 0, 0) && (isComment(r.Kind) || isStringOrRegularExpressionOrTemplateLiteral(r.Kind)) && r.Loc.Pos() <= lineEndPosition && r.Loc.End() > lineEndPosition { continue } @@ -879,7 +879,7 @@ func (w *formatSpanWorker) indentationIsDifferent(indentationString string, star return indentationString != w.sourceFile.Text()[startLinePosition:startLinePosition+len(indentationString)] } -func (w *formatSpanWorker) indentTriviaItems(trivia []*TextRangeWithKind, commentIndentation int, indentNextTokenOrTrivia bool, indentSingleLine func(item *TextRangeWithKind)) bool { +func (w *formatSpanWorker) indentTriviaItems(trivia []TextRangeWithKind, commentIndentation int, indentNextTokenOrTrivia bool, indentSingleLine func(item TextRangeWithKind)) bool { for _, triviaItem := range trivia { triviaInRange := triviaItem.Loc.ContainedBy(w.originalRange) switch triviaItem.Kind { @@ -1025,7 +1025,7 @@ func (w *formatSpanWorker) consumeTokenAndAdvanceScanner(currentTokenInfo *token if !rangeHasError { if lineAction == LineActionNone { // indent token only if end line of previous range does not match start line of the token - if savePreviousRange != nil { + if savePreviousRange != NewTextRangeWithKind(0, 0, 0) { prevEndLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, savePreviousRange.Loc.End()) indentToken = lastTriviaWasNewLine && tokenStartLine != prevEndLine } @@ -1048,7 +1048,7 @@ func (w *formatSpanWorker) consumeTokenAndAdvanceScanner(currentTokenInfo *token indentNextTokenOrTrivia := true if len(currentTokenInfo.leadingTrivia) > 0 { commentIndentation := dynamicIndenation.getIndentationForComment(currentTokenInfo.token.Kind, tokenIndentation, container) - indentNextTokenOrTrivia = w.indentTriviaItems(currentTokenInfo.leadingTrivia, commentIndentation, indentNextTokenOrTrivia, func(item *TextRangeWithKind) { + indentNextTokenOrTrivia = w.indentTriviaItems(currentTokenInfo.leadingTrivia, commentIndentation, indentNextTokenOrTrivia, func(item TextRangeWithKind) { w.insertIndentation(item.Loc.Pos(), commentIndentation, false) }) } From 64a1c90ce71b2ba51513548d4579e4a5bb3639fd Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 6 Jun 2025 16:59:46 -0700 Subject: [PATCH 06/11] Swap tokenInfo from pointer to concrete --- internal/format/rulesmap.go | 3 +++ internal/format/scanner.go | 50 ++++++++++++++++++++----------------- internal/format/span.go | 6 ++--- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/internal/format/rulesmap.go b/internal/format/rulesmap.go index e88810387b..adbb0221df 100644 --- a/internal/format/rulesmap.go +++ b/internal/format/rulesmap.go @@ -22,6 +22,9 @@ func getRules(context *formattingContext) []*ruleImpl { continue outer } } + if len(rules) == 0 { + rules = make([]*ruleImpl, 0, len(bucket)) + } rules = append(rules, rule) ruleActionMask |= rule.Action() } diff --git a/internal/format/scanner.go b/internal/format/scanner.go index d71635e518..181b823588 100644 --- a/internal/format/scanner.go +++ b/internal/format/scanner.go @@ -27,15 +27,16 @@ type tokenInfo struct { } type formattingScanner struct { - s *scanner.Scanner - startPos int - endPos int - savedPos int - lastTokenInfo *tokenInfo - lastScanAction scanAction - leadingTrivia []TextRangeWithKind - trailingTrivia []TextRangeWithKind - wasNewLine bool + 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 { @@ -54,14 +55,14 @@ func newFormattingScanner(text string, languageVariant core.LanguageVariant, sta res := worker.execute(fmtScn) - fmtScn.lastTokenInfo = nil + fmtScn.hasLastTokenInfo = false scan.Reset() return res } func (s *formattingScanner) advance() { - s.lastTokenInfo = nil + s.hasLastTokenInfo = false isStarted := s.s.TokenFullStart() != s.startPos if isStarted { @@ -124,7 +125,7 @@ func (s *formattingScanner) shouldRescanJsxText(node *ast.Node) bool { if ast.IsJsxText(node) { return true } - if !ast.IsJsxElement(node) || s.lastTokenInfo == nil { + if !ast.IsJsxElement(node) || s.hasLastTokenInfo == false { return false } @@ -160,14 +161,14 @@ const ( actionRescanJsxAttributeValue ) -func fixTokenKind(tokenInfo *tokenInfo, container *ast.Node) *tokenInfo { +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 { +func (s *formattingScanner) readTokenInfo(n *ast.Node) tokenInfo { // Debug.assert(isOnToken()); // !!! // normally scanner returns the smallest available token @@ -190,14 +191,15 @@ func (s *formattingScanner) readTokenInfo(n *ast.Node) *tokenInfo { expectedScanAction = actionScan } - if s.lastTokenInfo != nil && expectedScanAction == s.lastScanAction { + 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. - return fixTokenKind(s.lastTokenInfo, n) + s.lastTokenInfo = fixTokenKind(s.lastTokenInfo, n) + return s.lastTokenInfo } if s.s.TokenFullStart() != s.savedPos { @@ -237,13 +239,15 @@ func (s *formattingScanner) readTokenInfo(n *ast.Node) *tokenInfo { } } - s.lastTokenInfo = &tokenInfo{ + 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 fixTokenKind(s.lastTokenInfo, n) + return s.lastTokenInfo } func (s *formattingScanner) getNextToken(n *ast.Node, expectedScanAction scanAction) ast.Kind { @@ -298,7 +302,7 @@ func (s *formattingScanner) readEOFTokenRange() TextRangeWithKind { func (s *formattingScanner) isOnToken() bool { current := s.s.Token() - if s.lastTokenInfo != nil { + if s.hasLastTokenInfo { current = s.lastTokenInfo.token.Kind } return current != ast.KindEndOfFile && !ast.IsTrivia(current) @@ -306,7 +310,7 @@ func (s *formattingScanner) isOnToken() bool { func (s *formattingScanner) isOnEOF() bool { current := s.s.Token() - if s.lastTokenInfo != nil { + if s.hasLastTokenInfo { current = s.lastTokenInfo.token.Kind } return current == ast.KindEndOfFile @@ -316,7 +320,7 @@ func (s *formattingScanner) skipToEndOf(r *core.TextRange) { s.s.ResetTokenState(r.End()) s.savedPos = s.s.TokenFullStart() s.lastScanAction = actionScan - s.lastTokenInfo = nil + s.hasLastTokenInfo = false s.wasNewLine = false s.leadingTrivia = nil s.trailingTrivia = nil @@ -326,7 +330,7 @@ func (s *formattingScanner) skipToStartOf(r *core.TextRange) { s.s.ResetTokenState(r.Pos()) s.savedPos = s.s.TokenFullStart() s.lastScanAction = actionScan - s.lastTokenInfo = nil + s.hasLastTokenInfo = false s.wasNewLine = false s.leadingTrivia = nil s.trailingTrivia = nil @@ -341,7 +345,7 @@ func (s *formattingScanner) lastTrailingTriviaWasNewLine() bool { } func (s *formattingScanner) getTokenFullStart() int { - if s.lastTokenInfo != nil { + if s.hasLastTokenInfo { return s.lastTokenInfo.token.Loc.Pos() } return s.s.TokenFullStart() diff --git a/internal/format/span.go b/internal/format/span.go index 1f16f649b6..e1fd522da1 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -491,7 +491,7 @@ func (w *formatSpanWorker) processChildNodes( if w.formattingScanner.isOnToken() { tokenInfo = w.formattingScanner.readTokenInfo(parent) } else { - tokenInfo = nil + return } } @@ -499,7 +499,7 @@ func (w *formatSpanWorker) processChildNodes( // there might be the case when current token matches end token but does not considered as one // function (x: function) <-- // without this check close paren will be interpreted as list end token for function expression which is wrong - if tokenInfo != nil && tokenInfo.token.Kind == listEndToken && tokenInfo.token.Loc.ContainedBy(parent.Loc) { + if tokenInfo.token.Kind == listEndToken && tokenInfo.token.Loc.ContainedBy(parent.Loc) { // consume list end token w.consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent /*isListEndToken*/, true) } @@ -1002,7 +1002,7 @@ func (w *formatSpanWorker) recordInsert(start int, text string) { } } -func (w *formatSpanWorker) consumeTokenAndAdvanceScanner(currentTokenInfo *tokenInfo, parent *ast.Node, dynamicIndenation *dynamicIndenter, container *ast.Node, isListEndToken bool) { +func (w *formatSpanWorker) consumeTokenAndAdvanceScanner(currentTokenInfo tokenInfo, parent *ast.Node, dynamicIndenation *dynamicIndenter, container *ast.Node, isListEndToken bool) { // assert(currentTokenInfo.token.Loc.ContainedBy(parent.Loc)) // !!! lastTriviaWasNewLine := w.formattingScanner.lastTrailingTriviaWasNewLine() indentToken := false From 92399cd9e688b04ac16d47e33b78f62045a98927 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 6 Jun 2025 17:19:14 -0700 Subject: [PATCH 07/11] Use just one scratch space for iterating the current rules list --- internal/format/rulesmap.go | 8 ++------ internal/format/span.go | 12 ++++++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/format/rulesmap.go b/internal/format/rulesmap.go index adbb0221df..5c482821a0 100644 --- a/internal/format/rulesmap.go +++ b/internal/format/rulesmap.go @@ -7,10 +7,9 @@ import ( "github.com/microsoft/typescript-go/internal/ast" ) -func getRules(context *formattingContext) []*ruleImpl { +func getRules(context *formattingContext, rules []*ruleImpl) []*ruleImpl { bucket := getRulesMap()[getRuleBucketIndex(context.currentTokenSpan.Kind, context.nextTokenSpan.Kind)] if len(bucket) > 0 { - var rules []*ruleImpl ruleActionMask := ruleActionNone outer: for _, rule := range bucket { @@ -22,16 +21,13 @@ func getRules(context *formattingContext) []*ruleImpl { continue outer } } - if len(rules) == 0 { - rules = make([]*ruleImpl, 0, len(bucket)) - } rules = append(rules, rule) ruleActionMask |= rule.Action() } } return rules } - return nil + return rules } func getRuleBucketIndex(row ast.Kind, column ast.Kind) int { diff --git a/internal/format/span.go b/internal/format/span.go index e1fd522da1..16695cb19c 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -175,6 +175,8 @@ type formatSpanWorker struct { visitingIndenter *dynamicIndenter visitingNodeStartLine int visitingUndecoratedNodeStartLine int + + currentRules []*ruleImpl } func newFormatSpanWorker( @@ -196,6 +198,7 @@ func newFormatSpanWorker( requestKind: requestKind, rangeContainsError: rangeContainsError, sourceFile: sourceFile, + currentRules: make([]*ruleImpl, 0, 32), // increaseInsertionIndex should assert there are no more than 32 rules in a given bucket } } @@ -627,16 +630,17 @@ func (w *formatSpanWorker) processNode(node *ast.Node, contextNode *ast.Node, no func (w *formatSpanWorker) processPair(currentItem TextRangeWithKind, currentStartLine int, currentParent *ast.Node, previousItem TextRangeWithKind, previousStartLine int, previousParent *ast.Node, contextNode *ast.Node, dynamicIndentation *dynamicIndenter) LineAction { w.formattingContext.UpdateContext(previousItem, previousParent, currentItem, currentParent, contextNode) - rules := getRules(w.formattingContext) + w.currentRules = w.currentRules[:0] + w.currentRules = getRules(w.formattingContext, w.currentRules) trimTrailingWhitespaces := w.formattingContext.Options.TrimTrailingWhitespace != false lineAction := LineActionNone - if len(rules) > 0 { + if len(w.currentRules) > 0 { // Apply rules in reverse order so that higher priority rules (which are first in the array) // win in a conflict with lower priority rules. - for i := len(rules) - 1; i >= 0; i-- { - rule := rules[i] + for i := len(w.currentRules) - 1; i >= 0; i-- { + rule := w.currentRules[i] lineAction = w.applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine) if dynamicIndentation != nil { switch lineAction { From 64ea7e34e5fa7daa9f49576744e677283ed3d693 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 9 Jun 2025 12:42:07 -0700 Subject: [PATCH 08/11] Remove actual format cli endpoint for now --- internal/execute/tsc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/execute/tsc.go b/internal/execute/tsc.go index 4ba7f868c6..f85b3d2fb6 100644 --- a/internal/execute/tsc.go +++ b/internal/execute/tsc.go @@ -49,8 +49,8 @@ func CommandLine(sys System, cb cbType, commandLineArgs []string) ExitStatus { fmt.Fprint(sys.Writer(), "Build mode is currently unsupported."+sys.NewLine()) sys.EndWrite() return ExitStatusNotImplemented - case "-f": - return fmtMain(sys, commandLineArgs[1], commandLineArgs[1]) + // case "-f": + // return fmtMain(sys, commandLineArgs[1], commandLineArgs[1]) } } From 251955cffe23be51bced05aff29b10b2ad8828f9 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 9 Jun 2025 16:44:15 -0700 Subject: [PATCH 09/11] Fix lint --- internal/execute/tsc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/execute/tsc.go b/internal/execute/tsc.go index f85b3d2fb6..816ac94398 100644 --- a/internal/execute/tsc.go +++ b/internal/execute/tsc.go @@ -71,7 +71,7 @@ func fmtMain(sys System, input, output string) ExitStatus { fmt.Fprint(sys.Writer(), "File not found: "+input+sys.NewLine()) return ExitStatusNotImplemented } - text := string(fileContent) + text := fileContent pathified := tspath.ToPath(input, sys.GetCurrentDirectory(), true) sourceFile := parser.ParseSourceFile( string(pathified), From 859ca1390d0f4561c8dbd8b9f40901cfdea437df Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 9 Jun 2025 16:45:39 -0700 Subject: [PATCH 10/11] Remove somewhat duplicative benchmark --- internal/execute/fmt_test.go | 95 ------------------------------------ 1 file changed, 95 deletions(-) delete mode 100644 internal/execute/fmt_test.go diff --git a/internal/execute/fmt_test.go b/internal/execute/fmt_test.go deleted file mode 100644 index d248bb9463..0000000000 --- a/internal/execute/fmt_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package execute - -import ( - "fmt" - "io" - "os" - "runtime" - "testing" - "time" - - "github.com/microsoft/typescript-go/internal/bundled" - "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/repo" - "github.com/microsoft/typescript-go/internal/tspath" - "github.com/microsoft/typescript-go/internal/vfs" - "github.com/microsoft/typescript-go/internal/vfs/osvfs" -) - -func BenchmarkFormat(b *testing.B) { - checkerPath := tspath.CombinePaths(repo.TypeScriptSubmodulePath, "src", "compiler", "checker.ts") - - tmp := b.TempDir() - sys := newSystem() - - var count int - - b.ReportAllocs() - time.Sleep(5 * time.Second) - for b.Loop() { - out := tspath.CombinePaths(tmp, fmt.Sprintf("out%d.ts", count)) - code := fmtMain(sys, checkerPath, out) - if code != 0 { - b.Fatalf("Unexpected exit code: %d", code) - } - } -} - -type osSys struct { - writer io.Writer - fs vfs.FS - defaultLibraryPath string - newLine string - cwd string - start time.Time -} - -func (s *osSys) SinceStart() time.Duration { - return time.Since(s.start) -} - -func (s *osSys) Now() time.Time { - return time.Now() -} - -func (s *osSys) FS() vfs.FS { - return s.fs -} - -func (s *osSys) DefaultLibraryPath() string { - return s.defaultLibraryPath -} - -func (s *osSys) GetCurrentDirectory() string { - return s.cwd -} - -func (s *osSys) NewLine() string { - return s.newLine -} - -func (s *osSys) Writer() io.Writer { - return s.writer -} - -func (s *osSys) EndWrite() { - // do nothing, this is needed in the interface for testing - // todo: revisit if improving tsc/build/watch unittest baselines -} - -func newSystem() *osSys { - cwd, err := os.Getwd() - if err != nil { - fmt.Fprintf(os.Stderr, "Error getting current directory: %v\n", err) - os.Exit(int(ExitStatusInvalidProject_OutputsSkipped)) - } - - return &osSys{ - cwd: tspath.NormalizePath(cwd), - fs: bundled.WrapFS(osvfs.FS()), - defaultLibraryPath: bundled.LibPath(), - writer: os.Stdout, - newLine: core.IfElse(runtime.GOOS == "windows", "\r\n", "\n"), - start: time.Now(), - } -} From a995e274d571f39653f71918061d4374e3f666e5 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 10 Jun 2025 11:44:27 -0700 Subject: [PATCH 11/11] Update API --- internal/execute/tsc.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/execute/tsc.go b/internal/execute/tsc.go index 816ac94398..d16bbf13a4 100644 --- a/internal/execute/tsc.go +++ b/internal/execute/tsc.go @@ -77,7 +77,10 @@ func fmtMain(sys System, input, output string) ExitStatus { string(pathified), pathified, text, - core.ScriptTargetESNext, + &core.SourceFileAffectingCompilerOptions{ + EmitScriptTarget: core.ScriptTargetLatest, + }, + nil, scanner.JSDocParsingModeParseAll, ) ast.SetParentInChildren(sourceFile.AsNode())