Skip to content

Commit bb43bc8

Browse files
committed
fix(metadata): operator precedence and add some tracing
- Enforce exact TS operator precedence: Arrow functions (=>) before Unions (|) and Intersections (&). - Implement stripOuterParentheses to unwrap redundant outer groups and prevent parser depth-blindness. - Fix array loss: safely preserve [] notation for both base types and generics. - Correctly handle higher-order functions by re-joining subsequent arrow operator segments.
1 parent 00d8c39 commit bb43bc8

1 file changed

Lines changed: 76 additions & 27 deletions

File tree

src/generators/metadata/utils/typeParser.mjs

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,73 @@ const splitByOuterSeparator = (str, separator) => {
4040
pieces.push(current.trim());
4141
return pieces;
4242
};
43+
/**
44+
* Safely removes outer parentheses from a type string if they wrap the entire string.
45+
* This prevents "depth blindness" in the parser by unwrapping types like `(string | number)`
46+
* into `string | number`, while safely ignoring disconnected groups like `(A) | (B)`.
47+
*
48+
* @param {string} typeString The type string to evaluate and potentially unwrap.
49+
* @returns {string} The unwrapped type string, or the original string if not fully wrapped.
50+
*/
51+
export const stripOuterParentheses = typeString => {
52+
let trimmed = typeString.trim();
53+
54+
if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
55+
let depth = 0;
56+
let isValidWrapper = true;
57+
58+
// Iterate through the string, ignoring the last closing parenthesis
59+
for (let i = 0; i < trimmed.length - 1; i++) {
60+
if (trimmed[i] === '(') {
61+
depth++;
62+
} else if (trimmed[i] === ')') {
63+
depth--;
64+
}
65+
66+
// If depth hits 0 before the end, it means the parentheses don't wrap the whole string
67+
if (depth === 0) {
68+
isValidWrapper = false;
69+
break;
70+
}
71+
}
72+
73+
if (isValidWrapper) {
74+
return trimmed.slice(1, -1).trim();
75+
}
76+
}
77+
78+
return trimmed;
79+
};
4380
/**
4481
* Recursively parses advanced TypeScript types, including Unions, Intersections, Functions, and Nested Generics.
4582
* * @param {string} typeString The plain type string to evaluate
4683
* @param {Function} transformType The function used to resolve individual types into links
4784
* @returns {string|null} The formatted Markdown link(s), or null if the base type doesn't map
4885
*/
4986
export const parseType = (typeString, transformType) => {
50-
const trimmed = typeString.trim();
87+
// Clean the string and strip unnecessary outer parentheses to prevent depth blindness (e.g., "(string | number)" -> "string | number")
88+
const trimmed = stripOuterParentheses(typeString);
5189
if (!trimmed) {
5290
return null;
5391
}
5492

93+
// Handle Functions (=>)
94+
if (trimmed.includes('=>')) {
95+
const parts = splitByOuterSeparator(trimmed, '=>');
96+
if (parts.length > 1) {
97+
const params = parts[0];
98+
99+
// Join the rest back together to handle higher-order functions
100+
const returnType = parts.slice(1).join(' => ');
101+
102+
// Preserve the function signature, just link the return type for now
103+
// (Mapping param types inside the signature string is complex and often unnecessary for simple docs)
104+
const parsedReturn =
105+
parseType(returnType, transformType) || `\`<${returnType}>\``;
106+
return `${params} =&gt; ${parsedReturn}`;
107+
}
108+
}
109+
55110
// Handle Unions (|)
56111
if (trimmed.includes('|')) {
57112
const parts = splitByOuterSeparator(trimmed, '|');
@@ -76,45 +131,39 @@ export const parseType = (typeString, transformType) => {
76131
}
77132
}
78133

79-
// Handle Functions (=>)
80-
if (trimmed.includes('=>')) {
81-
const parts = splitByOuterSeparator(trimmed, '=>');
82-
if (parts.length === 2) {
83-
const params = parts[0];
84-
const returnType = parts[1];
134+
// Handle Generics (Base<Inner, Inner>)
135+
// Check if it's a generic wrapped in an array (e.g., Promise<string>[])
136+
const isGenericArray = trimmed.endsWith('[]');
137+
const genericTarget = isGenericArray ? trimmed.slice(0, -2).trim() : trimmed;
85138

86-
// Preserve the function signature, just link the return type for now
87-
// (Mapping param types inside the signature string is complex and often unnecessary for simple docs)
88-
const parsedReturn =
89-
parseType(returnType, transformType) || `\`<${returnType}>\``;
90-
return `${params} =&gt; ${parsedReturn}`;
91-
}
92-
}
139+
if (genericTarget.includes('<') && genericTarget.endsWith('>')) {
140+
const firstBracketIndex = genericTarget.indexOf('<');
141+
const baseType = genericTarget.slice(0, firstBracketIndex).trim();
142+
const innerType = genericTarget.slice(firstBracketIndex + 1, -1).trim();
93143

94-
// 3. Handle Generics (Base<Inner, Inner>)
95-
if (trimmed.includes('<') && trimmed.endsWith('>')) {
96-
const firstBracketIndex = trimmed.indexOf('<');
97-
const baseType = trimmed.slice(0, firstBracketIndex).trim();
98-
const innerType = trimmed.slice(firstBracketIndex + 1, -1).trim();
144+
const cleanBaseType = baseType.replace(/\[\]$/, ''); // Just in case of Base[]<Inner>
145+
const baseResult = transformType(cleanBaseType);
99146

100-
const baseResult = transformType(baseType.replace(/\[\]$/, ''));
101147
const baseFormatted = baseResult
102-
? `[\`<${baseType}>\`](${baseResult})`
103-
: `\`<${baseType}>\``;
148+
? `[\`<${cleanBaseType}>\`](${baseResult})`
149+
: `\`<${cleanBaseType}>\``;
104150

105-
// Split arguments safely by comma
106151
const innerArgs = splitByOuterSeparator(innerType, ',');
107152
const innerFormatted = innerArgs
108153
.map(arg => parseType(arg, transformType) || `\`<${arg}>\``)
109154
.join(', ');
110155

111-
return `${baseFormatted}&lt;${innerFormatted}&gt;`;
156+
return `${baseFormatted}&lt;${innerFormatted}&gt;${isGenericArray ? '[]' : ''}`;
112157
}
113158

114159
// Base Case: Plain Type (e.g., string, Buffer, Function)
115-
const result = transformType(trimmed.replace(/\[\]$/, ''));
116-
if (trimmed.length && result) {
117-
return `[\`<${trimmed}>\`](${result})`;
160+
// Preserve array notation for base types
161+
const isArray = trimmed.endsWith('[]');
162+
const cleanType = trimmed.replace(/\[\]$/, '');
163+
164+
const result = transformType(cleanType);
165+
if (cleanType.length && result) {
166+
return `[\`<${cleanType}>\`](${result})${isArray ? '[]' : ''}`;
118167
}
119168

120169
return null;

0 commit comments

Comments
 (0)