From 6f4c9268df56614efec3f9b255e1fe05359bac15 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 11 Aug 2025 13:16:57 -0700 Subject: [PATCH 01/13] basic jsdoc completions --- internal/ast/ast.go | 40 + internal/checker/printer.go | 5 + internal/format/api.go | 6 +- internal/format/span.go | 4 +- internal/format/util.go | 2 +- internal/fourslash/_scripts/failingTests.txt | 17 - .../completionInJsDocQualifiedNames_test.go | 2 +- .../tests/gen/completionJSDocNamePath_test.go | 2 +- .../gen/completionsJSDocNoCrash2_test.go | 2 +- .../gen/completionsJSDocNoCrash3_test.go | 2 +- ...ompletionsJsdocParamTypeBeforeName_test.go | 2 +- ...sDocFunctionTypeCompletionsNoCrash_test.go | 2 +- .../gen/jsdocExtendsTagCompletion_test.go | 2 +- .../gen/jsdocImplementsTagCompletion_test.go | 2 +- .../gen/jsdocImportTagCompletion1_test.go | 2 +- .../gen/jsdocOverloadTagCompletion_test.go | 2 +- .../gen/jsdocParameterNameCompletion_test.go | 2 +- .../tests/gen/jsdocPropTagCompletion_test.go | 2 +- .../gen/jsdocPropertyTagCompletion_test.go | 2 +- .../gen/jsdocSatisfiesTagCompletion1_test.go | 2 +- .../gen/jsdocSatisfiesTagCompletion2_test.go | 2 +- .../gen/jsdocTemplateTagCompletion_test.go | 2 +- ...TypedefTagTypeExpressionCompletion_test.go | 2 +- internal/ls/completions.go | 744 +++++++++++++++++- 24 files changed, 807 insertions(+), 45 deletions(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 87e9c79c53..5e31d45db3 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -1060,6 +1060,32 @@ func (n *Node) QuestionDotToken() *Node { panic("Unhandled case in Node.QuestionDotToken: " + n.Kind.String()) } +func (n *Node) TypeExpression() *Node { + switch n.Kind { + case KindJSDocPropertyTag, KindJSDocParameterTag: + return n.AsJSDocParameterOrPropertyTag().TypeExpression + case KindJSDocReturnTag: + return n.AsJSDocReturnTag().TypeExpression + case KindJSDocTypeTag: + return n.AsJSDocTypeTag().TypeExpression + case KindJSDocTypedefTag: + return n.AsJSDocTypedefTag().TypeExpression + case KindJSDocSatisfiesTag: + return n.AsJSDocSatisfiesTag().TypeExpression + } + panic("Unhandled case in Node.TypeExpression: " + n.Kind.String()) +} + +func (n *Node) ClassName() *Node { + switch n.Kind { + case KindJSDocAugmentsTag: + return n.AsJSDocAugmentsTag().ClassName + case KindJSDocImplementsTag: + return n.AsJSDocImplementsTag().ClassName + } + panic("Unhandled case in Node.ClassName: " + n.Kind.String()) +} + // Determines if `n` contains `descendant` by walking up the `Parent` pointers from `descendant`. This method panics if // `descendant` or one of its ancestors is not parented except when that node is a `SourceFile`. func (n *Node) Contains(descendant *Node) bool { @@ -2049,6 +2075,8 @@ type ( NamedExportsNode = Node UnionType = Node LiteralType = Node + JSDocNode = Node + BindingPatternNode = Node ) type ( @@ -9535,6 +9563,10 @@ func (node *JSDocTemplateTag) Clone(f NodeFactoryCoercible) *Node { return cloneNode(f.AsNodeFactory().NewJSDocTemplateTag(node.TagName, node.Constraint, node.TypeParameters, node.Comment), node.AsNode(), f.AsNodeFactory().hooks) } +func IsJSDocTemplateTag(n *Node) bool { + return n.Kind == KindJSDocTemplateTag +} + // JSDocParameterOrPropertyTag type JSDocParameterOrPropertyTag struct { JSDocTagBase @@ -9600,6 +9632,10 @@ func (node *JSDocParameterOrPropertyTag) Clone(f NodeFactoryCoercible) *Node { func (node *JSDocParameterOrPropertyTag) Name() *EntityName { return node.name } +func IsJSDocParameterTag(node *Node) bool { + return node.Kind == KindJSDocParameterTag +} + // JSDocReturnTag type JSDocReturnTag struct { JSDocTagBase @@ -9893,6 +9929,10 @@ func (node *JSDocImplementsTag) Clone(f NodeFactoryCoercible) *Node { return cloneNode(f.AsNodeFactory().NewJSDocImplementsTag(node.TagName, node.ClassName, node.Comment), node.AsNode(), f.AsNodeFactory().hooks) } +func IsJSDocImplementsTag(node *Node) bool { + return node.Kind == KindJSDocImplementsTag +} + // JSDocAugmentsTag type JSDocAugmentsTag struct { JSDocTagBase diff --git a/internal/checker/printer.go b/internal/checker/printer.go index 0fa158f83e..575baa3a70 100644 --- a/internal/checker/printer.go +++ b/internal/checker/printer.go @@ -369,3 +369,8 @@ func (c *Checker) formatUnionTypes(types []*Type) []*Type { } return result } + +func (c *Checker) TypeToTypeNode(t *Type, enclosingDeclaration *ast.Node, flags nodebuilder.Flags) *ast.TypeNode { + nodeBuilder := c.getNodeBuilder() + return nodeBuilder.TypeToTypeNode(t, enclosingDeclaration, flags, nodebuilder.InternalFlagsNone, nil) +} diff --git a/internal/format/api.go b/internal/format/api.go index db223ef543..7e11bd3266 100644 --- a/internal/format/api.go +++ b/internal/format/api.go @@ -80,7 +80,7 @@ func formatNodeLines(ctx context.Context, sourceFile *ast.SourceFile, node *ast. return nil } tokenStart := scanner.GetTokenPosOfNode(node, sourceFile, false) - lineStart := getLineStartPositionForPosition(tokenStart, sourceFile) + lineStart := GetLineStartPositionForPosition(tokenStart, sourceFile) span := core.NewTextRange(lineStart, node.End()) return FormatSpan(ctx, span, sourceFile, requestKind) } @@ -90,7 +90,7 @@ func FormatDocument(ctx context.Context, sourceFile *ast.SourceFile) []core.Text } func FormatSelection(ctx context.Context, sourceFile *ast.SourceFile, start int, end int) []core.TextChange { - return FormatSpan(ctx, core.NewTextRange(getLineStartPositionForPosition(start, sourceFile), end), sourceFile, FormatRequestKindFormatSelection) + return FormatSpan(ctx, core.NewTextRange(GetLineStartPositionForPosition(start, sourceFile), end), sourceFile, FormatRequestKindFormatSelection) } func FormatOnOpeningCurly(ctx context.Context, sourceFile *ast.SourceFile, position int) []core.TextChange { @@ -112,7 +112,7 @@ func FormatOnOpeningCurly(ctx context.Context, sourceFile *ast.SourceFile, posit * ``` * and we wouldn't want to move the closing brace. */ - textRange := core.NewTextRange(getLineStartPositionForPosition(scanner.GetTokenPosOfNode(outermostNode, sourceFile, false), sourceFile), position) + textRange := core.NewTextRange(GetLineStartPositionForPosition(scanner.GetTokenPosOfNode(outermostNode, sourceFile, false), sourceFile), position) return FormatSpan(ctx, textRange, sourceFile, FormatRequestKindFormatOnOpeningCurlyBrace) } diff --git a/internal/format/span.go b/internal/format/span.go index 87fd829daa..7c409c02c4 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -467,7 +467,7 @@ func (w *formatSpanWorker) processChildNodes( // }: {}; indentationOnListStartToken = w.indentationOnLastIndentedLine } else { - startLinePosition := getLineStartPositionForPosition(tokenInfo.token.Loc.Pos(), w.sourceFile) + startLinePosition := GetLineStartPositionForPosition(tokenInfo.token.Loc.Pos(), w.sourceFile) indentationOnListStartToken = findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.Loc.Pos(), w.sourceFile, w.formattingContext.Options) } @@ -577,7 +577,7 @@ func (w *formatSpanWorker) tryComputeIndentationForListItem(startPos int, endPos } } else { startLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, startPos) - startLinePosition := getLineStartPositionForPosition(startPos, w.sourceFile) + startLinePosition := GetLineStartPositionForPosition(startPos, w.sourceFile) column := findFirstNonWhitespaceColumn(startLinePosition, startPos, w.sourceFile, w.formattingContext.Options) if startLine != parentStartLine || startPos == column { // Use the base indent size if it is greater than diff --git a/internal/format/util.go b/internal/format/util.go index a7b08f2899..f6088272c8 100644 --- a/internal/format/util.go +++ b/internal/format/util.go @@ -75,7 +75,7 @@ func getCloseTokenForOpenToken(kind ast.Kind) ast.Kind { return ast.KindUnknown } -func getLineStartPositionForPosition(position int, sourceFile *ast.SourceFile) int { +func GetLineStartPositionForPosition(position int, sourceFile *ast.SourceFile) int { lineStarts := scanner.GetLineStarts(sourceFile) line, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, position) return int(lineStarts[line]) diff --git a/internal/fourslash/_scripts/failingTests.txt b/internal/fourslash/_scripts/failingTests.txt index 873155459d..34a7208f89 100644 --- a/internal/fourslash/_scripts/failingTests.txt +++ b/internal/fourslash/_scripts/failingTests.txt @@ -65,10 +65,8 @@ TestCompletionImportModuleSpecifierEndingTsxReact TestCompletionImportModuleSpecifierEndingUnsupportedExtension TestCompletionInFunctionLikeBody_includesPrimitiveTypes TestCompletionInJsDoc -TestCompletionInJsDocQualifiedNames TestCompletionInNamedImportLocation TestCompletionInUncheckedJSFile -TestCompletionJSDocNamePath TestCompletionListBuilderLocations_VariableDeclarations TestCompletionListForDerivedType1 TestCompletionListForTransitivelyExportedMembers04 @@ -131,9 +129,6 @@ TestCompletionsJSDocImportTagAttributesEmptyModuleSpecifier1 TestCompletionsJSDocImportTagAttributesErrorModuleSpecifier1 TestCompletionsJSDocImportTagEmptyModuleSpecifier1 TestCompletionsJSDocNoCrash1 -TestCompletionsJSDocNoCrash2 -TestCompletionsJSDocNoCrash3 -TestCompletionsJsdocParamTypeBeforeName TestCompletionsJsdocTag TestCompletionsJsdocTypeTagCast TestCompletionsJsxAttribute2 @@ -247,7 +242,6 @@ TestJsDocFunctionSignatures12 TestJsDocFunctionSignatures13 TestJsDocFunctionSignatures7 TestJsDocFunctionSignatures8 -TestJsDocFunctionTypeCompletionsNoCrash TestJsDocGenerics2 TestJsDocInheritDoc TestJsDocPropertyDescription1 @@ -266,26 +260,15 @@ TestJsDocTagsWithHyphen TestJsQuickInfoGenerallyAcceptableSize TestJsRequireQuickInfo TestJsdocCallbackTag -TestJsdocExtendsTagCompletion -TestJsdocImplementsTagCompletion -TestJsdocImportTagCompletion1 TestJsdocLink2 TestJsdocLink3 TestJsdocLink6 TestJsdocLink_findAllReferences1 -TestJsdocOverloadTagCompletion -TestJsdocParameterNameCompletion -TestJsdocPropTagCompletion -TestJsdocPropertyTagCompletion -TestJsdocSatisfiesTagCompletion1 -TestJsdocSatisfiesTagCompletion2 TestJsdocTemplatePrototypeCompletions -TestJsdocTemplateTagCompletion TestJsdocThrowsTagCompletion TestJsdocTypedefTag TestJsdocTypedefTag2 TestJsdocTypedefTagNamespace -TestJsdocTypedefTagTypeExpressionCompletion TestJsxFindAllReferencesOnRuntimeImportWithPaths1 TestLetQuickInfoAndCompletionList TestLocalFunction diff --git a/internal/fourslash/tests/gen/completionInJsDocQualifiedNames_test.go b/internal/fourslash/tests/gen/completionInJsDocQualifiedNames_test.go index 24e35f94b0..1758c02b7c 100644 --- a/internal/fourslash/tests/gen/completionInJsDocQualifiedNames_test.go +++ b/internal/fourslash/tests/gen/completionInJsDocQualifiedNames_test.go @@ -11,7 +11,7 @@ import ( func TestCompletionInJsDocQualifiedNames(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @allowJs: true // @Filename: /node_modules/foo/index.d.ts diff --git a/internal/fourslash/tests/gen/completionJSDocNamePath_test.go b/internal/fourslash/tests/gen/completionJSDocNamePath_test.go index 2a93607b6d..6011e23ed3 100644 --- a/internal/fourslash/tests/gen/completionJSDocNamePath_test.go +++ b/internal/fourslash/tests/gen/completionJSDocNamePath_test.go @@ -10,7 +10,7 @@ import ( func TestCompletionJSDocNamePath(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @noLib: true /** diff --git a/internal/fourslash/tests/gen/completionsJSDocNoCrash2_test.go b/internal/fourslash/tests/gen/completionsJSDocNoCrash2_test.go index 74cd0e8083..07f43dfa6b 100644 --- a/internal/fourslash/tests/gen/completionsJSDocNoCrash2_test.go +++ b/internal/fourslash/tests/gen/completionsJSDocNoCrash2_test.go @@ -10,7 +10,7 @@ import ( func TestCompletionsJSDocNoCrash2(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @strict: true // @filename: index.ts diff --git a/internal/fourslash/tests/gen/completionsJSDocNoCrash3_test.go b/internal/fourslash/tests/gen/completionsJSDocNoCrash3_test.go index b146ed0150..bac7331902 100644 --- a/internal/fourslash/tests/gen/completionsJSDocNoCrash3_test.go +++ b/internal/fourslash/tests/gen/completionsJSDocNoCrash3_test.go @@ -12,7 +12,7 @@ import ( func TestCompletionsJSDocNoCrash3(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @strict: true // @filename: index.ts diff --git a/internal/fourslash/tests/gen/completionsJsdocParamTypeBeforeName_test.go b/internal/fourslash/tests/gen/completionsJsdocParamTypeBeforeName_test.go index 348a89d8c8..56d8f40da2 100644 --- a/internal/fourslash/tests/gen/completionsJsdocParamTypeBeforeName_test.go +++ b/internal/fourslash/tests/gen/completionsJsdocParamTypeBeforeName_test.go @@ -10,7 +10,7 @@ import ( func TestCompletionsJsdocParamTypeBeforeName(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** @param /*name1*/ {/*type*/} /*name2*/ */ function toString(obj) {}` diff --git a/internal/fourslash/tests/gen/jsDocFunctionTypeCompletionsNoCrash_test.go b/internal/fourslash/tests/gen/jsDocFunctionTypeCompletionsNoCrash_test.go index 1204e50be6..7a5d243aec 100644 --- a/internal/fourslash/tests/gen/jsDocFunctionTypeCompletionsNoCrash_test.go +++ b/internal/fourslash/tests/gen/jsDocFunctionTypeCompletionsNoCrash_test.go @@ -10,7 +10,7 @@ import ( func TestJsDocFunctionTypeCompletionsNoCrash(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @returns {function/**/(): string} diff --git a/internal/fourslash/tests/gen/jsdocExtendsTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocExtendsTagCompletion_test.go index 8c8af65893..8acd316c68 100644 --- a/internal/fourslash/tests/gen/jsdocExtendsTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocExtendsTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocExtendsTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** @extends {/**/} */ class A {}` diff --git a/internal/fourslash/tests/gen/jsdocImplementsTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocImplementsTagCompletion_test.go index 7f5f5b8311..c207695616 100644 --- a/internal/fourslash/tests/gen/jsdocImplementsTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocImplementsTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocImplementsTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** @implements {/**/} */ class A {}` diff --git a/internal/fourslash/tests/gen/jsdocImportTagCompletion1_test.go b/internal/fourslash/tests/gen/jsdocImportTagCompletion1_test.go index 0fa2c80ff8..340677af9d 100644 --- a/internal/fourslash/tests/gen/jsdocImportTagCompletion1_test.go +++ b/internal/fourslash/tests/gen/jsdocImportTagCompletion1_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocImportTagCompletion1(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @allowJS: true // @checkJs: true diff --git a/internal/fourslash/tests/gen/jsdocOverloadTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocOverloadTagCompletion_test.go index 8138cf715a..c87be8afe0 100644 --- a/internal/fourslash/tests/gen/jsdocOverloadTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocOverloadTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocOverloadTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @allowJS: true // @checkJs: true diff --git a/internal/fourslash/tests/gen/jsdocParameterNameCompletion_test.go b/internal/fourslash/tests/gen/jsdocParameterNameCompletion_test.go index 3e2f5cf7d8..2a5b1bd389 100644 --- a/internal/fourslash/tests/gen/jsdocParameterNameCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocParameterNameCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocParameterNameCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @param /*0*/ diff --git a/internal/fourslash/tests/gen/jsdocPropTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocPropTagCompletion_test.go index 5b93e4d3b9..eb004ed071 100644 --- a/internal/fourslash/tests/gen/jsdocPropTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocPropTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocPropTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @typedef Foo diff --git a/internal/fourslash/tests/gen/jsdocPropertyTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocPropertyTagCompletion_test.go index 82719cfd9c..fc11da5f88 100644 --- a/internal/fourslash/tests/gen/jsdocPropertyTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocPropertyTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocPropertyTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @typedef {Object} Foo diff --git a/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion1_test.go b/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion1_test.go index f2997afbf9..bfd5f12d0b 100644 --- a/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion1_test.go +++ b/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion1_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocSatisfiesTagCompletion1(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @noEmit: true // @allowJS: true diff --git a/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion2_test.go b/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion2_test.go index 27538bc590..ae6e0d4288 100644 --- a/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion2_test.go +++ b/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion2_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocSatisfiesTagCompletion2(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @noEmit: true // @allowJS: true diff --git a/internal/fourslash/tests/gen/jsdocTemplateTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocTemplateTagCompletion_test.go index 23ff81f66e..52e9626807 100644 --- a/internal/fourslash/tests/gen/jsdocTemplateTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocTemplateTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocTemplateTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @template {/**/} T diff --git a/internal/fourslash/tests/gen/jsdocTypedefTagTypeExpressionCompletion_test.go b/internal/fourslash/tests/gen/jsdocTypedefTagTypeExpressionCompletion_test.go index d4af6bb12f..059588bca2 100644 --- a/internal/fourslash/tests/gen/jsdocTypedefTagTypeExpressionCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocTypedefTagTypeExpressionCompletion_test.go @@ -12,7 +12,7 @@ import ( func TestJsdocTypedefTagTypeExpressionCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `interface I { age: number; diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 8a40ca0793..d234a4636a 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -18,9 +18,12 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/format" "github.com/microsoft/typescript-go/internal/jsnum" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/lsutil" + "github.com/microsoft/typescript-go/internal/nodebuilder" + "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" "golang.org/x/text/collate" @@ -70,7 +73,8 @@ func ensureItemData(fileName string, pos int, list *lsproto.CompletionList) *lsp return list } -// *completionDataData | *completionDataKeyword +// !!! TODO: consolidate some of these into a single jsdoc data +// *completionDataData | *completionDataKeyword | *completionDataJSDocTagName | *completionDataJSDocTag | *completionDataJSDocParameterName type completionData = any type completionDataData struct { @@ -106,6 +110,14 @@ type completionDataKeyword struct { isNewIdentifierLocation bool } +type completionDataJSDocTagName struct{} + +type completionDataJSDocTag struct{} + +type completionDataJSDocParameterName struct { + tag *ast.JSDocParameterTag +} + type importStatementCompletionInfo struct { // !!! } @@ -383,13 +395,46 @@ func (l *LanguageService) getCompletionsAtPosition( data.isNewIdentifierLocation, optionalReplacementSpan, ) - // !!! jsdoc completion data cases + case *completionDataJSDocTagName: + // If the current position is a jsDoc tag name, only tag names should be provided for completion + items := getJSDocTagNameCompletions() + items = append(items, getJSDocParameterCompletions( + clientOptions, + file, + position, + checker, + compilerOptions, + preferences, + /*tagNameOnly*/ true, + )...) + return l.jsDocCompletionInfo(clientOptions, position, file, items) + case *completionDataJSDocTag: + // If the current position is a jsDoc tag, only tags should be provided for completion + items := getJSDocTagCompletions() + items = append(items, getJSDocParameterCompletions( + clientOptions, + file, + position, + checker, + compilerOptions, + preferences, + /*tagNameOnly*/ false, + )...) + return l.jsDocCompletionInfo(clientOptions, position, file, items) + case *completionDataJSDocParameterName: + return l.jsDocCompletionInfo(clientOptions, position, file, getJSDocParameterNameCompletions(data.tag)) default: panic("getCompletionData() returned unexpected type: " + fmt.Sprintf("%T", data)) } } -func getCompletionData(program *compiler.Program, typeChecker *checker.Checker, file *ast.SourceFile, position int, preferences *UserPreferences) completionData { +func getCompletionData( + program *compiler.Program, + typeChecker *checker.Checker, + file *ast.SourceFile, + position int, + preferences *UserPreferences, +) completionData { inCheckedFile := isCheckedFile(file, program.Options()) currentToken := astnav.GetTokenAtPosition(file, position) @@ -400,8 +445,79 @@ func getCompletionData(program *compiler.Program, typeChecker *checker.Checker, insideJsDocImportTag := false isInSnippetScope := false if insideComment != nil { - // !!! jsdoc - return nil + if hasDocComment(file, position) { + if r, _ := utf8.DecodeRuneInString(file.Text()[position:]); r == '@' { + // The current position is next to the '@' sign, when no tag name being provided yet. + // Provide a full list of tag names + return &completionDataJSDocTagName{} + } else { + // When completion is requested without "@", we will have check to make sure that + // there are no comments prefix the request position. We will only allow "*" and space. + // e.g + // /** |c| /* + // + // /** + // |c| + // */ + // + // /** + // * |c| + // */ + // + // /** + // * |c| + // */ + lineStart := format.GetLineStartPositionForPosition(position, file) + noCommentPrefix := true + for _, r := range file.Text()[lineStart:position] { + if !(stringutil.IsWhiteSpaceSingleLine(r) || r == '*' || r == '/' || r == '(' || r == ')' || r == '|') { + noCommentPrefix = false + break + } + } + if noCommentPrefix { + return &completionDataJSDocTagName{} + } + } + } + + // Completion should work inside certain JsDoc tags. For example: + // /** @type {number | string} */ + // Completion should work in the brackets + tag := getJSDocTagAtPosition(currentToken, position) + if tag != nil { + if tag.TagName().Pos() <= position && position <= tag.TagName().End() { + return &completionDataJSDocTagName{} + } + if ast.IsJSDocImportTag(tag) { + insideJsDocImportTag = true + } else { + typeExpression := tryGetTypeExpressionFromTag(tag) + if typeExpression != nil { + currentToken = astnav.GetTokenAtPosition(file, position) + if currentToken == nil || + (!ast.IsDeclarationName(currentToken) && + (currentToken.Parent.Kind != ast.KindJSDocPropertyTag || + currentToken.Parent.Name() != currentToken)) { + // Use as type location if inside tag's type expression + insideJSDocTagTypeExpression = isCurrentlyEditingNode(typeExpression, file, position) + } + } + if !insideJSDocTagTypeExpression && + ast.IsJSDocParameterTag(tag) && + (ast.NodeIsMissing(tag.Name()) || tag.Name().Pos() <= position && position <= tag.Name().End()) { + return &completionDataJSDocParameterName{ + tag: tag.AsJSDocParameterOrPropertyTag(), + } + } + } + } + + if !insideJSDocTagTypeExpression && !insideJsDocImportTag { + // Proceed if the current position is in JSDoc tag expression; otherwise it is a normal + // comment or the plain text part of a JSDoc comment, so no completion should be available + return nil + } } // The decision to provide completion depends on the contextToken, which is determined through the previousToken. @@ -4860,3 +4976,621 @@ func createCompletionDetailsForSymbol( func getCompletionItemActions(symbol *ast.Symbol) []codeAction { return nil } + +func hasDocComment(file *ast.SourceFile, position int) bool { + token := astnav.GetTokenAtPosition(file, position) + return ast.FindAncestor(token, (*ast.Node).IsJSDoc) != nil +} + +// Get the corresponding JSDocTag node if the position is in a JSDoc comment +func getJSDocTagAtPosition(node *ast.Node, position int) *ast.JSDocTag { + return ast.FindAncestorOrQuit(node, func(n *ast.Node) ast.FindAncestorResult { + if ast.IsJSDocTag(n) && n.Loc.ContainsInclusive(position) { + return ast.FindAncestorTrue + } + if n.IsJSDoc() { + return ast.FindAncestorQuit + } + return ast.FindAncestorFalse + }) +} + +func tryGetTypeExpressionFromTag(tag *ast.JSDocTag) *ast.Node { + if isTagWithTypeExpression(tag) { + var typeExpression *ast.Node + // !!! TODO: need to map the jsdoc type node to a synthetic type node here + if ast.IsJSDocTemplateTag(tag) { + typeExpression = tag.AsJSDocTemplateTag().Constraint + } else { + typeExpression = tag.TypeExpression() + } + if typeExpression != nil && typeExpression.Kind == ast.KindJSDocTypeExpression { + return typeExpression + } + } + if ast.IsJSDocAugmentsTag(tag) || ast.IsJSDocImplementsTag(tag) { + return tag.ClassName() + } + return nil +} + +func isTagWithTypeExpression(tag *ast.JSDocTag) bool { + switch tag.Kind { + case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag, ast.KindJSDocReturnTag, ast.KindJSDocTypeTag, + ast.KindJSDocTypedefTag, ast.KindJSDocSatisfiesTag: + return true + case ast.KindJSDocTemplateTag: + return tag.AsJSDocTemplateTag().Constraint != nil + default: + return false + } +} + +func (l *LanguageService) jsDocCompletionInfo( + clientOptions *lsproto.CompletionClientCapabilities, + position int, + file *ast.SourceFile, + items []*lsproto.CompletionItem, +) *lsproto.CompletionList { + defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) + itemDefaults := l.setItemDefaults( + clientOptions, + position, + file, + items, + &defaultCommitCharacters, + nil, /*optionalReplacementSpan*/ + ) + return &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: items, + } +} + +var jsDocTagNames = []string{ + "abstract", + "access", + "alias", + "argument", + "async", + "augments", + "author", + "borrows", + "callback", + "class", + "classdesc", + "constant", + "constructor", + "constructs", + "copyright", + "default", + "deprecated", + "description", + "emits", + "enum", + "event", + "example", + "exports", + "extends", + "external", + "field", + "file", + "fileoverview", + "fires", + "function", + "generator", + "global", + "hideconstructor", + "host", + "ignore", + "implements", + "import", + "inheritdoc", + "inner", + "instance", + "interface", + "kind", + "lends", + "license", + "link", + "linkcode", + "linkplain", + "listens", + "member", + "memberof", + "method", + "mixes", + "module", + "name", + "namespace", + "overload", + "override", + "package", + "param", + "private", + "prop", + "property", + "protected", + "public", + "readonly", + "requires", + "returns", + "satisfies", + "see", + "since", + "static", + "summary", + "template", + "this", + "throws", + "todo", + "tutorial", + "type", + "typedef", + "var", + "variation", + "version", + "virtual", + "yields", +} + +var jsDocTagNameCompletionItems = sync.OnceValue(func() []*lsproto.CompletionItem { + items := make([]*lsproto.CompletionItem, 0, len(jsDocTagNames)) + for _, tagName := range jsDocTagNames { + item := &lsproto.CompletionItem{ + Label: tagName, + Kind: ptrTo(lsproto.CompletionItemKindKeyword), + SortText: ptrTo(string(SortTextLocationPriority)), + } + items = append(items, item) + } + return items +}) + +var jsDocTagCompletionItems = sync.OnceValue(func() []*lsproto.CompletionItem { + items := make([]*lsproto.CompletionItem, 0, len(jsDocTagNames)) + for _, tagName := range jsDocTagNames { + item := &lsproto.CompletionItem{ + Label: "@" + tagName, + Kind: ptrTo(lsproto.CompletionItemKindKeyword), + SortText: ptrTo(string(SortTextLocationPriority)), + } + items = append(items, item) + } + return items +}) + +func getJSDocTagNameCompletions() []*lsproto.CompletionItem { + return cloneItems(jsDocTagNameCompletionItems()) +} + +func getJSDocTagCompletions() []*lsproto.CompletionItem { + return cloneItems(jsDocTagCompletionItems()) +} + +func getJSDocParameterCompletions( + clientOptions *lsproto.CompletionClientCapabilities, + file *ast.SourceFile, + position int, + typeChecker *checker.Checker, + options *core.CompilerOptions, + preferences *UserPreferences, + tagNameOnly bool, +) []*lsproto.CompletionItem { + currentToken := astnav.GetTokenAtPosition(file, position) + if !ast.IsJSDocTag(currentToken) && !currentToken.IsJSDoc() { + return nil + } + var jsDoc *ast.JSDocNode + if currentToken.IsJSDoc() { + jsDoc = currentToken + } else { + jsDoc = currentToken.Parent + } + if !jsDoc.IsJSDoc() { + return nil + } + fun := jsDoc.Parent + if !ast.IsFunctionLike(fun) { + return nil + } + + isJS := ast.IsSourceFileJS(file) + // isSnippet := clientSupportsItemSnippet(clientOptions) + isSnippet := false // !!! need snippet printer + paramTagCount := 0 + var tags []*ast.JSDocTag + if jsDoc.AsJSDoc().Tags != nil { + tags = jsDoc.AsJSDoc().Tags.Nodes + } + for _, tag := range tags { + if ast.IsJSDocParameterTag(tag) && tag.End() <= position { + paramTagCount++ + } + } + return core.MapNonNil(fun.Parameters(), func(param *ast.ParameterDeclarationNode) *lsproto.CompletionItem { + if len(param.JSDoc(file)) > 0 { + return nil // Parameter is already annotated. + } + if ast.IsIdentifier(param.Name()) { // Named parameter + tabstopCounter := 1 + paramName := param.Name().Text() + displayText := getJSDocParamAnnotation( + paramName, + param.Initializer(), + param.AsParameterDeclaration().DotDotDotToken, + isJS, + /*isObject*/ false, + /*isSnippet*/ false, + typeChecker, + options, + preferences, + &tabstopCounter, + ) + var snippetText string + if isSnippet { + snippetText = getJSDocParamAnnotation( + paramName, + param.Initializer(), + param.AsParameterDeclaration().DotDotDotToken, + isJS, + /*isObject*/ false, + /*isSnippet*/ true, + typeChecker, + options, + preferences, + &tabstopCounter, + ) + } + if tagNameOnly { // Remove `@` + displayText = displayText[1:] + if snippetText != "" { + snippetText = snippetText[1:] + } + } + + return &lsproto.CompletionItem{ + Label: displayText, + Kind: ptrTo(lsproto.CompletionItemKindVariable), + SortText: ptrTo(string(SortTextLocationPriority)), + InsertText: strPtrTo(snippetText), + InsertTextFormat: core.IfElse(isSnippet, ptrTo(lsproto.InsertTextFormatSnippet), nil), + } + } else if slices.Index(param.Parent.Parameters(), param) == paramTagCount { + // Destructuring parameter; do it positionally + paramPath := fmt.Sprintf("param%d", paramTagCount) + displayTextResult := generateJSDocParamTagsForDestructuring( + paramPath, + param.Name(), + param.Initializer(), + param.AsParameterDeclaration().DotDotDotToken, + isJS, + /*isSnippet*/ false, + typeChecker, + options, + preferences, + ) + var snippetText string + if isSnippet { + snippetTextResult := generateJSDocParamTagsForDestructuring( + paramPath, + param.Name(), + param.Initializer(), + param.AsParameterDeclaration().DotDotDotToken, + isJS, + /*isSnippet*/ true, + typeChecker, + options, + preferences, + ) + snippetText = strings.Join(snippetTextResult, options.NewLine.GetNewLineCharacter()+"* ") + } + displayText := strings.Join(displayTextResult, options.NewLine.GetNewLineCharacter()+"* ") + if tagNameOnly { // Remove `@` + displayText = strings.TrimPrefix(displayText, "@") + snippetText = strings.TrimPrefix(snippetText, "@") + } + return &lsproto.CompletionItem{ + Label: displayText, + Kind: ptrTo(lsproto.CompletionItemKindVariable), + SortText: ptrTo(string(SortTextLocationPriority)), + InsertText: strPtrTo(snippetText), + InsertTextFormat: core.IfElse(isSnippet, ptrTo(lsproto.InsertTextFormatSnippet), nil), + } + } + return nil + }) +} + +func getJSDocParamAnnotation( + paramName string, + initializer *ast.Expression, + dotDotDotToken *ast.TokenNode, + isJS bool, + isObject bool, + isSnippet bool, + typeChecker *checker.Checker, + options *core.CompilerOptions, + preferences *UserPreferences, + tabstopCounter *int, +) string { + if isSnippet { + // !!! Debug.assertIsDefined(tabstopCounter); + } + if initializer != nil { + paramName = getJSDocParamNameWithInitializer(paramName, initializer) + } + if isSnippet { + paramName = escapeSnippetText(paramName) + } + if isJS { + t := "*" + if isObject { + // !!! Debug.assert(!dotDotDotToken, `Cannot annotate a rest parameter with type 'Object'.`); + t = "Object" + } else { + if initializer != nil { + inferredType := typeChecker.GetTypeAtLocation(initializer.Parent) + if inferredType.Flags()&(checker.TypeFlagsAny|checker.TypeFlagsVoid) == 0 { + file := ast.GetSourceFileOfNode(initializer) + quotePreference := getQuotePreference(file, preferences) + builderFlags := core.IfElse( + quotePreference == quotePreferenceSingle, + nodebuilder.FlagsUseSingleQuotesForStringLiteralType, + nodebuilder.FlagsNone, + ) + typeNode := typeChecker.TypeToTypeNode(inferredType, ast.FindAncestor(initializer, ast.IsFunctionLike), builderFlags) + if typeNode != nil { + emitContext := printer.NewEmitContext() + // !!! snippet p + p := printer.NewPrinter(printer.PrinterOptions{ + RemoveComments: true, + // !!! + // Module: options.Module, + // ModuleResolution: options.ModuleResolution, + // Target: options.Target, + }, printer.PrintHandlers{}, emitContext) + emitContext.SetEmitFlags(typeNode, printer.EFSingleLine) + t = p.Emit(typeNode, file) + } + } + } + if isSnippet && t == "*" { + tabstop := *tabstopCounter + *tabstopCounter++ + t = fmt.Sprintf("${%d:%s}", tabstop, t) + } + } + dotDotDot := core.IfElse(!isObject && dotDotDotToken != nil, "...", "") + var description string + if isSnippet { + tabstop := *tabstopCounter + *tabstopCounter++ + description = fmt.Sprintf("${%d}", tabstop) + } + return fmt.Sprintf("@param {%s%s} %s %s", dotDotDot, t, paramName, description) + } else { + var description string + if isSnippet { + tabstop := *tabstopCounter + *tabstopCounter++ + description = fmt.Sprintf("${%d}", tabstop) + } + return fmt.Sprintf("@param %s %s", paramName, description) + } +} + +func getJSDocParamNameWithInitializer(paramName string, initializer *ast.Expression) string { + initializerText := strings.TrimSpace(scanner.GetTextOfNode(initializer)) + if strings.Contains(initializerText, "\n") || len(initializerText) > 80 { + return fmt.Sprintf("[%s]", paramName) + } + return fmt.Sprintf("[%s=%s]", paramName, initializerText) +} + +func generateJSDocParamTagsForDestructuring( + path string, + pattern *ast.BindingPatternNode, + initializer *ast.Expression, + dotDotDotToken *ast.TokenNode, + isJS bool, + isSnippet bool, + typeChecker *checker.Checker, + options *core.CompilerOptions, + preferences *UserPreferences, +) []string { + tabstopCounter := 1 + if !isJS { + return []string{getJSDocParamAnnotation( + path, + initializer, + dotDotDotToken, + isJS, + /*isObject*/ false, + isSnippet, + typeChecker, + options, + preferences, + &tabstopCounter, + )} + } + return jsDocParamPatternWorker( + path, + pattern, + initializer, + dotDotDotToken, + isJS, + isSnippet, + typeChecker, + options, + preferences, + &tabstopCounter, + ) +} + +func jsDocParamPatternWorker( + path string, + pattern *ast.BindingPatternNode, + initializer *ast.Expression, + dotDotDotToken *ast.TokenNode, + isJS bool, + isSnippet bool, + typeChecker *checker.Checker, + options *core.CompilerOptions, + preferences *UserPreferences, + counter *int, +) []string { + if ast.IsObjectBindingPattern(pattern) && dotDotDotToken == nil { + childCounter := *counter + rootParam := getJSDocParamAnnotation( + path, + initializer, + dotDotDotToken, + isJS, + /*isObject*/ true, + isSnippet, + typeChecker, + options, + preferences, + &childCounter, + ) + var childTags []string + for _, element := range pattern.Elements() { + elementTags := jsDocParamElementWorker( + path, + element, + initializer, + dotDotDotToken, + isJS, + isSnippet, + typeChecker, + options, + preferences, + &childCounter, + ) + if len(elementTags) == 0 { + childTags = nil + break + } + childTags = append(childTags, elementTags...) + } + if len(childTags) > 0 { + *counter = childCounter + return append([]string{rootParam}, childTags...) + } + } + return []string{ + getJSDocParamAnnotation( + path, + initializer, + dotDotDotToken, + isJS, + /*isObject*/ false, + isSnippet, + typeChecker, + options, + preferences, + counter, + ), + } +} + +// Assumes binding element is inside object binding pattern. +// We can't deeply annotate an array binding pattern. +func jsDocParamElementWorker( + path string, + element *ast.BindingElementNode, + initializer *ast.Expression, + dotDotDotToken *ast.TokenNode, + isJS bool, + isSnippet bool, + typeChecker *checker.Checker, + options *core.CompilerOptions, + preferences *UserPreferences, + counter *int, +) []string { + if ast.IsIdentifier(element.Name()) { // `{ b }` or `{ b: newB }` + var propertyName string + if element.PropertyName() != nil { + propertyName, _ = ast.TryGetTextOfPropertyName(element.PropertyName()) + } else { + propertyName = element.Name().Text() + } + if propertyName == "" { + return nil + } + paramName := fmt.Sprintf("%s.%s", path, propertyName) + return []string{ + getJSDocParamAnnotation( + paramName, + element.Initializer(), + element.AsBindingElement().DotDotDotToken, + isJS, + /*isObject*/ false, + isSnippet, + typeChecker, + options, + preferences, + counter, + ), + } + } else if element.PropertyName() != nil { // `{ b: {...} }` or `{ b: [...] }` + propertyName, _ := ast.TryGetTextOfPropertyName(element.PropertyName()) + if propertyName == "" { + return nil + } + return jsDocParamPatternWorker( + fmt.Sprintf("%s.%s", path, propertyName), + element.Name(), + element.Initializer(), + element.AsBindingElement().DotDotDotToken, + isJS, + isSnippet, + typeChecker, + options, + preferences, + counter, + ) + } + return nil +} + +func getJSDocParameterNameCompletions(tag *ast.JSDocParameterTag) []*lsproto.CompletionItem { + if !ast.IsIdentifier(tag.Name()) { + return nil + } + nameThusFar := tag.Name().Text() + jsDoc := tag.Parent + fn := jsDoc.Parent + if !ast.IsFunctionLike(fn) { + return nil + } + + var tags []*ast.JSDocTag + if jsDoc.AsJSDoc().Tags != nil { + tags = jsDoc.AsJSDoc().Tags.Nodes + } + + return core.MapNonNil(fn.Parameters(), func(param *ast.ParameterDeclarationNode) *lsproto.CompletionItem { + if !ast.IsIdentifier(param.Name()) { + return nil + } + + name := param.Name().Text() + if core.Some(tags, func(t *ast.JSDocTag) bool { + return t != tag.AsNode() && + ast.IsJSDocParameterTag(t) && + ast.IsIdentifier(t.Name()) && + t.Name().Text() == name + }) || nameThusFar != "" && !strings.HasPrefix(name, nameThusFar) { + return nil + } + + return &lsproto.CompletionItem{ + Label: name, + Kind: ptrTo(lsproto.CompletionItemKindVariable), + SortText: ptrTo(string(SortTextLocationPriority)), + } + }) +} From d0fe474775962691331f20bcdb223a29a19340c3 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 11 Aug 2025 15:35:04 -0700 Subject: [PATCH 02/13] jsdoc completion details --- .../tests/basicJSDocCompletions_test.go | 40 +++++++++++++++++++ internal/ls/completions.go | 31 +++++++++----- internal/ls/string_completions.go | 6 +-- 3 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 internal/fourslash/tests/basicJSDocCompletions_test.go diff --git a/internal/fourslash/tests/basicJSDocCompletions_test.go b/internal/fourslash/tests/basicJSDocCompletions_test.go new file mode 100644 index 0000000000..1b41220db4 --- /dev/null +++ b/internal/fourslash/tests/basicJSDocCompletions_test.go @@ -0,0 +1,40 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + . "github.com/microsoft/typescript-go/internal/fourslash/tests/util" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestBasicJSDocCompletions(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = ` +/** + * @/*1*/ + */ +function foo(x) { + return x + 1; +}` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyCompletions(t, "1", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + &lsproto.CompletionItem{ + Label: "link", + Kind: PtrTo(lsproto.CompletionItemKindKeyword), + Detail: PtrTo("link"), + }, + "param", + "returns", + }, + }, + }) +} diff --git a/internal/ls/completions.go b/internal/ls/completions.go index d234a4636a..98273db57e 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -73,7 +73,6 @@ func ensureItemData(fileName string, pos int, list *lsproto.CompletionList) *lsp return list } -// !!! TODO: consolidate some of these into a single jsdoc data // *completionDataData | *completionDataKeyword | *completionDataJSDocTagName | *completionDataJSDocTag | *completionDataJSDocParameterName type completionData = any @@ -4795,14 +4794,24 @@ func (l *LanguageService) getCompletionItemDetails( ) switch { case symbolCompletion.request != nil: - request := symbolCompletion.request - // !!! JSDoc completions - if core.Some(request.keywordCompletions, func(c *lsproto.CompletionItem) bool { - return c.Label == itemData.Name - }) { + request := *symbolCompletion.request + switch request := request.(type) { + case *completionDataJSDocTagName: + return createSimpleDetails(item, itemData.Name) + case *completionDataJSDocTag: return createSimpleDetails(item, itemData.Name) + case *completionDataJSDocParameterName: + return createSimpleDetails(item, itemData.Name) + case *completionDataKeyword: + if core.Some(request.keywordCompletions, func(c *lsproto.CompletionItem) bool { + return c.Label == itemData.Name + }) { + return createSimpleDetails(item, itemData.Name) + } + return item + default: + panic(fmt.Sprintf("Unexpected completion data type: %T", request)) } - return nil case symbolCompletion.symbol != nil: symbolDetails := symbolCompletion.symbol actions := getCompletionItemActions(symbolDetails.symbol) @@ -4826,13 +4835,13 @@ func (l *LanguageService) getCompletionItemDetails( }) { return createSimpleDetails(item, itemData.Name) } - return nil + return item } } type detailsData struct { symbol *symbolDetails - request *completionDataKeyword + request *completionData literal *literalValue cases *struct{} } @@ -4871,9 +4880,9 @@ func getSymbolCompletionFromItemData( return detailsData{} } - if completionData, ok := completionData.(*completionDataKeyword); ok { + if _, ok := completionData.(*completionDataData); !ok { return detailsData{ - request: completionData, + request: &completionData, } } diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go index 6af4212224..c81b3a79f2 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -669,7 +669,7 @@ func (l *LanguageService) getStringLiteralCompletionDetails( preferences *UserPreferences, ) *lsproto.CompletionItem { if contextToken == nil || !ast.IsStringLiteralLike(contextToken) { - return nil + return item } completions := l.getStringLiteralCompletionEntries( ctx, @@ -680,7 +680,7 @@ func (l *LanguageService) getStringLiteralCompletionDetails( preferences, ) if completions == nil { - return nil + return item } checker, done := program.GetTypeCheckerForFile(ctx, file) defer done() @@ -718,5 +718,5 @@ func stringLiteralCompletionDetails( } } } - return nil + return item } From 5681cd2e2e61fdebe91834b5b2529e30f0a03954 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 12 Aug 2025 18:48:13 -0700 Subject: [PATCH 03/13] add test for parameter jsdoc case and fix bugs --- internal/ast/ast.go | 3 + internal/ast/utilities.go | 40 ++++++++- internal/astnav/tokens.go | 55 +++++++----- .../tests/basicJSDocCompletions_test.go | 83 ++++++++++++++++--- internal/ls/completions.go | 17 ++-- internal/ls/utilities.go | 16 +--- internal/lsutil/children.go | 18 +--- ...GetTokenAtPosition.mapCode.ts.baseline.txt | 19 +---- ...uchingPropertyName.mapCode.ts.baseline.txt | 19 +---- 9 files changed, 171 insertions(+), 99 deletions(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 5e31d45db3..8dd73d8fed 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -897,6 +897,7 @@ func (n *Node) IsTypeOnly() bool { return false } +// If updating this function, also update `hasComment`. func (n *Node) CommentList() *NodeList { switch n.Kind { case KindJSDoc: @@ -1033,6 +1034,8 @@ func (n *Node) ElementList() *NodeList { return n.AsNamedImports().Elements case KindNamedExports: return n.AsNamedExports().Elements + case KindObjectBindingPattern, KindArrayBindingPattern: + return n.AsBindingPattern().Elements } panic("Unhandled case in Node.ElementList: " + n.Kind.String()) diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index b34fa3d51f..36af987a3c 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -2104,6 +2104,9 @@ func TryGetTextOfPropertyName(name *Node) (string, bool) { } // True if node is of a JSDoc kind that may contain comment text. +// NOTE: while this is a faithful port of Strada's implementation, +// it is unsafe to call `.Comments()` on a node that returns true from this function, +// becuase JSDoc type literal and JSDoc signature nodes do not have comments. func IsJSDocCommentContainingNode(node *Node) bool { return node.Kind == KindJSDoc || node.Kind == KindJSDocText || @@ -3017,13 +3020,27 @@ func IsTypeKeywordToken(node *Node) bool { return node.Kind == KindTypeKeyword } -// If node is a single comment JSDoc, we do not visit the comment node list. -func IsJSDocSingleCommentNodeList(parent *Node, nodeList *NodeList) bool { - return IsJSDocSingleCommentNode(parent) && nodeList == parent.AsJSDoc().Comment +// See `IsJSDocSingleCommentNode`. +func IsJSDocSingleCommentNodeList(nodeList *NodeList) bool { + if nodeList == nil || len(nodeList.Nodes) == 0 { + return false + } + parent := nodeList.Nodes[0].Parent + return IsJSDocSingleCommentNode(parent) && nodeList == parent.CommentList() } +// See `IsJSDocSingleCommentNode`. +func IsJSDocSingleCommentNodeComment(node *Node) bool { + if node == nil || node.Parent == nil { + return false + } + return IsJSDocSingleCommentNode(node.Parent) && node == node.Parent.CommentList().Nodes[0] +} + +// In Strada, if a JSDoc node has a single comment, that comment is represented as a string property +// as a simplification, and therefore that comment is not visited by `forEachChild`. func IsJSDocSingleCommentNode(node *Node) bool { - return node.Kind == KindJSDoc && node.AsJSDoc().Comment != nil && len(node.AsJSDoc().Comment.Nodes) == 1 + return hasComment(node.Kind) && node.CommentList() != nil && len(node.CommentList().Nodes) == 1 } func IsValidTypeOnlyAliasUseSite(useSite *Node) bool { @@ -3635,3 +3652,18 @@ func GetSemanticJsxChildren(children []*JsxChild) []*JsxChild { } }) } + +// Returns true if the node kind has a comment property. +func hasComment(kind Kind) bool { + switch kind { + case KindJSDoc, KindJSDocTag, KindJSDocAugmentsTag, KindJSDocImplementsTag, + KindJSDocDeprecatedTag, KindJSDocPublicTag, KindJSDocPrivateTag, KindJSDocProtectedTag, + KindJSDocReadonlyTag, KindJSDocOverrideTag, KindJSDocCallbackTag, KindJSDocOverloadTag, + KindJSDocParameterTag, KindJSDocPropertyTag, KindJSDocReturnTag, KindJSDocThisTag, + KindJSDocTypeTag, KindJSDocTemplateTag, KindJSDocTypedefTag, KindJSDocSeeTag, + KindJSDocSatisfiesTag, KindJSDocImportTag: + return true + default: + return false + } +} diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go index c4b7f8186b..0d840f5261 100644 --- a/internal/astnav/tokens.go +++ b/internal/astnav/tokens.go @@ -125,10 +125,8 @@ func getTokenAtPosition( return nodeList } - nodeVisitor := getNodeVisitor(visitNode, visitNodeList) - for { - VisitEachChildAndJSDoc(current, sourceFile, nodeVisitor) + VisitEachChildAndJSDoc(current, sourceFile, visitNode, visitNodeList) // If prevSubtree was set on the last iteration, it ends at the target position. // Check if the rightmost token of prevSubtree should be returned based on the // `includePrecedingTokenAtEndPosition` callback. @@ -217,7 +215,13 @@ func findRightmostNode(node *ast.Node) *ast.Node { } } -func VisitEachChildAndJSDoc(node *ast.Node, sourceFile *ast.SourceFile, visitor *ast.NodeVisitor) { +func VisitEachChildAndJSDoc( + node *ast.Node, + sourceFile *ast.SourceFile, + visitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node, + visitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList, +) { + visitor := getNodeVisitor(visitNode, visitNodes) if node.Flags&ast.NodeFlagsHasJSDoc != 0 { for _, jsdoc := range node.JSDoc(sourceFile) { if visitor.Hooks.VisitNode != nil { @@ -275,9 +279,6 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a } if nodeList != nil && len(nodeList.Nodes) > 0 { nodes := nodeList.Nodes - if ast.IsJSDocSingleCommentNodeList(n, nodeList) { - return nodeList - } index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, _ *ast.Node) int { // synthetic jsdoc nodes should have jsdocNode.End() <= n.Pos() if nodes[middle].Flags&ast.NodeFlagsReparsed != 0 { @@ -308,8 +309,7 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a } return nodeList } - nodeVisitor := getNodeVisitor(visitNode, visitNodes) - VisitEachChildAndJSDoc(n, sourceFile, nodeVisitor) + VisitEachChildAndJSDoc(n, sourceFile, visitNode, visitNodes) if foundChild != nil { // Note that the span of a node's tokens is [getStartOfNode(node, ...), node.end). @@ -420,9 +420,6 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN } visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { if nodeList != nil && len(nodeList.Nodes) > 0 { - if ast.IsJSDocSingleCommentNodeList(n, nodeList) { - return nodeList - } hasChildren = true index, _ := core.BinarySearchUniqueFunc(nodeList.Nodes, func(middle int, node *ast.Node) int { if node.End() > endPos { @@ -450,8 +447,7 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN } return nodeList } - nodeVisitor := getNodeVisitor(visitNode, visitNodes) - VisitEachChildAndJSDoc(n, sourceFile, nodeVisitor) + VisitEachChildAndJSDoc(n, sourceFile, visitNode, visitNodes) // Three cases: // 1. The answer is a token of `rightmostValidNode`. @@ -563,8 +559,7 @@ func FindNextToken(previousToken *ast.Node, parent *ast.Node, file *ast.SourceFi } return nodeList } - nodeVisitor := getNodeVisitor(visitNode, visitNodes) - VisitEachChildAndJSDoc(n, file, nodeVisitor) + VisitEachChildAndJSDoc(n, file, visitNode, visitNodes) // Cases: // 1. no answer exists // 2. answer is an unvisited token @@ -597,13 +592,33 @@ func getNodeVisitor( visitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node, visitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList, ) *ast.NodeVisitor { + var wrappedVisitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node + var wrappedVisitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList + if visitNode != nil { + wrappedVisitNode = func(n *ast.Node, v *ast.NodeVisitor) *ast.Node { + if ast.IsJSDocSingleCommentNodeComment(n) { + return n + } + return visitNode(n, v) + } + } + + if visitNodes != nil { + wrappedVisitNodes = func(n *ast.NodeList, v *ast.NodeVisitor) *ast.NodeList { + if ast.IsJSDocSingleCommentNodeList(n) { + return n + } + return visitNodes(n, v) + } + } + return ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: visitNode, - VisitToken: visitNode, - VisitNodes: visitNodes, + VisitNode: wrappedVisitNode, + VisitToken: wrappedVisitNode, + VisitNodes: wrappedVisitNodes, VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { if modifiers != nil { - visitNodes(&modifiers.NodeList, visitor) + wrappedVisitNodes(&modifiers.NodeList, visitor) } return modifiers }, diff --git a/internal/fourslash/tests/basicJSDocCompletions_test.go b/internal/fourslash/tests/basicJSDocCompletions_test.go index 1b41220db4..2a646ebd76 100644 --- a/internal/fourslash/tests/basicJSDocCompletions_test.go +++ b/internal/fourslash/tests/basicJSDocCompletions_test.go @@ -5,7 +5,6 @@ import ( "github.com/microsoft/typescript-go/internal/fourslash" . "github.com/microsoft/typescript-go/internal/fourslash/tests/util" - "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -13,27 +12,91 @@ func TestBasicJSDocCompletions(t *testing.T) { t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = ` +// @filename: file.js +// @allowJs: true /** * @/*1*/ */ function foo(x) { return x + 1; -}` +} + +/** + * /*2*/ + */ +function bar(x, { y }) { + return x + y; +} + +/** + * @param {number} x + * /*3*/ + */ +function baz(x, { y }) { + return x + y; +} + +/** + * @param {number} x + * @param {Object} param1 + * @param {n/*4*/} param1.y + */ +function baz(x, { y }) { + return x + y; +} +` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) - f.VerifyCompletions(t, "1", &fourslash.CompletionsExpectedList{ + // f.VerifyCompletions(t, "1", &fourslash.CompletionsExpectedList{ + // IsIncomplete: false, + // ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + // CommitCharacters: &DefaultCommitCharacters, + // }, + // Items: &fourslash.CompletionsExpectedItems{ + // Includes: []fourslash.CompletionsExpectedItem{ + // &lsproto.CompletionItem{ + // Label: "link", + // Kind: PtrTo(lsproto.CompletionItemKindKeyword), + // Detail: PtrTo("link"), + // }, + // "param", + // "returns", + // "param {*} x ", + // }, + // }, + // }) + // f.VerifyCompletions(t, "2", &fourslash.CompletionsExpectedList{ + // IsIncomplete: false, + // ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + // CommitCharacters: &DefaultCommitCharacters, + // }, + // Items: &fourslash.CompletionsExpectedItems{ + // Includes: []fourslash.CompletionsExpectedItem{ + // "@param", + // "@param {*} x ", + // }, + // }, + // }) + // f.VerifyCompletions(t, "3", &fourslash.CompletionsExpectedList{ + // IsIncomplete: false, + // ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + // CommitCharacters: &DefaultCommitCharacters, + // }, + // Items: &fourslash.CompletionsExpectedItems{ + // Includes: []fourslash.CompletionsExpectedItem{ + // "@param", + // "@param {Object} param1 \n* @param {*} param1.y ", + // }, + // }, + // }) + f.VerifyCompletions(t, "4", &fourslash.CompletionsExpectedList{ IsIncomplete: false, ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, }, Items: &fourslash.CompletionsExpectedItems{ Includes: []fourslash.CompletionsExpectedItem{ - &lsproto.CompletionItem{ - Label: "link", - Kind: PtrTo(lsproto.CompletionItemKindKeyword), - Detail: PtrTo("link"), - }, - "param", - "returns", + "number", }, }, }) diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 98273db57e..7f9b144e25 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -475,7 +475,7 @@ func getCompletionData( } } if noCommentPrefix { - return &completionDataJSDocTagName{} + return &completionDataJSDocTag{} } } } @@ -5214,13 +5214,18 @@ func getJSDocParameterCompletions( tags = jsDoc.AsJSDoc().Tags.Nodes } for _, tag := range tags { - if ast.IsJSDocParameterTag(tag) && tag.End() <= position { + if ast.IsJSDocParameterTag(tag) && + astnav.GetStartOfNode(tag, file, false /*includeJSDoc*/) < position && + ast.IsIdentifier(tag.Name()) { paramTagCount++ } } + paramIndex := -1 return core.MapNonNil(fun.Parameters(), func(param *ast.ParameterDeclarationNode) *lsproto.CompletionItem { - if len(param.JSDoc(file)) > 0 { - return nil // Parameter is already annotated. + paramIndex++ + if paramIndex < paramTagCount { + // This parameter is already annotated. + return nil } if ast.IsIdentifier(param.Name()) { // Named parameter tabstopCounter := 1 @@ -5266,9 +5271,9 @@ func getJSDocParameterCompletions( InsertText: strPtrTo(snippetText), InsertTextFormat: core.IfElse(isSnippet, ptrTo(lsproto.InsertTextFormatSnippet), nil), } - } else if slices.Index(param.Parent.Parameters(), param) == paramTagCount { + } else if paramIndex == paramTagCount { // Destructuring parameter; do it positionally - paramPath := fmt.Sprintf("param%d", paramTagCount) + paramPath := fmt.Sprintf("param%d", paramIndex) displayTextResult := generateJSDocParamTagsForDestructuring( paramPath, param.Name(), diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 3cd9ee217b..ab58122018 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -438,6 +438,9 @@ func probablyUsesSemicolons(file *ast.SourceFile) bool { var visit func(node *ast.Node) bool visit = func(node *ast.Node) bool { + if node.Flags&ast.NodeFlagsReparsed != 0 { + return false + } if lsutil.SyntaxRequiresTrailingSemicolonOrASI(node.Kind) { lastToken := lsutil.GetLastToken(node, file) if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken { @@ -1614,18 +1617,7 @@ func findContainingList(node *ast.Node, file *ast.SourceFile) *ast.NodeList { } return nodes } - nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: visitNode, - VisitToken: visitNode, - VisitNodes: visitNodes, - VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { - if modifiers != nil { - visitNodes(&modifiers.NodeList, visitor) - } - return modifiers - }, - }) - astnav.VisitEachChildAndJSDoc(node.Parent, file, nodeVisitor) + astnav.VisitEachChildAndJSDoc(node.Parent, file, visitNode, visitNodes) return list } diff --git a/internal/lsutil/children.go b/internal/lsutil/children.go index 651172d25a..7e6c0d1ed0 100644 --- a/internal/lsutil/children.go +++ b/internal/lsutil/children.go @@ -10,7 +10,7 @@ import ( // Replaces last(node.getChildren(sourceFile)) func GetLastChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { lastChildNode := GetLastVisitedChild(node, sourceFile) - if ast.IsJSDocSingleCommentNode(node) { + if ast.IsJSDocSingleCommentNode(node) && lastChildNode == nil { return nil } var tokenStartPos int @@ -67,7 +67,7 @@ func GetLastVisitedChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { return n } visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { - if nodeList != nil && len(nodeList.Nodes) > 0 && !ast.IsJSDocSingleCommentNodeList(node, nodeList) { + if nodeList != nil && len(nodeList.Nodes) > 0 { for i := len(nodeList.Nodes) - 1; i >= 0; i-- { if nodeList.Nodes[i].Flags&ast.NodeFlagsReparsed == 0 { lastChild = nodeList.Nodes[i] @@ -78,19 +78,7 @@ func GetLastVisitedChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { return nodeList } - nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: visitNode, - VisitToken: visitNode, - VisitNodes: visitNodeList, - VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { - if modifiers != nil { - visitNodeList(&modifiers.NodeList, visitor) - } - return modifiers - }, - }) - - astnav.VisitEachChildAndJSDoc(node, sourceFile, nodeVisitor) + astnav.VisitEachChildAndJSDoc(node, sourceFile, visitNode, visitNodeList) return lastChild } diff --git a/testdata/baselines/reference/astnav/GetTokenAtPosition.mapCode.ts.baseline.txt b/testdata/baselines/reference/astnav/GetTokenAtPosition.mapCode.ts.baseline.txt index c33d649c10..06da9dd9a0 100644 --- a/testdata/baselines/reference/astnav/GetTokenAtPosition.mapCode.ts.baseline.txt +++ b/testdata/baselines/reference/astnav/GetTokenAtPosition.mapCode.ts.baseline.txt @@ -51,29 +51,16 @@ 74 │ // which one makes the most sense, and grab the NodeArray from there. Do -〚Positions: [1611, 1730]〛 -【TS: JSDoc [1611, 1733)】 -《Go: JSDocText [1611, 1731)》 -66 │} -67 │ -68 │《【〚/** -69 │ * Tries to parse something into either "top-level" statements, or into blocks -70 │ * of class-context definitions. -71 │ 〛》*/】 -72 │function parse(sourceFile: SourceFile, content: string): NodeArray { -73 │ // We're going to speculatively parse different kinds of contexts to see - - -〚Positions: [1731, 1732]〛 +〚Positions: [1611, 1732]〛 【TS: JSDoc [1611, 1733)】 《Go: JSDoc [1607, 1733)》 64 │ }, 65 │ ); 66 │}《 67 │ -68 │【/** +68 │【〚/** 69 │ * Tries to parse something into either "top-level" statements, or into blocks 70 │ * of class-context definitions. -71 │ 〚*/〛】》 +71 │ */〛】》 72 │function parse(sourceFile: SourceFile, content: string): NodeArray { 73 │ // We're going to speculatively parse different kinds of contexts to see diff --git a/testdata/baselines/reference/astnav/GetTouchingPropertyName.mapCode.ts.baseline.txt b/testdata/baselines/reference/astnav/GetTouchingPropertyName.mapCode.ts.baseline.txt index 451fb23458..f99eb69681 100644 --- a/testdata/baselines/reference/astnav/GetTouchingPropertyName.mapCode.ts.baseline.txt +++ b/testdata/baselines/reference/astnav/GetTouchingPropertyName.mapCode.ts.baseline.txt @@ -58,29 +58,16 @@ 68 │/** -〚Positions: [1611, 1730]〛 -【TS: JSDoc [1611, 1733)】 -《Go: JSDocText [1611, 1731)》 -66 │} -67 │ -68 │《【〚/** -69 │ * Tries to parse something into either "top-level" statements, or into blocks -70 │ * of class-context definitions. -71 │ 〛》*/】 -72 │function parse(sourceFile: SourceFile, content: string): NodeArray { -73 │ // We're going to speculatively parse different kinds of contexts to see - - -〚Positions: [1731, 1732]〛 +〚Positions: [1611, 1732]〛 【TS: JSDoc [1611, 1733)】 《Go: JSDoc [1607, 1733)》 64 │ }, 65 │ ); 66 │}《 67 │ -68 │【/** +68 │【〚/** 69 │ * Tries to parse something into either "top-level" statements, or into blocks 70 │ * of class-context definitions. -71 │ 〚*/〛】》 +71 │ */〛】》 72 │function parse(sourceFile: SourceFile, content: string): NodeArray { 73 │ // We're going to speculatively parse different kinds of contexts to see From 586cc88be2025f1274371847213b4464dffa8fa7 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 12 Aug 2025 18:58:46 -0700 Subject: [PATCH 04/13] update failing --- internal/fourslash/_scripts/failingTests.txt | 1 - internal/fourslash/tests/gen/completionsJsdocTag_test.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/fourslash/_scripts/failingTests.txt b/internal/fourslash/_scripts/failingTests.txt index 34a7208f89..2bbbb8f568 100644 --- a/internal/fourslash/_scripts/failingTests.txt +++ b/internal/fourslash/_scripts/failingTests.txt @@ -129,7 +129,6 @@ TestCompletionsJSDocImportTagAttributesEmptyModuleSpecifier1 TestCompletionsJSDocImportTagAttributesErrorModuleSpecifier1 TestCompletionsJSDocImportTagEmptyModuleSpecifier1 TestCompletionsJSDocNoCrash1 -TestCompletionsJsdocTag TestCompletionsJsdocTypeTagCast TestCompletionsJsxAttribute2 TestCompletionsJsxAttributeInitializer2 diff --git a/internal/fourslash/tests/gen/completionsJsdocTag_test.go b/internal/fourslash/tests/gen/completionsJsdocTag_test.go index 645b1de8ab..5b3c8238f7 100644 --- a/internal/fourslash/tests/gen/completionsJsdocTag_test.go +++ b/internal/fourslash/tests/gen/completionsJsdocTag_test.go @@ -11,7 +11,7 @@ import ( func TestCompletionsJsdocTag(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @typedef {object} T From 735e964f43361a0e1c1535d96bdd1ac1e8cdabf7 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 12 Aug 2025 19:07:34 -0700 Subject: [PATCH 05/13] uncomment test --- .../tests/basicJSDocCompletions_test.go | 85 ++++++++++--------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/internal/fourslash/tests/basicJSDocCompletions_test.go b/internal/fourslash/tests/basicJSDocCompletions_test.go index 2a646ebd76..e97553a3db 100644 --- a/internal/fourslash/tests/basicJSDocCompletions_test.go +++ b/internal/fourslash/tests/basicJSDocCompletions_test.go @@ -5,6 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/fourslash" . "github.com/microsoft/typescript-go/internal/fourslash/tests/util" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -46,48 +47,48 @@ function baz(x, { y }) { } ` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) - // f.VerifyCompletions(t, "1", &fourslash.CompletionsExpectedList{ - // IsIncomplete: false, - // ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ - // CommitCharacters: &DefaultCommitCharacters, - // }, - // Items: &fourslash.CompletionsExpectedItems{ - // Includes: []fourslash.CompletionsExpectedItem{ - // &lsproto.CompletionItem{ - // Label: "link", - // Kind: PtrTo(lsproto.CompletionItemKindKeyword), - // Detail: PtrTo("link"), - // }, - // "param", - // "returns", - // "param {*} x ", - // }, - // }, - // }) - // f.VerifyCompletions(t, "2", &fourslash.CompletionsExpectedList{ - // IsIncomplete: false, - // ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ - // CommitCharacters: &DefaultCommitCharacters, - // }, - // Items: &fourslash.CompletionsExpectedItems{ - // Includes: []fourslash.CompletionsExpectedItem{ - // "@param", - // "@param {*} x ", - // }, - // }, - // }) - // f.VerifyCompletions(t, "3", &fourslash.CompletionsExpectedList{ - // IsIncomplete: false, - // ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ - // CommitCharacters: &DefaultCommitCharacters, - // }, - // Items: &fourslash.CompletionsExpectedItems{ - // Includes: []fourslash.CompletionsExpectedItem{ - // "@param", - // "@param {Object} param1 \n* @param {*} param1.y ", - // }, - // }, - // }) + f.VerifyCompletions(t, "1", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + &lsproto.CompletionItem{ + Label: "link", + Kind: PtrTo(lsproto.CompletionItemKindKeyword), + Detail: PtrTo("link"), + }, + "param", + "returns", + "param {*} x ", + }, + }, + }) + f.VerifyCompletions(t, "2", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + "@param", + "@param {*} x ", + }, + }, + }) + f.VerifyCompletions(t, "3", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + "@param", + "@param {Object} param1 \n* @param {*} param1.y ", + }, + }, + }) f.VerifyCompletions(t, "4", &fourslash.CompletionsExpectedList{ IsIncomplete: false, ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ From dbb019c30212c87d6ae565d33d7ff481b210f797 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 12 Aug 2025 19:10:35 -0700 Subject: [PATCH 06/13] remove comment --- internal/ls/completions.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 7f9b144e25..6246d2f297 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -5007,7 +5007,6 @@ func getJSDocTagAtPosition(node *ast.Node, position int) *ast.JSDocTag { func tryGetTypeExpressionFromTag(tag *ast.JSDocTag) *ast.Node { if isTagWithTypeExpression(tag) { var typeExpression *ast.Node - // !!! TODO: need to map the jsdoc type node to a synthetic type node here if ast.IsJSDocTemplateTag(tag) { typeExpression = tag.AsJSDocTemplateTag().Constraint } else { From 9f591cd4872d9966cc6c3b706f18deca7efb46fb Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 12 Aug 2025 19:18:04 -0700 Subject: [PATCH 07/13] fix bad spelling --- internal/ast/utilities.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 36af987a3c..fdf074d21b 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -2106,7 +2106,7 @@ func TryGetTextOfPropertyName(name *Node) (string, bool) { // True if node is of a JSDoc kind that may contain comment text. // NOTE: while this is a faithful port of Strada's implementation, // it is unsafe to call `.Comments()` on a node that returns true from this function, -// becuase JSDoc type literal and JSDoc signature nodes do not have comments. +// because JSDoc type literal and JSDoc signature nodes do not have comments. func IsJSDocCommentContainingNode(node *Node) bool { return node.Kind == KindJSDoc || node.Kind == KindJSDocText || From d9fdf3b58f78ba71258d48f31f24a3599472c431 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 13 Aug 2025 08:32:53 -0700 Subject: [PATCH 08/13] access string position directly --- internal/ls/completions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 6246d2f297..79616ceaa3 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -445,7 +445,7 @@ func getCompletionData( isInSnippetScope := false if insideComment != nil { if hasDocComment(file, position) { - if r, _ := utf8.DecodeRuneInString(file.Text()[position:]); r == '@' { + if file.Text()[position] == '@' { // The current position is next to the '@' sign, when no tag name being provided yet. // Provide a full list of tag names return &completionDataJSDocTagName{} From 29a2e9764df205fef320d1dfc7392e21cf2c0678 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 13 Aug 2025 12:39:25 -0700 Subject: [PATCH 09/13] update test with case that has param initializer --- .../tests/basicJSDocCompletions_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/fourslash/tests/basicJSDocCompletions_test.go b/internal/fourslash/tests/basicJSDocCompletions_test.go index e97553a3db..2e5b4ca92a 100644 --- a/internal/fourslash/tests/basicJSDocCompletions_test.go +++ b/internal/fourslash/tests/basicJSDocCompletions_test.go @@ -45,6 +45,13 @@ function baz(x, { y }) { function baz(x, { y }) { return x + y; } + +/** + * @/*5*/ + */ +function baz(x = 0) { + return x * 2; +} ` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) f.VerifyCompletions(t, "1", &fourslash.CompletionsExpectedList{ @@ -101,4 +108,16 @@ function baz(x, { y }) { }, }, }) + f.VerifyCompletions(t, "5", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + "param {number} [x=0] ", + }, + }, + }) } From f2d246dd8b0a73780825a1499d4972efa0555380 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 13 Aug 2025 16:15:29 -0700 Subject: [PATCH 10/13] Apply suggestions from code review Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> --- internal/ls/completions.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 79616ceaa3..9034083c1e 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -480,19 +480,17 @@ func getCompletionData( } } - // Completion should work inside certain JsDoc tags. For example: + // Completion should work inside certain JSDoc tags. For example: // /** @type {number | string} */ // Completion should work in the brackets - tag := getJSDocTagAtPosition(currentToken, position) - if tag != nil { + if tag := getJSDocTagAtPosition(currentToken, position); tag != nil { if tag.TagName().Pos() <= position && position <= tag.TagName().End() { return &completionDataJSDocTagName{} } if ast.IsJSDocImportTag(tag) { insideJsDocImportTag = true } else { - typeExpression := tryGetTypeExpressionFromTag(tag) - if typeExpression != nil { + if typeExpression := tryGetTypeExpressionFromTag(tag); typeExpression != nil { currentToken = astnav.GetTokenAtPosition(file, position) if currentToken == nil || (!ast.IsDeclarationName(currentToken) && From ec2c9bbe3d8444f52ecfda35ab283d667e1014b1 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 13 Aug 2025 16:22:20 -0700 Subject: [PATCH 11/13] Object -> object --- internal/ls/completions.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 9034083c1e..8a72ad4d72 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -5338,8 +5338,8 @@ func getJSDocParamAnnotation( if isJS { t := "*" if isObject { - // !!! Debug.assert(!dotDotDotToken, `Cannot annotate a rest parameter with type 'Object'.`); - t = "Object" + // !!! Debug.assert(!dotDotDotToken, `Cannot annotate a rest parameter with type 'object'.`); + t = "object" } else { if initializer != nil { inferredType := typeChecker.GetTypeAtLocation(initializer.Parent) From cb9e480a727cfde3a4374a9f2c731664d78b328f Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 13 Aug 2025 16:32:10 -0700 Subject: [PATCH 12/13] rename and move bad jsdoc function --- internal/ast/utilities.go | 13 ------------- internal/astnav/tokens.go | 13 +++++++++++-- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index fdf074d21b..3d8dcbf395 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -2103,19 +2103,6 @@ func TryGetTextOfPropertyName(name *Node) (string, bool) { return "", false } -// True if node is of a JSDoc kind that may contain comment text. -// NOTE: while this is a faithful port of Strada's implementation, -// it is unsafe to call `.Comments()` on a node that returns true from this function, -// because JSDoc type literal and JSDoc signature nodes do not have comments. -func IsJSDocCommentContainingNode(node *Node) bool { - return node.Kind == KindJSDoc || - node.Kind == KindJSDocText || - node.Kind == KindJSDocTypeLiteral || - node.Kind == KindJSDocSignature || - IsJSDocLinkLike(node) || - IsJSDocTag(node) -} - func IsJSDocNode(node *Node) bool { return node.Kind >= KindFirstJSDocNode && node.Kind <= KindLastJSDocNode } diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go index 0d840f5261..dffde94e89 100644 --- a/internal/astnav/tokens.go +++ b/internal/astnav/tokens.go @@ -144,7 +144,7 @@ func getTokenAtPosition( // we can in the AST. We've either found a token, or we need to run the scanner // to construct one that isn't stored in the AST. if next == nil { - if ast.IsTokenKind(current.Kind) || ast.IsJSDocCommentContainingNode(current) { + if ast.IsTokenKind(current.Kind) || shouldSkipChild(current) { return current } scanner := scanner.GetScannerForSourceFile(sourceFile, left) @@ -455,7 +455,7 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN // 3. The current node is a childless, token-less node. The answer is the current node. // Case 2: Look at unvisited trailing tokens that occur in between the rightmost visited nodes. - if !ast.IsJSDocCommentContainingNode(n) { // JSDoc nodes don't include trivia tokens as children. + if !shouldSkipChild(n) { // JSDoc nodes don't include trivia tokens as children. var startPos int if rightmostValidNode != nil { startPos = rightmostValidNode.End() @@ -624,3 +624,12 @@ func getNodeVisitor( }, }) } + +func shouldSkipChild(node *ast.Node) bool { + return node.Kind == ast.KindJSDoc || + node.Kind == ast.KindJSDocText || + node.Kind == ast.KindJSDocTypeLiteral || + node.Kind == ast.KindJSDocSignature || + ast.IsJSDocLinkLike(node) || + ast.IsJSDocTag(node) +} From ec55a18df240d4309faca248c319f1c3df5249a5 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 14 Aug 2025 09:32:07 -0700 Subject: [PATCH 13/13] update test --- internal/fourslash/tests/basicJSDocCompletions_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/fourslash/tests/basicJSDocCompletions_test.go b/internal/fourslash/tests/basicJSDocCompletions_test.go index 2e5b4ca92a..10dcaf412e 100644 --- a/internal/fourslash/tests/basicJSDocCompletions_test.go +++ b/internal/fourslash/tests/basicJSDocCompletions_test.go @@ -39,7 +39,7 @@ function baz(x, { y }) { /** * @param {number} x - * @param {Object} param1 + * @param {object} param1 * @param {n/*4*/} param1.y */ function baz(x, { y }) { @@ -92,7 +92,7 @@ function baz(x = 0) { Items: &fourslash.CompletionsExpectedItems{ Includes: []fourslash.CompletionsExpectedItem{ "@param", - "@param {Object} param1 \n* @param {*} param1.y ", + "@param {object} param1 \n* @param {*} param1.y ", }, }, })