Skip to content

Commit 31f64de

Browse files
committed
fix(completions): handle orphaned comment
1 parent 7437af2 commit 31f64de

2 files changed

Lines changed: 80 additions & 6 deletions

File tree

src/services/completions.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ import {
226226
isParameterPropertyModifier,
227227
isPartOfTypeNode,
228228
isPossiblyTypeArgumentPosition,
229+
isQualifiedName,
229230
isPrivateIdentifier,
230231
isPrivateIdentifierClassElementDeclaration,
231232
isPropertyAccessExpression,
@@ -256,10 +257,12 @@ import {
256257
isTypeOnlyImportDeclaration,
257258
isTypeOnlyImportOrExportDeclaration,
258259
isTypeParameterDeclaration,
260+
isTypeReferenceNode,
259261
isValidTypeOnlyAliasUseSite,
260262
isVariableDeclaration,
261263
isVariableLike,
262264
JsDoc,
265+
JSDoc,
263266
JSDocImportTag,
264267
JSDocParameterTag,
265268
JSDocPropertyTag,
@@ -315,6 +318,7 @@ import {
315318
ObjectTypeDeclaration,
316319
or,
317320
ParameterDeclaration,
321+
parseIsolatedJSDocComment,
318322
ParenthesizedTypeNode,
319323
positionBelongsToNode,
320324
positionIsASICandidate,
@@ -3321,6 +3325,9 @@ function getCompletionData(
33213325
let insideJsDocTagTypeExpression = false;
33223326
let insideJsDocImportTag = false;
33233327
let isInSnippetScope = false;
3328+
// For orphaned JSDoc with qualified name (e.g., t. in function foo(/** @type {t.} */) {})
3329+
// we need to track the left identifier text to enable member completions. See #62281.
3330+
let orphanedJsDocQualifiedNameLeft: Identifier | undefined;
33243331
if (insideComment) {
33253332
if (hasDocComment(sourceFile, position)) {
33263333
if (sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) {
@@ -3382,6 +3389,39 @@ function getCompletionData(
33823389
}
33833390
}
33843391
}
3392+
else {
3393+
// Fallback: Handle orphaned JSDoc comments not attached to any AST node.
3394+
// For example: function foo(/** @type {t.} */) {} - no parameter name means
3395+
// the parser creates 0 parameters and the JSDoc is not attached. See #62281.
3396+
const commentText = sourceFile.text.substring(insideComment.pos, insideComment.end);
3397+
const parsed = parseIsolatedJSDocComment(commentText);
3398+
if (parsed?.jsDoc?.tags) {
3399+
const posInComment = position - insideComment.pos;
3400+
for (const parsedTag of parsed.jsDoc.tags) { // TODO: Too slow (?)
3401+
const typeExpression = tryGetTypeExpressionFromTag(parsedTag);
3402+
if (typeExpression && typeExpression.pos < posInComment && posInComment <= typeExpression.end) {
3403+
insideJsDocTagTypeExpression = true;
3404+
// Check if we're after a dot in a QualifiedName (for member completions)
3405+
if (typeExpression.kind !== SyntaxKind.JSDocTypeExpression) break;
3406+
const typeNode = typeExpression.type;
3407+
if (isTypeReferenceNode(typeNode) && isQualifiedName(typeNode.typeName)) {
3408+
const qualifiedName = typeNode.typeName;
3409+
// Position after the dot means we want to complete members of the left side
3410+
const dotPos = qualifiedName.left.end;
3411+
if (posInComment > dotPos && isIdentifier(qualifiedName.left)) {
3412+
const leftText = commentText.substring(qualifiedName.left.pos, qualifiedName.left.end).trim();
3413+
// Find the corresponding JSDocImportTag in the source file
3414+
const found = findJsDocImportNamespaceIdentifier(sourceFile, leftText);
3415+
if (found) {
3416+
orphanedJsDocQualifiedNameLeft = found;
3417+
}
3418+
}
3419+
}
3420+
break;
3421+
}
3422+
}
3423+
}
3424+
}
33853425

33863426
if (!insideJsDocTagTypeExpression && !insideJsDocImportTag) {
33873427
// Proceed if the current position is in jsDoc tag expression; otherwise it is a normal
@@ -3456,7 +3496,8 @@ function getCompletionData(
34563496
isNewIdentifierLocation = importStatementCompletionInfo.isNewIdentifierLocation;
34573497
}
34583498
// Bail out if this is a known invalid completion location
3459-
if (!importStatementCompletionInfo.replacementSpan && isCompletionListBlocker(contextToken)) {
3499+
// Skip the blocker check if we're inside a JSDoc type expression (including orphaned JSDoc). See #62281.
3500+
if (!importStatementCompletionInfo.replacementSpan && !insideJsDocTagTypeExpression && !insideJsDocImportTag && isCompletionListBlocker(contextToken)) {
34603501
log("Returning an empty list because completion was requested in an invalid position.");
34613502
return keywordFilters
34623503
? keywordCompletionData(keywordFilters, isJsOnlyLocation, computeCommitCharactersAndIsNewIdentifier().isNewIdentifierLocation)
@@ -3596,6 +3637,14 @@ function getCompletionData(
35963637
}
35973638
}
35983639

3640+
// Handle orphaned JSDoc with qualified name (e.g., function foo(/** @type {t.} */) {}).
3641+
// We found the left identifier's AST node earlier; now use it for member completions.
3642+
if (orphanedJsDocQualifiedNameLeft) {
3643+
isRightOfDot = true;
3644+
node = orphanedJsDocQualifiedNameLeft;
3645+
}
3646+
3647+
35993648
const semanticStart = timestamp();
36003649
let completionKind = CompletionKind.None;
36013650
let hasUnresolvedAutoImports = false;
@@ -3720,6 +3769,30 @@ function getCompletionData(
37203769
return undefined;
37213770
}
37223771

3772+
/**
3773+
* Find a JSDocImportTag in the source file that creates a namespace with the given name.
3774+
* Used to enable member completions for orphaned JSDoc comments. See #62281.
3775+
*/
3776+
function findJsDocImportNamespaceIdentifier(sf: SourceFile, namespaceName: string): Identifier | undefined {
3777+
for (const statement of sf.statements) {
3778+
// JSDoc comments are attached to statements via the jsDoc property
3779+
const jsDocNodes = (statement as Node & { jsDoc?: JSDoc[] }).jsDoc;
3780+
if (!jsDocNodes) continue;
3781+
for (const jsDoc of jsDocNodes) {
3782+
if (!jsDoc.tags) continue;
3783+
for (const tag of jsDoc.tags) {
3784+
if (isJSDocImportTag(tag) && tag.importClause?.namedBindings) {
3785+
const namedBindings = tag.importClause.namedBindings;
3786+
if (isNamespaceImport(namedBindings) && namedBindings.name.text === namespaceName) {
3787+
return namedBindings.name;
3788+
}
3789+
}
3790+
}
3791+
}
3792+
}
3793+
return undefined;
3794+
}
3795+
37233796
function getTypeScriptMemberSymbols(): void {
37243797
// Right of dot member completion list
37253798
completionKind = CompletionKind.PropertyAccess;

tests/cases/fourslash/jsdocTypeCompletionInFunctionParameter.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,16 +147,17 @@ verify.completions({
147147
exact: undefined,
148148
});
149149

150-
// Cases 5 & 6: Parser limitations - NO completions
151-
// (the JSDoc is orphaned because the parser creates a function with 0 parameters
152-
// when there's no identifier after `*/`)
150+
// Cases 5 & 6: Previously parser limitations, now FIXED with orphaned JSDoc handling.
153151
verify.completions(
154152
{
155153
marker: "case5",
156-
exact: undefined,
154+
exact: [
155+
{ name: "MyType", kind: "interface", kindModifiers: "export" },
156+
{ name: "OtherType", kind: "interface", kindModifiers: "export" },
157+
],
157158
},
158159
{
159160
marker: "case6",
160-
exact: undefined,
161+
includes: [{ name: "SomeNumber", kind: "type" }],
161162
}
162163
);

0 commit comments

Comments
 (0)