diff --git a/adev/shared-docs/pipeline/api-gen/rendering/entities.ts b/adev/shared-docs/pipeline/api-gen/rendering/entities.ts index ee8aa7554de4..3e692e0e2cbb 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/entities.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/entities.ts @@ -94,7 +94,9 @@ export interface ConstantEntry extends DocEntry { } /** Documentation entity for a type alias. */ -export type TypeAliasEntry = ConstantEntry; +export interface TypeAliasEntry extends ConstantEntry { + generics: GenericEntry[]; +} /** Documentation entity for a TypeScript class. */ export interface ClassEntry extends DocEntry { diff --git a/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/code-transforms.spec.ts b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/code-transforms.spec.ts new file mode 100644 index 000000000000..c7c1e3448d19 --- /dev/null +++ b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/code-transforms.spec.ts @@ -0,0 +1,59 @@ +import {makeGenericsText} from '../../transforms/code-transforms'; + +describe('makeGenericsText', () => { + it('should return an empty string if no generics are provided', () => { + expect(makeGenericsText(undefined)).toBe(''); + expect(makeGenericsText([])).toBe(''); + }); + + it('should return a single generic type without constraints or default', () => { + const generics = [{name: 'T', constraint: undefined, default: undefined}]; + expect(makeGenericsText(generics)).toBe(''); + }); + + it('should handle a single generic type with a constraint', () => { + const generics = [{name: 'T', constraint: 'string', default: undefined}]; + expect(makeGenericsText(generics)).toBe(''); + }); + + it('should handle a single generic type with a default value', () => { + const generics = [{name: 'T', default: 'number', constraint: undefined}]; + expect(makeGenericsText(generics)).toBe(''); + }); + + it('should handle a single generic type with both constraint and default value', () => { + const generics = [{name: 'T', constraint: 'string', default: 'number'}]; + expect(makeGenericsText(generics)).toBe(''); + }); + + it('should handle multiple generic types without constraints or defaults', () => { + const generics = [ + {name: 'T', constraint: undefined, default: undefined}, + {name: 'U', constraint: undefined, default: undefined}, + ]; + expect(makeGenericsText(generics)).toBe(''); + }); + + it('should handle multiple generic types with constraints and defaults', () => { + const generics = [ + {name: 'T', constraint: 'string', default: 'number'}, + {name: 'U', constraint: 'boolean', default: undefined}, + {name: 'V', default: 'any', constraint: undefined}, + ]; + expect(makeGenericsText(generics)).toBe( + '', + ); + }); + + it('should handle complex generics with mixed constraints and defaults', () => { + const generics = [ + {name: 'A', constraint: 'string', default: undefined}, + {name: 'B', constraint: undefined, default: undefined}, + {name: 'C', default: 'number', constraint: undefined}, + {name: 'D', constraint: 'boolean', default: 'true'}, + ]; + expect(makeGenericsText(generics)).toBe( + '', + ); + }); +}); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts index 037b4a129432..e31d6fa11b45 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts @@ -9,6 +9,7 @@ import { DocEntry, FunctionSignatureMetadata, + GenericEntry, MemberEntry, MemberTags, ParameterEntry, @@ -242,7 +243,8 @@ export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData { } if (isTypeAliasEntry(entry)) { - const contents = `type ${entry.name} = ${entry.type}`; + const generics = makeGenericsText(entry.generics); + const contents = `type ${entry.name}${generics} = ${entry.type}`; if (isDeprecated) { const numberOfLinesOfCode = getNumberOfLinesOfCode(contents); @@ -332,8 +334,10 @@ function getMethodCodeLine( displayParamsInNewLines: boolean = false, isFunction: boolean = false, ): string { + const generics = makeGenericsText(member.generics); + displayParamsInNewLines &&= member.params.length > 0; - return `${isFunction ? 'function' : ''}${memberTags.join(' ')} ${member.name}(${displayParamsInNewLines ? '\n ' : ''}${member.params + return `${isFunction ? 'function' : ''}${memberTags.join(' ')} ${member.name}${generics}(${displayParamsInNewLines ? '\n ' : ''}${member.params .map((param) => mapParamEntry(param)) .join(`,${displayParamsInNewLines ? '\n ' : ' '}`)}${ displayParamsInNewLines ? '\n' : '' @@ -442,13 +446,7 @@ function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContents }; if (isClassEntry(entry) || isInterfaceEntry(entry)) { - const generics = - entry.generics?.length > 0 - ? `<${entry.generics - .map((g) => (g.constraint ? `${g.name} extends ${g.constraint}` : g.name)) - .join(', ')}>` - : ''; - + const generics = makeGenericsText(entry.generics); const extendsStr = entry.extends ? ` extends ${entry.extends}` : ''; // TODO: remove the ? when we distinguish Class & Decorator entries const implementsStr = @@ -501,3 +499,45 @@ export function addApiLinksToHtml(htmlString: string): string { return result; } + +/** + * Constructs a TypeScript generics string based on an array of generic type entries. + * + * This function takes an array of generic type entries and returns a formatted string + * representing TypeScript generics syntax, including any constraints and default values + * specified in each entry. + * + * @param generics - An array of `GenericEntry` objects representing the generics to be formatted, + * or `undefined` if there are no generics. + * + * @returns A formatted string representing TypeScript generics syntax, or an empty string if no generics are provided. + */ +export function makeGenericsText(generics: GenericEntry[] | undefined): string { + if (!generics?.length) { + return ''; + } + + const parts: string[] = ['<']; + + for (let index = 0; index < generics.length; index++) { + const {constraint, default: defaultVal, name} = generics[index]; + + parts.push(name); + + if (constraint) { + parts.push(' extends ', constraint); + } + + if (defaultVal !== undefined) { + parts.push(' = ', defaultVal); + } + + if (index < generics.length - 1) { + parts.push(', '); + } + } + + parts.push('>'); + + return parts.join(''); +} diff --git a/packages/compiler-cli/src/ngtsc/docs/src/entities.ts b/packages/compiler-cli/src/ngtsc/docs/src/entities.ts index cb207bccb2b9..0630f44084f2 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/entities.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/entities.ts @@ -100,14 +100,16 @@ export interface ConstantEntry extends DocEntry { } /** Documentation entity for a type alias. */ -export type TypeAliasEntry = ConstantEntry; +export interface TypeAliasEntry extends ConstantEntry { + generics: GenericEntry[]; +} /** Documentation entity for a TypeScript class. */ export interface ClassEntry extends DocEntry { isAbstract: boolean; members: MemberEntry[]; - generics: GenericEntry[]; extends?: string; + generics: GenericEntry[]; implements: string[]; } diff --git a/packages/compiler-cli/src/ngtsc/docs/src/type_alias_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/type_alias_extractor.ts index f2129dd87125..c8840f49d402 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/type_alias_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/type_alias_extractor.ts @@ -9,6 +9,7 @@ import ts from 'typescript'; import {EntryType} from './entities'; import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor'; +import {extractGenerics} from './generics_extractor'; /** Extract the documentation entry for a type alias. */ export function extractTypeAlias(declaration: ts.TypeAliasDeclaration) { @@ -20,6 +21,7 @@ export function extractTypeAlias(declaration: ts.TypeAliasDeclaration) { name: declaration.name.getText(), type: declaration.type.getText(), entryType: EntryType.TypeAlias, + generics: extractGenerics(declaration), rawComment: extractRawJsDoc(declaration), description: extractJsDocDescription(declaration), jsdocTags: extractJsDocTags(declaration), diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/type_alias_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/type_alias_doc_extraction_spec.ts index fc7481835cd2..949cc9a836e1 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/type_alias_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/type_alias_doc_extraction_spec.ts @@ -63,5 +63,26 @@ runInEachFileSystem(() => { age: number; }`); }); + + it('should extract type aliases based with generics', () => { + env.write( + 'index.ts', + ` + type Foo = undefined; + export type Bar = Foo; + `, + ); + + const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); + expect(docs.length).toBe(1); + + const typeAliasEntry = docs[0] as TypeAliasEntry; + expect(typeAliasEntry.name).toBe('Bar'); + expect(typeAliasEntry.entryType).toBe(EntryType.TypeAlias); + expect(typeAliasEntry.type).toBe('Foo'); + expect(typeAliasEntry.generics).toEqual([ + {name: 'T', constraint: 'string', default: undefined}, + ]); + }); }); });