feat: Add handler validation script to prevent undefined function errors
- Create validate-handlers.js script to check for undefined event handlers - Add npm run validate:handlers command - Add prebuild hook to run validation before builds - Add ESLint no-undef rule to catch undefined references - Add documentation in scripts/README-validation.md Prevents issues like 'ReferenceError: handleSaveEditGuestbook is not defined' by validating all onClick/onChange/onSubmit handlers are defined before use. The script: - Scans all React components for event handlers - Verifies functions are defined in component scope - Excludes props and imported functions - Runs automatically before builds - Can be run manually: npm run validate:handlers
This commit is contained in:
@@ -13,8 +13,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-require-imports": "off",
|
"@typescript-eslint/no-require-imports": "off",
|
||||||
|
"@typescript-eslint/no-undef": "error",
|
||||||
"react-hooks/exhaustive-deps": "warn",
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
"react/no-unescaped-entities": "off",
|
"react/no-unescaped-entities": "off",
|
||||||
|
"no-undef": "error",
|
||||||
"no-console": [
|
"no-console": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,7 +35,9 @@
|
|||||||
"test:redis": "tsx scripts/test-redis.ts",
|
"test:redis": "tsx scripts/test-redis.ts",
|
||||||
"redis:populate": "tsx scripts/populate-redis-test.ts",
|
"redis:populate": "tsx scripts/populate-redis-test.ts",
|
||||||
"redis:populate-app": "tsx scripts/populate-app-redis.ts",
|
"redis:populate-app": "tsx scripts/populate-app-redis.ts",
|
||||||
"redis:cleanup": "tsx scripts/cleanup-redis-test.ts"
|
"redis:cleanup": "tsx scripts/cleanup-redis-test.ts",
|
||||||
|
"validate:handlers": "node scripts/validate-handlers.js",
|
||||||
|
"prebuild": "npm run validate:handlers"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"schema": "prisma/schema.prisma"
|
"schema": "prisma/schema.prisma"
|
||||||
|
|||||||
92
scripts/README-validation.md
Normal file
92
scripts/README-validation.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Handler Validation Script
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `validate-handlers.js` script checks React components to ensure all event handler functions (onClick, onChange, onSubmit, etc.) are properly defined before being used.
|
||||||
|
|
||||||
|
## Why This Exists
|
||||||
|
|
||||||
|
This prevents runtime errors like `ReferenceError: handleSaveEditGuestbook is not defined` that occur when:
|
||||||
|
- A handler function is referenced in JSX but not defined in the component
|
||||||
|
- A function is accidentally deleted but still referenced
|
||||||
|
- A typo in function name causes undefined reference
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Manual Check
|
||||||
|
```bash
|
||||||
|
npm run validate:handlers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Check
|
||||||
|
The validation runs automatically before builds via the `prebuild` hook in `package.json`.
|
||||||
|
|
||||||
|
### CI/CD Integration
|
||||||
|
Add to your CI pipeline:
|
||||||
|
```yaml
|
||||||
|
- name: Validate handlers
|
||||||
|
run: npm run validate:handlers
|
||||||
|
```
|
||||||
|
|
||||||
|
## What It Checks
|
||||||
|
|
||||||
|
1. **Event Handlers**: Finds all `onClick`, `onChange`, `onSubmit` handlers
|
||||||
|
2. **Function Definitions**: Verifies functions are defined as:
|
||||||
|
- `const handleX = ...`
|
||||||
|
- `function handleX()`
|
||||||
|
- `const handleX = useCallback(...)`
|
||||||
|
- `const handleX = useMemo(...)`
|
||||||
|
3. **Props**: Excludes functions passed as props (handled by TypeScript)
|
||||||
|
4. **Imports**: Excludes functions imported from other modules
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
### Success
|
||||||
|
```
|
||||||
|
🔍 Validating handler functions...
|
||||||
|
✅ All handler functions are properly defined!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
```
|
||||||
|
🔍 Validating handler functions...
|
||||||
|
|
||||||
|
❌ src/components/features/admin/admin-dashboard.tsx
|
||||||
|
⚠️ Handler "handleSaveEditGuestbook" is referenced but not defined
|
||||||
|
|
||||||
|
❌ Validation failed: Found undefined handler functions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding New Handlers
|
||||||
|
|
||||||
|
When adding a new event handler:
|
||||||
|
|
||||||
|
1. **Define the function first**:
|
||||||
|
```typescript
|
||||||
|
const handleNewAction = async () => {
|
||||||
|
// implementation
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Then use it in JSX**:
|
||||||
|
```tsx
|
||||||
|
<Button onClick={handleNewAction}>Click me</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run validation**:
|
||||||
|
```bash
|
||||||
|
npm run validate:handlers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Cannot detect handlers in dynamically generated JSX
|
||||||
|
- May have false positives for complex prop patterns
|
||||||
|
- Does not check TypeScript types (use `npm run typecheck` for that)
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always define handlers before using them**
|
||||||
|
2. **Use TypeScript for type safety** (`npm run typecheck`)
|
||||||
|
3. **Run validation before committing** (`npm run validate:handlers`)
|
||||||
|
4. **Keep handlers close to where they're used** (same component or nearby)
|
||||||
148
scripts/validate-handlers.js
Executable file
148
scripts/validate-handlers.js
Executable file
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation script to check for undefined handler functions
|
||||||
|
* This script scans React components for onClick handlers and verifies
|
||||||
|
* that the referenced functions are defined in the component scope.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
const COMPONENT_DIR = path.join(__dirname, '../src/components');
|
||||||
|
const API_DIR = path.join(__dirname, '../src/app');
|
||||||
|
|
||||||
|
// Patterns to match handler references
|
||||||
|
const HANDLER_PATTERNS = [
|
||||||
|
/onClick\s*=\s*\{?\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\}?/g,
|
||||||
|
/onChange\s*=\s*\{?\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\}?/g,
|
||||||
|
/onSubmit\s*=\s*\{?\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\}?/g,
|
||||||
|
/onClick\s*=\s*\([^)]*\)\s*=>\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Patterns to match function definitions
|
||||||
|
const FUNCTION_PATTERNS = [
|
||||||
|
/const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)\s*=>|function)/g,
|
||||||
|
/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
|
||||||
|
/const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*useCallback\s*\(/g,
|
||||||
|
/const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*useMemo\s*\(/g,
|
||||||
|
];
|
||||||
|
|
||||||
|
function findFiles(dir, extensions = ['.tsx', '.ts', '.jsx', '.js']) {
|
||||||
|
const files = [];
|
||||||
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = path.join(dir, item.name);
|
||||||
|
|
||||||
|
if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
|
||||||
|
files.push(...findFiles(fullPath, extensions));
|
||||||
|
} else if (item.isFile() && extensions.some(ext => item.name.endsWith(ext))) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHandlers(content) {
|
||||||
|
const handlers = new Set();
|
||||||
|
|
||||||
|
for (const pattern of HANDLER_PATTERNS) {
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(content)) !== null) {
|
||||||
|
const handlerName = match[1];
|
||||||
|
// Skip common built-ins and React hooks
|
||||||
|
if (!['setState', 'useState', 'useEffect', 'useCallback', 'useMemo', 'prev', 'e', 'event', 'error'].includes(handlerName)) {
|
||||||
|
handlers.add(handlerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFunctions(content) {
|
||||||
|
const functions = new Set();
|
||||||
|
|
||||||
|
for (const pattern of FUNCTION_PATTERNS) {
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(content)) !== null) {
|
||||||
|
functions.add(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return functions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFile(filePath) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const handlers = extractHandlers(content);
|
||||||
|
const functions = extractFunctions(content);
|
||||||
|
|
||||||
|
const undefinedHandlers = [];
|
||||||
|
|
||||||
|
for (const handler of handlers) {
|
||||||
|
// Check if handler is defined as a function
|
||||||
|
if (!functions.has(handler)) {
|
||||||
|
// Check if it's a prop (passed from parent)
|
||||||
|
const isProp = new RegExp(`(?:props|\\{[^}]*)\\s*:\\s*\\{[^}]*${handler}[^}]*:`, 's').test(content) ||
|
||||||
|
new RegExp(`(?:interface|type)\\s+\\w+Props[^}]*${handler}[^}]*:`, 's').test(content);
|
||||||
|
|
||||||
|
// Check if it's from a hook or external import
|
||||||
|
const isImported = new RegExp(`(?:import|from).*${handler}`, 's').test(content);
|
||||||
|
|
||||||
|
if (!isProp && !isImported) {
|
||||||
|
undefinedHandlers.push(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefinedHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
console.log('🔍 Validating handler functions...\n');
|
||||||
|
|
||||||
|
const componentFiles = findFiles(COMPONENT_DIR);
|
||||||
|
const apiFiles = findFiles(API_DIR);
|
||||||
|
const allFiles = [...componentFiles, ...apiFiles];
|
||||||
|
|
||||||
|
let hasErrors = false;
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const file of allFiles) {
|
||||||
|
const undefinedHandlers = validateFile(file);
|
||||||
|
|
||||||
|
if (undefinedHandlers.length > 0) {
|
||||||
|
hasErrors = true;
|
||||||
|
const relativePath = path.relative(process.cwd(), file);
|
||||||
|
errors.push({
|
||||||
|
file: relativePath,
|
||||||
|
handlers: undefinedHandlers,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error(`❌ ${relativePath}`);
|
||||||
|
for (const handler of undefinedHandlers) {
|
||||||
|
console.error(` ⚠️ Handler "${handler}" is referenced but not defined`);
|
||||||
|
}
|
||||||
|
console.error('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
console.error('\n❌ Validation failed: Found undefined handler functions\n');
|
||||||
|
console.error('Please ensure all handler functions are defined before use.\n');
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log('✅ All handler functions are properly defined!\n');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { validateFile, extractHandlers, extractFunctions };
|
||||||
Reference in New Issue
Block a user