From 086cece42fc0661f333cd9b5de49503db08a305c Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 7 Nov 2024 17:26:47 +0000 Subject: [PATCH 1/2] docs(docs-infra): update default generic values and add constraints for type parameters in functions Before ```typescript createNodeRequestHandler( handler: T ): T; ``` ```typescript class NgIf { @Input() set ngIf(value: T); @Input() set ngIfThen(value: TemplateRef> | null); @Input() set ngIfElse(value: TemplateRef> | null); static ngTemplateGuard_ngIf: "binding"; static ngTemplateContextGuard(dir: NgIf, ctx: any): boolean; } ``` Now ```typescript createNodeRequestHandler( handler: T ): T; ``` ```typescript class NgIf { @Input() set ngIf(value: T); @Input() set ngIfThen(value: TemplateRef> | null); @Input() set ngIfElse(value: TemplateRef> | null); static ngTemplateGuard_ngIf: "binding"; static ngTemplateContextGuard(dir: NgIf, ctx: any): boolean; } ``` --- .../test/transforms/code-transforms.spec.ts | 59 +++++++++++++++++++ .../rendering/transforms/code-transforms.ts | 54 ++++++++++++++--- 2 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 adev/shared-docs/pipeline/api-gen/rendering/test/transforms/code-transforms.spec.ts 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..01dce7baaa15 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, @@ -332,8 +333,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,12 +445,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 @@ -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(''); +} From bb7da6a47fa2678ed7f3be21016248408c9f2225 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 8 Nov 2024 10:51:34 +0000 Subject: [PATCH 2/2] fix(compiler-cli): correct extraction of generics from type aliases **Before:** ```ts type HttpEvent = | HttpSentEvent | HttpHeaderResponse | HttpResponse | HttpProgressEvent | HttpUserEvent ``` **After:** ```ts type HttpEvent = | HttpSentEvent | HttpHeaderResponse | HttpResponse | HttpProgressEvent | HttpUserEvent ``` --- .../pipeline/api-gen/rendering/entities.ts | 4 +++- .../rendering/transforms/code-transforms.ts | 4 ++-- .../src/ngtsc/docs/src/entities.ts | 6 ++++-- .../ngtsc/docs/src/type_alias_extractor.ts | 2 ++ .../type_alias_doc_extraction_spec.ts | 21 +++++++++++++++++++ 5 files changed, 32 insertions(+), 5 deletions(-) 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/transforms/code-transforms.ts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.ts index 01dce7baaa15..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 @@ -243,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); @@ -446,7 +447,6 @@ function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContents if (isClassEntry(entry) || isInterfaceEntry(entry)) { const generics = makeGenericsText(entry.generics); - const extendsStr = entry.extends ? ` extends ${entry.extends}` : ''; // TODO: remove the ? when we distinguish Class & Decorator entries const implementsStr = 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}, + ]); + }); }); });