identical-functions
ESLint rule documentation for identical-functions
📡 Live from GitHub — This documentation is fetched directly from identical-functions.md and cached for 6 hours.
Keywords: code duplication, DRY principle, CWE-1104, ESLint rule, duplicate code, refactoring, SonarQube, auto-fix, LLM-optimized, code quality
Detects duplicate function implementations with DRY refactoring suggestions
Detects duplicate function implementations with DRY refactoring suggestions. This rule is part of eslint-plugin-maintainability and provides LLM-optimized error messages with fix suggestions.
💡 Provides suggestions
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-1104 (Code Duplication) |
| Severity | Medium (code quality) |
| Auto-Fix | ⚠️ Suggests fixes (manual application) |
| Category | Quality |
| ESLint MCP | ✅ Optimized for ESLint MCP integration |
| Best For | Large codebases, teams refactoring legacy code |
Error Message Format
The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:
⚠️ CWE-1104 OWASP:A03 CVSS:5.3 | Unmaintainable Third-Party Components detected | MEDIUM
Fix: Review and apply the recommended fix | https://owasp.org/Top10/A03_2021/Message Components
| Component | Purpose | Example |
|---|---|---|
| Risk Standards | Security benchmarks | CWE-1104 OWASP:A03 CVSS:5.3 |
| Issue Description | Specific vulnerability | Unmaintainable Third-Party Components detected |
| Severity & Compliance | Impact assessment | MEDIUM |
| Fix Instruction | Actionable remediation | Follow the remediation steps below |
| Technical Truth | Official reference | OWASP Top 10 |
Rule Details
Finds functions with identical or nearly identical implementations and suggests refactoring to follow the DRY (Don't Repeat Yourself) principle.
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
minLines | number | 3 | Minimum lines to consider for duplication |
similarityThreshold | number | 0.85 | Similarity threshold (0.5-1.0) |
ignoreTestFiles | boolean | true | Ignore test files (_.test.ts, _.spec.ts) |
Examples
❌ Incorrect
// 95% similar - duplicated logic!
function processUserOrder(order: UserOrder) {
if (!order.items || order.items.length === 0) {
throw new Error('Order has no items');
}
const total = order.items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
return {
orderId: order.id,
total: total,
status: 'processed',
};
}
function processGuestOrder(order: GuestOrder) {
if (!order.items || order.items.length === 0) {
throw new Error('Order has no items');
}
const total = order.items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
return {
orderId: order.id,
total: total,
status: 'processed',
};
}✅ Correct
// Extracted common logic
function processOrder<T extends BaseOrder>(order: T) {
if (!order.items || order.items.length === 0) {
throw new Error('Order has no items');
}
const total = order.items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
return {
orderId: order.id,
total: total,
status: 'processed',
};
}
// Or use composition
const processUserOrder = (order: UserOrder) => processOrder(order);
const processGuestOrder = (order: GuestOrder) => processOrder(order);Configuration Examples
Basic Usage
{
rules: {
'maintainability/identical-functions': ['error', {
minLines: 3,
similarityThreshold: 0.85
}]
}
}Strict Mode
{
rules: {
'maintainability/identical-functions': ['error', {
minLines: 2, // Catch even small duplications
similarityThreshold: 0.7, // Lower threshold = more sensitive
ignoreTestFiles: false // Check test files too
}]
}
}Relaxed Mode
{
rules: {
'maintainability/identical-functions': ['warn', {
minLines: 5, // Only large duplications
similarityThreshold: 0.95, // Must be nearly identical
ignoreTestFiles: true
}]
}
}Refactoring Patterns
1. Extract Generic Function
// Before: 3 similar functions
function formatUserName(user: User) {
return `${user.firstName} ${user.lastName}`.trim();
}
function formatAdminName(admin: Admin) {
return `${admin.firstName} ${admin.lastName}`.trim();
}
function formatGuestName(guest: Guest) {
return `${guest.firstName} ${guest.lastName}`.trim();
}
// After: One generic function
function formatFullName(person: { firstName: string; lastName: string }) {
return `${person.firstName} ${person.lastName}`.trim();
}2. Higher-Order Function
// Before: Similar validation functions
function validateEmail(email: string) {
if (!email) throw new Error('Email required');
if (!email.includes('@')) throw new Error('Invalid email');
return email;
}
function validatePhone(phone: string) {
if (!phone) throw new Error('Phone required');
if (phone.length < 10) throw new Error('Invalid phone');
return phone;
}
// After: Higher-order function
function createValidator<T>(
fieldName: string,
validators: Array<(value: T) => boolean>,
errorMessages: string[],
) {
return (value: T): T => {
if (!value) throw new Error(`${fieldName} required`);
validators.forEach((validator, index) => {
if (!validator(value)) {
throw new Error(errorMessages[index]);
}
});
return value;
};
}
const validateEmail = createValidator<string>(
'Email',
[(email) => email.includes('@')],
['Invalid email'],
);
const validatePhone = createValidator<string>(
'Phone',
[(phone) => phone.length >= 10],
['Invalid phone'],
);3. Strategy Pattern
// Before: Similar calculation methods
function calculateStandardShipping(weight: number) {
const baseRate = 5;
return weight * 2 + baseRate;
}
function calculateExpressShipping(weight: number) {
const baseRate = 15;
return weight * 3 + baseRate;
}
// After: Strategy pattern
interface ShippingStrategy {
baseRate: number;
weightMultiplier: number;
}
const shippingStrategies: Record<string, ShippingStrategy> = {
standard: { baseRate: 5, weightMultiplier: 2 },
express: { baseRate: 15, weightMultiplier: 3 },
};
function calculateShipping(type: string, weight: number) {
const strategy = shippingStrategies[type];
return weight * strategy.weightMultiplier + strategy.baseRate;
}Why This Matters
| Issue | Impact | Solution |
|---|---|---|
| 🔄 Maintenance | Fix bugs in multiple places | Single source of truth |
| 🐛 Bug Propagation | Same bug exists in all copies | Fix once, fix everywhere |
| 📈 Code Growth | Codebase grows unnecessarily | Extract common logic |
| 🧪 Testing | Must test same logic multiple times | Test once, reuse everywhere |
| 🔍 Refactoring | Changes require updating many places | Change in one place |
Similarity Calculation
The rule uses normalized AST comparison:
- Normalize function bodies (remove variable names, whitespace)
- Compare AST structures
- Calculate similarity score (0.0 - 1.0)
- Report if score > threshold
Comparison with Alternatives
| Feature | identical-functions | eslint-plugin-sonarjs | jscpd (copy-paste detector) |
|---|---|---|---|
| Code Duplication Detection | ✅ Yes | ⚠️ Limited | ✅ Yes |
| CWE Reference | ✅ CWE-1104 included | ⚠️ Limited | ❌ No |
| LLM-Optimized | ✅ Yes | ❌ No | ❌ No |
| ESLint MCP | ✅ Optimized | ❌ No | ❌ No |
| Fix Suggestions | ✅ Detailed | ⚠️ Basic | ❌ No |
| ESLint Integration | ✅ Native | ✅ Native | ❌ External tool |
Related Rules
cognitive-complexity- Measures code complexityno-console-log- Code quality enforcementno-deprecated-api- API modernization
Further Reading
- CWE-1104: Code Duplication - Official CWE entry
- DRY Principle - Don't Repeat Yourself
- SonarQube RSPEC-4144 - SonarQube duplication rule
- ESLint MCP Setup - Enable AI assistant integration
References
Inspired by SonarQube RSPEC-4144
Known False Negatives
The following patterns are not detected due to static analysis limitations:
Dynamic Variable References
Why: Static analysis cannot trace values stored in variables or passed through function parameters.
// ❌ NOT DETECTED - Value from variable
const value = externalSource();
processValue(value); // Variable origin not trackedMitigation: Implement runtime validation and review code manually. Consider using TypeScript branded types for validated inputs.
Imported Values
Why: When values come from imports, the rule cannot analyze their origin or construction.
// ❌ NOT DETECTED - Value from import
import { getValue } from './helpers';
processValue(getValue()); // Cross-file not trackedMitigation: Ensure imported values follow the same constraints. Use TypeScript for type safety.