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:
2026-01-22 17:30:22 +02:00
parent 0b6979e2d6
commit 5d8a72b0a5
4 changed files with 245 additions and 1 deletions

View File

@@ -13,8 +13,10 @@
}
],
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-undef": "error",
"react-hooks/exhaustive-deps": "warn",
"react/no-unescaped-entities": "off",
"no-undef": "error",
"no-console": [
"warn",
{

View File

@@ -35,7 +35,9 @@
"test:redis": "tsx scripts/test-redis.ts",
"redis:populate": "tsx scripts/populate-redis-test.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": {
"schema": "prisma/schema.prisma"

View 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
View 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 };