scan_code
Scan directories for code files (JS, TS, C#, PHP, CSS, etc.) and extract structural elements like functions and classes with line numbers. Customize output formats (XML, Markdown, JSON) and filter results by file patterns, element types, and modifiers for precise analysis.
Instructions
Scans a directory for code files (JS, TS, C#, PHP, CSS, respecting .gitignore) and lists definitions (functions, classes, etc.) with line numbers. Supports XML, Markdown, and JSON output.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| detailLevel | No | Level of detail to include in the output. | standard |
| directory | Yes | The absolute path to the directory to scan. Relative paths are not supported. | |
| excludeModifiers | No | Modifiers to exclude. | |
| excludeNamePattern | No | Regex pattern to exclude element names. | |
| excludePaths | No | File path patterns to exclude. | |
| excludeTypes | No | Element types to exclude. | |
| filePatterns | No | Glob patterns for file extensions to include. | |
| includeModifiers | No | Modifiers to include (e.g., public, private). | |
| includePaths | No | Additional file path patterns to include. | |
| includeTypes | No | Element types to include (e.g., class, method). | |
| namePattern | No | Regex pattern to match element names. | |
| outputFormat | No | Output format for the results. | markdown |
Input Schema (JSON Schema)
{
"properties": {
"detailLevel": {
"default": "standard",
"description": "Level of detail to include in the output.",
"enum": [
"minimal",
"standard",
"detailed"
],
"type": "string"
},
"directory": {
"description": "The absolute path to the directory to scan. Relative paths are not supported.",
"type": "string"
},
"excludeModifiers": {
"description": "Modifiers to exclude.",
"items": {
"type": "string"
},
"type": "array"
},
"excludeNamePattern": {
"description": "Regex pattern to exclude element names.",
"type": "string"
},
"excludePaths": {
"description": "File path patterns to exclude.",
"items": {
"type": "string"
},
"type": "array"
},
"excludeTypes": {
"description": "Element types to exclude.",
"items": {
"type": "string"
},
"type": "array"
},
"filePatterns": {
"default": [
"**/*.js",
"**/*.jsx",
"**/*.ts",
"**/*.tsx",
"**/*.cs",
"**/*.php",
"**/*.css",
"**/*.py"
],
"description": "Glob patterns for file extensions to include.",
"items": {
"type": "string"
},
"type": "array"
},
"includeModifiers": {
"description": "Modifiers to include (e.g., public, private).",
"items": {
"type": "string"
},
"type": "array"
},
"includePaths": {
"description": "Additional file path patterns to include.",
"items": {
"type": "string"
},
"type": "array"
},
"includeTypes": {
"description": "Element types to include (e.g., class, method).",
"items": {
"type": "string"
},
"type": "array"
},
"namePattern": {
"description": "Regex pattern to match element names.",
"type": "string"
},
"outputFormat": {
"default": "markdown",
"description": "Output format for the results.",
"enum": [
"xml",
"markdown",
"json"
],
"type": "string"
}
},
"required": [
"directory"
],
"type": "object"
}
Implementation Reference
- src/index.ts:1480-1599 (handler)MCP CallToolRequestSchema handler specifically for the 'scan_code' tool. Validates input parameters, constructs filter options, calls performScan, and returns formatted results as text content.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== "scan_code") { throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } const args = request.params.arguments; // Validate directory argument if (typeof args?.directory !== "string") { throw new McpError( ErrorCode.InvalidParams, "Missing or invalid 'directory' argument (must be a string)." ); } // Ensure directory is an absolute path if (!path.isAbsolute(args.directory)) { throw new McpError( ErrorCode.InvalidParams, "Invalid 'directory' argument: Path must be absolute." ); } const filePatterns = args?.filePatterns && Array.isArray(args.filePatterns) ? args.filePatterns : defaultFilePatterns; // Use consistent defaults const outputFormat = // Default to markdown unless 'xml' or 'json' is specified args?.outputFormat === "xml" ? "xml" : args?.outputFormat === "json" ? "json" : "markdown"; const detailLevel = args?.detailLevel === "minimal" || args?.detailLevel === "detailed" ? args.detailLevel : "standard"; // Default to standard unless specified // Build filter options from MCP arguments const filterOptions: FilterOptions = {}; // Process include-types - split comma-separated values if needed if (args?.includeTypes && Array.isArray(args.includeTypes)) { filterOptions.includeTypes = args.includeTypes.flatMap(t => typeof t === 'string' && t.includes(',') ? t.split(',') : t ); } // Process exclude-types - split comma-separated values if needed if (args?.excludeTypes && Array.isArray(args.excludeTypes)) { filterOptions.excludeTypes = args.excludeTypes.flatMap(t => typeof t === 'string' && t.includes(',') ? t.split(',') : t ); } // Process include-modifiers - split comma-separated values if needed if (args?.includeModifiers && Array.isArray(args.includeModifiers)) { filterOptions.includeModifiers = args.includeModifiers.flatMap(m => typeof m === 'string' && m.includes(',') ? m.split(',') : m ); } // Process exclude-modifiers - split comma-separated values if needed if (args?.excludeModifiers && Array.isArray(args.excludeModifiers)) { filterOptions.excludeModifiers = args.excludeModifiers.flatMap(m => typeof m === 'string' && m.includes(',') ? m.split(',') : m ); } if (args?.namePattern && typeof args.namePattern === 'string') { filterOptions.namePattern = args.namePattern; } if (args?.excludeNamePattern && typeof args.excludeNamePattern === 'string') { filterOptions.excludeNamePattern = args.excludeNamePattern; } // Process include-paths - split comma-separated values if needed if (args?.includePaths && Array.isArray(args.includePaths)) { filterOptions.includePaths = args.includePaths.flatMap(p => typeof p === 'string' && p.includes(',') ? p.split(',') : p ); } // Process exclude-paths - split comma-separated values if needed if (args?.excludePaths && Array.isArray(args.excludePaths)) { filterOptions.excludePaths = args.excludePaths.flatMap(p => typeof p === 'string' && p.includes(',') ? p.split(',') : p ); } try { // Call the refactored function const outputText = await performScan( args.directory, filePatterns, outputFormat, detailLevel as 'minimal' | 'standard' | 'detailed', filterOptions ); return { content: [ { type: "text", text: outputText, }, ], }; } catch (error: any) { console.error(`Error during scan_code execution: ${error}`); // If it's an error from performScan, wrap it in McpError if (error instanceof Error && !(error instanceof McpError)) { throw new McpError( ErrorCode.InternalError, `Failed to scan directory: ${error.message}` ); } // Re-throw McpErrors or other unexpected errors throw error; } });
- src/index.ts:1407-1473 (schema)Input schema defining the parameters for the 'scan_code' tool, including required 'directory' and optional filters, formats, etc.inputSchema: { type: "object", properties: { directory: { type: "string", description: "The absolute path to the directory to scan. Relative paths are not supported.", }, filePatterns: { type: "array", items: { type: "string" }, description: "Glob patterns for file extensions to include.", default: defaultFilePatterns, // Use consistent defaults }, outputFormat: { type: "string", enum: ["xml", "markdown", "json"], description: "Output format for the results.", default: "markdown", }, detailLevel: { type: "string", enum: ["minimal", "standard", "detailed"], description: "Level of detail to include in the output.", default: "standard", }, includeTypes: { type: "array", items: { type: "string" }, description: "Element types to include (e.g., class, method).", }, excludeTypes: { type: "array", items: { type: "string" }, description: "Element types to exclude.", }, includeModifiers: { type: "array", items: { type: "string" }, description: "Modifiers to include (e.g., public, private).", }, excludeModifiers: { type: "array", items: { type: "string" }, description: "Modifiers to exclude.", }, namePattern: { type: "string", description: "Regex pattern to match element names.", }, excludeNamePattern: { type: "string", description: "Regex pattern to exclude element names.", }, includePaths: { type: "array", items: { type: "string" }, description: "Additional file path patterns to include.", }, excludePaths: { type: "array", items: { type: "string" }, description: "File path patterns to exclude.", }, }, required: ["directory"],
- src/index.ts:1400-1478 (registration)Tool registration in the ListToolsRequestSchema handler, defining the 'scan_code' tool with name, description, and input schema.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "scan_code", description: "Scans a directory for code files (JS, TS, C#, PHP, CSS, respecting .gitignore) and lists definitions (functions, classes, etc.) with line numbers. Supports XML, Markdown, and JSON output.", inputSchema: { type: "object", properties: { directory: { type: "string", description: "The absolute path to the directory to scan. Relative paths are not supported.", }, filePatterns: { type: "array", items: { type: "string" }, description: "Glob patterns for file extensions to include.", default: defaultFilePatterns, // Use consistent defaults }, outputFormat: { type: "string", enum: ["xml", "markdown", "json"], description: "Output format for the results.", default: "markdown", }, detailLevel: { type: "string", enum: ["minimal", "standard", "detailed"], description: "Level of detail to include in the output.", default: "standard", }, includeTypes: { type: "array", items: { type: "string" }, description: "Element types to include (e.g., class, method).", }, excludeTypes: { type: "array", items: { type: "string" }, description: "Element types to exclude.", }, includeModifiers: { type: "array", items: { type: "string" }, description: "Modifiers to include (e.g., public, private).", }, excludeModifiers: { type: "array", items: { type: "string" }, description: "Modifiers to exclude.", }, namePattern: { type: "string", description: "Regex pattern to match element names.", }, excludeNamePattern: { type: "string", description: "Regex pattern to exclude element names.", }, includePaths: { type: "array", items: { type: "string" }, description: "Additional file path patterns to include.", }, excludePaths: { type: "array", items: { type: "string" }, description: "File path patterns to exclude.", }, }, required: ["directory"], }, }, ], }; });
- src/index.ts:819-1074 (helper)Main helper function performScan that implements the scanning logic: file discovery, Tree-sitter parsing, filtering, and output formatting.async function performScan( directory: string, filePatterns: string[], outputFormat: 'xml' | 'markdown' | 'json', detailLevel: 'minimal' | 'standard' | 'detailed' = 'standard', filterOptions: FilterOptions = {} ): Promise<string> { const startTime = Date.now(); console.error(`Starting scan in directory: ${directory}`); console.error(`File patterns: ${filePatterns.join(', ')}`); console.error(`Output format: ${outputFormat}, Detail level: ${detailLevel}`); console.error(`Filter options: ${JSON.stringify(filterOptions)}`); // Resolve the target directory relative to the current working directory const targetDir = path.resolve(process.cwd(), directory); console.error(`Resolved target directory: ${targetDir}`); try { // Check if the directory exists using async fs await fs.access(targetDir); } catch (error) { throw new Error(`Directory not found: ${targetDir}`); } // Find .gitignore const gitignorePath = await findGitignore(targetDir); const ignoreFilter = await getIgnoreFilter(gitignorePath); console.error(`Using .gitignore: ${gitignorePath || 'None found'}`); // --- File Discovery --- console.error("Starting file discovery..."); let files: Set<string>; // Declare files set here // Check if includePaths is provided and should be restrictive if (filterOptions.includePaths && filterOptions.includePaths.length > 0) { console.error("`includePaths` provided, operating in restrictive mode."); files = new Set<string>(); // Initialize as empty set // Process only the paths specified in includePaths for (const includePath of filterOptions.includePaths) { // Handle potential glob patterns within includePaths if necessary, // or treat them as specific files/directories. // Current logic reuses the file/directory handling from below. const absoluteIncludePath = path.resolve(targetDir, includePath); try { const stats = await fs.stat(absoluteIncludePath); if (stats.isFile()) { files.add(path.normalize(absoluteIncludePath)); console.error(`Added specific file from includePaths: ${absoluteIncludePath}`); } else if (stats.isDirectory()) { console.error(`Included path is a directory, scanning recursively: ${absoluteIncludePath}`); // Use fg to find files within the directory, respecting basic ignores const dirFiles = await fg(path.join(absoluteIncludePath, '**/*').replace(/\\/g, '/'), { dot: true, onlyFiles: true, absolute: true, ignore: ['**/node_modules/**', '**/.git/**'], // Consistent ignores }); dirFiles.forEach(f => files.add(path.normalize(f))); console.error(`Added ${dirFiles.length} files from included directory: ${absoluteIncludePath}`); } } catch (statError: any) { // Handle cases where includePath might be a glob pattern needing fg if (includePath.includes('*') || includePath.includes('?')) { console.error(`Included path looks like a glob, attempting glob match: ${includePath}`); try { const globMatches = await fg(path.join(targetDir, includePath).replace(/\\/g, '/'), { dot: true, onlyFiles: true, absolute: true, cwd: targetDir, ignore: ['**/node_modules/**', '**/.git/**'], }); globMatches.forEach(match => files.add(path.normalize(match))); console.error(`Added ${globMatches.length} files from includePaths glob: ${includePath}`); } catch (globError) { console.error(`Error matching includePaths glob ${includePath}:`, globError); } } else if (statError.code === 'ENOENT') { console.warn(`Warning: Included path not found: ${absoluteIncludePath}`); } else { console.error(`Error processing included path ${absoluteIncludePath}:`, statError); } } } console.error(`Total files after processing restrictive includePaths: ${files.size}`); } else { // --- Standard File Discovery using filePatterns (glob) --- console.error("No restrictive `includePaths`, using standard `filePatterns` globbing."); const globMatchedFiles = new Set<string>(); // Store absolute paths from glob matching // Combine default and provided patterns if necessary const combinedPatterns = [...new Set(filePatterns)]; console.error(`Combined glob patterns for search: ${combinedPatterns.join(', ')}`); // Execute glob patterns relative to the target directory const globPatterns = combinedPatterns.map(p => path.join(targetDir, p).replace(/\\/g, '/')); console.error(`Absolute glob patterns for fast-glob: ${globPatterns.join(', ')}`); try { const globMatches = await fg(globPatterns, { dot: true, onlyFiles: true, absolute: true, cwd: targetDir, ignore: ['**/node_modules/**', '**/.git/**'], }); console.error(`fast-glob matched ${globMatches.length} files initially.`); globMatches.forEach(match => globMatchedFiles.add(path.normalize(match))); } catch (globError) { console.error("Error during fast-glob execution:", globError); throw new Error("File globbing failed."); } files = new Set<string>(globMatchedFiles); // Start with glob results // --- Include specific non-glob paths (additive) --- // This part only runs if includePaths was NOT restrictive const nonGlobIncludes = filterOptions.includePaths?.filter(p => !p.includes('*') && !p.includes('?')) || []; if (nonGlobIncludes.length > 0) { console.error("Adding specific non-glob paths from `includePaths`..."); for (const includePath of nonGlobIncludes) { const absoluteIncludePath = path.resolve(targetDir, includePath); try { const stats = await fs.stat(absoluteIncludePath); if (stats.isFile()) { files.add(path.normalize(absoluteIncludePath)); console.error(`Added specific file from includePaths: ${absoluteIncludePath}`); } else if (stats.isDirectory()) { console.error(`Included path is a directory, scanning recursively: ${absoluteIncludePath}`); const dirFiles = await fg(path.join(absoluteIncludePath, '**/*').replace(/\\/g, '/'), { dot: true, onlyFiles: true, absolute: true, ignore: ['**/node_modules/**', '**/.git/**'], }); dirFiles.forEach(f => files.add(path.normalize(f))); console.error(`Added ${dirFiles.length} files from included directory: ${absoluteIncludePath}`); } } catch (statError: any) { if (statError.code === 'ENOENT') { console.warn(`Warning: Included path not found: ${absoluteIncludePath}`); } else { console.error(`Error stating included path ${absoluteIncludePath}:`, statError); } } } } } // --- End of Conditional File Discovery --- // --- Include specific non-glob paths if provided --- // This section title is now misleading, the logic is handled above. Remove/adjust comment. // const files = new Set<string>(globMatchedFiles); // This line is now inside the else block // The logic previously here (lines 883-911) has been integrated into the // conditional blocks above (restrictive 'if' and standard 'else') // to handle includePaths correctly in both scenarios. // --- Filtering based on .gitignore and excludePaths --- let filesToFilter = Array.from(files); console.error(`Total files before gitignore/exclude filtering: ${filesToFilter.length}`); // 1. Apply .gitignore filter filesToFilter = filesToFilter.filter(absPath => { const relativePath = path.relative(targetDir, absPath).replace(/\\/g, '/'); // Relative path for ignore check return ignoreFilter(relativePath); }); console.error(`Files after gitignore filtering: ${filesToFilter.length}`); // 2. Apply excludePaths filter (using minimatch) const excludePatterns = filterOptions.excludePaths || []; if (excludePatterns.length > 0) { console.error(`Applying excludePaths patterns: ${excludePatterns.join(', ')}`); const filteredFiles = filesToFilter.filter((absPath) => { const relativePath = path.relative(targetDir, absPath).replace(/\\/g, '/'); // Check if the relative path matches any exclude pattern const isExcluded = excludePatterns.some(pattern => minimatch(relativePath, pattern, { dot: true })); if (isExcluded) { // console.error(`Excluding file due to pattern '${excludePatterns.find(p => minimatch(relativePath, p))}': ${relativePath}`); } return !isExcluded; // Keep if not excluded }); const excludedCount = filesToFilter.length - filteredFiles.length; if (excludedCount > 0) { console.error(`Excluded ${excludedCount} files based on excludePaths patterns.`); } filesToFilter = filteredFiles; } console.error(`Files after excludePaths filtering: ${filesToFilter.length}`); // --- Parsing and Definition Extraction --- const results: { [filePath: string]: Definition[] } = {}; console.error(`Parsing ${filesToFilter.length} files...`); for (const absoluteFilePath of filesToFilter) { // Use relative path for keys in the results object for cleaner output const relativePath = path.relative(targetDir, absoluteFilePath).replace(/\\/g, '/'); // Use forward slashes try { const content = await fs.readFile(absoluteFilePath, "utf-8"); const definitions = parseCodeWithTreeSitter(content, absoluteFilePath); // Pass absolute path here if (definitions.length > 0) { // Only add files with definitions or errors results[absoluteFilePath] = definitions; // Store with absolute path initially // console.error(`Parsed ${relativePath}: Found ${definitions.length} potential definitions.`); } else { // console.error(`Parsed ${relativePath}: No definitions found.`); } } catch (error: any) { console.error(`Error reading or parsing file ${relativePath}:`, error.message); results[absoluteFilePath] = [{ type: "error", name: `Failed to read/parse: ${error.message}`, startLine: 0, endLine: 0 }]; } } console.error("Finished parsing files."); // --- Filtering Definitions --- console.error("Applying definition filters..."); const filteredResults: { [filePath: string]: Definition[] } = {}; for (const absoluteFilePath in results) { const definitions = results[absoluteFilePath]; const filteredDefs = applyFiltersToDefinitions(definitions, filterOptions); // Only include the file in the final results if it has non-error definitions after filtering const hasRealDefinitions = filteredDefs.some(def => def.type !== 'error'); if (hasRealDefinitions) { const relativePath = path.relative(targetDir, absoluteFilePath).replace(/\\/g, '/'); filteredResults[relativePath] = filteredDefs; // Use relative path for final output keys // console.error(`Filtered ${relativePath}: Kept ${filteredDefs.length} definitions.`); } else { // console.error(`Filtered ${relativePath}: No definitions kept.`); } } console.error("Finished applying definition filters."); // --- Formatting Output --- console.error(`Formatting results as ${outputFormat}...`); let outputText: string; switch (outputFormat) { case "xml": outputText = formatResultsXML(filteredResults, detailLevel); break; case "json": outputText = formatResultsJSON(filteredResults, detailLevel); break; case "markdown": default: // Pass the original directory path for relative path calculation in Markdown outputText = formatResultsMarkdown(filteredResults, detailLevel, directory); break; } console.error("Finished formatting results."); const endTime = Date.now(); console.error(`Scan completed in ${endTime - startTime}ms.`); return outputText; }
- src/index.ts:318-556 (helper)Key helper for parsing individual code files using Tree-sitter queries to extract code definitions (functions, classes, methods, etc.) with metrics and hierarchy.function parseCodeWithTreeSitter( code: string, filePath: string ): Definition[] { const definitions: Definition[] = []; const fileExt = path.extname(filePath).toLowerCase(); const language = languageMap[fileExt]; if (!language) { return [{ type: "error", name: "Unsupported file type", startLine: 0, endLine: 0 }]; } parser.setLanguage(language); let tree: Parser.Tree; try { tree = parser.parse(code); } catch (error: any) { // Use absolute paths for reliable comparison const absoluteFilePath = path.resolve(filePath); const absoluteTargetFilePath = path.resolve("src/index.ts"); // Resolve relative to CWD const errorLogPath = path.join(process.cwd(), "parser-error.log"); // Log in CWD if (absoluteFilePath === absoluteTargetFilePath) { const errorMessage = `ERROR: Tree-sitter failed to parse ${filePath} at ${new Date().toISOString()}.\nFull error:\n${JSON.stringify(error, null, 2)}\n---\n`; // Log the error asynchronously, but don't block parsing fs.appendFile(errorLogPath, errorMessage) .then(() => console.error(`ERROR: Tree-sitter failed to parse ${filePath}. See ${errorLogPath} for details.`)) .catch(logErr => console.error(`FATAL: Failed to write to ${errorLogPath}: ${logErr.message}`)); // Return a specific error definition to indicate parsing failure for this critical file return [{ type: "error", name: `Failed to parse self (${filePath})`, startLine: 0, endLine: 0 }]; } else { // For other files, just log the error and return an empty array console.error(`Error parsing ${filePath}:`, error); return [{ type: "error", name: `Failed to parse ${filePath}`, startLine: 0, endLine: 0 }]; } } const langQueries = queries[fileExt]; if (!langQueries) { return [{ type: "error", name: "No queries defined for file type", startLine: 0, endLine: 0 }]; } // Generate unique IDs let idCounter = 0; const generateId = () => `def-${idCounter++}`; for (const defType in langQueries) { const queryStr = langQueries[defType]; if (!queryStr) continue; // Skip empty queries (like CSS or JS enums) try { const query = new Parser.Query(language, queryStr); const matches = query.matches(tree.rootNode); for (const match of matches) { const nameNode = match.captures.find((c: Parser.QueryCapture) => c.name === "name")?.node; const modifierNode = match.captures.find((c: Parser.QueryCapture) => c.name === "modifier")?.node; const definitionNode = match.captures.find((c: Parser.QueryCapture) => c.name === defType)?.node; // Use defType capture if (!nameNode || !definitionNode) continue; // Capture optional fields const dataTypeNode = match.captures.find((c: Parser.QueryCapture) => c.name === "dataType")?.node; const valueNode = match.captures.find((c: Parser.QueryCapture) => c.name === "value")?.node; const returnTypeNode = match.captures.find((c: Parser.QueryCapture) => c.name === "return_type")?.node; const paramsNode = match.captures.find((c: Parser.QueryCapture) => c.name === "params")?.node; // Capture params block // Determine parent type based on language and definition type let parentType: string | undefined; const localScopeTypes = ['variable', 'local_variable']; // Types typically defined within functions/methods // Parent type is determined later by the tree traversal approach // Local scope types don't need special handling since we use node traversal const definition: Definition = { id: generateId(), type: defType, name: nameNode.text, startLine: definitionNode.startPosition.row + 1, endLine: definitionNode.endPosition.row + 1, loc: definitionNode.endPosition.row - definitionNode.startPosition.row + 1, // Calculate LoC modifier: modifierNode?.text, dataType: dataTypeNode?.text, value: valueNode?.text, returnType: returnTypeNode?.text, children: [], // Initialize children array // Initialize metrics - parameterCount and complexity calculated later parameterCount: 0, complexity: 1, }; // Extract signature for methods/functions if possible if (['method', 'function'].includes(defType)) { // Calculate complexity for functions/methods definition.complexity = calculateComplexity(definitionNode); // Calculate Complexity // Attempt to get the full signature text const startPos = definitionNode.startPosition; const endPos = definitionNode.endPosition; const sourceLines = code.split('\n'); // Heuristic: Try to capture the signature line(s) // This might need refinement based on language specifics let signature = ''; if (startPos.row === endPos.row) { signature = sourceLines[startPos.row].substring(startPos.column, endPos.column); } else { // Multi-line signature (less common for just the signature part) // Try to capture up to the opening brace '{' or equivalent const openingBraceIndex = definitionNode.text.indexOf('{'); signature = openingBraceIndex > -1 ? definitionNode.text.substring(0, openingBraceIndex).trim() : definitionNode.text.split('\n')[0].trim(); } // Clean up signature (optional) signature = signature.replace(/\s+/g, ' ').trim(); // definition.signature = signature; // Add if needed later // Extract parameters if paramsNode exists if (paramsNode) { const parameters: Parameter[] = []; const paramCaptures = query.captures(paramsNode); // Query within the params node // Find parameter names and types (adjust query capture names as needed) let paramMatch: { name?: string, type?: string } = {}; for (const capture of paramCaptures) { if (capture.name === 'param_name') { paramMatch.name = capture.node.text; } else if (capture.name === 'param_type') { paramMatch.type = capture.node.text; } // When we have a name, add the parameter and reset if (paramMatch.name) { parameters.push({ name: paramMatch.name, type: paramMatch.type }); paramMatch = {}; // Reset for the next parameter } } // Fallback: If captures don't work well, parse the text directly (less robust) if (parameters.length === 0 && paramsNode.text.length > 2) { // Avoid empty "()" const paramList = paramsNode.text.slice(1, -1).split(','); // Remove () and split parameters.push(...paramList.map((p: string) => { const trimmed = p.trim(); // Basic type inference (example for TS/PHP like syntax) const typeMatch = trimmed.match(/^(\S+)\s+(\$\S+|\S+)/); // e.g., "string $name" or "int count" if (typeMatch) { return { name: typeMatch[2], type: typeMatch[1] }; } return { name: trimmed }; // Just name if no type found }).filter((p: { name: string; type?: string; }) => p.name)); // Filter out empty params } definition.parameters = parameters; definition.parameterCount = parameters.length; // Calculate Parameter Count // Find calls within this function/method body const callQueryStr = langQueries['call']; if (callQueryStr) { try { const callQuery = new Parser.Query(language, callQueryStr); const callMatches = callQuery.matches(definitionNode); // Search within the definition node const calledNames = new Set<string>(); // Use Set to avoid duplicates for (const callMatch of callMatches) { const callNameNode = callMatch.captures.find((c: Parser.QueryCapture) => c.name === "call_name")?.node; if (callNameNode) { calledNames.add(callNameNode.text); } } if (calledNames.size > 0) { definition.calls = Array.from(calledNames); } } catch (callQueryError: any) { console.warn(`Warning: Failed to execute call query for ${defType} ${definition.name} in ${filePath}:`, callQueryError.message); } } } // Extract return type if returnTypeNode exists if (returnTypeNode) { definition.returnType = returnTypeNode.text; } else { // Fallback: Try to capture from signature text (language-specific) // Example for PHP/TS style: function getName(): string { ... } const returnTypeMatch = signature.match(/:\s*(\w+)\s*\{?$/); if (returnTypeMatch) { definition.returnType = returnTypeMatch[1]; } } } definitions.push(definition); } } catch (queryError: any) { console.error(`Error executing query for ${defType} in ${filePath}:`, queryError); // Continue to next definition type } } // --- Parent-Child Relationship Logic --- // Create a map for quick lookup by ID const definitionMap = new Map(definitions.map(def => [def.id, def])); definitions.forEach(def => { const node = tree.rootNode.descendantForPosition({ row: def.startLine - 1, column: 0 }); // Get node at start line if (node) { // No redundant parent-child relationship logic (removed) // More robust parent finding: traverse up the syntax tree let potentialParentNode = node.parent; let parent: Definition | undefined = undefined; while (potentialParentNode) { parent = definitions.find(p => p.id !== def.id && p.startLine === potentialParentNode!.startPosition.row + 1 && p.endLine === potentialParentNode!.endPosition.row + 1 && ['class', 'namespace', 'interface', 'enum', 'method', 'function'].includes(p.type) // Plausible parent types ); if (parent) break; // Found a direct parent definition potentialParentNode = potentialParentNode.parent; } if (parent) { def.parentId = parent.id; if (!parent.children) { parent.children = []; } if (def.id) { // Ensure def.id is defined before pushing parent.children.push(def.id); } } } }); return definitions; }