fix: address validation improvements from AI agent feedback

- Filter out @version and internal properties from warnings
- Deduplicate validation errors (keep most specific message)
- Add basic code syntax validation for JavaScript and Python
- Check for unbalanced braces/parentheses
- Validate Python indentation consistency
- Add n8n-specific pattern checks (return statements, input access)
- Bump version to 2.4.2

Based on comprehensive AI agent review showing 9.5/10 rating

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-06-24 09:55:09 +02:00
parent e7b6eace85
commit 46e00f0709
4 changed files with 270 additions and 2 deletions

View File

@@ -385,8 +385,10 @@ export class ConfigValidator {
message: 'Code cannot be empty',
fix: 'Add your code logic'
});
return;
}
// Security checks
if (code?.includes('eval(') || code?.includes('exec(')) {
warnings.push({
type: 'security',
@@ -394,13 +396,23 @@ export class ConfigValidator {
suggestion: 'Avoid using eval/exec with untrusted input'
});
}
// Basic syntax validation
if (config.language === 'python') {
this.validatePythonSyntax(code, errors, warnings);
} else {
this.validateJavaScriptSyntax(code, errors, warnings);
}
// n8n-specific patterns
this.validateN8nCodePatterns(code, config.language || 'javascript', warnings);
}
/**
* Check for common configuration issues
*/
private static checkCommonIssues(
nodeType: string,
_nodeType: string,
config: Record<string, any>,
properties: any[],
warnings: ValidationWarning[],
@@ -411,6 +423,11 @@ export class ConfigValidator {
const configuredKeys = Object.keys(config);
for (const key of configuredKeys) {
// Skip internal properties that are always present
if (key === '@version' || key.startsWith('_')) {
continue;
}
if (!visibleProps.find(p => p.name === key)) {
warnings.push({
type: 'inefficient',
@@ -464,4 +481,129 @@ export class ConfigValidator {
}
}
}
/**
* Basic JavaScript syntax validation
*/
private static validateJavaScriptSyntax(
code: string,
errors: ValidationError[],
warnings: ValidationWarning[]
): void {
// Check for common syntax errors
const openBraces = (code.match(/\{/g) || []).length;
const closeBraces = (code.match(/\}/g) || []).length;
if (openBraces !== closeBraces) {
errors.push({
type: 'invalid_value',
property: 'jsCode',
message: 'Unbalanced braces detected',
fix: 'Check that all { have matching }'
});
}
const openParens = (code.match(/\(/g) || []).length;
const closeParens = (code.match(/\)/g) || []).length;
if (openParens !== closeParens) {
errors.push({
type: 'invalid_value',
property: 'jsCode',
message: 'Unbalanced parentheses detected',
fix: 'Check that all ( have matching )'
});
}
// Check for unterminated strings
const stringMatches = code.match(/(["'`])(?:(?=(\\?))\2.)*?\1/g) || [];
const quotesInStrings = stringMatches.join('').match(/["'`]/g)?.length || 0;
const totalQuotes = (code.match(/["'`]/g) || []).length;
if ((totalQuotes - quotesInStrings) % 2 !== 0) {
warnings.push({
type: 'inefficient',
message: 'Possible unterminated string detected',
suggestion: 'Check that all strings are properly closed'
});
}
}
/**
* Basic Python syntax validation
*/
private static validatePythonSyntax(
code: string,
errors: ValidationError[],
warnings: ValidationWarning[]
): void {
// Check indentation consistency
const lines = code.split('\n');
const indentTypes = new Set<string>();
lines.forEach(line => {
const indent = line.match(/^(\s+)/);
if (indent) {
if (indent[1].includes('\t')) indentTypes.add('tabs');
if (indent[1].includes(' ')) indentTypes.add('spaces');
}
});
if (indentTypes.size > 1) {
errors.push({
type: 'invalid_value',
property: 'pythonCode',
message: 'Mixed tabs and spaces in indentation',
fix: 'Use either tabs or spaces consistently, not both'
});
}
// Check for colons after control structures
const controlStructures = /^\s*(if|elif|else|for|while|def|class|try|except|finally|with)\s+.*[^:]\s*$/gm;
if (controlStructures.test(code)) {
warnings.push({
type: 'inefficient',
message: 'Missing colon after control structure',
suggestion: 'Add : at the end of if/for/def/class statements'
});
}
}
/**
* Validate n8n-specific code patterns
*/
private static validateN8nCodePatterns(
code: string,
language: string,
warnings: ValidationWarning[]
): void {
// Check for return statement
const hasReturn = language === 'python'
? /return\s+/.test(code)
: /return\s+/.test(code);
if (!hasReturn) {
warnings.push({
type: 'missing_common',
message: 'No return statement found',
suggestion: 'Code node should return data for the next node. Add: return items (Python) or return items; (JavaScript)'
});
}
// Check for common n8n patterns
if (language === 'javascript') {
if (!code.includes('items') && !code.includes('$input')) {
warnings.push({
type: 'missing_common',
message: 'Code doesn\'t reference input items',
suggestion: 'Access input data with: items or $input.all()'
});
}
} else if (language === 'python') {
if (!code.includes('items') && !code.includes('_input')) {
warnings.push({
type: 'missing_common',
message: 'Code doesn\'t reference input items',
suggestion: 'Access input data with: items variable'
});
}
}
}
}

View File

@@ -68,6 +68,9 @@ export class EnhancedConfigValidator extends ConfigValidator {
// Add operation-specific enhancements
this.addOperationSpecificEnhancements(nodeType, config, enhancedResult);
// Deduplicate errors
enhancedResult.errors = this.deduplicateErrors(enhancedResult.errors);
// Add examples from ExampleGenerator if there are errors
if (enhancedResult.errors.length > 0) {
this.addExamplesFromGenerator(nodeType, enhancedResult);
@@ -202,6 +205,10 @@ export class EnhancedConfigValidator extends ConfigValidator {
this.enhanceHttpRequestValidation(config, result);
break;
case 'nodes-base.code':
// Code node uses base validation which includes syntax checks
break;
case 'nodes-base.openAi':
NodeSpecificValidators.validateOpenAI(context);
break;
@@ -407,4 +414,31 @@ export class EnhancedConfigValidator extends ConfigValidator {
});
}
}
/**
* Deduplicate errors based on property and type
* Prefers more specific error messages over generic ones
*/
private static deduplicateErrors(errors: ValidationError[]): ValidationError[] {
const seen = new Map<string, ValidationError>();
for (const error of errors) {
const key = `${error.property}-${error.type}`;
const existing = seen.get(key);
if (!existing) {
seen.set(key, error);
} else {
// Keep the error with more specific message or fix
const existingLength = (existing.message?.length || 0) + (existing.fix?.length || 0);
const newLength = (error.message?.length || 0) + (error.fix?.length || 0);
if (newLength > existingLength) {
seen.set(key, error);
}
}
}
return Array.from(seen.values());
}
}