Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/compiler/processingDiagnostic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
33 changes: 26 additions & 7 deletions internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand All @@ -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)
Expand All @@ -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
}
Expand All @@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down