From 9a33548937c66bc8acc5d92ec65128652eeeb8c4 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:26:22 -0800 Subject: [PATCH 1/5] Cache name table on SourceFile --- internal/ast/ast.go | 60 ++++++++++++++++++++++++++++++++ internal/ls/completions.go | 2 +- internal/ls/findallreferences.go | 2 +- internal/ls/utilities.go | 45 ------------------------ 4 files changed, 62 insertions(+), 47 deletions(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index caad579102..f90e0805af 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -10801,6 +10801,8 @@ type SourceFile struct { tokenFactory *NodeFactory declarationMapMu sync.Mutex declarationMap map[string][]*Node + nameTableOnce sync.Once + nameTable map[string]int } func (f *NodeFactory) NewSourceFile(opts SourceFileParseOptions, text string, statements *NodeList, endOfFileToken *TokenNode) *Node { @@ -10944,6 +10946,64 @@ func (node *SourceFile) ECMALineMap() []core.TextPos { return lineMap } +// GetNameTable returns a map of all names in the file to their positions. +// If the name appears more than once, the value is -1. +func (file *SourceFile) GetNameTable() map[string]int { + file.nameTableOnce.Do(func() { + file.nameTable = file.initializeNameTable() + }) + return file.nameTable +} + +func (file *SourceFile) initializeNameTable() map[string]int { + nameTable := make(map[string]int) + + isTagName := func(node *Node) bool { + return node.Parent != nil && IsJSDocTag(node.Parent) && node.Parent.TagName() == node + } + + isArgumentOfElementAccessExpression := func(node *Node) bool { + return node != nil && node.Parent != nil && + node.Parent.Kind == KindElementAccessExpression && + node.Parent.AsElementAccessExpression().ArgumentExpression == node + } + + // We want to store any numbers/strings if they were a name that could be + // related to a declaration. So, if we have 'import x = require("something")' + // then we want 'something' to be in the name table. Similarly, if we have + // "a['propname']" then we want to store "propname" in the name table. + literalIsName := func(node *Node) bool { + return IsDeclarationName(node) || + node.Parent.Kind == KindExternalModuleReference || + isArgumentOfElementAccessExpression(node) || + IsLiteralComputedPropertyDeclarationName(node) + } + + var walk func(node *Node) bool + walk = func(node *Node) bool { + if IsIdentifier(node) && !isTagName(node) && node.Text() != "" || + IsStringOrNumericLiteralLike(node) && literalIsName(node) || + IsPrivateIdentifier(node) { + text := node.Text() + if _, ok := nameTable[text]; ok { + nameTable[text] = -1 + } else { + nameTable[text] = node.Pos() + } + } + + node.ForEachChild(walk) + jsdocNodes := node.JSDoc(file) + for _, jsdoc := range jsdocNodes { + jsdoc.ForEachChild(walk) + } + return false + } + + file.AsNode().ForEachChild(walk) + return nameTable +} + func (node *SourceFile) IsBound() bool { return node.isBound.Load() } diff --git a/internal/ls/completions.go b/internal/ls/completions.go index bdf7deae13..e8e7003b36 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -3582,7 +3582,7 @@ func (l *LanguageService) getJSCompletionEntries( uniqueNames *collections.Set[string], sortedEntries []*lsproto.CompletionItem, ) []*lsproto.CompletionItem { - nameTable := getNameTable(file) + nameTable := file.GetNameTable() for name, pos := range nameTable { // Skip identifiers produced only from the current location if pos == position { diff --git a/internal/ls/findallreferences.go b/internal/ls/findallreferences.go index 7f96fdb8a4..5744271afa 100644 --- a/internal/ls/findallreferences.go +++ b/internal/ls/findallreferences.go @@ -2391,7 +2391,7 @@ func (state *refState) forEachRelatedSymbol( // Search for all occurrences of an identifier in a source file (and filter out the ones that match). func (state *refState) searchForName(sourceFile *ast.SourceFile, search *refSearch) { - if _, ok := getNameTable(sourceFile)[search.escapedText]; ok { + if _, ok := sourceFile.GetNameTable()[search.escapedText]; ok { state.getReferencesInSourceFile(sourceFile, search, true /*addReferencesHere*/) } } diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 0760b90e1d..777b742ef2 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -452,47 +452,6 @@ func isSeparator(node *ast.Node, candidate *ast.Node) bool { return candidate != nil && node.Parent != nil && (candidate.Kind == ast.KindCommaToken || (candidate.Kind == ast.KindSemicolonToken && node.Parent.Kind == ast.KindObjectLiteralExpression)) } -// Returns a map of all names in the file to their positions. -// !!! cache this -func getNameTable(file *ast.SourceFile) map[string]int { - nameTable := make(map[string]int) - var walk func(node *ast.Node) bool - - walk = func(node *ast.Node) bool { - if ast.IsIdentifier(node) && !isTagName(node) && node.Text() != "" || - ast.IsStringOrNumericLiteralLike(node) && literalIsName(node) || - ast.IsPrivateIdentifier(node) { - text := node.Text() - if _, ok := nameTable[text]; ok { - nameTable[text] = -1 - } else { - nameTable[text] = node.Pos() - } - } - - node.ForEachChild(walk) - jsdocNodes := node.JSDoc(file) - for _, jsdoc := range jsdocNodes { - jsdoc.ForEachChild(walk) - } - return false - } - - file.ForEachChild(walk) - return nameTable -} - -// We want to store any numbers/strings if they were a name that could be -// related to a declaration. So, if we have 'import x = require("something")' -// then we want 'something' to be in the name table. Similarly, if we have -// "a['propname']" then we want to store "propname" in the name table. -func literalIsName(node *ast.NumericOrStringLikeLiteral) bool { - return ast.IsDeclarationName(node) || - node.Parent.Kind == ast.KindExternalModuleReference || - isArgumentOfElementAccessExpression(node) || - ast.IsLiteralComputedPropertyDeclarationName(node) -} - func isLiteralNameOfPropertyDeclarationOrIndexAccess(node *ast.Node) bool { // utilities switch node.Parent.Kind { @@ -582,10 +541,6 @@ func findReferenceInPosition(refs []*ast.FileReference, pos int) *ast.FileRefere return core.Find(refs, func(ref *ast.FileReference) bool { return ref.TextRange.ContainsInclusive(pos) }) } -func isTagName(node *ast.Node) bool { - return node.Parent != nil && ast.IsJSDocTag(node.Parent) && node.Parent.TagName() == node -} - // Assumes `candidate.pos <= position` holds. func positionBelongsToNode(candidate *ast.Node, position int, file *ast.SourceFile) bool { if candidate.Pos() > position { From d861861e512abe84937bf20710c2220f9dcc454c Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:27:26 -0800 Subject: [PATCH 2/5] inline --- internal/ast/ast.go | 83 ++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index f90e0805af..9839ebcf70 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -10950,58 +10950,55 @@ func (node *SourceFile) ECMALineMap() []core.TextPos { // If the name appears more than once, the value is -1. func (file *SourceFile) GetNameTable() map[string]int { file.nameTableOnce.Do(func() { - file.nameTable = file.initializeNameTable() - }) - return file.nameTable -} + nameTable := make(map[string]int) -func (file *SourceFile) initializeNameTable() map[string]int { - nameTable := make(map[string]int) + isTagName := func(node *Node) bool { + return node.Parent != nil && IsJSDocTag(node.Parent) && node.Parent.TagName() == node + } - isTagName := func(node *Node) bool { - return node.Parent != nil && IsJSDocTag(node.Parent) && node.Parent.TagName() == node - } + isArgumentOfElementAccessExpression := func(node *Node) bool { + return node != nil && node.Parent != nil && + node.Parent.Kind == KindElementAccessExpression && + node.Parent.AsElementAccessExpression().ArgumentExpression == node + } - isArgumentOfElementAccessExpression := func(node *Node) bool { - return node != nil && node.Parent != nil && - node.Parent.Kind == KindElementAccessExpression && - node.Parent.AsElementAccessExpression().ArgumentExpression == node - } + // We want to store any numbers/strings if they were a name that could be + // related to a declaration. So, if we have 'import x = require("something")' + // then we want 'something' to be in the name table. Similarly, if we have + // "a['propname']" then we want to store "propname" in the name table. + literalIsName := func(node *Node) bool { + return IsDeclarationName(node) || + node.Parent.Kind == KindExternalModuleReference || + isArgumentOfElementAccessExpression(node) || + IsLiteralComputedPropertyDeclarationName(node) + } - // We want to store any numbers/strings if they were a name that could be - // related to a declaration. So, if we have 'import x = require("something")' - // then we want 'something' to be in the name table. Similarly, if we have - // "a['propname']" then we want to store "propname" in the name table. - literalIsName := func(node *Node) bool { - return IsDeclarationName(node) || - node.Parent.Kind == KindExternalModuleReference || - isArgumentOfElementAccessExpression(node) || - IsLiteralComputedPropertyDeclarationName(node) - } + var walk func(node *Node) bool + walk = func(node *Node) bool { + if IsIdentifier(node) && !isTagName(node) && node.Text() != "" || + IsStringOrNumericLiteralLike(node) && literalIsName(node) || + IsPrivateIdentifier(node) { + text := node.Text() + if _, ok := nameTable[text]; ok { + nameTable[text] = -1 + } else { + nameTable[text] = node.Pos() + } + } - var walk func(node *Node) bool - walk = func(node *Node) bool { - if IsIdentifier(node) && !isTagName(node) && node.Text() != "" || - IsStringOrNumericLiteralLike(node) && literalIsName(node) || - IsPrivateIdentifier(node) { - text := node.Text() - if _, ok := nameTable[text]; ok { - nameTable[text] = -1 - } else { - nameTable[text] = node.Pos() + node.ForEachChild(walk) + jsdocNodes := node.JSDoc(file) + for _, jsdoc := range jsdocNodes { + jsdoc.ForEachChild(walk) } + return false } - node.ForEachChild(walk) - jsdocNodes := node.JSDoc(file) - for _, jsdoc := range jsdocNodes { - jsdoc.ForEachChild(walk) - } - return false - } + file.AsNode().ForEachChild(walk) - file.AsNode().ForEachChild(walk) - return nameTable + file.nameTable = nameTable + }) + return file.nameTable } func (node *SourceFile) IsBound() bool { From 930150f668125c94f5d921186f948bc3dd945998 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:27:40 -0800 Subject: [PATCH 3/5] Prealloc a bit --- internal/ast/ast.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 9839ebcf70..a5b55011ea 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -10950,7 +10950,7 @@ func (node *SourceFile) ECMALineMap() []core.TextPos { // If the name appears more than once, the value is -1. func (file *SourceFile) GetNameTable() map[string]int { file.nameTableOnce.Do(func() { - nameTable := make(map[string]int) + nameTable := make(map[string]int, file.IdentifierCount) isTagName := func(node *Node) bool { return node.Parent != nil && IsJSDocTag(node.Parent) && node.Parent.TagName() == node From 4c29a7fe88421b06723a33831b76a63b49db8a23 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:41:44 -0800 Subject: [PATCH 4/5] Move around --- internal/ast/ast.go | 24 +----------------------- internal/ast/utilities.go | 21 +++++++++++++++++++++ internal/ls/utilities.go | 5 ----- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index a5b55011ea..4939307b77 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -10952,27 +10952,6 @@ func (file *SourceFile) GetNameTable() map[string]int { file.nameTableOnce.Do(func() { nameTable := make(map[string]int, file.IdentifierCount) - isTagName := func(node *Node) bool { - return node.Parent != nil && IsJSDocTag(node.Parent) && node.Parent.TagName() == node - } - - isArgumentOfElementAccessExpression := func(node *Node) bool { - return node != nil && node.Parent != nil && - node.Parent.Kind == KindElementAccessExpression && - node.Parent.AsElementAccessExpression().ArgumentExpression == node - } - - // We want to store any numbers/strings if they were a name that could be - // related to a declaration. So, if we have 'import x = require("something")' - // then we want 'something' to be in the name table. Similarly, if we have - // "a['propname']" then we want to store "propname" in the name table. - literalIsName := func(node *Node) bool { - return IsDeclarationName(node) || - node.Parent.Kind == KindExternalModuleReference || - isArgumentOfElementAccessExpression(node) || - IsLiteralComputedPropertyDeclarationName(node) - } - var walk func(node *Node) bool walk = func(node *Node) bool { if IsIdentifier(node) && !isTagName(node) && node.Text() != "" || @@ -10993,8 +10972,7 @@ func (file *SourceFile) GetNameTable() map[string]int { } return false } - - file.AsNode().ForEachChild(walk) + file.ForEachChild(walk) file.nameTable = nameTable }) diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index c2adc539ab..6338dedc58 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -4159,3 +4159,24 @@ func GetRestParameterElementType(node *ParameterDeclarationNode) *Node { } return nil } + +func isTagName(node *Node) bool { + return node.Parent != nil && IsJSDocTag(node.Parent) && node.Parent.TagName() == node +} + +// We want to store any numbers/strings if they were a name that could be +// related to a declaration. So, if we have 'import x = require("something")' +// then we want 'something' to be in the name table. Similarly, if we have +// "a['propname']" then we want to store "propname" in the name table. +func literalIsName(node *Node) bool { + return IsDeclarationName(node) || + node.Parent.Kind == KindExternalModuleReference || + isArgumentOfElementAccessExpression(node) || + IsLiteralComputedPropertyDeclarationName(node) +} + +func isArgumentOfElementAccessExpression(node *Node) bool { + return node != nil && node.Parent != nil && + node.Parent.Kind == KindElementAccessExpression && + node.Parent.AsElementAccessExpression().ArgumentExpression == node +} diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 777b742ef2..db625e7b9a 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -483,11 +483,6 @@ func isObjectBindingElementWithoutPropertyName(bindingElement *ast.Node) bool { bindingElement.PropertyName() == nil } -func isArgumentOfElementAccessExpression(node *ast.Node) bool { - return node != nil && node.Parent != nil && - node.Parent.Kind == ast.KindElementAccessExpression && - node.Parent.AsElementAccessExpression().ArgumentExpression == node -} func isRightSideOfPropertyAccess(node *ast.Node) bool { return node.Parent.Kind == ast.KindPropertyAccessExpression && node.Parent.Name() == node From 52807573f30c6d0fad1f01ad14a2e99c13d581ba Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:57:44 -0800 Subject: [PATCH 5/5] fmt --- internal/ls/utilities.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index db625e7b9a..9ffa49ae30 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -483,7 +483,6 @@ func isObjectBindingElementWithoutPropertyName(bindingElement *ast.Node) bool { bindingElement.PropertyName() == nil } - func isRightSideOfPropertyAccess(node *ast.Node) bool { return node.Parent.Kind == ast.KindPropertyAccessExpression && node.Parent.Name() == node }