From abf57e092eba78f4f6d7cf3cbc75bb32d021923c Mon Sep 17 00:00:00 2001 From: Phillip Markert Date: Thu, 13 Aug 2026 16:34:22 -0400 Subject: [PATCH] fix(react): infer constructor return type instead of void MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generateSignature fell back to `void` for any entry without a `Returns:` line. Node's API docs omit that line on constructors by convention, so every constructor rendered as returning `void` — e.g. `new Agent(options?): void` on http.html. Pass the heading metadata through so constructors fall back to their own class name instead. Refs: https://github.com/nodejs/doc-kit/issues/953 Signed-off-by: Phillip Markert --- .changeset/constructor-return-types.md | 5 + .../utils/__tests__/signature.test.mjs | 252 ++++++++---------- .../react/src/jsx-ast/utils/signature.mjs | 26 +- 3 files changed, 126 insertions(+), 157 deletions(-) create mode 100644 .changeset/constructor-return-types.md diff --git a/.changeset/constructor-return-types.md b/.changeset/constructor-return-types.md new file mode 100644 index 00000000..b58e256e --- /dev/null +++ b/.changeset/constructor-return-types.md @@ -0,0 +1,5 @@ +--- +'@doc-kit/generator-react': patch +--- + +Constructors now render their own class as the return type instead of `void` diff --git a/packages/react/src/jsx-ast/utils/__tests__/signature.test.mjs b/packages/react/src/jsx-ast/utils/__tests__/signature.test.mjs index 03cc854c..c6661df3 100644 --- a/packages/react/src/jsx-ast/utils/__tests__/signature.test.mjs +++ b/packages/react/src/jsx-ast/utils/__tests__/signature.test.mjs @@ -6,80 +6,88 @@ import { generateSignature, getFullName } from '../signature.mjs'; describe('generateSignature', () => { describe('function signatures', () => { it('formats union return types without spaces as spaced', () => { - const sig = generateSignature( - 'foo', - { - params: [], - return: { type: 'string|number' }, - }, - '' - ); + const sig = generateSignature('foo', { + params: [], + return: { type: 'string|number' }, + }); assert.strictEqual(sig, 'foo(): string | number'); }); it('preserves already spaced union return types', () => { - const sig = generateSignature( - 'bar', - { - params: [], - return: { type: 'Promise | undefined' }, - }, - '' - ); + const sig = generateSignature('bar', { + params: [], + return: { type: 'Promise | undefined' }, + }); assert.strictEqual(sig, 'bar(): Promise | undefined'); }); it('omits return type when undefined', () => { - const sig = generateSignature( - 'baz', - { - params: [], - return: undefined, - }, - '' - ); + const sig = generateSignature('baz', { + params: [], + return: undefined, + }); assert.strictEqual(sig, 'baz(): void'); }); it('handles empty return type', () => { + const sig = generateSignature('test', { + params: [], + return: null, + }); + + assert.strictEqual(sig, 'test(): void'); + }); + + it('prefixes a constructor and infers its return type', () => { const sig = generateSignature( - 'test', + 'Constructor', { params: [], - return: null, + return: undefined, }, - '' + { type: 'ctor' } ); - assert.strictEqual(sig, 'test(): void'); + // Node's docs omit `Returns:` on constructors, but a constructor never + // returns `void` — it yields an instance of its own class. + assert.strictEqual(sig, 'new Constructor(): Constructor'); }); - it('includes prefix when provided', () => { + it('keeps an explicit return type on a constructor', () => { const sig = generateSignature( 'Constructor', { params: [], - return: undefined, + return: { type: 'net.Socket' }, }, - 'new ' + { type: 'ctor' } ); - assert.strictEqual(sig, 'new Constructor(): void'); + assert.strictEqual(sig, 'new Constructor(): net.Socket'); }); - it('handles complex union types with multiple pipes', () => { + it('falls back when a return entry carries no type', () => { const sig = generateSignature( - 'complexFunc', + 'noType', { params: [], - return: { type: 'string|number|boolean|null' }, + return: { type: undefined }, }, - '' + { type: 'method' } ); + assert.strictEqual(sig, 'noType(): void'); + }); + + it('handles complex union types with multiple pipes', () => { + const sig = generateSignature('complexFunc', { + params: [], + return: { type: 'string|number|boolean|null' }, + }); + assert.strictEqual( sig, 'complexFunc(): string | number | boolean | null' @@ -87,14 +95,10 @@ describe('generateSignature', () => { }); it('filters empty parts in union types', () => { - const sig = generateSignature( - 'filterFunc', - { - params: [], - return: { type: 'string||number|' }, - }, - '' - ); + const sig = generateSignature('filterFunc', { + params: [], + return: { type: 'string||number|' }, + }); assert.strictEqual(sig, 'filterFunc(): string | number'); }); @@ -102,101 +106,73 @@ describe('generateSignature', () => { describe('parameters', () => { it('handles single parameter without optional flag or default', () => { - const sig = generateSignature( - 'singleParam', - { - params: [{ name: 'value', optional: false }], - return: undefined, - }, - '' - ); + const sig = generateSignature('singleParam', { + params: [{ name: 'value', optional: false }], + return: undefined, + }); assert.strictEqual(sig, 'singleParam(value): void'); }); it('handles multiple parameters', () => { - const sig = generateSignature( - 'multiParam', - { - params: [ - { name: 'first', optional: false }, - { name: 'second', optional: false }, - ], - return: undefined, - }, - '' - ); + const sig = generateSignature('multiParam', { + params: [ + { name: 'first', optional: false }, + { name: 'second', optional: false }, + ], + return: undefined, + }); assert.strictEqual(sig, 'multiParam(first, second): void'); }); it('marks optional parameters with question mark', () => { - const sig = generateSignature( - 'optionalParam', - { - params: [ - { name: 'required', optional: false }, - { name: 'optional', optional: true }, - ], - return: undefined, - }, - '' - ); + const sig = generateSignature('optionalParam', { + params: [ + { name: 'required', optional: false }, + { name: 'optional', optional: true }, + ], + return: undefined, + }); assert.strictEqual(sig, 'optionalParam(required, optional?): void'); }); it('marks parameters with defaults as optional', () => { - const sig = generateSignature( - 'defaultParam', - { - params: [ - { name: 'normal', optional: false }, - { name: 'withDefault', optional: false, default: 'defaultValue' }, - ], - return: undefined, - }, - '' - ); + const sig = generateSignature('defaultParam', { + params: [ + { name: 'normal', optional: false }, + { name: 'withDefault', optional: false, default: 'defaultValue' }, + ], + return: undefined, + }); assert.strictEqual(sig, 'defaultParam(normal, withDefault?): void'); }); it('handles parameters that are both optional and have defaults', () => { - const sig = generateSignature( - 'bothOptionalAndDefault', - { - params: [{ name: 'param', optional: true, default: 'value' }], - return: undefined, - }, - '' - ); + const sig = generateSignature('bothOptionalAndDefault', { + params: [{ name: 'param', optional: true, default: 'value' }], + return: undefined, + }); assert.strictEqual(sig, 'bothOptionalAndDefault(param?): void'); }); it('handles empty params array', () => { - const sig = generateSignature( - 'noParams', - { - params: [], - return: { type: 'string' }, - }, - '' - ); + const sig = generateSignature('noParams', { + params: [], + return: { type: 'string' }, + }); assert.strictEqual(sig, 'noParams(): string'); }); it('handles params without optional property', () => { - const sig = generateSignature( - 'implicitOptional', - { - params: [{ name: 'param1' }, { name: 'param2', default: 'value' }], - return: undefined, - }, - '' - ); + const sig = generateSignature('implicitOptional', { + params: [{ name: 'param1' }, { name: 'param2', default: 'value' }], + return: undefined, + }); assert.strictEqual(sig, 'implicitOptional(param1, param2?): void'); }); @@ -204,41 +180,33 @@ describe('generateSignature', () => { describe('class signatures', () => { it('generates class signature with extends clause', () => { - const sig = generateSignature( - 'MyClass', - { - params: [], - extends: { type: 'BaseClass' }, - }, - '' - ); + const sig = generateSignature('MyClass', { + params: [], + extends: { type: 'BaseClass' }, + }); assert.strictEqual(sig, 'class MyClass extends BaseClass'); }); - it('generates class signature with extends and prefix', () => { + it('does not prefix a class signature', () => { const sig = generateSignature( 'MyClass', { params: [], extends: { type: 'BaseClass' }, }, - 'abstract ' + { type: 'class' } ); - assert.strictEqual(sig, 'class abstract MyClass extends BaseClass'); + assert.strictEqual(sig, 'class MyClass extends BaseClass'); }); it('ignores params and return type for class with extends', () => { - const sig = generateSignature( - 'MyClass', - { - params: [{ name: 'ignored', optional: false }], - return: { type: 'ignored' }, - extends: { type: 'BaseClass' }, - }, - '' - ); + const sig = generateSignature('MyClass', { + params: [{ name: 'ignored', optional: false }], + return: { type: 'ignored' }, + extends: { type: 'BaseClass' }, + }); assert.strictEqual(sig, 'class MyClass extends BaseClass'); }); @@ -250,7 +218,7 @@ describe('generateSignature', () => { params: [{ name: 'param', optional: false }], return: { type: 'MyClass' }, }, - 'new ' + { type: 'ctor' } ); assert.strictEqual(sig, 'new MyClass(param): MyClass'); @@ -259,27 +227,19 @@ describe('generateSignature', () => { describe('edge cases', () => { it('handles null return type', () => { - const sig = generateSignature( - 'nullReturn', - { - params: [], - return: null, - }, - '' - ); + const sig = generateSignature('nullReturn', { + params: [], + return: null, + }); assert.strictEqual(sig, 'nullReturn(): void'); }); it('handles missing extends property', () => { - const sig = generateSignature( - 'NoExtends', - { - params: [{ name: 'param' }], - return: { type: 'void' }, - }, - '' - ); + const sig = generateSignature('NoExtends', { + params: [{ name: 'param' }], + return: { type: 'void' }, + }); assert.strictEqual(sig, 'NoExtends(param): void'); }); diff --git a/packages/react/src/jsx-ast/utils/signature.mjs b/packages/react/src/jsx-ast/utils/signature.mjs index 1f6edf71..d7032f32 100644 --- a/packages/react/src/jsx-ast/utils/signature.mjs +++ b/packages/react/src/jsx-ast/utils/signature.mjs @@ -13,20 +13,28 @@ import { JSX_IMPORTS } from '../../html/constants.mjs'; * * @param {string} functionName - The name of the function or class. * @param {import('@doc-kit/core/utils/signature/types').MethodSignature} signature - The parsed signature object. - * @param {string} prefix - Optional prefix, i.e. `'new '` for constructors. + * @param {import('@doc-kit/core/generators/metadata/types').HeadingData} [heading] - Metadata of the heading being documented. */ export const generateSignature = ( functionName, { params, return: returnType, extends: extendsType }, - prefix = '' + heading ) => { + const isConstructor = heading?.type === 'ctor'; + const prefix = isConstructor ? 'new ' : ''; + // Class with `extends` clause if (extendsType) { return `class ${prefix}${functionName} extends ${extendsType.type}`; } + // A constructor always yields an instance of its own class, so `void` is + // never a correct fallback for one. Node's docs omit the `Returns:` line on + // constructors by convention, so infer it from the class name instead. + const fallbackReturn = isConstructor ? functionName : 'void'; + // Function or method - const returnStr = (returnType ? `: ${returnType.type}` : ': void') + const returnStr = `: ${returnType?.type ?? fallbackReturn}` .split('|') .map(part => part.trim()) .filter(Boolean) @@ -53,10 +61,10 @@ export const generateSignature = ( * * @param {string} functionName - The function name to display. * @param {import('@doc-kit/core/utils/signature/types').MethodSignature} signature - Signature object with parameter and return type info. - * @param {string} prefix - Optional prefix like `'new '`. + * @param {import('@doc-kit/core/generators/metadata/types').HeadingData} [heading] - Metadata of the heading being documented. */ -export const createSignatureCodeBlock = (functionName, signature, prefix) => { - const sig = generateSignature(functionName, signature, prefix); +export const createSignatureCodeBlock = (functionName, signature, heading) => { + const sig = generateSignature(functionName, signature, heading); const highlighted = highlighter.highlightToHast(sig, 'typescript'); return createElement('div', { class: 'signature' }, [highlighted]); @@ -123,11 +131,7 @@ export const insertSignatureCodeBlock = ({ children }, { data }, idx) => { children.splice( idx, 0, - createSignatureCodeBlock( - displayName, - signature, - data.type === 'ctor' ? 'new ' : '' - ) + createSignatureCodeBlock(displayName, signature, data) ); };