diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 397ad9cbef..ea547e0e12 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -856,6 +856,7 @@ type Checker struct { activeTypeMappersCaches []map[string]*Type ambientModulesOnce sync.Once ambientModules []*ast.Symbol + spellingSuggestionBuffers *core.SpellingSuggestionBuffers } func NewChecker(program Program) *Checker { @@ -1007,6 +1008,7 @@ func NewChecker(program Program) *Checker { c.anyBaseTypeIndexInfo = &IndexInfo{keyType: c.stringType, valueType: c.anyType, isReadonly: false} c.emptyStringType = c.getStringLiteralType("") c.zeroType = c.getNumberLiteralType(0) + c.spellingSuggestionBuffers = &core.SpellingSuggestionBuffers{} c.zeroBigIntType = c.getBigIntLiteralType(jsnum.PseudoBigInt{}) c.typeofType = c.getUnionType(core.Map(slices.Sorted(maps.Keys(typeofNEFacts)), c.getStringLiteralType)) c.flowLoopCache = make(map[FlowLoopKey]*Type) @@ -1716,7 +1718,7 @@ func (c *Checker) getSpellingSuggestionForName(name string, symbols []*ast.Symbo } return "" } - return core.GetSpellingSuggestion(name, symbols, getCandidateName) + return core.GetSpellingSuggestion(name, symbols, getCandidateName, c.spellingSuggestionBuffers) } func (c *Checker) onSuccessfullyResolvedSymbol(errorLocation *ast.Node, result *ast.Symbol, meaning ast.SymbolFlags, lastLocation *ast.Node, associatedDeclarationForContainingInitializerOrBindingName *ast.Node, withinDeferredContext bool) { @@ -26236,7 +26238,7 @@ func (c *Checker) getSuggestionForNonexistentIndexSignature(objectType *Type, ex func (c *Checker) getSuggestedTypeForNonexistentStringLiteralType(source *Type, target *Type) *Type { candidates := core.Filter(target.Types(), func(t *Type) bool { return t.flags&TypeFlagsStringLiteral != 0 }) - return core.GetSpellingSuggestion(getStringLiteralValue(source), candidates, getStringLiteralValue) + return core.GetSpellingSuggestion(getStringLiteralValue(source), candidates, getStringLiteralValue, c.spellingSuggestionBuffers) } func getIndexNodeForAccessExpression(accessNode *ast.Node) *ast.Node { diff --git a/internal/compiler/processingDiagnostic.go b/internal/compiler/processingDiagnostic.go index 83bac967ab..ea8319f59d 100644 --- a/internal/compiler/processingDiagnostic.go +++ b/internal/compiler/processingDiagnostic.go @@ -49,7 +49,7 @@ func (d *processingDiagnostic) toDiagnostic(program *Program) *ast.Diagnostic { case fileIncludeKindLibReferenceDirective: libName := tspath.ToFileNameLowerCase(loc.ref.FileName) unqualifiedLibName := strings.TrimSuffix(strings.TrimPrefix(libName, "lib."), ".d.ts") - suggestion := core.GetSpellingSuggestion(unqualifiedLibName, tsoptions.Libs, core.Identity) + suggestion := core.GetSpellingSuggestion(unqualifiedLibName, tsoptions.Libs, core.Identity, nil) return loc.diagnosticAt(core.IfElse( suggestion != "", diagnostics.Cannot_find_lib_definition_for_0_Did_you_mean_1, diff --git a/internal/core/core.go b/internal/core/core.go index e5579f6e57..304e94f7d2 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -452,6 +452,10 @@ func GetScriptKindFromFileName(fileName string) ScriptKind { return ScriptKindUnknown } +type SpellingSuggestionBuffers struct { + previous, current []float64 +} + // Given a name and a list of names that are *not* equal to the name, return a spelling suggestion if there is one that is close enough. // Names less than length 3 only check for case-insensitive equality. // @@ -465,7 +469,7 @@ func GetScriptKindFromFileName(fileName string) ScriptKind { // and 1 insertion/deletion at 3 characters) // // @internal -func GetSpellingSuggestion[T any](name string, candidates []T, getName func(T) string) T { +func GetSpellingSuggestion[T any](name string, candidates []T, getName func(T) string, buffers *SpellingSuggestionBuffers) T { maximumLengthDifference := max(2, int(float64(len(name))*0.34)) bestDistance := math.Floor(float64(len(name))*0.4) + 1 // If the best result is worse than this, don't bother. runeName := []rune(name) @@ -483,7 +487,7 @@ func GetSpellingSuggestion[T any](name string, candidates []T, getName func(T) s if len(candidateName) < 3 && !strings.EqualFold(candidateName, name) { continue } - distance := levenshteinWithMax(runeName, []rune(candidateName), bestDistance-0.1) + distance := levenshteinWithMax(runeName, []rune(candidateName), bestDistance-0.1, buffers) if distance < 0 { continue } @@ -495,9 +499,24 @@ func GetSpellingSuggestion[T any](name string, candidates []T, getName func(T) s return bestCandidate } -func levenshteinWithMax(s1 []rune, s2 []rune, maxValue float64) float64 { - previous := make([]float64, len(s2)+1) - current := make([]float64, len(s2)+1) +func ensureSize(slice []float64, desired int) []float64 { + if cap(slice) < desired { + return make([]float64, desired) + } + return slice[:desired] +} + +func levenshteinWithMax(s1 []rune, s2 []rune, maxValue float64, buffers *SpellingSuggestionBuffers) float64 { + if buffers == nil { + buffers = &SpellingSuggestionBuffers{} + } + + buffers.previous = ensureSize(buffers.previous, len(s2)+1) + previous := buffers.previous + + buffers.current = ensureSize(buffers.current, len(s2)+1) + current := buffers.current + big := maxValue + 0.01 for i := range previous { previous[i] = float64(i) @@ -521,10 +540,10 @@ func levenshteinWithMax(s1 []rune, s2 []rune, maxValue float64) float64 { if c1 == s2[j-1] { dist = previous[j-1] } else { - dist = math.Min(previous[j]+1, math.Min(current[j-1]+1, substitutionDistance)) + dist = min(previous[j]+1, min(current[j-1]+1, substitutionDistance)) } current[j] = dist - colMin = math.Min(colMin, dist) + colMin = min(colMin, dist) } for j := maxJ + 1; j <= len(s2); j++ { current[j] = big diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 67bb38207c..fe46426c41 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -1884,7 +1884,7 @@ func (p *Parser) parseErrorForMissingSemicolonAfter(node *ast.Node) { return } // The user alternatively might have misspelled or forgotten to add a space after a common keyword. - suggestion := core.GetSpellingSuggestion(expressionText, viableKeywordSuggestions, func(s string) string { return s }) + suggestion := core.GetSpellingSuggestion(expressionText, viableKeywordSuggestions, func(s string) string { return s }, nil) if suggestion == "" { suggestion = getSpaceSuggestion(expressionText) }