-
Notifications
You must be signed in to change notification settings - Fork 45
feat(metadata): support advanced generics using recursion #763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
9975bb3
bb6c438
c69122a
00d8c39
bb43bc8
d343adb
e746538
9d04e81
0115291
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| /** | ||
| * Safely splits a string by a given set of separators at depth 0 (ignoring those inside < > or ( )). | ||
| * | ||
| * @param {string} str The string to split | ||
| * @param {string} separator The separator to split by (e.g., '|', '&', ',', '=>') | ||
| * @returns {string[]} The split pieces | ||
| */ | ||
| const splitByOuterSeparator = (str, separator) => { | ||
| const pieces = []; | ||
| let current = ''; | ||
| let depth = 0; | ||
|
|
||
| for (let i = 0; i < str.length; i++) { | ||
| const char = str[i]; | ||
|
|
||
| // Track depth using brackets and parentheses | ||
| if (char === '<' || char === '(') { | ||
| depth++; | ||
| } else if ((char === '>' && str[i - 1] !== '=') || char === ')') { | ||
| depth--; | ||
| } | ||
|
|
||
| // Check for multi-character separators like '=>' | ||
| const isArrow = separator === '=>' && char === '=' && str[i + 1] === '>'; | ||
| // Check for single-character separators | ||
| const isCharSeparator = separator === char; | ||
|
|
||
| if (depth === 0 && (isCharSeparator || isArrow)) { | ||
| pieces.push(current.trim()); | ||
| current = ''; | ||
| if (isArrow) { | ||
| i++; | ||
| } // skip the '>' part of '=>' | ||
| continue; | ||
| } | ||
|
|
||
| current += char; | ||
| } | ||
|
|
||
| pieces.push(current.trim()); | ||
| return pieces; | ||
| }; | ||
| /** | ||
| * Safely removes outer parentheses from a type string if they wrap the entire string. | ||
| * This prevents "depth blindness" in the parser by unwrapping types like `(string | number)` | ||
| * into `string | number`, while safely ignoring disconnected groups like `(A) | (B)`. | ||
| * | ||
| * @param {string} typeString The type string to evaluate and potentially unwrap. | ||
| * @returns {string} The unwrapped type string, or the original string if not fully wrapped. | ||
| */ | ||
| export const stripOuterParentheses = typeString => { | ||
|
moshams272 marked this conversation as resolved.
Outdated
|
||
| let trimmed = typeString.trim(); | ||
|
|
||
| if (trimmed.startsWith('(') && trimmed.endsWith(')')) { | ||
| let depth = 0; | ||
| let isValidWrapper = true; | ||
|
|
||
| // Iterate through the string, ignoring the last closing parenthesis | ||
| for (let i = 0; i < trimmed.length - 1; i++) { | ||
| if (trimmed[i] === '(') { | ||
| depth++; | ||
| } else if (trimmed[i] === ')') { | ||
| depth--; | ||
| } | ||
|
|
||
| // If depth hits 0 before the end, it means the parentheses don't wrap the whole string | ||
| if (depth === 0) { | ||
| isValidWrapper = false; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (isValidWrapper) { | ||
| return trimmed.slice(1, -1).trim(); | ||
| } | ||
| } | ||
|
|
||
| return trimmed; | ||
| }; | ||
| /** | ||
| * Recursively parses advanced TypeScript types, including Unions, Intersections, Functions, and Nested Generics. | ||
| * * @param {string} typeString The plain type string to evaluate | ||
| * @param {Function} transformType The function used to resolve individual types into links | ||
| * @returns {string|null} The formatted Markdown link(s), or null if the base type doesn't map | ||
| */ | ||
| export const parseType = (typeString, transformType) => { | ||
| // Clean the string and strip unnecessary outer parentheses to prevent depth blindness (e.g., "(string | number)" -> "string | number") | ||
| const trimmed = stripOuterParentheses(typeString); | ||
| if (!trimmed) { | ||
| return null; | ||
| } | ||
|
|
||
| // Handle Functions (=>) | ||
| if (trimmed.includes('=>')) { | ||
| const parts = splitByOuterSeparator(trimmed, '=>'); | ||
| if (parts.length > 1) { | ||
| const params = parts[0]; | ||
|
|
||
| // Join the rest back together to handle higher-order functions | ||
| const returnType = parts.slice(1).join(' => '); | ||
|
|
||
| // Preserve the function signature, just link the return type for now | ||
| // (Mapping param types inside the signature string is complex and often unnecessary for simple docs) | ||
| const parsedReturn = | ||
| parseType(returnType, transformType) || `\`<${returnType}>\``; | ||
| return `${params} => ${parsedReturn}`; | ||
| } | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Handle Unions (|) | ||
| if (trimmed.includes('|')) { | ||
| const parts = splitByOuterSeparator(trimmed, '|'); | ||
| if (parts.length > 1) { | ||
| // Re-evaluate each part recursively and join with ' | ' | ||
| const resolvedParts = parts.map( | ||
| p => parseType(p, transformType) || `\`<${p}>\`` | ||
| ); | ||
| return resolvedParts.join(' | '); | ||
| } | ||
| } | ||
|
|
||
| // Handle Intersections (&) | ||
| if (trimmed.includes('&')) { | ||
| const parts = splitByOuterSeparator(trimmed, '&'); | ||
| if (parts.length > 1) { | ||
| // Re-evaluate each part recursively and join with ' & ' | ||
| const resolvedParts = parts.map( | ||
| p => parseType(p, transformType) || `\`<${p}>\`` | ||
| ); | ||
| return resolvedParts.join(' & '); | ||
| } | ||
| } | ||
|
|
||
| // Handle Generics (Base<Inner, Inner>) | ||
| // Check if it's a generic wrapped in an array (e.g., Promise<string>[]) | ||
| const isGenericArray = trimmed.endsWith('[]'); | ||
| const genericTarget = isGenericArray ? trimmed.slice(0, -2).trim() : trimmed; | ||
|
|
||
| if (genericTarget.includes('<') && genericTarget.endsWith('>')) { | ||
| const firstBracketIndex = genericTarget.indexOf('<'); | ||
| const baseType = genericTarget.slice(0, firstBracketIndex).trim(); | ||
| const innerType = genericTarget.slice(firstBracketIndex + 1, -1).trim(); | ||
|
|
||
| const cleanBaseType = baseType.replace(/\[\]$/, ''); // Just in case of Base[]<Inner> | ||
| const baseResult = transformType(cleanBaseType); | ||
|
|
||
| const baseFormatted = baseResult | ||
| ? `[\`<${cleanBaseType}>\`](${baseResult})` | ||
| : `\`<${cleanBaseType}>\``; | ||
|
|
||
| const innerArgs = splitByOuterSeparator(innerType, ','); | ||
| const innerFormatted = innerArgs | ||
| .map(arg => parseType(arg, transformType) || `\`<${arg}>\``) | ||
| .join(', '); | ||
|
|
||
| return `${baseFormatted}<${innerFormatted}>${isGenericArray ? '[]' : ''}`; | ||
| } | ||
|
|
||
| // Base Case: Plain Type (e.g., string, Buffer, Function) | ||
| // Preserve array notation for base types | ||
| const isArray = trimmed.endsWith('[]'); | ||
| const cleanType = trimmed.replace(/\[\]$/, ''); | ||
|
|
||
| const result = transformType(cleanType); | ||
| if (cleanType.length && result) { | ||
| return `[\`<${cleanType}>\`](${result})${isArray ? '[]' : ''}`; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Array type link text formatting changed from old behaviorLow Severity For plain array types like Reviewed by Cursor Bugbot for commit d343adb. Configure here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
|
|
||
| return null; | ||
| }; | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MyTypeshould be handled, no?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, Parsing individual types inside the parameter string (handling colons, commas, optional params ?, etc.) requires a much deeper level of AST-like parsing and I focused on generics and left the parameter string for now as I will open another PR soon to handle this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wanted to drop a quick update: regarding your question about
myTypenot being parsed, I went ahead and implemented the function signature parsing.