Skip to content
Draft
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
28 changes: 14 additions & 14 deletions src/generators/metadata/utils/__tests__/transformers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ describe('transformTypeToReferenceLink', () => {
it('should transform a JavaScript primitive type into a Markdown link', () => {
strictEqual(
transformTypeToReferenceLink('string'),
'[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)'
'<code><[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)></code>'
);
});

it('should transform a JavaScript global type into a Markdown link', () => {
strictEqual(
transformTypeToReferenceLink('Array'),
'[`<Array>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)'
'<code><[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)></code>'
);
});

Expand All @@ -23,70 +23,70 @@ describe('transformTypeToReferenceLink', () => {
transformTypeToReferenceLink('SomeOtherType', {
SomeOtherType: 'fromTypeMap',
}),
'[`<SomeOtherType>`](fromTypeMap)'
'<code><[SomeOtherType](fromTypeMap)></code>'
);
});

it('should transform a basic Generic type into a Markdown link', () => {
strictEqual(
transformTypeToReferenceLink('{Promise<string>}'),
'[`<Promise>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)&gt;'
'<code><[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)>></code>'
);
});

it('should partially transform a Generic type if only one part is known', () => {
strictEqual(
transformTypeToReferenceLink('{CustomType<string>}', {}),
'`<CustomType>`&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)&gt;'
'<code><CustomType<[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)>></code>'
);
});

it('should transform a Generic type with an inner union like {Promise<string|boolean>}', () => {
strictEqual(
transformTypeToReferenceLink('{Promise<string|boolean>}', {}),
'[`<Promise>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) | [`<boolean>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)&gt;'
'<code><[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) | [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)>></code>'
);
});

it('should transform multi-parameter generics like {Map<string, number>}', () => {
strictEqual(
transformTypeToReferenceLink('{Map<string, number>}', {}),
'[`<Map>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type), [`<number>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type)&gt;'
'<code><[Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type), [number](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type)>></code>'
);
});

it('should handle outer unions with generics like {Promise<string|number> | boolean}', () => {
strictEqual(
transformTypeToReferenceLink('{Promise<string|number> | boolean}', {}),
'[`<Promise>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) | [`<number>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type)&gt; | [`<boolean>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)'
'<code><[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) | [number](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type)> | [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)></code>'
);
});

it('should transform an intersection type joined with & into linked parts', () => {
strictEqual(
transformTypeToReferenceLink('{string&boolean}', {}),
'[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) & [`<boolean>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)'
'<code><[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) & [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)></code>'
);
});

it('should handle an intersection with generics like {Map<string, number>&Array<string>}', () => {
strictEqual(
transformTypeToReferenceLink('{Map<string, number>&Array<string>}', {}),
'[`<Map>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type), [`<number>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type)&gt; & [`<Array>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)&gt;'
'<code><[Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type), [number](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type)> & [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)>></code>'
);
});

it('should transform a function returning a Generic type', () => {
strictEqual(
transformTypeToReferenceLink('(err: Error) => Promise<boolean>', {}),
'(err: [`<Error>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error)) =&gt; [`<Promise>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)&lt;[`<boolean>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)&gt;'
'<code><(err: [Error](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error)) => [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)>></code>'
);
});

it('should respect precedence: Unions (|) are weaker than Intersections (&)', () => {
strictEqual(
transformTypeToReferenceLink('string | number & boolean', {}),
'[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) | [`<number>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type) & [`<boolean>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)'
'<code><[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) | [number](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type) & [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)></code>'
);
});

Expand All @@ -95,7 +95,7 @@ describe('transformTypeToReferenceLink', () => {
'(str: string[]) => Promise<Map<string, number & string>, Map<string | number>>';

const expected =
'(str: [`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)[]) =&gt; [`<Promise>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)&lt;[`<Map>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type), [`<number>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type) & [`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)&gt;, [`<Map>`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)&lt;[`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) | [`<number>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type)&gt;&gt;';
'<code><(str: [string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)[]) => [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type), [number](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type) & [string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)>, [Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type) | [number](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#number_type)>>></code>';

strictEqual(transformTypeToReferenceLink(input, {}), expected);
});
Expand All @@ -105,7 +105,7 @@ describe('transformTypeToReferenceLink', () => {
'(cb: ([first, second]: string[]) => void) => ({ id, name }: User) => boolean';

const expected =
'(cb: ([first, second]: [`<string>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)[]) =&gt; `<void>`) =&gt; ({ id, name }: [`<User>`](userLink)) =&gt; [`<boolean>`](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)';
'<code><(cb: ([first, second]: [string](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#string_type)[]) => void) => ({ id, name }: [User](userLink)) => [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Data_structures#boolean_type)></code>';

strictEqual(
transformTypeToReferenceLink(input, { User: 'userLink' }),
Expand Down
41 changes: 26 additions & 15 deletions src/generators/metadata/utils/typeParser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ const walkAtDepthZero = (str, onToken) => {
}
};

/** Format a known type as a Markdown link, or as a bare code span. */
/** Resolve a bare type name to its inner rendering with URL */
const formatType = (name, transformType) => {
const url = transformType(name);
return url ? `[\`<${name}>\`](${url})` : `\`<${name}>\``;
return url ? `[${name}](${url})` : name;
};

/** Resolve a sub-expression recursively, falling back to a code span. */
/** Resolve a sub-expression recursively, falling back to the bare text. */
const resolveOr = (part, transformType) =>
parseType(part, transformType) || `\`<${part.trim()}>\``;
parseInner(part, transformType) ?? part.trim();

/**
* Splits `str` by `separator` at depth 0. `separator` is a single
Expand Down Expand Up @@ -75,8 +75,6 @@ const splitByOuterSeparator = (str, separator) => {
const stripOuterParentheses = typeString => {
let s = typeString.trim();
while (s.length >= 2 && s.startsWith('(') && s.endsWith(')')) {
// The outer `(` matches the outer `)` if depth doesn't hit 0
// anywhere before the final character.
let wrapsWhole = true;
walkAtDepthZero(s.slice(0, -1), i => {
if (i > 0) {
Expand Down Expand Up @@ -172,20 +170,18 @@ const parseFunctionSignature = (signature, transformType) => {
const paramType = colonParts.slice(1).join(':');
return `${paramName}: ${resolveOr(paramType, transformType)}`;
}
return parseType(arg, transformType) || arg;
return parseInner(arg, transformType) ?? arg;
});

return `${prefix}(${parsedArgs.join(', ')})`;
};

/**
* Recursively parses TypeScript types into Markdown links.
* Recursively parses a type into its inner rendering.
*
* @param {string} typeString The type string to evaluate.
* @param {(name: string) => string | null | undefined} transformType Resolves a bare type name to a URL, or returns falsy.
* @returns {string | null} Markdown for the type, or null when the base type doesn't resolve.
* @returns {string | null} Inner markup
*/
export const parseType = (typeString, transformType) => {
const parseInner = (typeString, transformType) => {
const trimmed = stripOuterParentheses(typeString);
if (!trimmed) {
return null;
Expand All @@ -197,7 +193,7 @@ export const parseType = (typeString, transformType) => {
const left = trimmed.slice(0, op.index).trim();
const right = trimmed.slice(op.index + op.width).trim();
const sig = parseFunctionSignature(left, transformType);
return `${sig} =&gt; ${resolveOr(right, transformType)}`;
return `${sig} => ${resolveOr(right, transformType)}`;
}

// Union / intersection
Expand All @@ -219,7 +215,7 @@ export const parseType = (typeString, transformType) => {
const inner = splitByOuterSeparator(innerType, ',')
.map(arg => resolveOr(arg, transformType))
.join(', ');
return `${formatType(baseType, transformType)}&lt;${inner}&gt;${arrayTail}`;
return `${formatType(baseType, transformType)}<${inner}>${arrayTail}`;
}

// Plain base type.
Expand All @@ -232,5 +228,20 @@ export const parseType = (typeString, transformType) => {
return null;
}

return `[\`<${core}>\`](${url})${arrayTail}`;
return `${formatType(core, transformType)}${arrayTail}`;
};

/**
* Recursively parses TypeScript types into Markdown, wrapped in a code span.
*
* @param {string} typeString The type string to evaluate.
* @param {(name: string) => string | null | undefined} transformType Resolves a bare type name to a URL, or returns falsy.
* @returns {string | null} Markdown for the type, or null when the base type doesn't resolve.
*/
export const parseType = (typeString, transformType) => {
const inner = parseInner(typeString, transformType);
if (inner === null) {
return null;
}
return `<code><${inner}></code>`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type output breaks typed list parsing

High Severity

Wrapping resolved types as <code><…></code> with inner markdown links like [string](url) no longer matches what downstream code expects: link nodes whose child is inlineCode starting with <. Typed parameter lists then fail strongly-typed detection and extractTypeAnnotations leaves type unset, so signature tables and legacy HTML type-link handling break.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f394719. Configure here.

};
Loading