diff --git a/code/lib/angular-cm/src/extract-arg-types.test.ts b/code/lib/angular-cm/src/extract-arg-types.test.ts new file mode 100644 index 000000000000..d35b9313602d --- /dev/null +++ b/code/lib/angular-cm/src/extract-arg-types.test.ts @@ -0,0 +1,67 @@ +/** + * How a declared input type reaches the props table, as both a summary and a control. + * + * The sources go through the real analyzer rather than hand-written type strings, because the two + * halves drifting apart is the defect this guards: `TypeIndex.render` learned to emit `new ` and + * `` prefixes while the sbType predicate still only accepted a leading `(`, so a correct summary + * came at the cost of the control. Feeding the predicate strings a test author picked would not + * have caught that. + */ +import { describe, expect, it } from 'vitest'; + +import { componentIn } from './analyzer/__testutils__/inline-source.ts'; +import { extractArgTypesFromData } from './extract-arg-types.ts'; + +const inputTyped = (type: string) => { + const component = componentIn(` + import { Component, Input } from '@angular/core'; + + export class Thing {} + + @Component({ selector: 'sb-probe', template: '' }) + export class ProbeComponent { + @Input() value!: ${type}; + } + `); + return extractArgTypesFromData(component, { metadataJson: undefined, propsTable: 'all' }).value; +}; + +const FUNCTION_CONTROL = { name: 'function' }; + +describe('function-typed inputs', () => { + it('keeps the signature and the function control for a plain arrow type', () => { + const arg = inputTyped('(value: number) => string'); + expect(arg.table?.type?.summary).toBe('(value: number) => string'); + expect(arg.type).toEqual(FUNCTION_CONTROL); + }); + + it('keeps both for optional and rest parameters', () => { + const arg = inputTyped('(a?: string, ...rest: number[]) => void'); + expect(arg.table?.type?.summary).toBe('(a?: string, ...rest: number[]) => void'); + expect(arg.type).toEqual(FUNCTION_CONTROL); + }); + + it('keeps both for a generic signature, whose rendered type leads with its type parameters', () => { + const arg = inputTyped('(value: T) => T'); + expect(arg.table?.type?.summary).toBe('(value: T) => T'); + expect(arg.type).toEqual(FUNCTION_CONTROL); + }); + + it('keeps both for a constructor type, whose rendered type leads with `new`', () => { + const arg = inputTyped('new (value: number) => Thing'); + expect(arg.table?.type?.summary).toBe('new (value: number) => Thing'); + expect(arg.type).toEqual(FUNCTION_CONTROL); + }); + + it('keeps both for a generic constructor type, which leads with both', () => { + const arg = inputTyped('new (value: T) => Thing'); + expect(arg.table?.type?.summary).toBe('new (value: T) => Thing'); + expect(arg.type).toEqual(FUNCTION_CONTROL); + }); + + it('does not read a type that merely mentions a signature as a function', () => { + const arg = inputTyped('Array<(value: number) => string>'); + expect(arg.table?.type?.summary).toBe('Array<(value: number) => string>'); + expect(arg.type).not.toEqual(FUNCTION_CONTROL); + }); +}); diff --git a/code/lib/angular-cm/src/extract-arg-types.ts b/code/lib/angular-cm/src/extract-arg-types.ts index 762a1968516f..2b9905fef45f 100644 --- a/code/lib/angular-cm/src/extract-arg-types.ts +++ b/code/lib/angular-cm/src/extract-arg-types.ts @@ -223,8 +223,10 @@ const resolveTypealias = ( return resolveTypealias(typeAlias.rawtype, metadataJson, componentFile, seen); }; +// `TypeIndex.render` leads a constructor type with `new ` and a generic signature with its type +// parameters, so accepting only a leading `(` dropped both onto the `empty-enum` catch-all. const isFunctionTypeString = (type: string): boolean => - type === 'function' || /^\(.*\)\s*=>/.test(type); + type === 'function' || /^(new\s+)?(<.*>\s*)?\(.*\)\s*=>/.test(type); const extractType = ( property: Property, diff --git a/code/lib/angular-compodoc/src/extract-arg-types.test.ts b/code/lib/angular-compodoc/src/extract-arg-types.test.ts index a8793ed9e23f..5e29134a1060 100644 --- a/code/lib/angular-compodoc/src/extract-arg-types.test.ts +++ b/code/lib/angular-compodoc/src/extract-arg-types.test.ts @@ -338,6 +338,22 @@ describe('modern', () => { }); }); + it('maps a signature that leads with `new` or with type parameters', () => { + expect(extractMember({ type: 'new (value: number) => Thing' }).type).toEqual({ + name: 'function', + }); + expect(extractMember({ type: '(value: T) => T' }).type).toEqual({ name: 'function' }); + expect(extractMember({ type: 'new (value: T) => Thing' }).type).toEqual({ + name: 'function', + }); + }); + + it('does not read a type that merely mentions a signature as a function', () => { + expect(extractMember({ type: 'Array<(value: number) => string>' }).type).not.toEqual({ + name: 'function', + }); + }); + it('leaves them on the other/empty-enum catch-all with the flag off', () => { expect(extractMember({ type: 'function' }, { modern: false }).type).toEqual({ name: 'other', diff --git a/code/lib/angular-compodoc/src/extract-arg-types.ts b/code/lib/angular-compodoc/src/extract-arg-types.ts index 04ee626c26f4..27a298f558ad 100644 --- a/code/lib/angular-compodoc/src/extract-arg-types.ts +++ b/code/lib/angular-compodoc/src/extract-arg-types.ts @@ -256,8 +256,10 @@ const resolveTypealias = ( return resolveTypealias(typeAlias.rawtype, compodocJson, componentFile, seen); }; +// A signature can lead with `new ` for a constructor type or with its type parameters when generic, +// so accepting only a leading `(` dropped both onto the `empty-enum` catch-all. const isFunctionTypeString = (compodocType: string): boolean => - compodocType === 'function' || /^\(.*\)\s*=>/.test(compodocType); + compodocType === 'function' || /^(new\s+)?(<.*>\s*)?\(.*\)\s*=>/.test(compodocType); export const extractType = ( property: Property,