Skip to content

Commit 336dda7

Browse files
authored
Implement auto-import code actions, port tests and fix some bugs (#2053)
1 parent 35a5221 commit 336dda7

File tree

192 files changed

+7298
-176
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

192 files changed

+7298
-176
lines changed

internal/ast/utilities.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,10 +1391,6 @@ func GetNameOfDeclaration(declaration *Node) *Node {
13911391
return nil
13921392
}
13931393

1394-
func GetImportClauseOfDeclaration(declaration *Declaration) *ImportClause {
1395-
return declaration.ImportClause().AsImportClause()
1396-
}
1397-
13981394
func GetNonAssignedNameOfDeclaration(declaration *Node) *Node {
13991395
// !!!
14001396
switch declaration.Kind {

internal/astnav/tokens.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,3 +672,60 @@ func shouldSkipChild(node *ast.Node) bool {
672672
ast.IsJSDocLinkLike(node) ||
673673
ast.IsJSDocTag(node)
674674
}
675+
676+
// FindChildOfKind searches for a child node or token of the specified kind within a containing node.
677+
// This function scans through both AST nodes and intervening tokens to find the first match.
678+
func FindChildOfKind(containingNode *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) *ast.Node {
679+
lastNodePos := containingNode.Pos()
680+
scan := scanner.GetScannerForSourceFile(sourceFile, lastNodePos)
681+
682+
var foundChild *ast.Node
683+
visitNode := func(node *ast.Node) bool {
684+
if node == nil || node.Flags&ast.NodeFlagsReparsed != 0 {
685+
return false
686+
}
687+
// Look for child in preceding tokens.
688+
startPos := lastNodePos
689+
for startPos < node.Pos() {
690+
tokenKind := scan.Token()
691+
tokenFullStart := scan.TokenFullStart()
692+
tokenEnd := scan.TokenEnd()
693+
token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, containingNode)
694+
if tokenKind == kind {
695+
foundChild = token
696+
return true
697+
}
698+
startPos = tokenEnd
699+
scan.Scan()
700+
}
701+
if node.Kind == kind {
702+
foundChild = node
703+
return true
704+
}
705+
706+
lastNodePos = node.End()
707+
scan.ResetPos(lastNodePos)
708+
return false
709+
}
710+
711+
ast.ForEachChildAndJSDoc(containingNode, sourceFile, visitNode)
712+
713+
if foundChild != nil {
714+
return foundChild
715+
}
716+
717+
// Look for child in trailing tokens.
718+
startPos := lastNodePos
719+
for startPos < containingNode.End() {
720+
tokenKind := scan.Token()
721+
tokenFullStart := scan.TokenFullStart()
722+
tokenEnd := scan.TokenEnd()
723+
token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, containingNode)
724+
if tokenKind == kind {
725+
return token
726+
}
727+
startPos = tokenEnd
728+
scan.Scan()
729+
}
730+
return nil
731+
}

internal/checker/checker.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2859,7 +2859,7 @@ func (c *Checker) getDeprecatedSuggestionNode(node *ast.Node) *ast.Node {
28592859
case ast.KindTaggedTemplateExpression:
28602860
return c.getDeprecatedSuggestionNode(node.AsTaggedTemplateExpression().Tag)
28612861
case ast.KindJsxOpeningElement, ast.KindJsxSelfClosingElement:
2862-
return c.getDeprecatedSuggestionNode(getTagNameOfNode(node))
2862+
return c.getDeprecatedSuggestionNode(node.TagName())
28632863
case ast.KindElementAccessExpression:
28642864
return node.AsElementAccessExpression().ArgumentExpression
28652865
case ast.KindPropertyAccessExpression:
@@ -30342,7 +30342,7 @@ func (c *Checker) hasContextualTypeWithNoGenericTypes(node *ast.Node, checkMode
3034230342
// If check mode has `CheckMode.RestBindingElement`, we skip binding pattern contextual types,
3034330343
// as we want the type of a rest element to be generic when possible.
3034430344
if (ast.IsIdentifier(node) || ast.IsPropertyAccessExpression(node) || ast.IsElementAccessExpression(node)) &&
30345-
!((ast.IsJsxOpeningElement(node.Parent) || ast.IsJsxSelfClosingElement(node.Parent)) && getTagNameOfNode(node.Parent) == node) {
30345+
!((ast.IsJsxOpeningElement(node.Parent) || ast.IsJsxSelfClosingElement(node.Parent)) && node.Parent.TagName() == node) {
3034630346
contextualType := c.getContextualType(node, core.IfElse(checkMode&CheckModeRestBindingElement != 0, ContextFlagsSkipBindingPatterns, ContextFlagsNone))
3034730347
if contextualType != nil {
3034830348
return !c.isGenericType(contextualType)

internal/checker/exports.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,15 @@ func (c *Checker) GetIndexSignaturesAtLocation(node *ast.Node) []*ast.Node {
170170
func (c *Checker) GetResolvedSymbol(node *ast.Node) *ast.Symbol {
171171
return c.getResolvedSymbol(node)
172172
}
173+
174+
func (c *Checker) GetJsxNamespace(location *ast.Node) string {
175+
return c.getJsxNamespace(location)
176+
}
177+
178+
func (c *Checker) ResolveName(name string, location *ast.Node, meaning ast.SymbolFlags, excludeGlobals bool) *ast.Symbol {
179+
return c.resolveName(location, name, meaning, nil, true, excludeGlobals)
180+
}
181+
182+
func (c *Checker) GetSymbolFlags(symbol *ast.Symbol) ast.SymbolFlags {
183+
return c.getSymbolFlags(symbol)
184+
}

internal/checker/utilities.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,13 @@ func IsKnownSymbol(symbol *ast.Symbol) bool {
971971
return isLateBoundName(symbol.Name)
972972
}
973973

974+
func IsPrivateIdentifierSymbol(symbol *ast.Symbol) bool {
975+
if symbol == nil {
976+
return false
977+
}
978+
return strings.HasPrefix(symbol.Name, ast.InternalSymbolNamePrefix+"#")
979+
}
980+
974981
func isLateBoundName(name string) bool {
975982
return len(name) >= 2 && name[0] == '\xfe' && name[1] == '@'
976983
}
@@ -1061,10 +1068,6 @@ func isNonNullAccess(node *ast.Node) bool {
10611068
return ast.IsAccessExpression(node) && ast.IsNonNullExpression(node.Expression())
10621069
}
10631070

1064-
func getTagNameOfNode(node *ast.Node) *ast.Node {
1065-
return node.TagName()
1066-
}
1067-
10681071
func getBindingElementPropertyName(node *ast.Node) *ast.Node {
10691072
return node.PropertyNameOrName()
10701073
}

internal/fourslash/_scripts/convertFourslash.mts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
175175
case "applyCodeActionFromCompletion":
176176
// `verify.applyCodeActionFromCompletion(...)`
177177
return parseVerifyApplyCodeActionFromCompletionArgs(callExpression.arguments);
178+
case "importFixAtPosition":
179+
// `verify.importFixAtPosition(...)`
180+
return parseImportFixAtPositionArgs(callExpression.arguments);
178181
case "quickInfoAt":
179182
case "quickInfoExists":
180183
case "quickInfoIs":
@@ -542,6 +545,46 @@ function parseVerifyApplyCodeActionArgs(arg: ts.Expression): string | undefined
542545
return `&fourslash.ApplyCodeActionFromCompletionOptions{\n${props.join("\n")}\n}`;
543546
}
544547

548+
function parseImportFixAtPositionArgs(args: readonly ts.Expression[]): VerifyImportFixAtPositionCmd[] | undefined {
549+
if (args.length < 1 || args.length > 3) {
550+
console.error(`Expected 1-3 arguments in verify.importFixAtPosition, got ${args.map(arg => arg.getText()).join(", ")}`);
551+
return undefined;
552+
}
553+
const arrayArg = getArrayLiteralExpression(args[0]);
554+
if (!arrayArg) {
555+
console.error(`Expected array literal for first argument in verify.importFixAtPosition, got ${args[0].getText()}`);
556+
return undefined;
557+
}
558+
const expectedTexts: string[] = [];
559+
for (const elem of arrayArg.elements) {
560+
const strElem = getStringLiteralLike(elem);
561+
if (!strElem) {
562+
console.error(`Expected string literal in verify.importFixAtPosition array, got ${elem.getText()}`);
563+
return undefined;
564+
}
565+
expectedTexts.push(getGoMultiLineStringLiteral(strElem.text));
566+
}
567+
568+
// If the array is empty, we should still generate valid Go code
569+
if (expectedTexts.length === 0) {
570+
expectedTexts.push(""); // This will be handled specially in code generation
571+
}
572+
573+
let preferences: string | undefined;
574+
if (args.length > 2 && ts.isObjectLiteralExpression(args[2])) {
575+
preferences = parseUserPreferences(args[2]);
576+
if (!preferences) {
577+
console.error(`Unrecognized user preferences in verify.importFixAtPosition: ${args[2].getText()}`);
578+
return undefined;
579+
}
580+
}
581+
return [{
582+
kind: "verifyImportFixAtPosition",
583+
expectedTexts,
584+
preferences: preferences || "nil /*preferences*/",
585+
}];
586+
}
587+
545588
const completionConstants = new Map([
546589
["completion.globals", "CompletionGlobals"],
547590
["completion.globalTypes", "CompletionGlobalTypes"],
@@ -1240,6 +1283,21 @@ function parseUserPreferences(arg: ts.ObjectLiteralExpression): string | undefin
12401283
case "quotePreference":
12411284
preferences.push(`QuotePreference: lsutil.QuotePreference(${prop.initializer.getText()})`);
12421285
break;
1286+
case "autoImportFileExcludePatterns":
1287+
const arrayArg = getArrayLiteralExpression(prop.initializer);
1288+
if (!arrayArg) {
1289+
return undefined;
1290+
}
1291+
const patterns: string[] = [];
1292+
for (const elem of arrayArg.elements) {
1293+
const strElem = getStringLiteralLike(elem);
1294+
if (!strElem) {
1295+
return undefined;
1296+
}
1297+
patterns.push(getGoStringLiteral(strElem.text));
1298+
}
1299+
preferences.push(`AutoImportFileExcludePatterns: []string{${patterns.join(", ")}}`);
1300+
break;
12431301
case "includeInlayParameterNameHints":
12441302
let paramHint;
12451303
if (!ts.isStringLiteralLike(prop.initializer)) {
@@ -1701,6 +1759,12 @@ interface VerifyBaselineInlayHintsCmd {
17011759
preferences: string;
17021760
}
17031761

1762+
interface VerifyImportFixAtPositionCmd {
1763+
kind: "verifyImportFixAtPosition";
1764+
expectedTexts: string[];
1765+
preferences: string;
1766+
}
1767+
17041768
interface GoToCmd {
17051769
kind: "goTo";
17061770
// !!! `selectRange` and `rangeStart` require parsing variables and `test.ranges()[n]`
@@ -1739,7 +1803,8 @@ type Cmd =
17391803
| VerifyQuickInfoCmd
17401804
| VerifyBaselineRenameCmd
17411805
| VerifyRenameInfoCmd
1742-
| VerifyBaselineInlayHintsCmd;
1806+
| VerifyBaselineInlayHintsCmd
1807+
| VerifyImportFixAtPositionCmd;
17431808

17441809
function generateVerifyCompletions({ marker, args, isNewIdentifierLocation, andApplyCodeActionArgs }: VerifyCompletionsCmd): string {
17451810
let expectedList: string;
@@ -1840,6 +1905,14 @@ function generateBaselineInlayHints({ span, preferences }: VerifyBaselineInlayHi
18401905
return `f.VerifyBaselineInlayHints(t, ${span}, ${preferences})`;
18411906
}
18421907

1908+
function generateImportFixAtPosition({ expectedTexts, preferences }: VerifyImportFixAtPositionCmd): string {
1909+
// Handle empty array case
1910+
if (expectedTexts.length === 1 && expectedTexts[0] === "") {
1911+
return `f.VerifyImportFixAtPosition(t, []string{}, ${preferences})`;
1912+
}
1913+
return `f.VerifyImportFixAtPosition(t, []string{\n${expectedTexts.join(",\n")},\n}, ${preferences})`;
1914+
}
1915+
18431916
function generateCmd(cmd: Cmd): string {
18441917
switch (cmd.kind) {
18451918
case "verifyCompletions":
@@ -1878,6 +1951,8 @@ function generateCmd(cmd: Cmd): string {
18781951
return `f.VerifyRenameFailed(t, ${cmd.preferences})`;
18791952
case "verifyBaselineInlayHints":
18801953
return generateBaselineInlayHints(cmd);
1954+
case "verifyImportFixAtPosition":
1955+
return generateImportFixAtPosition(cmd);
18811956
default:
18821957
let neverCommand: never = cmd;
18831958
throw new Error(`Unknown command kind: ${neverCommand as Cmd["kind"]}`);

0 commit comments

Comments
 (0)