UNPKG

meld

Version:

Meld: A template language for LLM prompts

563 lines (464 loc) 17.1 kB
#!/usr/bin/env node /** * Script to create specific test cases that highlight differences * between meld-ast 3.0.1 and 3.3.0 */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const { promisify } = require('util'); const writeFileAsync = promisify(fs.writeFile); const mkdirAsync = promisify(fs.mkdir); const COMPARISON_DIR = path.join(process.cwd(), 'meld-ast-comparison'); const CASES_DIR = path.join(COMPARISON_DIR, 'specific-cases'); // Define test cases that specifically target potential changes const TEST_CASES = [ { name: 'array-notation-simple', description: 'Tests simple array access notation', content: ` @data fruits = ["apple", "banana", "cherry"] Bracket notation: {{fruits[0]}}, {{fruits[1]}}, {{fruits[2]}} ` }, { name: 'array-notation-nested', description: 'Tests nested array access notation', content: ` @data users = [ { name: "Alice", hobbies: ["reading", "hiking"] }, { name: "Bob", hobbies: ["gaming", "cooking"] } ] User 1: {{users[0].name}} - {{users[0].hobbies[0]}} User 2: {{users[1].name}} - {{users[1].hobbies[1]}} ` }, { name: 'array-variable-index', description: 'Tests array access with variable index', content: ` @data fruits = ["apple", "banana", "cherry"] @data index = 1 Using variable index: {{fruits[index]}} ` }, { name: 'array-variable-expression', description: 'Tests array access with expression in index', content: ` @data fruits = ["apple", "banana", "cherry"] @data offset = 1 Using expression index: {{fruits[1 + offset]}} ` }, { name: 'array-string-keys', description: 'Tests objects with string keys that look like array indices', content: ` @data obj = { "0": "zero", "1": "one", "2": "two" } Accessing object with numeric keys: {{obj["0"]}}, {{obj["1"]}} ` }, { name: 'complex-expressions', description: 'Tests complex expressions with array access', content: ` @data nested = { items: [ { values: [10, 20, 30] }, { values: [40, 50, 60] } ] } Nested complex: {{nested.items[0].values[2]}} Math expression: {{nested.items[1].values[2 - 1]}} ` }, { name: 'multi-dimensional-arrays', description: 'Tests multi-dimensional array access', content: ` @data matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] Matrix access: {{matrix[0][0]}}, {{matrix[1][1]}}, {{matrix[2][2]}} ` }, { name: 'bracket-notation-properties', description: 'Tests bracket notation for property access', content: ` @data user = { "first name": "John", "last-name": "Doe", "age": 30 } Bracket property access: {{user["first name"]}}, {{user["last-name"]}} ` }, { name: 'mixed-notation', description: 'Tests mixed dot and bracket notation', content: ` @data users = [ { name: "Alice", "contact info": { email: "alice@example.com", phones: ["123-456-7890", "098-765-4321"] } } ] Mixed notation: {{users[0].name}}, {{users[0]["contact info"].email}}, {{users[0]["contact info"].phones[1]}} ` } ]; async function run() { try { // Ensure directories exist await ensureDirectoryExists(CASES_DIR); await ensureDirectoryExists(path.join(CASES_DIR, '3.0.1')); await ensureDirectoryExists(path.join(CASES_DIR, '3.3.0')); // Create test case documentation let documentation = `# Specific Test Cases\n\n`; documentation += `This directory contains test cases designed to highlight specific differences between meld-ast 3.0.1 and 3.3.0.\n\n`; // Create each test case for (const testCase of TEST_CASES) { console.log(`Creating test case: ${testCase.name}`); // Write test case file const testFilePath = path.join(CASES_DIR, `${testCase.name}.meld`); await writeFileAsync(testFilePath, testCase.content); // Add to documentation documentation += `## ${testCase.name}\n\n`; documentation += `${testCase.description}\n\n`; documentation += "```meld\n" + testCase.content + "\n```\n\n"; } // Save documentation await writeFileAsync(path.join(CASES_DIR, 'README.md'), documentation); // Create AST comparison script await createASTComparisonScript(); console.log('Test cases created successfully. You can now run the AST comparison script to analyze differences.'); } catch (error) { console.error('Error creating test cases:', error); process.exit(1); } } async function ensureDirectoryExists(directory) { try { await mkdirAsync(directory, { recursive: true }); } catch (err) { if (err.code !== 'EEXIST') throw err; } } async function createASTComparisonScript() { const scriptContent = `#!/usr/bin/env node /** * Script to compare AST outputs between meld-ast versions * for the specific test cases */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const { promisify } = require('util'); const writeFileAsync = promisify(fs.writeFile); const readFileAsync = promisify(fs.readFile); const readDirAsync = promisify(fs.readdir); const COMPARISON_DIR = path.join(process.cwd(), 'meld-ast-comparison'); const CASES_DIR = path.join(COMPARISON_DIR, 'specific-cases'); async function run() { try { // Get all test case files const files = await readDirAsync(CASES_DIR); const testFiles = files.filter(file => file.endsWith('.meld')); // Process each version for (const version of ['3.0.1', '3.3.0']) { console.log(\`\\n=== Processing meld-ast \${version} ===\`); try { // Install the specific version console.log(\`Installing meld-ast@\${version}...\`); execSync('rm -rf node_modules', { stdio: 'inherit' }); execSync('npm install', { stdio: 'inherit' }); execSync(\`npm install meld-ast@\${version}\`, { stdio: 'inherit' }); // Process each test file for (const testFile of testFiles) { const baseName = path.basename(testFile, '.meld'); console.log(\`Processing \${baseName}...\`); const content = await readFileAsync(path.join(CASES_DIR, testFile), 'utf8'); // Create AST generator script with properly escaped content const astScriptContent = \` const { parse } = require('meld-ast'); async function main() { const content = \\\`\${content.replace(/\\\`/g, '\\\\\\\`')}\\\`; const result = await parse(content, { preserveCodeFences: true, failFast: false, trackLocations: true, validateNodes: true }); console.log(JSON.stringify(result, null, 2)); } main().catch(console.error); \`; const scriptPath = path.join(CASES_DIR, version, \`\${baseName}-ast-generator.js\`); await writeFileAsync(scriptPath, astScriptContent); // Generate AST output try { const astOutput = execSync(\`node \${scriptPath}\`, { encoding: 'utf8' }); await writeFileAsync(path.join(CASES_DIR, version, \`\${baseName}-ast.json\`), astOutput); // Create a human-readable report highlighting important parts const ast = JSON.parse(astOutput); const summary = generateASTSummary(ast, baseName); await writeFileAsync(path.join(CASES_DIR, version, \`\${baseName}-summary.md\`), summary); } catch (error) { await writeFileAsync( path.join(CASES_DIR, version, \`\${baseName}-error.log\`), error.toString() ); } } } catch (error) { console.error(\`Error processing version \${version}:\`, error); } } // Generate comparison report await generateComparisonReport(testFiles); } catch (error) { console.error('Error running AST comparison:', error); process.exit(1); } } function generateASTSummary(ast, testName) { let summary = \`# AST Summary for \${testName}\n\n\`; // Ensure we have an AST to work with if (!ast || !ast.ast || !Array.isArray(ast.ast)) { return summary + 'Invalid or empty AST structure.\\n'; } // Add overall stats summary += \`## Overall Structure\n\n\`; summary += \`- Total nodes: \${countNodes(ast.ast)}\n\`; summary += \`- Top-level nodes: \${ast.ast.length}\n\`; // Extract and summarize interesting parts based on the test name if (testName.includes('array-notation')) { // Find interpolation expressions const interpolations = findNodesOfType(ast.ast, 'Interpolation'); summary += \`\n## Interpolation Expressions (\${interpolations.length})\n\n\`; interpolations.forEach((node, index) => { summary += \`### Interpolation #\${index + 1}\n\n\`; if (node.expression) { summary += \`Expression type: \${node.expression.type}\n\n\`; summary += \`\`\`json\n\${JSON.stringify(node.expression, null, 2)}\n\`\`\`\n\n\`; } else { summary += \`No expression found.\n\n\`; } }); } else if (testName.includes('bracket-notation')) { // Find member expressions const members = findNodesByPredicate(ast.ast, node => node.type === 'MemberExpression' || node.type === 'ComputedMemberExpression' ); summary += \`\n## Member Expressions (\${members.length})\n\n\`; members.forEach((node, index) => { summary += \`### Member Expression #\${index + 1}\n\n\`; summary += \`Type: \${node.type}\n\`; if (node.property) { summary += \`Property: \${JSON.stringify(node.property)}\n\`; } summary += \`\n\`\`\`json\n\${JSON.stringify(node, null, 2)}\n\`\`\`\n\n\`; }); } else { // General summary for other test cases const nodeTypes = countNodeTypes(ast.ast); summary += \`\n## Node Types\n\n\`; Object.entries(nodeTypes).forEach(([type, count]) => { summary += \`- \${type}: \${count}\n\`; }); } return summary; } function countNodes(nodes) { if (!Array.isArray(nodes)) return 0; let count = nodes.length; for (const node of nodes) { if (node && typeof node === 'object') { for (const key in node) { if (Array.isArray(node[key])) { count += countNodes(node[key]); } else if (node[key] && typeof node[key] === 'object') { count += 1 + countNodes(Object.values(node[key]).filter(v => Array.isArray(v))); } } } } return count; } function countNodeTypes(nodes) { const types = {}; function traverse(node) { if (!node || typeof node !== 'object') return; if (node.type) { types[node.type] = (types[node.type] || 0) + 1; } for (const key in node) { if (Array.isArray(node[key])) { node[key].forEach(traverse); } else if (node[key] && typeof node[key] === 'object') { traverse(node[key]); } } } nodes.forEach(traverse); return types; } function findNodesOfType(nodes, targetType) { const results = []; function traverse(node) { if (!node || typeof node !== 'object') return; if (node.type === targetType) { results.push(node); } for (const key in node) { if (Array.isArray(node[key])) { node[key].forEach(traverse); } else if (node[key] && typeof node[key] === 'object') { traverse(node[key]); } } } nodes.forEach(traverse); return results; } function findNodesByPredicate(nodes, predicate) { const results = []; function traverse(node) { if (!node || typeof node !== 'object') return; if (predicate(node)) { results.push(node); } for (const key in node) { if (Array.isArray(node[key])) { node[key].forEach(traverse); } else if (node[key] && typeof node[key] === 'object') { traverse(node[key]); } } } nodes.forEach(traverse); return results; } async function generateComparisonReport(testFiles) { let report = \`# AST Comparison Report\n\n\`; report += \`Comparison of meld-ast 3.0.1 vs 3.3.0 for specific test cases\n\n\`; // Process each test case for (const testFile of testFiles) { const baseName = path.basename(testFile, '.meld'); report += \`## \${baseName}\n\n\`; // Get ASTs for both versions const ast301Path = path.join(CASES_DIR, '3.0.1', \`\${baseName}-ast.json\`); const ast330Path = path.join(CASES_DIR, '3.3.0', \`\${baseName}-ast.json\`); if (!fs.existsSync(ast301Path) || !fs.existsSync(ast330Path)) { report += \`Could not compare ASTs - one or both files missing.\n\n\`; continue; } try { const ast301 = JSON.parse(await readFileAsync(ast301Path, 'utf8')); const ast330 = JSON.parse(await readFileAsync(ast330Path, 'utf8')); // Find key differences in the AST structure const differences = findKeyDifferences(ast301, ast330); if (differences.length > 0) { report += \`### Key Differences\n\n\`; differences.forEach(diff => { report += \`- \${diff}\n\`; }); } else { report += \`No significant structural differences detected.\n\`; } // Specifically highlight array notation changes const arrayNotationChanges = findArrayNotationChanges(ast301, ast330); if (arrayNotationChanges.length > 0) { report += \`\n### Array Notation Changes\n\n\`; arrayNotationChanges.forEach(change => { report += \`- \${change}\n\`; }); } } catch (error) { report += \`Error comparing ASTs: \${error.message}\n\`; } report += \`\n---\n\n\`; } await writeFileAsync(path.join(CASES_DIR, 'comparison-report.md'), report); } function findKeyDifferences(ast1, ast2) { const differences = []; // Compare top-level structure if (!ast1.ast || !ast2.ast) { differences.push('One or both ASTs is missing the ast field'); return differences; } if (ast1.ast.length !== ast2.ast.length) { differences.push(\`Number of top-level nodes differs: \${ast1.ast.length} vs \${ast2.ast.length}\`); } // Compare node types const types1 = countNodeTypes(ast1.ast); const types2 = countNodeTypes(ast2.ast); const allTypes = new Set([...Object.keys(types1), ...Object.keys(types2)]); for (const type of allTypes) { const count1 = types1[type] || 0; const count2 = types2[type] || 0; if (count1 !== count2) { differences.push(\`Node type '\${type}' count differs: \${count1} vs \${count2}\`); } } // Compare key node structures based on node type const interpolations1 = findNodesOfType(ast1.ast, 'Interpolation'); const interpolations2 = findNodesOfType(ast2.ast, 'Interpolation'); if (interpolations1.length === interpolations2.length) { // Compare interpolation expressions for (let i = 0; i < interpolations1.length; i++) { const expr1 = interpolations1[i].expression; const expr2 = interpolations2[i].expression; if (expr1 && expr2 && expr1.type !== expr2.type) { differences.push(\`Interpolation #\${i+1} expression type changed: \${expr1.type} → \${expr2.type}\`); } } } return differences; } function findArrayNotationChanges(ast1, ast2) { const changes = []; // Find array accesses in both ASTs const arrayAccesses1 = findNodesByPredicate(ast1.ast, node => node.type === 'ComputedMemberExpression' || (node.type === 'MemberExpression' && node.computed === true) ); const arrayAccesses2 = findNodesByPredicate(ast2.ast, node => node.type === 'ComputedMemberExpression' || (node.type === 'MemberExpression' && node.computed === true) || (node.type === 'MemberExpression' && typeof node.property === 'object' && node.property.type === 'Literal' && !isNaN(parseInt(node.property.value))) ); // Check if there are different numbers of array accesses if (arrayAccesses1.length !== arrayAccesses2.length) { changes.push(\`Number of array access expressions differs: \${arrayAccesses1.length} vs \${arrayAccesses2.length}\`); } // Check for type changes in corresponding positions const minLength = Math.min(arrayAccesses1.length, arrayAccesses2.length); for (let i = 0; i < minLength; i++) { const access1 = arrayAccesses1[i]; const access2 = arrayAccesses2[i]; if (access1.type !== access2.type) { changes.push(\`Array access #\${i+1} changed from \${access1.type} to \${access2.type}\`); } // Check property access style if (access1.property && access2.property && access1.property.type !== access2.property.type) { changes.push(\`Array access #\${i+1} property type changed from \${access1.property.type} to \${access2.property.type}\`); } } return changes; } // Run the script run().catch(console.error);