@@ -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 ;
0 commit comments