Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4e1531c
Wip
jakebailey Nov 4, 2025
84cef63
Fix test parser for fourslash difference
jakebailey Nov 11, 2025
8cf4fd6
Add tests
jakebailey Nov 11, 2025
8132a8c
Silence failing
jakebailey Nov 11, 2025
a1d5c83
Revert "Silence failing"
jakebailey Nov 11, 2025
51b6ac8
redo
jakebailey Nov 11, 2025
b240edb
Fix crash
jakebailey Nov 11, 2025
0461314
Skip some fourslash tests
jakebailey Nov 11, 2025
09a03e8
Fix more
jakebailey Nov 11, 2025
4415f67
Fix more
jakebailey Nov 11, 2025
04fd6f8
Fix more
jakebailey Nov 11, 2025
8b4438b
Silence some failing
jakebailey Nov 11, 2025
7aad58e
Fix more
jakebailey Nov 11, 2025
f05de71
fix again
jakebailey Nov 11, 2025
4c75fb1
fix another crash
jakebailey Nov 11, 2025
073d6ac
Skip some more specifier tests
jakebailey Nov 11, 2025
e30e852
Skip more
jakebailey Nov 11, 2025
b2e526d
Fix bad type removal
jakebailey Nov 11, 2025
fecddf8
hm
jakebailey Nov 11, 2025
24f1016
Work around
jakebailey Nov 11, 2025
fe7e17d
some more
jakebailey Nov 11, 2025
23cbb42
some more
jakebailey Nov 11, 2025
b465029
Fix bug in regular tests
jakebailey Nov 11, 2025
6cf1697
Skip more
jakebailey Nov 11, 2025
8b608fd
Skip broken tests
jakebailey Nov 11, 2025
7f77630
Fix misported code
jakebailey Nov 11, 2025
7d6746d
Rerun updatefailing
jakebailey Nov 11, 2025
21cd3a3
Use existing method
jakebailey Nov 11, 2025
818a2e1
refactor to more closely match original code
jakebailey Nov 11, 2025
610a470
Remove not needed helper
jakebailey Nov 11, 2025
4fa6f94
Remove rando added lsutils package file
jakebailey Nov 11, 2025
4d309bf
Drop rando comments about porting
jakebailey Nov 11, 2025
3b2df7a
Remove redundant stuff, oops
jakebailey Nov 11, 2025
6a4ce43
No, it really was a dupe, and fix a misport
jakebailey Nov 11, 2025
0019401
Yay, more stuff was fixed by this
jakebailey Nov 11, 2025
0ba0e5e
Dead code
jakebailey Nov 11, 2025
6c6c1d4
Mega dedupe
jakebailey Nov 11, 2025
038aff1
Even more stuff fixed
jakebailey Nov 11, 2025
5704058
codeActionForFix
jakebailey Nov 11, 2025
d4a3985
slight renaming and cleanup
jakebailey Nov 11, 2025
f80b990
Stop using the wrong position helper
jakebailey Nov 11, 2025
9425684
Document bad helpers I hate
jakebailey Nov 11, 2025
11c2d15
Cleanup
jakebailey Nov 11, 2025
88cfaa5
Temporarily work around ordering problem
jakebailey Nov 11, 2025
c6efb66
Delete dupe helper
jakebailey Nov 11, 2025
d26c966
More duplication, sigh
jakebailey Nov 11, 2025
96a03c3
Remove another dupe
jakebailey Nov 11, 2025
66c2dc4
Small cleanups after reviewing the code
jakebailey Nov 11, 2025
db2fe61
Merge branch 'main' into jabaile/import-fixes
jakebailey Nov 12, 2025
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
4 changes: 0 additions & 4 deletions internal/ast/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -1391,10 +1391,6 @@ func GetNameOfDeclaration(declaration *Node) *Node {
return nil
}

func GetImportClauseOfDeclaration(declaration *Declaration) *ImportClause {
return declaration.ImportClause().AsImportClause()
}

func GetNonAssignedNameOfDeclaration(declaration *Node) *Node {
// !!!
switch declaration.Kind {
Expand Down
57 changes: 57 additions & 0 deletions internal/astnav/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,3 +672,60 @@ func shouldSkipChild(node *ast.Node) bool {
ast.IsJSDocLinkLike(node) ||
ast.IsJSDocTag(node)
}

// FindChildOfKind searches for a child node or token of the specified kind within a containing node.
// This function scans through both AST nodes and intervening tokens to find the first match.
func FindChildOfKind(containingNode *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) *ast.Node {
lastNodePos := containingNode.Pos()
scan := scanner.GetScannerForSourceFile(sourceFile, lastNodePos)

var foundChild *ast.Node
visitNode := func(node *ast.Node) bool {
if node == nil || node.Flags&ast.NodeFlagsReparsed != 0 {
return false
}
// Look for child in preceding tokens.
startPos := lastNodePos
for startPos < node.Pos() {
tokenKind := scan.Token()
tokenFullStart := scan.TokenFullStart()
tokenEnd := scan.TokenEnd()
token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, containingNode)
if tokenKind == kind {
foundChild = token
return true
}
startPos = tokenEnd
scan.Scan()
}
if node.Kind == kind {
foundChild = node
return true
}

lastNodePos = node.End()
scan.ResetPos(lastNodePos)
return false
}

ast.ForEachChildAndJSDoc(containingNode, sourceFile, visitNode)

if foundChild != nil {
return foundChild
}

// Look for child in trailing tokens.
startPos := lastNodePos
for startPos < containingNode.End() {
tokenKind := scan.Token()
tokenFullStart := scan.TokenFullStart()
tokenEnd := scan.TokenEnd()
token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, containingNode)
if tokenKind == kind {
return token
}
startPos = tokenEnd
scan.Scan()
}
return nil
}
4 changes: 2 additions & 2 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2859,7 +2859,7 @@ func (c *Checker) getDeprecatedSuggestionNode(node *ast.Node) *ast.Node {
case ast.KindTaggedTemplateExpression:
return c.getDeprecatedSuggestionNode(node.AsTaggedTemplateExpression().Tag)
case ast.KindJsxOpeningElement, ast.KindJsxSelfClosingElement:
return c.getDeprecatedSuggestionNode(getTagNameOfNode(node))
return c.getDeprecatedSuggestionNode(node.TagName())
case ast.KindElementAccessExpression:
return node.AsElementAccessExpression().ArgumentExpression
case ast.KindPropertyAccessExpression:
Expand Down Expand Up @@ -30342,7 +30342,7 @@ func (c *Checker) hasContextualTypeWithNoGenericTypes(node *ast.Node, checkMode
// If check mode has `CheckMode.RestBindingElement`, we skip binding pattern contextual types,
// as we want the type of a rest element to be generic when possible.
if (ast.IsIdentifier(node) || ast.IsPropertyAccessExpression(node) || ast.IsElementAccessExpression(node)) &&
!((ast.IsJsxOpeningElement(node.Parent) || ast.IsJsxSelfClosingElement(node.Parent)) && getTagNameOfNode(node.Parent) == node) {
!((ast.IsJsxOpeningElement(node.Parent) || ast.IsJsxSelfClosingElement(node.Parent)) && node.Parent.TagName() == node) {
contextualType := c.getContextualType(node, core.IfElse(checkMode&CheckModeRestBindingElement != 0, ContextFlagsSkipBindingPatterns, ContextFlagsNone))
if contextualType != nil {
return !c.isGenericType(contextualType)
Expand Down
12 changes: 12 additions & 0 deletions internal/checker/exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,15 @@ func (c *Checker) GetIndexSignaturesAtLocation(node *ast.Node) []*ast.Node {
func (c *Checker) GetResolvedSymbol(node *ast.Node) *ast.Symbol {
return c.getResolvedSymbol(node)
}

func (c *Checker) GetJsxNamespace(location *ast.Node) string {
return c.getJsxNamespace(location)
}

func (c *Checker) ResolveName(name string, location *ast.Node, meaning ast.SymbolFlags, excludeGlobals bool) *ast.Symbol {
return c.resolveName(location, name, meaning, nil, true, excludeGlobals)
}

func (c *Checker) GetSymbolFlags(symbol *ast.Symbol) ast.SymbolFlags {
return c.getSymbolFlags(symbol)
}
11 changes: 7 additions & 4 deletions internal/checker/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,13 @@ func IsKnownSymbol(symbol *ast.Symbol) bool {
return isLateBoundName(symbol.Name)
}

func IsPrivateIdentifierSymbol(symbol *ast.Symbol) bool {
if symbol == nil {
return false
}
return strings.HasPrefix(symbol.Name, ast.InternalSymbolNamePrefix+"#")
}

func isLateBoundName(name string) bool {
return len(name) >= 2 && name[0] == '\xfe' && name[1] == '@'
}
Expand Down Expand Up @@ -1061,10 +1068,6 @@ func isNonNullAccess(node *ast.Node) bool {
return ast.IsAccessExpression(node) && ast.IsNonNullExpression(node.Expression())
}

func getTagNameOfNode(node *ast.Node) *ast.Node {
return node.TagName()
}

func getBindingElementPropertyName(node *ast.Node) *ast.Node {
return node.PropertyNameOrName()
}
Expand Down
77 changes: 76 additions & 1 deletion internal/fourslash/_scripts/convertFourslash.mts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
case "applyCodeActionFromCompletion":
// `verify.applyCodeActionFromCompletion(...)`
return parseVerifyApplyCodeActionFromCompletionArgs(callExpression.arguments);
case "importFixAtPosition":
// `verify.importFixAtPosition(...)`
return parseImportFixAtPositionArgs(callExpression.arguments);
case "quickInfoAt":
case "quickInfoExists":
case "quickInfoIs":
Expand Down Expand Up @@ -542,6 +545,46 @@ function parseVerifyApplyCodeActionArgs(arg: ts.Expression): string | undefined
return `&fourslash.ApplyCodeActionFromCompletionOptions{\n${props.join("\n")}\n}`;
}

function parseImportFixAtPositionArgs(args: readonly ts.Expression[]): VerifyImportFixAtPositionCmd[] | undefined {
if (args.length < 1 || args.length > 3) {
console.error(`Expected 1-3 arguments in verify.importFixAtPosition, got ${args.map(arg => arg.getText()).join(", ")}`);
return undefined;
}
const arrayArg = getArrayLiteralExpression(args[0]);
if (!arrayArg) {
console.error(`Expected array literal for first argument in verify.importFixAtPosition, got ${args[0].getText()}`);
return undefined;
}
const expectedTexts: string[] = [];
for (const elem of arrayArg.elements) {
const strElem = getStringLiteralLike(elem);
if (!strElem) {
console.error(`Expected string literal in verify.importFixAtPosition array, got ${elem.getText()}`);
return undefined;
}
expectedTexts.push(getGoMultiLineStringLiteral(strElem.text));
}

// If the array is empty, we should still generate valid Go code
if (expectedTexts.length === 0) {
expectedTexts.push(""); // This will be handled specially in code generation
}

let preferences: string | undefined;
if (args.length > 2 && ts.isObjectLiteralExpression(args[2])) {
preferences = parseUserPreferences(args[2]);
if (!preferences) {
console.error(`Unrecognized user preferences in verify.importFixAtPosition: ${args[2].getText()}`);
return undefined;
}
}
return [{
kind: "verifyImportFixAtPosition",
expectedTexts,
preferences: preferences || "nil /*preferences*/",
}];
}

const completionConstants = new Map([
["completion.globals", "CompletionGlobals"],
["completion.globalTypes", "CompletionGlobalTypes"],
Expand Down Expand Up @@ -1240,6 +1283,21 @@ function parseUserPreferences(arg: ts.ObjectLiteralExpression): string | undefin
case "quotePreference":
preferences.push(`QuotePreference: lsutil.QuotePreference(${prop.initializer.getText()})`);
break;
case "autoImportFileExcludePatterns":
const arrayArg = getArrayLiteralExpression(prop.initializer);
if (!arrayArg) {
return undefined;
}
const patterns: string[] = [];
for (const elem of arrayArg.elements) {
const strElem = getStringLiteralLike(elem);
if (!strElem) {
return undefined;
}
patterns.push(getGoStringLiteral(strElem.text));
}
preferences.push(`AutoImportFileExcludePatterns: []string{${patterns.join(", ")}}`);
break;
case "includeInlayParameterNameHints":
let paramHint;
if (!ts.isStringLiteralLike(prop.initializer)) {
Expand Down Expand Up @@ -1701,6 +1759,12 @@ interface VerifyBaselineInlayHintsCmd {
preferences: string;
}

interface VerifyImportFixAtPositionCmd {
kind: "verifyImportFixAtPosition";
expectedTexts: string[];
preferences: string;
}

interface GoToCmd {
kind: "goTo";
// !!! `selectRange` and `rangeStart` require parsing variables and `test.ranges()[n]`
Expand Down Expand Up @@ -1739,7 +1803,8 @@ type Cmd =
| VerifyQuickInfoCmd
| VerifyBaselineRenameCmd
| VerifyRenameInfoCmd
| VerifyBaselineInlayHintsCmd;
| VerifyBaselineInlayHintsCmd
| VerifyImportFixAtPositionCmd;

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

function generateImportFixAtPosition({ expectedTexts, preferences }: VerifyImportFixAtPositionCmd): string {
// Handle empty array case
if (expectedTexts.length === 1 && expectedTexts[0] === "") {
return `f.VerifyImportFixAtPosition(t, []string{}, ${preferences})`;
}
return `f.VerifyImportFixAtPosition(t, []string{\n${expectedTexts.join(",\n")},\n}, ${preferences})`;
}

function generateCmd(cmd: Cmd): string {
switch (cmd.kind) {
case "verifyCompletions":
Expand Down Expand Up @@ -1878,6 +1951,8 @@ function generateCmd(cmd: Cmd): string {
return `f.VerifyRenameFailed(t, ${cmd.preferences})`;
case "verifyBaselineInlayHints":
return generateBaselineInlayHints(cmd);
case "verifyImportFixAtPosition":
return generateImportFixAtPosition(cmd);
default:
let neverCommand: never = cmd;
throw new Error(`Unknown command kind: ${neverCommand as Cmd["kind"]}`);
Expand Down
Loading