200 lines
6.4 KiB
JavaScript
200 lines
6.4 KiB
JavaScript
/**
|
|
* On File Change Hook
|
|
*
|
|
* This hook runs when files are modified during a Claude Code session. (Claude Code modifies files)
|
|
* Use it to automatically run checks, update related files, or provide feedback.
|
|
*/
|
|
|
|
const { execSync } = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
module.exports = async (context) => {
|
|
const { files, operation } = context;
|
|
|
|
const notifications = [];
|
|
const warnings = [];
|
|
const errors = [];
|
|
|
|
// Process each changed file
|
|
for (const file of files) {
|
|
const { filePath, changeType } = file;
|
|
|
|
// Get file extension
|
|
const ext = path.extname(filePath);
|
|
|
|
// Run language-specific checks
|
|
try {
|
|
// JavaScript/TypeScript files
|
|
if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
|
|
await checkJavaScript(filePath, notifications, warnings, errors);
|
|
}
|
|
|
|
// Python files
|
|
if (['.py'].includes(ext)) {
|
|
await checkPython(filePath, notifications, warnings, errors);
|
|
}
|
|
|
|
// Test files
|
|
if (isTestFile(filePath)) {
|
|
notifications.push(`✅ Test file ${path.basename(filePath)} modified`);
|
|
}
|
|
|
|
// Check if corresponding test file exists
|
|
if (!isTestFile(filePath) && isSourceFile(filePath)) {
|
|
const testFilePath = getTestFilePath(filePath);
|
|
if (!fs.existsSync(testFilePath)) {
|
|
warnings.push(`⚠️ No test file found for ${path.basename(filePath)}. Consider creating ${path.basename(testFilePath)}`);
|
|
}
|
|
}
|
|
|
|
// Check file size
|
|
if (fs.existsSync(filePath)) {
|
|
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').length;
|
|
|
|
if (lines > 600) {
|
|
warnings.push(`⚠️ ${path.basename(filePath)} is ${lines} lines. Consider breaking into smaller modules.`);
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`Error processing ${filePath}:`, error.message);
|
|
}
|
|
}
|
|
|
|
// Log changes
|
|
console.error(JSON.stringify({
|
|
timestamp: new Date().toISOString(),
|
|
operation,
|
|
filesChanged: files.length,
|
|
notifications: notifications.length,
|
|
warnings: warnings.length,
|
|
errors: errors.length,
|
|
}));
|
|
|
|
// Return feedback to user
|
|
return {
|
|
notifications: notifications.length > 0 ? notifications : undefined,
|
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
errors: errors.length > 0 ? errors : undefined,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Check JavaScript/TypeScript files
|
|
*/
|
|
async function checkJavaScript(filePath, notifications, warnings, errors) {
|
|
if (!fs.existsSync(filePath)) return;
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
|
|
// Check for console.log statements
|
|
const consoleLogs = (content.match(/console\.log\(/g) || []).length;
|
|
if (consoleLogs > 0) {
|
|
warnings.push(`⚠️ Found ${consoleLogs} console.log statement(s) in ${path.basename(filePath)}`);
|
|
}
|
|
|
|
// Check for TODO/FIXME
|
|
const todos = (content.match(/\/\/\s*(TODO|FIXME)/g) || []).length;
|
|
if (todos > 0) {
|
|
notifications.push(`📝 Found ${todos} TODO/FIXME comment(s) in ${path.basename(filePath)}`);
|
|
}
|
|
|
|
// Try to run ESLint if available
|
|
try {
|
|
execSync(`npx eslint --quiet "${filePath}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
notifications.push(`✅ ESLint: ${path.basename(filePath)} passed`);
|
|
} catch (error) {
|
|
// ESLint found issues or not installed
|
|
if (error.stdout && error.stdout.length > 0) {
|
|
const issueCount = (error.stdout.match(/error/g) || []).length;
|
|
if (issueCount > 0) {
|
|
warnings.push(`⚠️ ESLint found ${issueCount} issue(s) in ${path.basename(filePath)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for long functions (basic heuristic)
|
|
const functions = content.match(/function\s+\w+\s*\([^)]*\)\s*\{/g) || [];
|
|
for (const func of functions) {
|
|
const funcStart = content.indexOf(func);
|
|
const funcBody = content.slice(funcStart);
|
|
const lines = funcBody.split('\n').slice(0, 100).join('\n');
|
|
const braceCount = (lines.match(/\{/g) || []).length - (lines.match(/\}/g) || []).length;
|
|
|
|
if (lines.split('\n').length > 100 && braceCount === 0) {
|
|
warnings.push(`Potentially long function detected in ${path.basename(filePath)}`);
|
|
break; // Only warn once per file
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check Python files
|
|
*/
|
|
async function checkPython(filePath, notifications, warnings, errors) {
|
|
if (!fs.existsSync(filePath)) return;
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
|
|
// Check for print statements (potential debug code)
|
|
const prints = (content.match(/print\(/g) || []).length;
|
|
if (prints > 0) {
|
|
warnings.push(`Found ${prints} print statement(s) in ${path.basename(filePath)}`);
|
|
}
|
|
|
|
// Try to run pylint if available
|
|
try {
|
|
execSync(`pylint --errors-only "${filePath}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
notifications.push(`✅ Pylint: ${path.basename(filePath)} passed`);
|
|
} catch (error) {
|
|
// Pylint found issues or not installed
|
|
if (error.stdout && error.stdout.length > 0) {
|
|
warnings.push(`Pylint found issues in ${path.basename(filePath)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if file is a test file
|
|
*/
|
|
function isTestFile(filePath) {
|
|
const fileName = path.basename(filePath).toLowerCase();
|
|
return fileName.includes('.test.') ||
|
|
fileName.includes('.spec.') ||
|
|
fileName.includes('_test.') ||
|
|
fileName.startsWith('test_') ||
|
|
filePath.includes('/tests/') ||
|
|
filePath.includes('/test/') ||
|
|
filePath.includes('/__tests__/');
|
|
}
|
|
|
|
/**
|
|
* Check if file is a source file (not config, docs, etc.)
|
|
*/
|
|
function isSourceFile(filePath) {
|
|
const ext = path.extname(filePath);
|
|
const sourceExts = ['.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.go', '.rs', '.rb', '.php'];
|
|
return sourceExts.includes(ext) && !isTestFile(filePath);
|
|
}
|
|
|
|
/**
|
|
* Get the expected test file path for a source file
|
|
*/
|
|
function getTestFilePath(filePath) {
|
|
const ext = path.extname(filePath);
|
|
const baseName = path.basename(filePath, ext);
|
|
const dirName = path.dirname(filePath);
|
|
|
|
// Try common test file patterns
|
|
const patterns = [
|
|
path.join(dirName, `${baseName}.test${ext}`),
|
|
path.join(dirName, `${baseName}.spec${ext}`),
|
|
path.join(dirName, '__tests__', `${baseName}.test${ext}`),
|
|
path.join(dirName, '..', 'tests', `${baseName}.test${ext}`),
|
|
];
|
|
|
|
// Return first pattern (most common)
|
|
return patterns[0];
|
|
}
|