tree-ast-grep-mcp
Version:
Simple, direct ast-grep wrapper for AI coding agents. Zero abstractions, maximum performance.
458 lines • 21.3 kB
JavaScript
/**
* Validates ast-grep patterns and emits structured diagnostics for tool consumers.
*/
export class EnhancedPatternValidator {
workspaceRoot;
/**
* Persist workspace context for language specific validation.
*/
constructor(workspaceRoot) {
this.workspaceRoot = workspaceRoot;
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
validatePattern(pattern, language, context) {
const result = {
valid: true,
errors: [],
warnings: [],
diagnostics: {
patternType: this.classifyPattern(pattern),
metavariables: this.extractAndValidateMetavariables(pattern),
languageCompatibility: this.checkLanguageCompatibility(pattern, language),
complexity: this.assessPatternComplexity(pattern)
}
};
// Basic syntax validation
const syntaxValidation = this.validateSyntax(pattern);
if (!syntaxValidation.valid) {
result.valid = false;
result.errors.push(...syntaxValidation.errors);
result.warnings.push(...syntaxValidation.warnings);
}
// Metavariable validation (addresses QA Issue #1)
const metavarValidation = this.validateMetavariables(pattern, language);
if (!metavarValidation.valid) {
result.valid = false;
result.errors.push(...metavarValidation.errors);
result.warnings.push(...metavarValidation.warnings);
}
// Language-specific validation (addresses QA Issue #5)
if (language) {
const langValidation = this.validateLanguageSpecificPattern(pattern, language);
if (!langValidation.valid) {
result.valid = false;
result.errors.push(...langValidation.errors);
result.warnings.push(...langValidation.warnings);
}
}
// Pattern reliability assessment
const reliabilityAssessment = this.assessPatternReliability(pattern, language);
result.warnings.push(...reliabilityAssessment.warnings);
result.diagnostics = { ...result.diagnostics, ...reliabilityAssessment.diagnostics };
return result;
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
validateNestedPattern(primaryPattern, insidePattern, hasPattern, notPattern, language) {
const result = {
valid: true,
errors: [],
warnings: [],
diagnostics: {
complexity: 'nested',
patterns: {
primary: primaryPattern,
inside: insidePattern,
has: hasPattern,
not: notPattern
}
}
};
// Validate primary pattern
const primaryValidation = this.validatePattern(primaryPattern, language);
if (!primaryValidation.valid) {
result.valid = false;
result.errors.push(`Primary pattern invalid: ${primaryValidation.errors.join(', ')}`);
}
// Validate contextual patterns
if (insidePattern) {
const insideValidation = this.validatePattern(insidePattern, language);
if (!insideValidation.valid) {
result.valid = false;
result.errors.push(`Inside pattern invalid: ${insideValidation.errors.join(', ')}`);
}
// Check if inside pattern is suitable for nesting
if (!this.isValidContainerPattern(insidePattern)) {
result.warnings.push(`Inside pattern "${insidePattern}" may not be suitable as a container. Consider using patterns with block structures like "class $NAME { $$$ }" or "function $NAME() { $$$ }"`);
}
}
if (hasPattern) {
const hasValidation = this.validatePattern(hasPattern, language);
if (!hasValidation.valid) {
result.valid = false;
result.errors.push(`Has pattern invalid: ${hasValidation.errors.join(', ')}`);
}
}
if (notPattern) {
const notValidation = this.validatePattern(notPattern, language);
if (!notValidation.valid) {
result.valid = false;
result.errors.push(`Not pattern invalid: ${notValidation.errors.join(', ')}`);
}
}
// Check for pattern compatibility
const compatibilityCheck = this.checkPatternCompatibility(primaryPattern, insidePattern, hasPattern, notPattern);
if (!compatibilityCheck.valid) {
result.valid = false;
result.errors.push(...compatibilityCheck.errors);
}
result.warnings.push(...compatibilityCheck.warnings);
return result;
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
validateMetavariables(pattern, language) {
const result = { valid: true, errors: [], warnings: [] };
const metavars = this.extractAndValidateMetavariables(pattern);
// Check for problematic metavariable patterns identified by QA
if (metavars.problematic.length > 0) {
result.warnings.push(`Potentially unreliable metavariables detected: ${metavars.problematic.join(', ')}`);
// Specific guidance for $ARGS vs $$$ issue
if (metavars.problematic.some(mv => mv.includes('ARGS') || mv.includes('PARAMS'))) {
result.warnings.push(`QA GUIDANCE: Metavariable $ARGS may have low reliability (20% success rate). Consider using $$$ for function parameters instead, which has 80% success rate.`);
// Suggest alternative patterns
if (language === 'python' && pattern.includes('def')) {
result.warnings.push(`SUGGESTION: Use "def $FUNC_NAME($$$): $$$" instead of "def $FUNC_NAME($ARGS): $$$" for better reliability`);
}
if ((language === 'javascript' || language === 'typescript') && pattern.includes('function')) {
result.warnings.push(`SUGGESTION: Use "function $FUNC_NAME($$$) { $$$ }" instead of "function $FUNC_NAME($ARGS) { $$$ }" for better reliability`);
}
}
}
// Check for invalid metavariable syntax
const invalidMetavars = pattern.match(/\$[a-z][a-zA-Z0-9_]*/g);
if (invalidMetavars) {
result.valid = false;
result.errors.push(`Invalid metavariable syntax: ${invalidMetavars.join(', ')}. Metavariables must use UPPERCASE: $VAR, $NAME, $ARGS, etc.`);
}
// Check for incomplete multi-metavariables
const incompleteMulti = pattern.match(/\$\$(?!\$)[a-zA-Z_]/g);
if (incompleteMulti) {
result.valid = false;
result.errors.push(`Incomplete multi-metavariable syntax: ${incompleteMulti.join(', ')}. Use $$$ for multi-node matching.`);
}
return result;
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
extractAndValidateMetavariables(pattern) {
const single = [];
const multi = [];
const problematic = [];
const reliable = [];
// Extract single metavariables: $VAR, $_, $VAR1
const singleMatches = pattern.match(/\$[A-Z_][A-Z0-9_]*/g) || [];
for (const match of singleMatches) {
const varName = match.slice(1);
single.push(varName);
// Identify potentially problematic metavariables based on QA findings
if (['ARGS', 'PARAMS', 'ARGUMENTS'].includes(varName)) {
problematic.push(match);
}
else {
reliable.push(match);
}
}
// Extract multi-metavariables: $$$VAR, $$$
const multiMatches = pattern.match(/\$\$\$[A-Z_]*[A-Z0-9_]*/g) || [];
for (const match of multiMatches) {
const varName = match.slice(3) || 'BODY';
multi.push(varName);
reliable.push(match); // Multi-metavariables are generally more reliable
}
return { single, multi, problematic, reliable };
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
assessPatternReliability(pattern, language) {
const warnings = [];
const diagnostics = {
reliabilityScore: 100, // Start with 100%
issues: []
};
// QA Issue #1: Function argument patterns have low reliability
if (pattern.includes('$ARGS') || pattern.includes('$PARAMS')) {
diagnostics.reliabilityScore -= 60; // QA reported 20% success rate
diagnostics.issues.push('Function argument metavariables have low reliability');
warnings.push('Pattern contains $ARGS/$PARAMS which have low reliability. Consider using $$$ instead.');
}
// QA Issue #2: Nested patterns often fail
if (this.isNestedPattern(pattern)) {
diagnostics.reliabilityScore -= 40;
diagnostics.issues.push('Nested patterns have reliability issues');
warnings.push('Complex nested patterns may fail. Consider breaking into simpler components or using contextual rules.');
}
// QA Issue #4: Exception handling patterns often fail
if (this.isExceptionPattern(pattern)) {
diagnostics.reliabilityScore -= 30;
diagnostics.issues.push('Exception handling patterns have known issues');
warnings.push('Exception handling patterns may not match as expected. Verify with simple test cases.');
}
// Language-specific reliability (QA Issue #5)
if (language === 'javascript' && this.isFunctionPattern(pattern)) {
diagnostics.reliabilityScore -= 20; // QA reported 25% success rate for JS functions
diagnostics.issues.push('JavaScript function patterns have reduced reliability');
warnings.push('JavaScript function patterns have known reliability issues. Test thoroughly.');
}
diagnostics.reliabilityScore = Math.max(0, diagnostics.reliabilityScore);
return { warnings, diagnostics };
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
validateLanguageSpecificPattern(pattern, language) {
const result = { valid: true, errors: [], warnings: [] };
switch (language.toLowerCase()) {
case 'python':
return this.validatePythonPattern(pattern);
case 'javascript':
case 'typescript':
return this.validateJavaScriptPattern(pattern);
case 'java':
return this.validateJavaPattern(pattern);
default:
result.warnings.push(`Language-specific validation not available for "${language}". Pattern may need adjustment.`);
}
return result;
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
validatePythonPattern(pattern) {
const result = { valid: true, errors: [], warnings: [] };
// Function definition patterns
if (pattern.includes('def') && !pattern.includes(':')) {
result.errors.push('Python function patterns must include colon: def $NAME($ARGS): $$$');
}
// Class definition patterns
if (pattern.includes('class') && !pattern.includes(':')) {
result.errors.push('Python class patterns must include colon: class $NAME: $$$ or class $NAME($BASE): $$$');
}
// Exception handling patterns (addresses QA Issue #4)
if (pattern.includes('except') && !pattern.includes('try')) {
result.warnings.push('Exception patterns work better when including try block: try: $$$ except $EXCEPTION: $$$');
}
// Import patterns
if (pattern.includes('import') && pattern.includes('from') && !pattern.match(/from .+ import/)) {
result.warnings.push('Python import patterns should follow "from $MODULE import $ITEM" structure');
}
return result;
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
validateJavaScriptPattern(pattern) {
const result = { valid: true, errors: [], warnings: [] };
// Function patterns - QA reported low success rate
if (pattern.includes('function')) {
if (!pattern.includes('(') || !pattern.includes(')')) {
result.errors.push('JavaScript function patterns need parentheses: function $NAME($ARGS) { $$$ }');
}
// Specific guidance for QA Issue #5
result.warnings.push('QA WARNING: JavaScript function patterns have ~25% success rate. Test thoroughly and consider alternatives.');
if (pattern.includes('$ARGS')) {
result.warnings.push('RELIABILITY ISSUE: $ARGS in JavaScript functions may not match reliably. Consider using $$$ instead.');
}
}
// Class method patterns (addresses QA Issue #2)
if (pattern.includes('class') && pattern.includes('$')) {
if (!this.isValidJavaScriptClassPattern(pattern)) {
result.warnings.push('JavaScript class method patterns are complex. Consider using separate patterns for class and methods.');
}
}
// Arrow function patterns
if (pattern.includes('=>')) {
if (!pattern.match(/\$\w+\s*=.*=>/)) {
result.warnings.push('Arrow function patterns should follow: const $NAME = ($ARGS) => $BODY structure');
}
}
// Async function patterns
if (pattern.includes('async')) {
result.warnings.push('Async function patterns may need special handling. Test with both async/await and Promise syntax.');
}
return result;
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
validateJavaPattern(pattern) {
const result = { valid: true, errors: [], warnings: [] };
// Method patterns
if (pattern.includes('public') || pattern.includes('private') || pattern.includes('protected')) {
if (!pattern.includes('(') || !pattern.includes(')')) {
result.errors.push('Java method patterns need parentheses: public $TYPE $NAME($ARGS) { $$$ }');
}
}
// Class patterns
if (pattern.includes('class') && !pattern.includes('{')) {
result.warnings.push('Java class patterns usually need braces: class $NAME { $$$ }');
}
return result;
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
isValidContainerPattern(pattern) {
// Container patterns should have block structures
const containerIndicators = [
/class\s+\$\w+.*\{\s*\$\$\$/, // class $NAME { $$$ }
/function\s+\$\w+.*\{\s*\$\$\$/, // function $NAME() { $$$ }
/def\s+\$\w+.*:\s*\$\$\$/, // def $NAME(): $$$
/try\s*\{\s*\$\$\$/, // try { $$$ }
/if\s*\(.*\)\s*\{\s*\$\$\$/ // if (...) { $$$ }
];
return containerIndicators.some(indicator => indicator.test(pattern));
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
checkPatternCompatibility(primaryPattern, insidePattern, hasPattern, notPattern) {
const result = { valid: true, errors: [], warnings: [] };
// Check if patterns have compatible metavariables
const primaryMetavars = this.extractAndValidateMetavariables(primaryPattern);
if (insidePattern) {
const insideMetavars = this.extractAndValidateMetavariables(insidePattern);
// Check for metavariable conflicts
const conflicts = primaryMetavars.single.filter(mv => insideMetavars.single.includes(mv));
if (conflicts.length > 0) {
result.warnings.push(`Metavariable conflicts between primary and inside patterns: ${conflicts.join(', ')}`);
}
}
return result;
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
classifyPattern(pattern) {
if (this.isFunctionPattern(pattern))
return 'function';
if (this.isClassPattern(pattern))
return 'class';
if (this.isImportPattern(pattern))
return 'import';
if (this.isExceptionPattern(pattern))
return 'exception';
if (this.isVariablePattern(pattern))
return 'variable';
if (this.isNestedPattern(pattern))
return 'nested';
return 'simple';
}
/**
* Validate an ast-grep pattern and gather structured diagnostics for tooling.
*/
assessPatternComplexity(pattern) {
let score = 0;
// Count metavariables
const metavarCount = (pattern.match(/\$[A-Z_][A-Z0-9_]*/g) || []).length;
score += metavarCount;
// Multi-metavariables add complexity
const multiMetavarCount = (pattern.match(/\$\$\$/g) || []).length;
score += multiMetavarCount * 2;
// Nested structures add complexity
if (this.isNestedPattern(pattern))
score += 3;
// Keywords add some complexity
const keywords = ['class', 'function', 'def', 'try', 'catch', 'except', 'if', 'for', 'while'];
const keywordCount = keywords.filter(kw => pattern.includes(kw)).length;
score += keywordCount;
if (score <= 2)
return 'simple';
if (score <= 5)
return 'moderate';
if (score <= 10)
return 'complex';
return 'very_complex';
}
checkLanguageCompatibility(pattern, language) {
const compatibility = [];
if (!language) {
compatibility.push('Language not specified - pattern may not parse correctly');
return compatibility;
}
// Check for language-specific keywords
const pythonKeywords = ['def', 'class', 'import', 'from', 'try', 'except'];
const jsKeywords = ['function', 'class', 'const', 'let', 'var', 'import', 'export'];
const javaKeywords = ['public', 'private', 'protected', 'class', 'interface', 'static'];
const lang = language.toLowerCase();
if (lang === 'python') {
const hasJsKeywords = jsKeywords.some(kw => pattern.includes(kw) && !pythonKeywords.includes(kw));
if (hasJsKeywords) {
compatibility.push('Pattern contains JavaScript-specific keywords that may not work in Python');
}
}
else if (lang === 'javascript' || lang === 'typescript') {
const hasPythonKeywords = pythonKeywords.some(kw => pattern.includes(kw) && !jsKeywords.includes(kw));
if (hasPythonKeywords) {
compatibility.push('Pattern contains Python-specific keywords that may not work in JavaScript');
}
}
return compatibility;
}
validateSyntax(pattern) {
const result = { valid: true, errors: [], warnings: [] };
// Check for basic syntax issues
const brackets = { '(': ')', '[': ']', '{': '}' };
const stack = [];
for (const char of pattern) {
if (char in brackets) {
stack.push(brackets[char]);
}
else if (Object.values(brackets).includes(char)) {
if (stack.pop() !== char) {
result.valid = false;
result.errors.push('Pattern has unmatched brackets, parentheses, or braces');
break;
}
}
}
if (stack.length > 0) {
result.valid = false;
result.errors.push('Pattern has unclosed brackets, parentheses, or braces');
}
return result;
}
// Pattern type detection helpers
isFunctionPattern(pattern) {
return /\b(function|def|func)\b/.test(pattern);
}
isClassPattern(pattern) {
return /\bclass\b/.test(pattern);
}
isImportPattern(pattern) {
return /\b(import|from|#include)\b/.test(pattern);
}
isExceptionPattern(pattern) {
return /\b(try|catch|except|finally|throw)\b/.test(pattern);
}
isVariablePattern(pattern) {
return /\b(var|let|const|def|int|string)\b/.test(pattern);
}
isNestedPattern(pattern) {
// Simple heuristic for nested patterns
const blockCount = (pattern.match(/\{\s*\$\$\$/g) || []).length;
const metavarCount = (pattern.match(/\$[A-Z_]/g) || []).length;
return blockCount > 1 || (blockCount > 0 && metavarCount > 3);
}
isValidJavaScriptClassPattern(pattern) {
// Check if it's a well-formed JavaScript class pattern
return /class\s+\$\w+.*\{.*\}/.test(pattern.replace(/\s+/g, ' '));
}
}
//# sourceMappingURL=pattern-validator.js.map