453 lines
16 KiB
Go
453 lines
16 KiB
Go
package ls
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/microsoft/typescript-go/internal/ast"
|
|
"github.com/microsoft/typescript-go/internal/astnav"
|
|
"github.com/microsoft/typescript-go/internal/checker"
|
|
"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/locale"
|
|
"github.com/microsoft/typescript-go/internal/ls/autoimport"
|
|
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
|
"github.com/microsoft/typescript-go/internal/scanner"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
)
|
|
|
|
var importFixErrorCodes = []int32{
|
|
diagnostics.Cannot_find_name_0.Code(),
|
|
diagnostics.Cannot_find_name_0_Did_you_mean_1.Code(),
|
|
diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0.Code(),
|
|
diagnostics.Cannot_find_name_0_Did_you_mean_the_static_member_1_0.Code(),
|
|
diagnostics.Cannot_find_namespace_0.Code(),
|
|
diagnostics.X_0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.Code(),
|
|
diagnostics.X_0_only_refers_to_a_type_but_is_being_used_as_a_value_here.Code(),
|
|
diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer.Code(),
|
|
diagnostics.X_0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type.Code(),
|
|
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slashjquery.Code(),
|
|
diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_1_or_later.Code(),
|
|
diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_include_dom.Code(),
|
|
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_save_dev_types_Slashjest_or_npm_i_save_dev_types_Slashmocha_and_then_add_jest_or_mocha_to_the_types_field_in_your_tsconfig.Code(),
|
|
diagnostics.Cannot_find_name_0_Did_you_mean_to_write_this_in_an_async_function.Code(),
|
|
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slashjquery_and_then_add_jquery_to_the_types_field_in_your_tsconfig.Code(),
|
|
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_save_dev_types_Slashjest_or_npm_i_save_dev_types_Slashmocha.Code(),
|
|
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashnode.Code(),
|
|
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashnode_and_then_add_node_to_the_types_field_in_your_tsconfig.Code(),
|
|
diagnostics.Cannot_find_namespace_0_Did_you_mean_1.Code(),
|
|
diagnostics.Cannot_extend_an_interface_0_Did_you_mean_implements.Code(),
|
|
diagnostics.This_JSX_tag_requires_0_to_be_in_scope_but_it_could_not_be_found.Code(),
|
|
}
|
|
|
|
const (
|
|
importFixID = "fixMissingImport"
|
|
)
|
|
|
|
// ImportFixProvider is the CodeFixProvider for import-related fixes
|
|
var ImportFixProvider = &CodeFixProvider{
|
|
ErrorCodes: importFixErrorCodes,
|
|
GetCodeActions: getImportCodeActions,
|
|
FixIds: []string{importFixID},
|
|
GetAllCodeActions: getAllImportCodeActions,
|
|
}
|
|
|
|
type fixInfo struct {
|
|
fix *autoimport.Fix
|
|
symbolName string
|
|
errorIdentifierText string
|
|
isJsxNamespaceFix bool
|
|
}
|
|
|
|
func getImportCodeActions(ctx context.Context, fixContext *CodeFixContext) ([]*CodeAction, error) {
|
|
info, err := getFixInfos(ctx, fixContext, fixContext.ErrorCode, fixContext.Span.Pos())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(info) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var actions []*CodeAction
|
|
for _, fixInfo := range info {
|
|
edits, description := fixInfo.fix.Edits(
|
|
ctx,
|
|
fixContext.SourceFile,
|
|
fixContext.Program.Options(),
|
|
fixContext.LS.FormatOptions(),
|
|
fixContext.LS.converters,
|
|
fixContext.LS.UserPreferences(),
|
|
)
|
|
|
|
actions = append(actions, &CodeAction{
|
|
Description: description,
|
|
Changes: edits,
|
|
FixID: importFixID,
|
|
FixAllDescription: diagnostics.Add_all_missing_imports.Localize(locale.FromContext(ctx)),
|
|
})
|
|
}
|
|
return actions, nil
|
|
}
|
|
|
|
func getAllImportCodeActions(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) {
|
|
if tspath.IsDynamicFileName(fixContext.SourceFile.FileName()) {
|
|
return nil, nil
|
|
}
|
|
|
|
allDiagnostics := fixContext.Program.GetSemanticDiagnostics(ctx, fixContext.SourceFile)
|
|
|
|
var importDiags []*ast.Diagnostic
|
|
for _, diag := range allDiagnostics {
|
|
if containsErrorCode(importFixErrorCodes, diag.Code()) {
|
|
importDiags = append(importDiags, diag)
|
|
}
|
|
}
|
|
|
|
if len(importDiags) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
view, err := fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if view == nil {
|
|
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
|
|
}
|
|
|
|
ch, done := fixContext.Program.GetTypeChecker(ctx)
|
|
defer done()
|
|
|
|
importAdder := autoimport.NewImportAdder(
|
|
ctx,
|
|
fixContext.Program,
|
|
ch,
|
|
fixContext.SourceFile,
|
|
view,
|
|
fixContext.LS.FormatOptions(),
|
|
fixContext.LS.converters,
|
|
fixContext.LS.UserPreferences(),
|
|
)
|
|
|
|
for _, diag := range importDiags {
|
|
if err := addImportFromDiagnostic(ctx, importAdder, diag, fixContext); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if !importAdder.HasFixes() {
|
|
return nil, nil
|
|
}
|
|
|
|
return &CombinedCodeActions{
|
|
Description: diagnostics.Add_all_missing_imports.Localize(locale.FromContext(ctx)),
|
|
Changes: importAdder.Edits(),
|
|
}, nil
|
|
}
|
|
|
|
// addImportFromDiagnostic finds the best import fix for a diagnostic and adds it to the adder.
|
|
func addImportFromDiagnostic(ctx context.Context, importAdder autoimport.ImportAdder, diag *ast.Diagnostic, fixContext *CodeFixContext) error {
|
|
diagFixContext := &CodeFixContext{
|
|
SourceFile: fixContext.SourceFile,
|
|
Span: core.NewTextRange(diag.Pos(), diag.End()),
|
|
ErrorCode: diag.Code(),
|
|
Program: fixContext.Program,
|
|
LS: fixContext.LS,
|
|
}
|
|
|
|
infos, err := getFixInfos(ctx, diagFixContext, diag.Code(), diag.Pos())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(infos) > 0 {
|
|
importAdder.AddImportFix(infos[0].fix)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int32, pos int) ([]*fixInfo, error) {
|
|
// Can't compute import fixes for dynamic/untitled files since they don't have real file paths
|
|
if tspath.IsDynamicFileName(fixContext.SourceFile.FileName()) {
|
|
return nil, nil
|
|
}
|
|
|
|
symbolToken := astnav.GetTokenAtPosition(fixContext.SourceFile, pos)
|
|
|
|
var view *autoimport.View
|
|
var info []*fixInfo
|
|
|
|
if errorCode == diagnostics.X_0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.Code() {
|
|
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
|
|
info = getFixesInfoForUMDImport(ctx, fixContext, symbolToken, view)
|
|
} else if !ast.IsIdentifier(symbolToken) {
|
|
return nil, nil
|
|
} else if errorCode == diagnostics.X_0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type.Code() {
|
|
ch, done := fixContext.Program.GetTypeChecker(ctx)
|
|
defer done()
|
|
compilerOptions := fixContext.Program.Options()
|
|
symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
|
|
|
|
var allTypeOnlyFixes []*fixInfo
|
|
for _, sn := range symbolNames {
|
|
if !sn.isTypeOnly {
|
|
continue
|
|
}
|
|
fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, sn.name, fixContext.Program)
|
|
if fix != nil {
|
|
allTypeOnlyFixes = append(allTypeOnlyFixes, &fixInfo{fix: fix, symbolName: sn.name, errorIdentifierText: symbolToken.Text()})
|
|
}
|
|
}
|
|
|
|
// For JSX opening tags, there can be separate type-only errors for both the tag name
|
|
// identifier and the JSX namespace identifier. When both produce valid fixes, we
|
|
// disambiguate using the diagnostic message, which quotes the symbol name in single
|
|
// quotes (e.g., "'React' cannot be used as a value..."). If filtering yields nothing
|
|
// (e.g., due to localization), fall back to returning all candidates.
|
|
diagnosticMessage := ""
|
|
if fixContext.Diagnostic != nil {
|
|
diagnosticMessage = fixContext.Diagnostic.Message.AsString()
|
|
}
|
|
if len(allTypeOnlyFixes) > 1 && diagnosticMessage != "" {
|
|
for _, fi := range allTypeOnlyFixes {
|
|
if strings.Contains(diagnosticMessage, "'"+fi.symbolName+"'") {
|
|
info = append(info, fi)
|
|
}
|
|
}
|
|
}
|
|
if len(info) == 0 {
|
|
info = allTypeOnlyFixes
|
|
}
|
|
return info, nil
|
|
} else {
|
|
var err error
|
|
view, err = fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if view != nil {
|
|
info = getFixesInfoForNonUMDImport(ctx, fixContext, symbolToken, view)
|
|
}
|
|
}
|
|
|
|
// Sort fixes by preference
|
|
if view == nil {
|
|
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
|
|
}
|
|
return sortFixInfo(info, fixContext, view), nil
|
|
}
|
|
|
|
func getFixesInfoForUMDImport(ctx context.Context, fixContext *CodeFixContext, token *ast.Node, view *autoimport.View) []*fixInfo {
|
|
ch, done := fixContext.Program.GetTypeChecker(ctx)
|
|
defer done()
|
|
|
|
umdSymbol := getUmdSymbol(token, ch)
|
|
if umdSymbol == nil {
|
|
return nil
|
|
}
|
|
|
|
export := autoimport.SymbolToExport(umdSymbol, ch)
|
|
isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(token)
|
|
|
|
var result []*fixInfo
|
|
for _, fix := range view.GetFixes(ctx, export, false, isValidTypeOnlyUseSite, nil) {
|
|
errorIdentifierText := ""
|
|
if ast.IsIdentifier(token) {
|
|
errorIdentifierText = token.Text()
|
|
}
|
|
result = append(result, &fixInfo{
|
|
fix: fix,
|
|
symbolName: umdSymbol.Name,
|
|
errorIdentifierText: errorIdentifierText,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func getUmdSymbol(token *ast.Node, ch *checker.Checker) *ast.Symbol {
|
|
// try the identifier to see if it is the umd symbol
|
|
var umdSymbol *ast.Symbol
|
|
if ast.IsIdentifier(token) {
|
|
umdSymbol = ch.GetResolvedSymbol(token)
|
|
}
|
|
if isUMDExportSymbol(umdSymbol) {
|
|
return umdSymbol
|
|
}
|
|
|
|
// The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`.
|
|
parent := token.Parent
|
|
if (ast.IsJsxOpeningLikeElement(parent) && parent.TagName() == token) ||
|
|
ast.IsJsxOpeningFragment(parent) {
|
|
var location *ast.Node
|
|
if ast.IsJsxOpeningLikeElement(parent) {
|
|
location = token
|
|
} else {
|
|
location = parent
|
|
}
|
|
jsxNamespace := ch.GetJsxNamespace(parent)
|
|
parentSymbol := ch.ResolveName(jsxNamespace, location, ast.SymbolFlagsValue, false /* excludeGlobals */)
|
|
if isUMDExportSymbol(parentSymbol) {
|
|
return parentSymbol
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isUMDExportSymbol(symbol *ast.Symbol) bool {
|
|
return symbol != nil && len(symbol.Declarations) > 0 &&
|
|
symbol.Declarations[0] != nil &&
|
|
ast.IsNamespaceExportDeclaration(symbol.Declarations[0])
|
|
}
|
|
|
|
func getFixesInfoForNonUMDImport(ctx context.Context, fixContext *CodeFixContext, symbolToken *ast.Node, view *autoimport.View) []*fixInfo {
|
|
ch, done := fixContext.Program.GetTypeChecker(ctx)
|
|
defer done()
|
|
compilerOptions := fixContext.Program.Options()
|
|
|
|
isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(symbolToken)
|
|
symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
|
|
var allInfo []*fixInfo
|
|
|
|
// Compute usage position for JSDoc import type fixes
|
|
usagePosition := fixContext.LS.converters.PositionToLineAndCharacter(fixContext.SourceFile, core.TextPos(scanner.GetTokenPosOfNode(symbolToken, fixContext.SourceFile, false)))
|
|
|
|
for _, sn := range symbolNames {
|
|
// Type-only imports are handled by the promotion code path, not the auto-import path.
|
|
if sn.isTypeOnly {
|
|
continue
|
|
}
|
|
|
|
symbolName := sn.name
|
|
// "default" is a keyword and not a legal identifier for the import
|
|
if symbolName == "default" {
|
|
continue
|
|
}
|
|
|
|
isJSXTagName := symbolName == symbolToken.Text() && ast.IsJsxTagName(symbolToken)
|
|
queryKind := autoimport.QueryKindExactMatch
|
|
if isJSXTagName {
|
|
queryKind = autoimport.QueryKindCaseInsensitiveMatch
|
|
}
|
|
|
|
exports := view.Search(symbolName, queryKind)
|
|
for _, export := range exports {
|
|
if isJSXTagName && !(export.Name() == symbolName || export.IsRenameable()) {
|
|
continue
|
|
}
|
|
|
|
fixes := view.GetFixes(ctx, export, isJSXTagName, isValidTypeOnlyUseSite, &usagePosition)
|
|
for _, fix := range fixes {
|
|
allInfo = append(allInfo, &fixInfo{
|
|
fix: fix,
|
|
symbolName: symbolName,
|
|
isJsxNamespaceFix: symbolName != symbolToken.Text(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return allInfo
|
|
}
|
|
|
|
func getTypeOnlyPromotionFix(ctx context.Context, sourceFile *ast.SourceFile, symbolToken *ast.Node, symbolName string, program *compiler.Program) *autoimport.Fix {
|
|
ch, done := program.GetTypeChecker(ctx)
|
|
defer done()
|
|
|
|
// Get the symbol at the token location
|
|
symbol := ch.ResolveName(symbolName, symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */)
|
|
if symbol == nil {
|
|
return nil
|
|
}
|
|
|
|
// Get the type-only alias declaration
|
|
typeOnlyAliasDeclaration := ch.GetTypeOnlyAliasDeclaration(symbol)
|
|
if typeOnlyAliasDeclaration == nil || ast.GetSourceFileOfNode(typeOnlyAliasDeclaration) != sourceFile {
|
|
return nil
|
|
}
|
|
|
|
return &autoimport.Fix{
|
|
AutoImportFix: &lsproto.AutoImportFix{
|
|
Kind: lsproto.AutoImportFixKindPromoteTypeOnly,
|
|
},
|
|
TypeOnlyAliasDeclaration: typeOnlyAliasDeclaration,
|
|
}
|
|
}
|
|
|
|
type symbolNameInfo struct {
|
|
name string
|
|
isTypeOnly bool // whether the symbol currently resolves to a type-only import
|
|
}
|
|
|
|
func getSymbolNamesToImport(sourceFile *ast.SourceFile, ch *checker.Checker, symbolToken *ast.Node, compilerOptions *core.CompilerOptions) []symbolNameInfo {
|
|
parent := symbolToken.Parent
|
|
if (ast.IsJsxOpeningLikeElement(parent) || ast.IsJsxClosingElement(parent)) &&
|
|
parent.TagName() == symbolToken &&
|
|
jsxModeNeedsExplicitImport(compilerOptions.Jsx) {
|
|
jsxNamespace := ch.GetJsxNamespace(sourceFile.AsNode())
|
|
if needsJsxNamespaceFix(jsxNamespace, symbolToken, ch) {
|
|
var result []symbolNameInfo
|
|
if !scanner.IsIntrinsicJsxName(symbolToken.Text()) {
|
|
compSymbol := ch.ResolveName(symbolToken.Text(), symbolToken, ast.SymbolFlagsValue, false /* excludeGlobals */)
|
|
if compSymbol == nil {
|
|
result = append(result, symbolNameInfo{name: symbolToken.Text()})
|
|
} else if ch.GetTypeOnlyAliasDeclaration(compSymbol) != nil {
|
|
result = append(result, symbolNameInfo{name: symbolToken.Text(), isTypeOnly: true})
|
|
}
|
|
}
|
|
nsIsTypeOnly := false
|
|
if nsSymbol := ch.ResolveName(jsxNamespace, symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */); nsSymbol != nil {
|
|
nsIsTypeOnly = ch.GetTypeOnlyAliasDeclaration(nsSymbol) != nil
|
|
}
|
|
result = append(result, symbolNameInfo{name: jsxNamespace, isTypeOnly: nsIsTypeOnly})
|
|
return result
|
|
}
|
|
}
|
|
tokenIsTypeOnly := false
|
|
if sym := ch.ResolveName(symbolToken.Text(), symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */); sym != nil {
|
|
tokenIsTypeOnly = ch.GetTypeOnlyAliasDeclaration(sym) != nil
|
|
}
|
|
return []symbolNameInfo{{name: symbolToken.Text(), isTypeOnly: tokenIsTypeOnly}}
|
|
}
|
|
|
|
func needsJsxNamespaceFix(jsxNamespace string, symbolToken *ast.Node, ch *checker.Checker) bool {
|
|
if scanner.IsIntrinsicJsxName(symbolToken.Text()) {
|
|
return true
|
|
}
|
|
namespaceSymbol := ch.ResolveName(jsxNamespace, symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */)
|
|
if namespaceSymbol == nil {
|
|
return true
|
|
}
|
|
if slices.ContainsFunc(namespaceSymbol.Declarations, ast.IsTypeOnlyImportOrExportDeclaration) {
|
|
return (namespaceSymbol.Flags & ast.SymbolFlagsValue) == 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
func jsxModeNeedsExplicitImport(jsx core.JsxEmit) bool {
|
|
return jsx == core.JsxEmitReact || jsx == core.JsxEmitReactNative
|
|
}
|
|
|
|
func sortFixInfo(fixes []*fixInfo, fixContext *CodeFixContext, view *autoimport.View) []*fixInfo {
|
|
if len(fixes) == 0 {
|
|
return fixes
|
|
}
|
|
|
|
// Create a copy to avoid modifying the original
|
|
sorted := make([]*fixInfo, len(fixes))
|
|
copy(sorted, fixes)
|
|
|
|
// Sort by:
|
|
// 1. JSX namespace fixes last
|
|
// 2. Fix comparison using view.CompareFixes
|
|
slices.SortFunc(sorted, func(a, b *fixInfo) int {
|
|
// JSX namespace fixes should come last
|
|
if cmp := core.CompareBooleans(a.isJsxNamespaceFix, b.isJsxNamespaceFix); cmp != 0 {
|
|
return cmp
|
|
}
|
|
return view.CompareFixesForSorting(a.fix, b.fix)
|
|
})
|
|
|
|
return sorted
|
|
}
|