@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
713 lines (664 loc) • 21.1 kB
JavaScript
/**
* Utility function for parsing function arguments and handling nested structures
* in JavaScript/TypeScript code with structured representations.
*/
/**
* Structured argument types for discriminating between different code constructs:
* - `[Array]`: Literal array - e.g., `['1', '2', '3']`
* - `[String, Array]`: Function call - e.g., `['func', ['a', 'b']]`
* - `[String, Array, Array]`: Function with generics - e.g., `['Component', [{ foo: 'string' }], []]`
* - `[String, Array, null]`: Type with generics - e.g., `['Theme', ['"dark" | "light"'], null]`
* - `[Array, any]`: Simple arrow function - e.g., `[['evt'], 'evt.preventDefault()']`
* - `[Array, [any, any], any]`: Typed arrow function - e.g., `[['data'], ['string', 'Promise<string>'], ['Promise.resolve', ['data']]]`
* - `['as', string, any]`: TypeScript type assertion - e.g., `['as', 'React.FC<Props>', 'Component']`
* - `Record<string, any>`: Object literal - e.g., `{ key: 'value' }`
* - `string`: Plain string value
*/
/**
* Type guard and extractor for literal arrays
* @param value - The value to check
* @returns Object with items array if it's a literal array, false otherwise
*/
export function isArray(value) {
if (Array.isArray(value) && value.length === 1 && Array.isArray(value[0])) {
return {
items: value
};
}
return false;
}
/**
* Type guard and extractor for function calls
* @param value - The value to check
* @returns Object with name and arguments if it's a function call, false otherwise
*/
export function isFunction(value) {
if (Array.isArray(value) && value.length === 2 && typeof value[0] === 'string' && Array.isArray(value[1])) {
return {
name: value[0],
arguments: value[1]
};
}
return false;
}
/**
* Type guard and extractor for generics (both function and type generics)
* @param value - The value to check
* @returns Object with name, generics, and arguments (or null for types) if it's a generic, false otherwise
*/
export function isGeneric(value) {
if (Array.isArray(value) && value.length === 3 && typeof value[0] === 'string' && Array.isArray(value[1])) {
return {
name: value[0],
generics: value[1],
arguments: value[2]
};
}
return false;
}
/**
* Type guard and extractor for arrow functions
* @param value - The value to check
* @returns Object with args, types (if typed), and returnValue if it's an arrow function, false otherwise
*/
export function isArrowFunction(value) {
if (Array.isArray(value) && Array.isArray(value[0])) {
if (value.length === 2) {
// Simple arrow function: [Array, any]
return {
args: value[0],
returnValue: value[1]
};
}
if (value.length === 3 && Array.isArray(value[1]) && value[1].length === 2) {
// Typed arrow function: [Array, [inputTypes, outputTypes], any]
return {
args: value[0],
types: [value[1][0], value[1][1]],
returnValue: value[2]
};
}
}
return false;
}
/**
* Type guard and extractor for object literals
* @param value - The value to check
* @returns Object with properties if it's an object literal, false otherwise
*/
export function isObjectLiteral(value) {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return {
properties: value
};
}
return false;
}
/**
* Type guard and extractor for TypeScript type assertions
* @param value - The value to check
* @returns Object with type and expression if it's a type assertion, false otherwise
*/
export function isTypeAssertion(value) {
if (Array.isArray(value) && value.length === 3 && value[0] === 'as' && typeof value[1] === 'string') {
return {
type: value[1],
expression: value[2]
};
}
return false;
}
/**
* Main API: Parse arguments and return structured representation
* This is the primary parsing function optimized for recursive data structures
*/
export function parseFunctionArguments(str) {
return parseArgumentsRecursive(str);
}
/**
* Parse entire file and extract all exports with their function calls
* Returns a mapping of export names to their function call information
*/
export function parseFileExports(fileContent) {
const exports = {};
// Find all export statements that assign function calls
const exportRegex = /export\s+const\s+(\w+)\s*=\s*(\w+)\s*\(/g;
let match = exportRegex.exec(fileContent);
while (match !== null) {
const exportName = match[1];
const functionName = match[2];
const callStartIndex = match.index;
const parenIndex = match.index + match[0].length - 1; // Position of opening parenthesis
// Find the matching closing parenthesis
let parenCount = 0;
let callEndIndex = -1;
for (let i = parenIndex; i < fileContent.length; i += 1) {
if (fileContent[i] === '(') {
parenCount += 1;
} else if (fileContent[i] === ')') {
parenCount -= 1;
if (parenCount === 0) {
callEndIndex = i;
break;
}
}
}
if (callEndIndex !== -1) {
// Extract the arguments content between parentheses
const argumentsContent = fileContent.substring(parenIndex + 1, callEndIndex);
exports[exportName] = {
functionName,
// Parse the arguments using existing logic
arguments: parseFunctionArguments(argumentsContent),
sourceRange: [callStartIndex, callEndIndex + 1]
};
}
match = exportRegex.exec(fileContent);
}
return exports;
}
/**
* Internal recursive parsing function
*/
function parseArgumentsRecursive(str) {
const result = [];
let current = '';
let parenCount = 0;
let braceCount = 0;
let bracketCount = 0;
let angleCount = 0;
let inSingleLineComment = false;
let inMultiLineComment = false;
let inString = false;
let stringChar = '';
for (let i = 0; i < str.length; i += 1) {
const char = str[i];
const nextChar = str[i + 1];
// Handle comments
if (!inString && !inSingleLineComment && !inMultiLineComment) {
if (char === '/' && nextChar === '/') {
inSingleLineComment = true;
current += char;
continue;
}
if (char === '/' && nextChar === '*') {
inMultiLineComment = true;
current += char;
continue;
}
}
if (inSingleLineComment && char === '\n') {
inSingleLineComment = false;
current += char;
continue;
}
if (inMultiLineComment && char === '*' && nextChar === '/') {
inMultiLineComment = false;
current += char + nextChar;
i += 1; // Skip next character
continue;
}
if (inSingleLineComment || inMultiLineComment) {
current += char;
continue;
}
// Handle strings
if (!inString && (char === '"' || char === "'" || char === '`')) {
inString = true;
stringChar = char;
current += char;
continue;
}
if (inString && char === stringChar && str[i - 1] !== '\\') {
inString = false;
stringChar = '';
current += char;
continue;
}
if (inString) {
current += char;
continue;
}
// Handle brackets and parentheses
if (char === '(') {
parenCount += 1;
} else if (char === ')') {
parenCount -= 1;
} else if (char === '{') {
braceCount += 1;
} else if (char === '}') {
braceCount -= 1;
} else if (char === '[') {
bracketCount += 1;
} else if (char === ']') {
bracketCount -= 1;
} else if (char === '<') {
angleCount += 1;
} else if (char === '>' && str[i - 1] !== '=') {
// Only count > as closing angle bracket if it's not part of =>
angleCount -= 1;
} else if (char === ',' && parenCount === 0 && braceCount === 0 && bracketCount === 0 && angleCount === 0) {
const trimmedPart = current.trim();
if (trimmedPart) {
result.push(parseElement(trimmedPart));
}
current = '';
continue;
}
current += char;
}
// Handle the last part
if (current.trim()) {
const trimmedPart = current.trim();
result.push(parseElement(trimmedPart));
}
return result;
}
/**
* Parse a single element and determine its type/structure
*/
function parseElement(element) {
let trimmed = element.trim();
// Remove comments
trimmed = removeComments(trimmed);
// Handle object literals FIRST before checking for 'as'
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
return parseObjectLiteral(trimmed);
}
// Handle TypeScript 'as' type assertions with structured representation
if (trimmed.includes(' as ')) {
return parseTypeAssertion(trimmed);
}
// Handle array literals
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
return parseArrayLiteral(trimmed);
}
// Handle arrow functions
if (trimmed.includes('=>')) {
return parseArrowFunction(trimmed);
}
// Handle function calls and generics
if (trimmed.includes('(') || trimmed.includes('<')) {
return parseFunctionOrGeneric(trimmed);
}
// Everything else is just a string
return trimmed;
}
/**
* Remove comments from a string
*/
function removeComments(str) {
let result = '';
let inSingleLineComment = false;
let inMultiLineComment = false;
let inString = false;
let stringChar = '';
for (let i = 0; i < str.length; i += 1) {
const char = str[i];
const nextChar = str[i + 1];
// Handle strings first
if (!inSingleLineComment && !inMultiLineComment && !inString && (char === '"' || char === "'" || char === '`')) {
inString = true;
stringChar = char;
result += char;
continue;
}
if (inString && char === stringChar && str[i - 1] !== '\\') {
inString = false;
stringChar = '';
result += char;
continue;
}
if (inString) {
result += char;
continue;
}
// Handle comments
if (!inSingleLineComment && !inMultiLineComment) {
if (char === '/' && nextChar === '/') {
inSingleLineComment = true;
i += 1; // Skip next character
continue;
}
if (char === '/' && nextChar === '*') {
inMultiLineComment = true;
i += 1; // Skip next character
continue;
}
}
if (inSingleLineComment && char === '\n') {
inSingleLineComment = false;
result += char;
continue;
}
if (inMultiLineComment && char === '*' && nextChar === '/') {
inMultiLineComment = false;
i += 1; // Skip next character
continue;
}
if (inSingleLineComment || inMultiLineComment) {
continue;
}
result += char;
}
return result.trim();
}
/**
* Parse object literal like { key: value, other: data }
*/
function parseObjectLiteral(str) {
const content = str.slice(1, -1).trim(); // Remove { }
if (!content) {
return {};
}
const obj = {};
// Parse object properties manually to handle complex types
const properties = parseObjectProperties(content);
for (const prop of properties) {
const colonIndex = prop.indexOf(':');
if (colonIndex !== -1) {
const key = prop.substring(0, colonIndex).trim();
const value = prop.substring(colonIndex + 1).trim();
// Parse the value
const parsedValue = parseElement(value);
// For object properties: preserve strings as-is, but only wrap array LITERALS in another array
if (typeof parsedValue === 'string' && !Array.isArray(parsedValue)) {
obj[key] = parsedValue;
} else if (Array.isArray(parsedValue)) {
// Only double-wrap array literals (parsed from [1, 2, 3])
// Functions and generics should remain as single arrays
const originalValue = value.trim();
if (originalValue.startsWith('[') && originalValue.endsWith(']')) {
// This is an array literal - double wrap it
obj[key] = [parsedValue];
} else {
// This is a function call or generic - keep as single array
obj[key] = parsedValue;
}
} else {
obj[key] = parsedValue;
}
} else {
// Shorthand property like { foo } -> { foo: 'foo' }
const trimmed = prop.trim();
obj[trimmed] = trimmed;
}
}
return obj;
}
/**
* Parse object properties, handling complex nested types
*/
function parseObjectProperties(content) {
const properties = [];
let current = '';
let depth = 0;
let inString = false;
let stringChar = '';
for (let i = 0; i < content.length; i += 1) {
const char = content[i];
const nextChar = content[i + 1];
if (!inString && (char === '"' || char === "'" || char === '`')) {
inString = true;
stringChar = char;
} else if (inString && char === stringChar && content[i - 1] !== '\\') {
inString = false;
stringChar = '';
}
if (!inString) {
if (char === '<' || char === '{' || char === '(' || char === '[') {
depth += 1;
} else if (char === '>' && nextChar !== '=' && content[i - 1] !== '=') {
// Only count > as closing bracket if it's not part of => or >=
depth -= 1;
} else if (char === '}' || char === ')' || char === ']') {
depth -= 1;
} else if (char === ',' && depth === 0) {
if (current.trim()) {
properties.push(current.trim());
}
current = '';
continue;
}
}
current += char;
}
if (current.trim()) {
properties.push(current.trim());
}
return properties;
}
/**
* Parse array literal like [1, 2, 3]
*/
function parseArrayLiteral(str) {
const content = str.slice(1, -1).trim(); // Remove [ ]
if (!content) {
return [];
}
return parseArgumentsRecursive(content);
}
/**
* Parse arrow function like (a) => a + 1 or (data: string): Promise<string> => Promise.resolve(data)
*/
function parseArrowFunction(str) {
const arrowIndex = str.indexOf('=>');
const leftPart = str.substring(0, arrowIndex).trim();
const rightPart = str.substring(arrowIndex + 2).trim();
// Parse arguments
let args = [];
let types;
if (leftPart.startsWith('(') && leftPart.includes(')')) {
const parenEnd = leftPart.lastIndexOf(')');
const argsPart = leftPart.substring(1, parenEnd);
const afterParen = leftPart.substring(parenEnd + 1).trim();
args = argsPart ? parseArgumentsRecursive(argsPart) : [];
// Check for return type annotation
if (afterParen.startsWith(':')) {
const returnType = afterParen.substring(1).trim();
// Extract input types from args if they have type annotations
const inputTypes = args.map(arg => {
if (typeof arg === 'string' && arg.includes(':')) {
return arg.split(':')[1].trim();
}
return 'any';
});
// Clean argument names (remove type annotations)
args = args.map(arg => {
if (typeof arg === 'string' && arg.includes(':')) {
return arg.split(':')[0].trim();
}
return arg;
});
types = [inputTypes.length === 1 ? inputTypes[0] : inputTypes, returnType];
}
} else {
args = [leftPart];
}
const returnValue = parseElement(rightPart);
if (types) {
return [args, types, returnValue];
}
return [args, returnValue];
}
/**
* Parse function calls and generics like func(a, b) or Component<{ foo: string }>
*/
function parseFunctionOrGeneric(str) {
// Check for generics first
const angleStart = str.indexOf('<');
const parenStart = str.indexOf('(');
if (angleStart !== -1 && (parenStart === -1 || angleStart < parenStart)) {
return parseGeneric(str);
}
if (parenStart !== -1) {
const result = parseFunctionCall(str);
// If parseFunctionCall detected property access and returned [str], unwrap it
if (Array.isArray(result) && result.length === 1 && result[0] === str) {
return str;
}
return result;
}
return str;
}
/**
* Parse generic content while preserving nested structures
*/
function parseGenericContent(content) {
const elements = [];
let current = '';
let parenCount = 0;
let braceCount = 0;
let bracketCount = 0;
let angleCount = 0;
let inString = false;
let stringChar = '';
for (let i = 0; i < content.length; i += 1) {
const char = content[i];
if (!inString && (char === '"' || char === "'")) {
inString = true;
stringChar = char;
current += char;
} else if (inString && char === stringChar && content[i - 1] !== '\\') {
inString = false;
stringChar = '';
current += char;
} else if (!inString) {
if (char === '(') {
parenCount += 1;
} else if (char === ')') {
parenCount -= 1;
} else if (char === '{') {
braceCount += 1;
} else if (char === '}') {
braceCount -= 1;
} else if (char === '[') {
bracketCount += 1;
} else if (char === ']') {
bracketCount -= 1;
} else if (char === '<') {
angleCount += 1;
} else if (char === '>') {
angleCount -= 1;
} else if (char === ',' && parenCount === 0 && braceCount === 0 && bracketCount === 0 && angleCount === 0) {
if (current.trim()) {
elements.push(parseElement(current.trim()));
}
current = '';
continue;
}
current += char;
} else {
current += char;
}
}
if (current.trim()) {
elements.push(parseElement(current.trim()));
}
return elements;
}
/**
* Parse generic like Component<{ foo: string }> or Theme<"dark" | "light", Component[]>
*/
function parseGeneric(str) {
const angleStart = str.indexOf('<');
const name = str.substring(0, angleStart).trim();
// Find matching closing angle bracket
let angleCount = 0;
let angleEnd = -1;
for (let i = angleStart; i < str.length; i += 1) {
if (str[i] === '<') {
angleCount += 1;
} else if (str[i] === '>') {
angleCount -= 1;
if (angleCount === 0) {
angleEnd = i;
break;
}
}
}
if (angleEnd === -1) {
return [str];
}
const genericContent = str.substring(angleStart + 1, angleEnd).trim();
const afterGeneric = str.substring(angleEnd + 1).trim();
// Parse generic content - don't split on commas within generics for unions and arrays
const generics = genericContent ? parseGenericContent(genericContent) : [];
// Check if there are function arguments after the generic
if (afterGeneric.startsWith('(') && afterGeneric.endsWith(')')) {
const argContent = afterGeneric.slice(1, -1).trim();
const args = argContent ? parseArgumentsRecursive(argContent) : [];
return [name, generics, args];
}
// For standalone generics like Component<Props>, treat as function call with empty args
// This matches the test expectation for Component<{ foo: string }> -> ['Component', [{ foo: 'string' }], []]
return [name, generics, []];
}
/**
* Parse function call like func(a, b)
*/
function parseFunctionCall(str) {
const parenStart = str.indexOf('(');
const name = str.substring(0, parenStart).trim();
// Find matching closing parenthesis
let parenCount = 0;
let parenEnd = -1;
for (let i = parenStart; i < str.length; i += 1) {
if (str[i] === '(') {
parenCount += 1;
} else if (str[i] === ')') {
parenCount -= 1;
if (parenCount === 0) {
parenEnd = i;
break;
}
}
}
if (parenEnd === -1) {
return [str];
}
// Check if there's meaningful continuation after the closing parenthesis
const remainingLength = str.length - parenEnd - 1;
if (remainingLength > 0) {
// Skip whitespace to find the first meaningful character
let i = parenEnd + 1;
while (i < str.length && (str[i] === ' ' || str[i] === '\t' || str[i] === '\n' || str[i] === '\r')) {
i += 1;
}
if (i < str.length) {
const firstChar = str[i];
if (firstChar === '.' || firstChar === '[' || firstChar === '(' || firstChar === '!') {
// Property access, bracket notation, chained calls, or non-null assertion
return [str];
}
if (firstChar === '?' && i + 1 < str.length && str[i + 1] === '.') {
// Optional chaining
return [str];
}
}
}
const argContent = str.substring(parenStart + 1, parenEnd).trim();
if (!argContent) {
return [name, []];
}
const args = parseArgumentsRecursive(argContent);
// Special case: if there's a single array literal argument, flatten it
if (args.length === 1 && Array.isArray(args[0])) {
return [name, args[0]];
}
return [name, args];
}
/**
* Parse TypeScript type assertion like "Component as React.FC<Props>"
*/
function parseTypeAssertion(str) {
const asIndex = str.indexOf(' as ');
if (asIndex === -1) {
return [str]; // fallback to string if no 'as' found
}
const expression = str.substring(0, asIndex).trim();
const type = str.substring(asIndex + 4).trim(); // +4 for ' as '
// Parse the expression part recursively in case it's complex
const parsedExpression = parseElement(expression);
return ['as', type, parsedExpression];
}