diff --git a/internal/checker/nodebuilderimpl.go b/internal/checker/nodebuilderimpl.go index 2eda08fd60c..b92ec8f522a 100644 --- a/internal/checker/nodebuilderimpl.go +++ b/internal/checker/nodebuilderimpl.go @@ -2605,12 +2605,36 @@ func (b *NodeBuilderImpl) addPropertyToElementList(propertySymbol *ast.Symbol, t } propertySignature := b.f.NewPropertySignatureDeclaration(modifiers, propertyName, optionalToken, propertyTypeNode, nil) - b.setCommentRange(propertySignature, propertySymbol.ValueDeclaration) + b.preserveCommentsOn(propertySignature, propertySymbol) typeElements = append(typeElements, propertySignature) return typeElements } +// preserveCommentsOn copies documentation comments from a property symbol onto a synthesized node +// for declaration emit. Properties declared via a JSDoc `@property`/`@typedef` tag are reparsed into +// property signatures whose comment lives on a synthetic JSDoc rather than in a source comment range, +// so it is reattached as a synthetic leading comment (mirroring the old emitter's `preserveCommentsOn`). +// Otherwise the comment range of the value declaration is copied. +func (b *NodeBuilderImpl) preserveCommentsOn(node *ast.Node, propertySymbol *ast.Symbol) { + jsdocPropertyDeclaration := core.Find(propertySymbol.Declarations, func(d *ast.Node) bool { + return d.Flags&ast.NodeFlagsReparsed != 0 && d.Flags&ast.NodeFlagsHasJSDoc != 0 + }) + if jsdocPropertyDeclaration != nil { + jsdoc := core.FirstOrNil(jsdocPropertyDeclaration.EagerJSDoc(ast.GetSourceFileOfNode(jsdocPropertyDeclaration))) + if jsdoc != nil { + commentText := scanner.GetTextOfJSDocComment(jsdoc.AsJSDoc().Comment) + if commentText != "" { + comment := "*\n * " + strings.ReplaceAll(commentText, "\n", "\n * ") + "\n " + b.e.AddSyntheticLeadingComment(node, ast.KindMultiLineCommentTrivia, comment, true /*hasTrailingNewLine*/) + } + } + } else if propertySymbol.ValueDeclaration != nil { + // Copy comments to node for declaration emit + b.setCommentRange(node, propertySymbol.ValueDeclaration) + } +} + func (b *NodeBuilderImpl) createTypeNodesFromResolvedType(resolvedType *StructuredType) *ast.NodeList { if b.checkTruncationLength() { if b.ctx.flags&nodebuilder.FlagsNoTruncation != 0 { diff --git a/testdata/baselines/reference/conformance/jsDeclarationsTypedefPropertyComments.js b/testdata/baselines/reference/conformance/jsDeclarationsTypedefPropertyComments.js new file mode 100644 index 00000000000..6f18216a246 --- /dev/null +++ b/testdata/baselines/reference/conformance/jsDeclarationsTypedefPropertyComments.js @@ -0,0 +1,53 @@ +//// [tests/cases/conformance/jsdoc/declarations/jsDeclarationsTypedefPropertyComments.ts] //// + +//// [lib.js] +/** + * @typedef {Object} Foo + * @property {boolean} bool Whether `.bool` is true or not + */ +export class C { + /** @returns {Foo} */ + getFoo() { return { bool: false }; } +} + +//// [main.js] +import { C } from './lib.js'; + +export class Main { + constructor() { + this.c = new C(); + } + + getFoo() { return { ...this.c.getFoo() }; } +} + + + + +//// [lib.d.ts] +export type Foo = { + /** + * Whether `.bool` is true or not + */ + bool: boolean; +}; +/** + * @typedef {Object} Foo + * @property {boolean} bool Whether `.bool` is true or not + */ +export declare class C { + /** @returns {Foo} */ + getFoo(): Foo; +} +//// [main.d.ts] +import { C } from './lib.js'; +export declare class Main { + c: C; + constructor(); + getFoo(): { + /** + * Whether `.bool` is true or not + */ + bool: boolean; + }; +} diff --git a/testdata/baselines/reference/conformance/jsDeclarationsTypedefPropertyComments.symbols b/testdata/baselines/reference/conformance/jsDeclarationsTypedefPropertyComments.symbols new file mode 100644 index 00000000000..246f6b4cfee --- /dev/null +++ b/testdata/baselines/reference/conformance/jsDeclarationsTypedefPropertyComments.symbols @@ -0,0 +1,40 @@ +//// [tests/cases/conformance/jsdoc/declarations/jsDeclarationsTypedefPropertyComments.ts] //// + +=== lib.js === +/** + * @typedef {Object} Foo + * @property {boolean} bool Whether `.bool` is true or not + */ +export class C { +>C : Symbol(C, Decl(lib.js, 0, 0)) + + /** @returns {Foo} */ + getFoo() { return { bool: false }; } +>getFoo : Symbol(C.getFoo, Decl(lib.js, 4, 16)) +>bool : Symbol(bool, Decl(lib.js, 6, 23)) +} + +=== main.js === +import { C } from './lib.js'; +>C : Symbol(C, Decl(main.js, 0, 8)) + +export class Main { +>Main : Symbol(Main, Decl(main.js, 0, 29)) + + constructor() { + this.c = new C(); +>this.c : Symbol(Main.c, Decl(main.js, 3, 19)) +>this : Symbol(Main, Decl(main.js, 0, 29)) +>c : Symbol(Main.c, Decl(main.js, 3, 19)) +>C : Symbol(C, Decl(main.js, 0, 8)) + } + + getFoo() { return { ...this.c.getFoo() }; } +>getFoo : Symbol(Main.getFoo, Decl(main.js, 5, 5)) +>this.c.getFoo : Symbol(C.getFoo, Decl(lib.js, 4, 16)) +>this.c : Symbol(Main.c, Decl(main.js, 3, 19)) +>this : Symbol(Main, Decl(main.js, 0, 29)) +>c : Symbol(Main.c, Decl(main.js, 3, 19)) +>getFoo : Symbol(C.getFoo, Decl(lib.js, 4, 16)) +} + diff --git a/testdata/baselines/reference/conformance/jsDeclarationsTypedefPropertyComments.types b/testdata/baselines/reference/conformance/jsDeclarationsTypedefPropertyComments.types new file mode 100644 index 00000000000..d7ea4cbebd2 --- /dev/null +++ b/testdata/baselines/reference/conformance/jsDeclarationsTypedefPropertyComments.types @@ -0,0 +1,46 @@ +//// [tests/cases/conformance/jsdoc/declarations/jsDeclarationsTypedefPropertyComments.ts] //// + +=== lib.js === +/** + * @typedef {Object} Foo + * @property {boolean} bool Whether `.bool` is true or not + */ +export class C { +>C : C + + /** @returns {Foo} */ + getFoo() { return { bool: false }; } +>getFoo : () => Foo +>{ bool: false } : { bool: false; } +>bool : false +>false : false +} + +=== main.js === +import { C } from './lib.js'; +>C : typeof C + +export class Main { +>Main : Main + + constructor() { + this.c = new C(); +>this.c = new C() : C +>this.c : any +>this : this +>c : any +>new C() : C +>C : typeof C + } + + getFoo() { return { ...this.c.getFoo() }; } +>getFoo : () => { bool: boolean; } +>{ ...this.c.getFoo() } : { bool: boolean; } +>this.c.getFoo() : import("./lib.js").Foo +>this.c.getFoo : () => import("./lib.js").Foo +>this.c : C +>this : this +>c : C +>getFoo : () => import("./lib.js").Foo +} + diff --git a/testdata/tests/cases/conformance/jsdoc/declarations/jsDeclarationsTypedefPropertyComments.ts b/testdata/tests/cases/conformance/jsdoc/declarations/jsDeclarationsTypedefPropertyComments.ts new file mode 100644 index 00000000000..c3e5a85a182 --- /dev/null +++ b/testdata/tests/cases/conformance/jsdoc/declarations/jsDeclarationsTypedefPropertyComments.ts @@ -0,0 +1,26 @@ +// @allowJs: true +// @checkJs: true +// @declaration: true +// @emitDeclarationOnly: true +// @outDir: ./out + +// @filename: lib.js +/** + * @typedef {Object} Foo + * @property {boolean} bool Whether `.bool` is true or not + */ +export class C { + /** @returns {Foo} */ + getFoo() { return { bool: false }; } +} + +// @filename: main.js +import { C } from './lib.js'; + +export class Main { + constructor() { + this.c = new C(); + } + + getFoo() { return { ...this.c.getFoo() }; } +}