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
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { getDeclarationNodesForPluginScope } from '../get_declaration_nodes_for_
import { buildApiDeclarationTopNode } from './build_api_declaration';
import { isNamedNode } from '../tsmorph_utils';
import { getTypeKind } from './get_type_kind';
import { getSignature } from './get_signature';
import { getSignature, normalizeVerboseSignatures } from './get_signature';
import { buildBasicApiDeclaration } from './build_basic_api_declaration';
import { buildVariableDec } from './build_variable_dec';
import { buildCallSignatureDec } from './build_call_signature_dec';
Expand Down Expand Up @@ -144,8 +144,8 @@ it('Function inside interface has a label', () => {
expect(fn?.type).toBe(TypeKind.FunctionKind);
});

// FAILING: https://github.com/elastic/kibana/issues/120125
it.skip('Test ReactElement signature', () => {
// https://github.com/elastic/kibana/issues/120125
it('Test ReactElement signature', () => {
const node = nodes.find((n) => getNodeName(n) === 'AReactElementFn');
expect(node).toBeDefined();
const def = buildApiDeclarationTopNode(node!, {
Expand Down Expand Up @@ -662,6 +662,63 @@ describe('getSignature edge cases', () => {
});
});

describe('normalizeVerboseSignatures', () => {
it('collapses ReactNode expansion back to React.ReactNode', () => {
const input =
'string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | null | undefined';
expect(normalizeVerboseSignatures(input)).toBe('React.ReactNode');
});

it('collapses ReactNode expansion when embedded in a larger signature', () => {
const input =
'(children: string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | null | undefined) => void';
expect(normalizeVerboseSignatures(input)).toBe('(children: React.ReactNode) => void');
});

it('strips the second generic from ReactElement', () => {
const input = 'React.ReactElement<MyProps, string | React.JSXElementConstructor<any>>';
expect(normalizeVerboseSignatures(input)).toBe('React.ReactElement<MyProps>');
});

it('strips the second generic from ReactElement without the React. prefix', () => {
const input = 'ReactElement<any, string | React.JSXElementConstructor<any>>';
expect(normalizeVerboseSignatures(input)).toBe('React.ReactElement<any>');
});

it('collapses ComponentClass | FunctionComponent to ComponentType', () => {
const input = 'React.ComponentClass<{}, any> | React.FunctionComponent<{}>';
expect(normalizeVerboseSignatures(input)).toBe('React.ComponentType');
});

it('strips empty props default from ComponentType', () => {
const input = 'React.ComponentType<{}>';
expect(normalizeVerboseSignatures(input)).toBe('React.ComponentType');
});

it('strips ComponentType<{}> when embedded in a union', () => {
const input = 'React.ComponentType<{}> | undefined';
expect(normalizeVerboseSignatures(input)).toBe('React.ComponentType | undefined');
});

it('preserves ComponentType with non-empty props', () => {
const input = 'React.ComponentType<MyProps>';
expect(normalizeVerboseSignatures(input)).toBe('React.ComponentType<MyProps>');
});

it('applies multiple rules in a single signature', () => {
const input =
'{ node: string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | null | undefined; component: React.ComponentType<{}> }';
expect(normalizeVerboseSignatures(input)).toBe(
'{ node: React.ReactNode; component: React.ComponentType }'
);
});

it('returns the input unchanged when no patterns match', () => {
const input = '(a: string, b: number) => boolean';
expect(normalizeVerboseSignatures(input)).toBe(input);
});
});

describe('getReferences edge cases', () => {
it('returns empty array for node without external references', () => {
const project = new Project({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export function getSignature(
);
}

signature = normalizeVerboseSignatures(signature);

// Don't return the signature if it's the same as the type (string, string)
if (getTypeKind(node).toString() === signature) return undefined;

Expand All @@ -86,15 +88,68 @@ export function getSignature(
return undefined;
}

// This post-reference-extraction hack catches a *different* verbose `ReactElement` expansion
// than {@link signatureNormalizations}. After `extractImportReferences` splits the signature
// into reference link segments, the second generic appears as a separate string chunk using
// `React.Component` (not `React.JSXElementConstructor`). Both mechanisms are needed until
// this legacy hack can be replaced by a pre-extraction normalization rule.
return referenceLinks.map((link) => {
// This is such a terrible hack, but the docs look really terrible with it, and I'm not sure of a better way to solve it.
// See for context. This is what the second default generic type of `ReactElement` expands to. Blech!
if (
link ===
', string | ((props: any) => React.ReactElement<any, string | any | (new (props: any) => React.Component<any, any, any>)> | null) | (new (props: any) => React.Component<any, any, any>)>'
// ', string | ((props: any) => React.ReactElement<any, string | any | (new (props: any) => React.Component<any, a'
) {
return '>';
} else return link;
});
}

/**
* Signature normalization rules. Each entry maps a verbose expanded generic
* default back to the concise form that developers actually write.
*
* New entries can be added here as more verbose patterns are discovered.
*/
const signatureNormalizations: Array<{ pattern: RegExp; replacement: string }> = [
// Order matters: `ReactNode` must be collapsed before `ReactElement` because the
// `ReactNode` expansion contains a full `ReactElement<any, ...>` that would otherwise
// be shortened first, preventing the `ReactNode` pattern from matching.

// `React.ReactNode` expands to a long union of primitive types, `ReactElement`, `Iterable`,
// `ReactPortal`, `null`, and `undefined`. Collapse it back to the alias.
// NOTE: This pattern is coupled to `@types/react@18`. If Kibana upgrades to React 19 types
// (which adds `bigint` and changes the union shape), this regex must be updated to match.
{
pattern:
/string \| number \| boolean \| React\.ReactElement<any, string \| React\.JSXElementConstructor<any>> \| Iterable<React\.ReactNode> \| React\.ReactPortal \| null \| undefined/g,
replacement: 'React.ReactNode',
},
// `ReactElement` has a second generic default (`string | React.JSXElementConstructor<any>`)
// that expands into verbose output; strip it while keeping the first generic.
{
pattern:
/React(?:\.ReactElement|Element)<([^,>]+),\s*string \|\s*React\.JSXElementConstructor<any>>/g,
replacement: 'React.ReactElement<$1>',
},
// `React.ComponentClass<{}, any> | React.FunctionComponent<{}>` is the expansion of
// `React.ComponentType<{}>`. Collapse it back.
{
pattern: /React\.ComponentClass<\{\}, any> \| React\.FunctionComponent<\{\}>/g,
replacement: 'React.ComponentType',
},
// `React.ComponentType<{}>` uses an empty props default; strip it for readability.
{
pattern: /React\.ComponentType<\{\}>/g,
replacement: 'React.ComponentType',
},
];

/**
* Applies all {@link signatureNormalizations} to collapse verbose expanded
* generic defaults back to their concise aliases.
*/
export function normalizeVerboseSignatures(signature: string): string {
return signatureNormalizations.reduce(
(sig, { pattern, replacement }) => sig.replace(pattern, replacement),
signature
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ describe('Types', () => {
"section": "def-public.MyProps",
"text": "MyProps",
},
", string | React.JSXElementConstructor<any>>",
">",
]
`);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"label": "component",
"description": [],
"signature": [
"React.ComponentType<{}> | undefined"
"React.ComponentType | undefined"
],
"path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts",
"lineNumber": 50,
Expand Down Expand Up @@ -1263,7 +1263,7 @@
"label": "component",
"description": [],
"signature": [
"React.ComponentClass<{}, any> | React.FunctionComponent<{}>"
"React.ComponentType"
],
"path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/classes.ts",
"lineNumber": 117,
Expand Down Expand Up @@ -1512,7 +1512,7 @@
"section": "def-public.MyProps",
"text": "MyProps"
},
", string | React.JSXElementConstructor<any>>"
">"
],
"path": "packages/kbn-docs-utils/src/integration_tests/__fixtures__/src/plugin_a/public/types.ts",
"lineNumber": 65,
Expand Down
Loading