Skip to content

Improve memory usage of formatter #1107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 10, 2025
55 changes: 55 additions & 0 deletions internal/execute/tsc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,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
Expand All @@ -28,6 +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])
}
}

Expand All @@ -39,6 +62,38 @@ 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 := fileContent
pathified := tspath.ToPath(input, sys.GetCurrentDirectory(), true)
sourceFile := parser.ParseSourceFile(
string(pathified),
pathified,
text,
&core.SourceFileAffectingCompilerOptions{
EmitScriptTarget: core.ScriptTargetLatest,
},
nil,
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())
Expand Down
12 changes: 3 additions & 9 deletions internal/format/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
Expand Down
6 changes: 5 additions & 1 deletion internal/format/indent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
10 changes: 5 additions & 5 deletions internal/format/rulecontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 2 additions & 3 deletions internal/format/rulesmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -28,7 +27,7 @@ func getRules(context *formattingContext) []*ruleImpl {
}
return rules
}
return nil
return rules
}

func getRuleBucketIndex(row ast.Kind, column ast.Kind) int {
Expand Down
64 changes: 34 additions & 30 deletions internal/format/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,30 @@ 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 {
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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -287,7 +291,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(),
Expand All @@ -298,15 +302,15 @@ 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)
}

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
Expand All @@ -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
Expand All @@ -326,13 +330,13 @@ 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
}

func (s *formattingScanner) getCurrentLeadingTrivia() []*TextRangeWithKind {
func (s *formattingScanner) getCurrentLeadingTrivia() []TextRangeWithKind {
return s.leadingTrivia
}

Expand All @@ -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()
Expand Down
Loading