Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions code/lib/angular-cm/src/extract-arg-types.test.ts
Original file line number Diff line number Diff line change
@@ -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
* `<T>` 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('<T>(value: T) => T');
expect(arg.table?.type?.summary).toBe('<T>(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 <T>(value: T) => Thing');
expect(arg.table?.type?.summary).toBe('new <T>(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);
});
});
4 changes: 3 additions & 1 deletion code/lib/angular-cm/src/extract-arg-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions code/lib/angular-compodoc/src/extract-arg-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<T>(value: T) => T' }).type).toEqual({ name: 'function' });
expect(extractMember({ type: 'new <T>(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',
Expand Down
4 changes: 3 additions & 1 deletion code/lib/angular-compodoc/src/extract-arg-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading