UNPKG

tree-ast-grep-mcp

Version:

Simple, direct ast-grep wrapper for AI coding agents. Zero abstractions, maximum performance.

748 lines 36.6 kB
import * as fs from 'fs/promises'; import * as path from 'path'; /** * Validates tool parameters against workspace boundaries and security policies. */ export class ParameterValidator { workspaceRoot; blockedPaths; /** * Persist the workspace root and populate security guardrails. */ constructor(workspaceRoot) { this.workspaceRoot = path.resolve(workspaceRoot); this.blockedPaths = this.getBlockedPaths(); } /** * Enumerate filesystem locations that tools must never modify. */ getBlockedPaths() { const systemPaths = [ '/etc', '/bin', '/usr', '/sys', '/proc', // Unix system dirs 'C:\\Windows', 'C:\\Program Files', // Windows system dirs path.join(process.env.HOME || '', '.ssh'), // SSH keys path.join(process.env.HOME || '', '.aws'), // AWS credentials 'node_modules/.bin', // Binary executables ]; return systemPaths.map(p => path.resolve(p)); } // Validate search parameters /** * Validate search tool parameters and sanitize defaults. */ validateSearchParams(params) { const result = { valid: true, errors: [], warnings: [] }; // Validate pattern if (!params.pattern || typeof params.pattern !== 'string' || params.pattern.trim().length === 0) { result.valid = false; result.errors.push('Pattern cannot be empty'); return result; } // Check for dangerous patterns const dangerousPatterns = ['rm -rf', 'del /f', 'format c:', '> /dev/null']; if (dangerousPatterns.some(dangerous => params.pattern.includes(dangerous))) { result.valid = false; result.errors.push('Pattern contains potentially dangerous commands'); return result; } // Validate AST pattern syntax const patternValidation = this.validateAstPattern(params.pattern, params.language); if (!patternValidation.valid) { result.valid = false; result.errors.push(...patternValidation.errors); result.warnings.push(...patternValidation.warnings); } // Validate paths if provided if (params.paths) { const pathValidation = this.validatePaths(params.paths); if (!pathValidation.valid) { result.valid = false; result.errors.push(...pathValidation.errors); } } // Validate context if (params.context !== undefined) { if (typeof params.context !== 'number' || params.context < 0 || params.context > 10) { result.valid = false; result.errors.push('Context must be a number between 0 and 10'); } } // Validate maxMatches if (params.maxMatches !== undefined) { if (typeof params.maxMatches !== 'number' || params.maxMatches < 1 || params.maxMatches > 10000) { result.valid = false; result.errors.push('maxMatches must be a number between 1 and 10000'); } } // Validate timeoutMs if (params.timeoutMs !== undefined) { if (typeof params.timeoutMs !== 'number' || params.timeoutMs < 1000 || params.timeoutMs > 120000) { result.valid = false; result.errors.push('timeoutMs must be between 1000 and 120000 milliseconds'); } } // Validate perFileMatchLimit if (params.perFileMatchLimit !== undefined) { if (typeof params.perFileMatchLimit !== 'number' || params.perFileMatchLimit < 1 || params.perFileMatchLimit > 1000) { result.valid = false; result.errors.push('perFileMatchLimit must be a number between 1 and 1000'); } } // Validate ignorePath/root/workdir if (params.ignorePath !== undefined && !Array.isArray(params.ignorePath)) { result.valid = false; result.errors.push('ignorePath must be an array of strings'); } if (params.root !== undefined && typeof params.root !== 'string') { result.valid = false; result.errors.push('root must be a string path'); } if (params.workdir !== undefined && typeof params.workdir !== 'string') { result.valid = false; result.errors.push('workdir must be a string path'); } // Validate code/stdinFilepath if (params.code !== undefined && typeof params.code !== 'string') { result.valid = false; result.errors.push('code must be a string'); } if (params.stdinFilepath !== undefined && typeof params.stdinFilepath !== 'string') { result.valid = false; result.errors.push('stdinFilepath must be a string path'); } if (params.code && params.paths && params.paths.length > 0) { result.warnings.push('Ignoring paths since code is provided for stdin search'); } // Validate jsonStyle/follow/threads if (params.jsonStyle !== undefined && !['stream', 'pretty', 'compact'].includes(params.jsonStyle)) { result.valid = false; result.errors.push('jsonStyle must be one of: stream, pretty, compact'); } if (params.follow !== undefined && typeof params.follow !== 'boolean') { result.valid = false; result.errors.push('follow must be a boolean'); } if (params.threads !== undefined && (typeof params.threads !== 'number' || params.threads < 1 || params.threads > 64)) { result.valid = false; result.errors.push('threads must be a number between 1 and 64'); } result.sanitized = { pattern: params.pattern.trim(), paths: params.paths || ['.'], language: params.language || this.detectLanguageFromPattern(params.pattern.trim()), context: params.context ?? 3, include: params.include, exclude: params.exclude || this.getDefaultExcludes(), maxMatches: params.maxMatches ?? 100, timeoutMs: params.timeoutMs, relativePaths: params.relativePaths ?? false, perFileMatchLimit: params.perFileMatchLimit, noIgnore: params.noIgnore ?? false, ignorePath: params.ignorePath, root: params.root, workdir: params.workdir, code: params.code, stdinFilepath: params.stdinFilepath, jsonStyle: params.jsonStyle || 'stream', follow: params.follow ?? false, threads: params.threads, }; return result; } // Validate replace parameters /** * Validate replace tool parameters and enforce workspace boundaries. */ validateReplaceParams(params) { const result = { valid: true, errors: [], warnings: [] }; // Validate pattern if (!params.pattern || typeof params.pattern !== 'string' || params.pattern.trim().length === 0) { result.valid = false; result.errors.push('Pattern cannot be empty'); return result; } // Validate replacement if (params.replacement === undefined || params.replacement === null) { result.valid = false; result.errors.push('Replacement cannot be null or undefined'); return result; } // Validate AST pattern syntax const patternValidation = this.validateAstPattern(params.pattern, params.language); if (!patternValidation.valid) { result.valid = false; result.errors.push(...patternValidation.errors); result.warnings.push(...patternValidation.warnings); } // Validate pattern-replacement consistency const consistencyValidation = this.validatePatternReplacementConsistency(params.pattern, params.replacement); if (!consistencyValidation.valid) { result.valid = false; result.errors.push(...consistencyValidation.errors); result.warnings.push(...consistencyValidation.warnings); } // Validate paths if provided if (params.paths) { const pathValidation = this.validatePaths(params.paths); if (!pathValidation.valid) { result.valid = false; result.errors.push(...pathValidation.errors); } } // New validations if (params.timeoutMs !== undefined && (typeof params.timeoutMs !== 'number' || params.timeoutMs < 1000 || params.timeoutMs > 180000)) { result.valid = false; result.errors.push('timeoutMs must be between 1000 and 180000 milliseconds'); } if (params.relativePaths !== undefined && typeof params.relativePaths !== 'boolean') { result.valid = false; result.errors.push('relativePaths must be a boolean'); } if (params.jsonStyle !== undefined && !['stream', 'pretty', 'compact'].includes(params.jsonStyle)) { result.valid = false; result.errors.push('jsonStyle must be one of: stream, pretty, compact'); } if (params.follow !== undefined && typeof params.follow !== 'boolean') { result.valid = false; result.errors.push('follow must be a boolean'); } if (params.threads !== undefined && (typeof params.threads !== 'number' || params.threads < 1 || params.threads > 64)) { result.valid = false; result.errors.push('threads must be a number between 1 and 64'); } if (params.noIgnore !== undefined && typeof params.noIgnore !== 'boolean') { result.valid = false; result.errors.push('noIgnore must be a boolean'); } if (params.ignorePath !== undefined && !Array.isArray(params.ignorePath)) { result.valid = false; result.errors.push('ignorePath must be an array of strings'); } if (params.root !== undefined && typeof params.root !== 'string') { result.valid = false; result.errors.push('root must be a string path'); } if (params.workdir !== undefined && typeof params.workdir !== 'string') { result.valid = false; result.errors.push('workdir must be a string path'); } if (params.code !== undefined && typeof params.code !== 'string') { result.valid = false; result.errors.push('code must be a string'); } if (params.stdinFilepath !== undefined && typeof params.stdinFilepath !== 'string') { result.valid = false; result.errors.push('stdinFilepath must be a string path'); } result.sanitized = { pattern: params.pattern.trim(), replacement: String(params.replacement), paths: params.paths || ['.'], language: params.language || this.detectLanguageFromPattern(params.pattern.trim()), dryRun: params.dryRun ?? true, interactive: params.interactive ?? false, include: params.include, exclude: params.exclude || this.getDefaultExcludes(), timeoutMs: params.timeoutMs, relativePaths: params.relativePaths ?? false, jsonStyle: params.jsonStyle || 'stream', follow: params.follow ?? false, threads: params.threads, noIgnore: params.noIgnore ?? false, ignorePath: params.ignorePath, root: params.root, workdir: params.workdir, code: params.code, stdinFilepath: params.stdinFilepath, }; return result; } // Validate rewrite parameters /** * Validate rewrite tool parameters including pattern and rewrite rules. */ validateRewriteParams(params) { const result = { valid: true, errors: [], warnings: [] }; // Validate rules if (!params.rules || typeof params.rules !== 'string' || params.rules.trim().length === 0) { result.valid = false; result.errors.push('Rules cannot be empty'); return result; } // Validate paths if provided if (params.paths) { const pathValidation = this.validatePaths(params.paths); if (!pathValidation.valid) { result.valid = false; result.errors.push(...pathValidation.errors); } } result.sanitized = { rules: params.rules.trim(), paths: params.paths || ['.'], language: params.language, dryRun: params.dryRun ?? true, }; return result; } // Validate paths for security /** * Ensure provided paths stay within the workspace and are accessible. */ validatePaths(paths) { const result = { valid: true, errors: [], warnings: [] }; const sanitizedPaths = []; for (const inputPath of paths) { try { // Resolve and normalize path const resolvedPath = path.resolve(this.workspaceRoot, inputPath); const normalizedRoot = path.resolve(this.workspaceRoot); const relativeFromRoot = path.relative(normalizedRoot, resolvedPath); // Security check: ensure path is within workspace using robust relative check if (relativeFromRoot === '' || relativeFromRoot === '.') { // resolvedPath is the root itself; allow } else if (relativeFromRoot.startsWith('..' + path.sep) || relativeFromRoot === '..') { result.valid = false; result.errors.push(`Path "${inputPath}" is outside workspace root`); continue; } // Check for blocked system directories if (this.blockedPaths.some(blocked => resolvedPath.startsWith(blocked))) { result.valid = false; result.errors.push(`Access to system directory "${inputPath}" is blocked`); continue; } sanitizedPaths.push(resolvedPath); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); result.valid = false; result.errors.push(`Invalid path "${inputPath}": ${errorMessage}`); } } result.sanitized = sanitizedPaths; return result; } // Validate resource limits async validateResourceLimits(paths) { const result = { valid: true, errors: [], warnings: [] }; const maxFileSize = parseInt(process.env.MAX_FILE_SIZE || '10485760'); // 10MB const maxFiles = parseInt(process.env.MAX_FILES || '100000'); let totalFiles = 0; for (const dirPath of paths) { try { const absolutePath = path.resolve(this.workspaceRoot, dirPath); const stats = await fs.stat(absolutePath); if (stats.isFile()) { totalFiles++; if (stats.size > maxFileSize) { result.warnings.push(`File "${absolutePath}" (${stats.size} bytes) exceeds size limit`); } } else if (stats.isDirectory()) { // Count files in directory const files = await this.countFilesRecursive(absolutePath); totalFiles += files; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); result.warnings.push(`Cannot access "${path.resolve(this.workspaceRoot, dirPath)}": ${errorMessage}`); } } if (totalFiles > maxFiles) { result.valid = false; result.errors.push(`Total files (${totalFiles}) exceeds limit (${maxFiles})`); } return result; } /** * Estimate file counts by walking directories with safeguards. */ async countFilesRecursive(dir) { let count = 0; try { const items = await fs.readdir(dir); for (const item of items) { const itemPath = path.join(dir, item); try { const stats = await fs.stat(itemPath); if (stats.isFile()) { count++; } else if (stats.isDirectory() && !item.startsWith('.')) { count += await this.countFilesRecursive(itemPath); } } catch { // Skip inaccessible files } } } catch { // Skip inaccessible directories } return count; } // Extract metavariables from AST pattern /** * Identify single and multi node metavariables within a pattern. */ extractMetavariables(pattern) { const single = []; const multi = []; // Match single metavariables: $VAR, $_, $VAR1 const singleMatches = pattern.match(/\$[A-Z_][A-Z0-9_]*/g) || []; single.push(...singleMatches.map(m => m.slice(1))); // Remove $ // Match multi metavariables: $$$VAR, $$$ const multiMatches = pattern.match(/\$\$\$[A-Z_]*[A-Z0-9_]*/g) || []; multi.push(...multiMatches.map(m => m.slice(3))); // Remove $$$ return { single, multi }; } // Validate AST pattern syntax /** * Validate that the ast-grep pattern is syntactically correct for the language. */ validateAstPattern(pattern, language) { const result = { valid: true, errors: [], warnings: [] }; // Check for basic syntax issues const trimmedPattern = pattern.trim(); // Check for unmatched brackets/parentheses const brackets = { '(': ')', '[': ']', '{': '}' }; const stack = []; for (const char of trimmedPattern) { if (char in brackets) { stack.push(brackets[char]); } else if (Object.values(brackets).includes(char)) { if (stack.pop() !== char) { result.errors.push('Pattern has unmatched brackets, parentheses, or braces'); result.valid = false; break; } } } // Check for invalid metavariable syntax const invalidMetavars = trimmedPattern.match(/\$[a-z][a-zA-Z0-9_]*/g); if (invalidMetavars) { result.errors.push(`Invalid metavariable syntax: ${invalidMetavars.join(', ')}. Metavariables must start with uppercase letter or underscore (e.g., $VAR, $_)`); result.valid = false; } // Check for incomplete metavariables ($$X where X is not $ or whitespace/boundary) // This should catch things like $$a but not $$$ const incompleteMetavars = trimmedPattern.match(/\$\$(?!\$)[a-zA-Z_]/g); if (incompleteMetavars) { result.errors.push('Incomplete multi-metavariable syntax. Use $$$ for multi-node matching'); result.valid = false; } // Check for bare $$$ usage - it's valid but named is clearer const bareTripleVars = trimmedPattern.match(/\$\$\$(?![A-Z_][A-Z0-9_]*)/g); if (bareTripleVars && bareTripleVars.length > 0) { result.warnings.push('Consider using named multi-metavariables (e.g., $$$ARGS, $$$BODY) instead of bare $$$ for better readability (bare $$$ is valid)'); } // Language-specific validation if (language) { const langValidation = this.validateLanguageSpecificPattern(trimmedPattern, language); if (!langValidation.valid) { result.errors.push(...langValidation.errors); result.warnings.push(...langValidation.warnings); result.valid = false; } } else if (this.requiresLanguageHint(trimmedPattern)) { result.warnings.push('Pattern may benefit from specifying a language parameter for better matching'); } return result; } // Validate pattern-replacement consistency /** * Ensure replacement templates match the captured metavariables. */ validatePatternReplacementConsistency(pattern, replacement) { const result = { valid: true, errors: [], warnings: [] }; const patternVars = this.extractMetavariables(pattern); const replacementVars = this.extractMetavariables(replacement); // Check that all replacement metavariables exist in pattern const allPatternVars = [...patternVars.single, ...patternVars.multi]; const allReplacementVars = [...replacementVars.single, ...replacementVars.multi]; const undefinedVars = allReplacementVars.filter(rv => !allPatternVars.includes(rv)); if (undefinedVars.length > 0) { result.valid = false; result.errors.push(`Replacement uses undefined metavariables: ${undefinedVars.map(v => '$' + v).join(', ')}. Available from pattern: ${allPatternVars.map(v => '$' + v).join(', ')}`); } // Warn about unused pattern variables const unusedVars = allPatternVars.filter(pv => !allReplacementVars.includes(pv) && pv !== '_'); if (unusedVars.length > 0) { result.warnings.push(`Pattern defines unused metavariables: ${unusedVars.map(v => '$' + v).join(', ')}`); } return result; } // Check if pattern requires language hint /** * Determine if a pattern needs an explicit language selection. */ requiresLanguageHint(pattern) { // Patterns that typically need language context const languageSpecificKeywords = [ 'function', 'class', 'interface', 'import', 'export', // JS/TS 'def', 'class', 'import', 'from', // Python 'public', 'private', 'protected', 'static', // Java/C# 'fn', 'impl', 'trait', 'use', // Rust 'func', 'type', 'var', 'const', // Go ]; return languageSpecificKeywords.some(keyword => pattern.includes(keyword)); } // Language-specific pattern validation /** * Run language specific validation rules for advanced patterns. */ validateLanguageSpecificPattern(pattern, language) { const result = { valid: true, errors: [], warnings: [] }; switch (language.toLowerCase()) { case 'javascript': case 'typescript': // Check for common JS/TS patterns that need body if (/\bclass\s+\$[A-Z_][A-Z0-9_]*\s*(?:extends\s+[^{]*)?\s*\{(?![\s\S]*\$\$\$)/.test(pattern)) { result.errors.push('JavaScript class pattern needs body. Use: class $NAME { $$$ } or class $NAME extends $BASE { $$$ }'); result.valid = false; } if (/\bfunction\s+\$[A-Z_][A-Z0-9_]*\s*\([^)]*\)\s*\{(?![\s\S]*\$\$\$)/.test(pattern)) { result.errors.push('JavaScript function pattern needs body. Use: function $NAME($ARGS) { $$$ }'); result.valid = false; } // CRITICAL: Check for complex patterns known to cause issues with ast-grep if (/\bfunction\s+\$[A-Z_][A-Z0-9_]*\s*\([^)]*\)\s*\{\s*\$\$\$\s*\}/.test(pattern)) { result.warnings.push('Complex function patterns with $$$ may not match reliably in ast-grep. Consider simpler patterns like "function $NAME($ARGS)" or use named body variables like "$BODY" instead of "$$$".'); } // Check for problematic multi-metavariable usage if (pattern.includes('$$$') && /\bfunction|\bclass|\bif|\bfor|\bwhile/.test(pattern)) { result.warnings.push('Complex structural patterns with $$$ have known reliability issues. Consider: 1) Using named metavariables like $BODY instead of $$$, 2) Breaking into simpler patterns, or 3) Using contextual matching with inside/has patterns.'); } if (/\btry\s*\{(?![\s\S]*\$\$\$)/.test(pattern)) { result.errors.push('JavaScript try pattern needs body. Use: try { $$$ }'); result.valid = false; } if (/\bcatch\s*\([^)]*\)\s*\{(?![\s\S]*\$\$\$)/.test(pattern)) { result.errors.push('JavaScript catch pattern needs body. Use: catch ($ERROR) { $$$ }'); result.valid = false; } if (/\bfinally\s*\{(?![\s\S]*\$\$\$)/.test(pattern)) { result.errors.push('JavaScript finally pattern needs body. Use: finally { $$$ }'); result.valid = false; } // General JS checks if (pattern.includes('function') && !pattern.includes('(')) { result.warnings.push('JavaScript function patterns usually need parentheses: function $NAME($ARGS)'); } if (pattern.includes('import') && !pattern.includes('from')) { result.warnings.push('JavaScript import patterns often need "from": import $WHAT from $WHERE'); } break; case 'java': // Check for common Java patterns if (pattern.includes('public') && !pattern.includes('(') && !pattern.includes('{')) { result.warnings.push('Java method patterns usually need parentheses and braces: public $TYPE $NAME($ARGS) { $$$ }'); } break; case 'python': // Check for common Python patterns that need body if (/\bclass\s+\$[A-Z_][A-Z0-9_]*\s*:(?![\s\S]*\$\$\$)/.test(pattern)) { result.errors.push('Python class pattern needs body. Use: class $NAME: $$$ or class $NAME($BASE): $$$'); result.valid = false; } if (/\bdef\s+\$[A-Z_][A-Z0-9_]*\s*\([^)]*\)\s*:(?![\s\S]*\$\$\$)/.test(pattern)) { result.errors.push('Python function pattern needs body. Use: def $NAME($ARGS): $$$'); result.valid = false; } if (/\bexcept\s+[^:]*:(?![\s\S]*\$\$\$)/.test(pattern)) { result.errors.push('Python except pattern needs body. Use: except $EXCEPTION as $VAR:\\n $$$'); result.valid = false; } if (/\btry\s*:(?![\s\S]*\$\$\$)/.test(pattern)) { result.errors.push('Python try pattern needs body. Use: try:\\n $$$'); result.valid = false; } if (/\bfinally\s*:(?![\s\S]*\$\$\$)/.test(pattern)) { result.errors.push('Python finally pattern needs body. Use: finally:\\n $$$'); result.valid = false; } // General Python colon check if (pattern.includes('def') && !pattern.includes(':')) { result.warnings.push('Python function patterns need colon: def $NAME($ARGS): $$$'); } break; default: // Unknown language - provide general guidance if (this.requiresLanguageHint(pattern)) { result.warnings.push(`Language "${language}" not specifically supported. Pattern may need adjustment for proper AST parsing.`); } } return result; } // Get default exclude patterns /** * Provide default exclude globs to avoid scanning generated content. */ getDefaultExcludes() { return [ 'node_modules/**', '.git/**', 'dist/**', 'build/**', 'coverage/**', '*.min.js', '*.bundle.js', '.next/**', '.vscode/**', '.idea/**' ]; } // Detect language from pattern content /** * Infer a probable language from a pattern when none is specified. */ detectLanguageFromPattern(pattern) { // JavaScript/TypeScript patterns if (pattern.match(/\b(function|const|let|var|import|export|class|interface|type)\b/)) { if (pattern.includes('interface') || pattern.includes('type ') || pattern.includes(': ')) { return 'typescript'; } return 'javascript'; } // Python patterns if (pattern.match(/\b(def|class|import|from|if __name__|print)\b/) || pattern.includes(':')) { return 'python'; } // Java patterns if (pattern.match(/\b(public|private|protected|static|class|interface|extends|implements)\b/)) { return 'java'; } // Rust patterns if (pattern.match(/\b(fn|impl|trait|struct|enum|use|mod)\b/)) { return 'rust'; } // Go patterns if (pattern.match(/\b(func|type|var|const|package|import)\b/)) { return 'go'; } // C/C++ patterns if (pattern.match(/\b(#include|struct|typedef|void|int|char|float|double)\b/)) { return 'cpp'; } return undefined; } // Enhanced error translation with context awareness translateAstGrepError(errorMessage, context) { // File not found errors if (errorMessage.includes('cannot find the file') || errorMessage.includes('No such file') || errorMessage.includes('system cannot find the file')) { return `File not found. Check that the path exists and is accessible from workspace root: ${this.workspaceRoot}. Tip: Use absolute paths or ensure files exist in the workspace.`; } // Permission denied errors if (errorMessage.includes('permission denied') || errorMessage.includes('access denied')) { return `Permission denied accessing file or directory. Ensure the MCP server has read access to the specified paths.`; } // Pattern syntax errors if (errorMessage.includes('pattern') && (errorMessage.includes('error') || errorMessage.includes('invalid'))) { const patternContext = context?.pattern ? ` Pattern: "${context.pattern}"` : ''; return `Invalid AST pattern syntax.${patternContext} Check metavariable usage: $VAR (single nodes), $$$ (multi-node). Ensure proper brackets and quotes.`; } // Language detection errors if (errorMessage.includes('language') && (errorMessage.includes('not supported') || errorMessage.includes('unknown'))) { const availableLanguages = ['javascript', 'typescript', 'python', 'java', 'rust', 'go', 'cpp', 'c', 'html', 'css']; return `Language detection failed. Specify language explicitly or check file extensions. Available languages: ${availableLanguages.join(', ')}`; } // Parsing errors if (errorMessage.includes('parse error') || errorMessage.includes('syntax error')) { const languageContext = context?.language ? ` for ${context.language}` : ''; return `Code parsing failed${languageContext}. Check if files contain valid syntax for the specified language. Some files may be corrupted or in binary format.`; } // Timeout errors if (errorMessage.includes('timeout') || errorMessage.includes('timed out')) { const pathContext = context?.paths ? ` Paths: ${Array.isArray(context.paths) ? context.paths.join(', ') : context.paths}` : ''; return `Operation timed out. Try: 1) Narrowing search paths, 2) Simplifying patterns, 3) Excluding large directories like node_modules.${pathContext}`; } // Empty results (not an error but needs better messaging) if (errorMessage.includes('no matches found') || errorMessage.includes('0 matches')) { const suggestions = [ 'Check if files contain the expected patterns', 'Verify language parameter matches file types', 'Try simpler patterns first', 'Use metavariables like $VAR for flexible matching' ]; return `No matches found. Suggestions: ${suggestions.join(', ')}`; } // Binary/executable errors if (errorMessage.includes('not found') && errorMessage.includes('ast-grep')) { return `ast-grep binary not found. Ensure binary is installed or use --auto-install flag. Check AST_GREP_BINARY_PATH environment variable.`; } // Resource exhaustion if (errorMessage.includes('too many') || errorMessage.includes('limit exceeded')) { return `Resource limits exceeded. Try: 1) Reducing search scope, 2) Using include/exclude patterns, 3) Increasing timeout limits.`; } // Network/download errors (for binary management) if (errorMessage.includes('download') || errorMessage.includes('network')) { return `Network error downloading ast-grep binary. Check internet connection or use --use-system flag to use system-installed ast-grep.`; } // Path traversal warnings if (errorMessage.includes('outside workspace') || errorMessage.includes('blocked path')) { return `Path outside workspace boundaries. For security, only paths within the workspace root are allowed: ${this.workspaceRoot}`; } // Return enhanced error with workspace context return `${errorMessage} (Workspace: ${this.workspaceRoot})`; } // Validate rule builder parameters /** * Validate rule builder inputs before generating or executing ast-grep rules. */ validateRuleBuilderParams(params) { const result = { valid: true, errors: [], warnings: [] }; if (!params || typeof params !== 'object') { result.valid = false; result.errors.push('Parameters must be an object'); return result; } if (!params.id || typeof params.id !== 'string' || params.id.trim().length === 0) { result.valid = false; result.errors.push('id is required'); } if (!params.language || typeof params.language !== 'string' || params.language.trim().length === 0) { result.valid = false; result.errors.push('language is required'); } if (!params.pattern || typeof params.pattern !== 'string' || params.pattern.trim().length === 0) { result.valid = false; result.errors.push('pattern is required'); } // Basic AST pattern validation const patternValidation = this.validateAstPattern(params.pattern, params.language); if (!patternValidation.valid) { result.valid = false; result.errors.push(...patternValidation.errors); result.warnings.push(...patternValidation.warnings); } result.sanitized = { id: String(params.id).trim(), language: String(params.language).trim(), pattern: String(params.pattern).trim(), message: params.message ? String(params.message) : undefined, severity: params.severity || 'warning', kind: params.kind ? String(params.kind) : undefined, insidePattern: params.insidePattern ? String(params.insidePattern) : undefined, hasPattern: params.hasPattern ? String(params.hasPattern) : undefined, notPattern: params.notPattern ? String(params.notPattern) : undefined, where: Array.isArray(params.where) ? params.where.map((w) => ({ metavariable: String(w.metavariable), regex: w.regex ? String(w.regex) : undefined, notRegex: w.notRegex ? String(w.notRegex) : undefined, equals: w.equals ? String(w.equals) : undefined, includes: w.includes ? String(w.includes) : undefined, })) : undefined, fix: params.fix ? String(params.fix) : undefined, }; return result; } } //# sourceMappingURL=validator.js.map