You've configured ESLint. You've added Prettier. You've got 200 rules enabled.
Your codebase still has architecture problems that no linter will catch.
Here's what comes after you've maxed out the basics.
What ESLint Actually Does
ESLint is excellent at:
- Code style enforcement
- Catching common bugs (unused variables, unreachable code)
- Framework-specific rules (React hooks, Vue patterns)
- Security basics (no-eval, no-implied-eval)
ESLint is terrible at:
- Understanding your architecture
- Seeing cross-file dependencies
- Detecting feature duplication
- Knowing if code is actually used
It's a line-by-line checker, not a system analyzer.
The ESLint Ceiling
// ESLint says: ✅ No issues
// File 1: src/utils/format.js
export function formatDate(date) {
return date.toISOString().split('T')[0];
}
// File 2: src/helpers/dates.js
export function dateToString(d) {
return d.toISOString().split('T')[0];
}
// File 3: src/components/DatePicker.jsx
const formatForDisplay = (date) => date.toISOString().split('T')[0];
Three implementations of the same thing. ESLint can't see this because each file is individually correct.
Level 2: TypeScript Strict Mode
TypeScript catches what ESLint misses:
// ESLint: ✅
// TypeScript strict: ❌
function getUser(id) { // Parameter 'id' implicitly has 'any' type
return users.find(u => u.id === id); // Object is possibly 'undefined'
}
Enable these flags:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true
}
}
This catches runtime errors at compile time. Essential for production JavaScript.
Level 3: Dependency Analysis
Now we're beyond what ships with your IDE.
Circular Dependencies:
// utils/auth.js
import { api } from './api';
// utils/api.js
import { getToken } from './auth';
// Circular! Works until it doesn't.
Tools like madge or dependency-cruiser catch this:
npx madge --circular src/
# Circular dependencies found:
# src/utils/auth.js -> src/utils/api.js -> src/utils/auth.js
Unused Exports:
// Exported but never imported anywhere
export function legacyHelper() { ... } // Dead code
Standard linters don't track cross-file usage.
Level 4: Architectural Rules
Define rules about your structure:
// dependency-cruiser config
module.exports = {
forbidden: [
{
name: 'no-circular',
from: {},
to: { circular: true }
},
{
name: 'no-components-to-pages',
from: { path: '^src/components' },
to: { path: '^src/pages' }
},
{
name: 'no-direct-db-from-components',
from: { path: '^src/components' },
to: { path: '^src/db' }
}
]
};
Now you're enforcing architecture, not just syntax.
Level 5: Graph-Based Analysis
This is where it gets powerful.
Call Graph Analysis:
// Instead of "is this function correct?"
// Ask "what happens when this function is called?"
const callGraph = await analyzeCallGraph('handleCheckout');
// Returns:
// handleCheckout
// ├── validateCart (5 calls)
// ├── processPayment (1 call)
// │ ├── stripeClient.charge
// │ └── updateOrderStatus
// ├── sendConfirmation (1 call)
// │ ├── emailService.send
// │ └── analyticsService.track
// └── clearCart (1 call)
Now you see the blast radius of any change.
Feature Detection:
// Automatically identify features from code structure
const features = await discoverFeatures(workspaceId);
// Returns:
// - "User Authentication" (auth/, middleware/auth.js, 12 files)
// - "Payment Processing" (checkout/, payments/, 23 files)
// - "Search" (search/, api/search.js, 8 files)
No manual documentation. Features emerge from code clustering.
Level 6: Runtime Analysis
Static analysis has limits. Some things need runtime data:
Dead Code (Actually Dead):
// Static analysis: "This is exported and imported"
// Runtime analysis: "This is never called in production"
export function experimentalFeature() {
// Feature flag was never enabled
// 500 lines of unused code
}
Production telemetry + static analysis = complete picture.
Performance Hotspots:
// Static analysis: "This function exists"
// Runtime analysis: "This function is called 10,000x per request"
function inefficientSort(items) {
return items.sort((a, b) => expensiveComparison(a, b));
}
The Modern JavaScript Analysis Stack
| Layer | Tool | Catches | |-------|------|---------| | 1. Style | ESLint + Prettier | Formatting, basic bugs | | 2. Types | TypeScript strict | Type errors, null checks | | 3. Dependencies | madge, dependency-cruiser | Circular deps, architecture violations | | 4. Complexity | eslint-plugin-complexity | Cognitive load | | 5. Security | npm audit, Snyk | Known vulnerabilities | | 6. Architecture | Graph analysis (Glue) | Cross-file understanding |
Practical Setup
package.json scripts:
{
"scripts": {
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"deps:check": "dependency-cruiser src/ --config .dependency-cruiser.js",
"deps:circular": "madge --circular src/",
"analyze": "npm run lint && npm run typecheck && npm run deps:check"
}
}
CI Pipeline:
analyze:
steps:
- run: npm run lint
- run: npm run typecheck
- run: npm run deps:check
- run: npx bundlesize # Check bundle doesn't grow
What Still Requires Human Review
No tool catches:
- Wrong abstraction level
- Poor naming that's technically valid
- Missing error handling for edge cases
- Business logic errors
- Performance issues without profiling
Tools enforce rules. Humans ensure quality.
The Integration Point
The best analysis connects all layers:
ESLint finding: "Unused variable in handlePayment"
+ TypeScript: "Possible null reference"
+ Call graph: "handlePayment called by 15 endpoints"
+ Runtime: "handlePayment called 50k times/day"
= Prioritized, contextual issue
"High-priority fix: null check in handlePayment
affects 15 endpoints serving 50k daily requests"
That's what we built with Glue — connecting static analysis, type information, and architectural understanding into a complete picture.
The Bottom Line
ESLint is the floor, not the ceiling.
Modern JavaScript analysis should include:
- Linting (ESLint) — catches obvious issues
- Type checking (TypeScript) — catches runtime errors
- Dependency analysis — catches architectural drift
- Graph analysis — catches systemic issues
Each layer catches what the previous layer misses. Together, they give you confidence that your JavaScript actually works.