Skip to content
Merged
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
66 changes: 41 additions & 25 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1689,32 +1689,50 @@ func (c *Checker) getSuggestedLibForNonExistentName(name string) string {
return ""
}

func (c *Checker) getPrimitiveAliasSymbols() {
var symbols []*ast.Symbol
for _, name := range []string{"string", "number", "boolean", "object", "bigint", "symbol"} {
symbols = append(symbols, c.newSymbol(ast.SymbolFlagsTypeAlias, name))
}
}

func (c *Checker) getSuggestedSymbolForNonexistentSymbol(location *ast.Node, outerName string, meaning ast.SymbolFlags) *ast.Symbol {
return c.resolveNameForSymbolSuggestion(location, outerName, meaning, nil /*nameNotFoundMessage*/, false /*isUse*/, false /*excludeGlobals*/)
}

var primitiveTypeAliasSuggestions = sync.OnceValue(func() map[string]*ast.Symbol {
result := make(map[string]*ast.Symbol, 6)
for _, e := range []struct{ primitive, builtin string }{
{"string", "String"},
{"number", "Number"},
{"boolean", "Boolean"},
{"object", "Object"},
{"bigint", "BigInt"},
{"symbol", "Symbol"},
} {
sym := &ast.Symbol{}
sym.Flags = ast.SymbolFlagsTypeAlias | ast.SymbolFlagsTransient
sym.Name = e.primitive
result[e.builtin] = sym
}
return result
})

func getPrimitiveTypeAliasSuggestions(symbols ast.SymbolTable) iter.Seq[*ast.Symbol] {
return func(yield func(*ast.Symbol) bool) {
for builtinName, suggestion := range primitiveTypeAliasSuggestions() {
if _, ok := symbols[builtinName]; ok {
if !yield(suggestion) {
return
}
}
}
}
}

func (c *Checker) getSuggestionForSymbolNameLookup(symbols ast.SymbolTable, name string, meaning ast.SymbolFlags) *ast.Symbol {
symbol := c.getSymbol(symbols, name, meaning)
if symbol != nil {
return symbol
}
allSymbols := slices.AppendSeq(make([]*ast.Symbol, 0, len(symbols)), maps.Values(symbols))
var extras iter.Seq[*ast.Symbol]
if meaning&ast.SymbolFlagsGlobalLookup != 0 {
for _, s := range []string{"stringString", "numberNumber", "booleanBoolean", "objectObject", "bigintBigInt", "symbolSymbol"} {
if _, ok := symbols[s[len(s)/2:]]; ok {
allSymbols = append(allSymbols, c.newSymbol(ast.SymbolFlagsTypeAlias, s[:len(s)/2]))
}
}
extras = getPrimitiveTypeAliasSuggestions(symbols)
}
c.sortSymbols(allSymbols)
return c.getSpellingSuggestionForName(name, allSymbols, meaning)
return c.getSpellingSuggestionForName(name, core.ConcatenateSeq(maps.Values(symbols), extras), meaning)
}

// Given a name and a list of symbols whose names are *not* equal to the name, return a spelling suggestion if there is
Expand All @@ -1730,7 +1748,7 @@ func (c *Checker) getSuggestionForSymbolNameLookup(symbols ast.SymbolTable, name
// - Whose length differs from the target name by more than 0.34 of the length of the name.
// - Whose levenshtein distance is more than 0.4 of the length of the name (0.4 allows 1 substitution/transposition
// for every 5 characters, and 1 insertion/deletion at 3 characters)
func (c *Checker) getSpellingSuggestionForName(name string, symbols []*ast.Symbol, meaning ast.SymbolFlags) *ast.Symbol {
func (c *Checker) getSpellingSuggestionForName(name string, symbols iter.Seq[*ast.Symbol], meaning ast.SymbolFlags) *ast.Symbol {
getCandidateName := func(candidate *ast.Symbol) string {
candidateName := ast.SymbolName(candidate)
if len(candidateName) == 0 || candidateName[0] == '"' || candidateName[0] == '\xFE' {
Expand All @@ -1747,7 +1765,7 @@ func (c *Checker) getSpellingSuggestionForName(name string, symbols []*ast.Symbo
}
return ""
}
return core.GetSpellingSuggestion(name, symbols, getCandidateName)
return core.GetSpellingSuggestion(name, symbols, getCandidateName, c.compareSymbols)
}

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 @@ -4643,7 +4661,7 @@ func (c *Checker) checkMemberForOverrideModifier(node *ast.Node, staticType *Typ
}

func (c *Checker) getSuggestedSymbolForNonexistentClassMember(name string, baseType *Type) *ast.Symbol {
return c.getSpellingSuggestionForName(name, c.getPropertiesOfType(baseType), ast.SymbolFlagsClassMember)
return c.getSpellingSuggestionForName(name, slices.Values(c.getPropertiesOfType(baseType)), ast.SymbolFlagsClassMember)
}

func (c *Checker) checkIndexConstraints(t *Type, symbol *ast.Symbol, isStaticIndex bool) {
Expand Down Expand Up @@ -11174,7 +11192,7 @@ func (c *Checker) getSuggestedSymbolForNonexistentProperty(name *ast.Node, conta
return c.isValidPropertyAccessForCompletions(parent, containingType, prop)
})
}
return c.getSpellingSuggestionForName(name.Text(), props, ast.SymbolFlagsValue)
return c.getSpellingSuggestionForName(name.Text(), slices.Values(props), ast.SymbolFlagsValue)
}

// Checks if an existing property access is valid for completions purposes.
Expand Down Expand Up @@ -15395,9 +15413,7 @@ func (c *Checker) tryGetQualifiedNameAsValue(node *ast.Node) *ast.Symbol {
}

func (c *Checker) getSuggestedSymbolForNonexistentModule(name *ast.Node, targetModule *ast.Symbol) *ast.Symbol {
exports := slices.Collect(maps.Values(c.getExportsOfModule(targetModule)))
c.sortSymbols(exports)
return c.getSpellingSuggestionForName(name.Text(), exports, ast.SymbolFlagsModuleMember)
return c.getSpellingSuggestionForName(name.Text(), maps.Values(c.getExportsOfModule(targetModule)), ast.SymbolFlagsModuleMember)
}

func (c *Checker) getFullyQualifiedName(symbol *ast.Symbol, containingLocation *ast.Node) string {
Expand Down Expand Up @@ -26495,7 +26511,7 @@ func (c *Checker) typeHasStaticProperty(propName string, containingType *Type) b
}

func (c *Checker) getSuggestionForNonexistentProperty(name string, containingType *Type) string {
symbol := c.getSpellingSuggestionForName(name, c.getPropertiesOfType(containingType), ast.SymbolFlagsValue)
symbol := c.getSpellingSuggestionForName(name, slices.Values(c.getPropertiesOfType(containingType)), ast.SymbolFlagsValue)
if symbol != nil {
return symbol.Name
}
Expand Down Expand Up @@ -26524,8 +26540,8 @@ 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)
candidates := core.FilterSeq(target.Types(), func(t *Type) bool { return t.flags&TypeFlagsStringLiteral != 0 })
return core.GetSpellingSuggestion(getStringLiteralValue(source), candidates, getStringLiteralValue, CompareTypes)
}

func getIndexNodeForAccessExpression(accessNode *ast.Node) *ast.Node {
Expand Down
2 changes: 1 addition & 1 deletion internal/checker/jsx.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ func (c *Checker) getSuggestedSymbolForNonexistentJSXAttribute(name string, cont
if jsxSpecific != nil {
return jsxSpecific
}
return c.getSpellingSuggestionForName(name, properties, ast.SymbolFlagsValue)
return c.getSpellingSuggestionForName(name, slices.Values(properties), ast.SymbolFlagsValue)
}

func (c *Checker) getJSXFragmentType(node *ast.Node) *Type {
Expand Down
3 changes: 2 additions & 1 deletion internal/compiler/processingDiagnostic.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package compiler

import (
"slices"
"strings"

"github.com/microsoft/typescript-go/internal/ast"
Expand Down Expand Up @@ -49,7 +50,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.GetSpellingSuggestionForStrings(unqualifiedLibName, slices.Values(tsoptions.Libs))
return loc.diagnosticAt(core.IfElse(
suggestion != "",
diagnostics.Cannot_find_lib_definition_for_0_Did_you_mean_1,
Expand Down
37 changes: 30 additions & 7 deletions internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ func Filter[T any](slice []T, f func(T) bool) []T {
return slice
}

func FilterSeq[T any](slice []T, f func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for _, value := range slice {
if f(value) {
if !yield(value) {
return
}
}
}
}
}

func FilterIndex[T any](slice []T, f func(T, int, []T) bool) []T {
for i, value := range slice {
if !f(value, i, slice) {
Expand Down Expand Up @@ -529,14 +541,15 @@ 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 iter.Seq[T], getName func(T) string, compare func(T, T) int) 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.
bestDistance := math.Floor(float64(len(name))*0.4) + 0.9 // If the best result is worse than this, don't bother.
runeName := []rune(name)
buffers := levenshteinBuffersPool.Get().(*levenshteinBuffers)
defer levenshteinBuffersPool.Put(buffers)
var bestCandidate T
for _, candidate := range candidates {
hasBest := false
for candidate := range candidates {
candidateName := getName(candidate)
maxLen := max(len(candidateName), len(name))
minLen := min(len(candidateName), len(name))
Expand All @@ -549,18 +562,28 @@ func GetSpellingSuggestion[T any](name string, candidates []T, getName func(T) s
if len(candidateName) < 3 && !strings.EqualFold(candidateName, name) {
continue
}
distance := levenshteinWithMax(buffers, runeName, []rune(candidateName), bestDistance-0.1)
distance := levenshteinWithMax(buffers, runeName, []rune(candidateName), bestDistance)
if distance < 0 {
continue
}
debug.Assert(distance < bestDistance) // Else `levenshteinWithMax` should return undefined
bestDistance = distance
bestCandidate = candidate
debug.Assert(distance <= bestDistance) // Else `levenshteinWithMax` should return undefined
if distance < bestDistance {
bestDistance = distance
bestCandidate = candidate
hasBest = true
} else if !hasBest || compare(candidate, bestCandidate) < 0 {
bestCandidate = candidate
Comment thread
jakebailey marked this conversation as resolved.
hasBest = true
}
}
}
return bestCandidate
}

func GetSpellingSuggestionForStrings(name string, candidates iter.Seq[string]) string {
return GetSpellingSuggestion(name, candidates, Identity, strings.Compare)
}

type levenshteinBuffers struct {
previous []float64
current []float64
Expand Down
2 changes: 1 addition & 1 deletion internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2031,7 +2031,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.GetSpellingSuggestionForStrings(expressionText, slices.Values(viableKeywordSuggestions))
if suggestion == "" {
suggestion = getSpaceSuggestion(expressionText)
}
Expand Down
35 changes: 9 additions & 26 deletions internal/scanner/regexp.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scanner

import (
"maps"
"math"
"strconv"
"strings"
Expand Down Expand Up @@ -951,37 +952,23 @@ func (p *regExpParser) scanCharacterClassEscape() bool {
}

func (p *regExpParser) getSpellingSuggestionForUnicodePropertyName(name string) string {
candidates := make([]string, 0, len(nonBinaryUnicodeProperties))
for k := range nonBinaryUnicodeProperties {
candidates = append(candidates, k)
}
return core.GetSpellingSuggestion(name, candidates, func(s string) string { return s })
return core.GetSpellingSuggestionForStrings(name, maps.Keys(nonBinaryUnicodeProperties))
}

func (p *regExpParser) getSpellingSuggestionForUnicodePropertyValue(propertyName string, value string) string {
values := valuesOfNonBinaryUnicodeProperties[propertyName]
if values == nil {
return ""
}
candidates := make([]string, 0, values.Len())
for k := range values.Keys() {
candidates = append(candidates, k)
}
return core.GetSpellingSuggestion(value, candidates, func(s string) string { return s })
return core.GetSpellingSuggestionForStrings(value, maps.Keys(values.Keys()))
}

func (p *regExpParser) getSpellingSuggestionForUnicodePropertyNameOrValue(name string) string {
var candidates []string
for k := range valuesOfNonBinaryUnicodeProperties["General_Category"].Keys() {
candidates = append(candidates, k)
}
for k := range binaryUnicodeProperties.Keys() {
candidates = append(candidates, k)
}
for k := range binaryUnicodePropertiesOfStrings.Keys() {
candidates = append(candidates, k)
}
return core.GetSpellingSuggestion(name, candidates, func(s string) string { return s })
return core.GetSpellingSuggestionForStrings(name, core.ConcatenateSeq(
maps.Keys(valuesOfNonBinaryUnicodeProperties["General_Category"].Keys()),
maps.Keys(binaryUnicodeProperties.Keys()),
maps.Keys(binaryUnicodePropertiesOfStrings.Keys()),
))
}

func (p *regExpParser) scanWordCharacters() string {
Expand Down Expand Up @@ -1062,11 +1049,7 @@ func (p *regExpParser) run() {
if !p.groupSpecifiers[reference.name] {
p.error(diagnostics.There_is_no_capturing_group_named_0_in_this_regular_expression, reference.pos, reference.end-reference.pos, reference.name)
if len(p.groupSpecifiers) > 0 {
specifiers := make([]string, 0, len(p.groupSpecifiers))
for k := range p.groupSpecifiers {
specifiers = append(specifiers, k)
}
suggestion := core.GetSpellingSuggestion(reference.name, specifiers, func(s string) string { return s })
suggestion := core.GetSpellingSuggestionForStrings(reference.name, maps.Keys(p.groupSpecifiers))
if suggestion != "" {
p.error(diagnostics.Did_you_mean_0, reference.pos, reference.end-reference.pos, suggestion)
}
Expand Down
Loading