ESLint InterlaceESLint Interlace
Plugin: maintainabilityRules

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

AspectDetails
CWE ReferenceCWE-1104 (Code Duplication)
SeverityMedium (code quality)
Auto-Fix⚠️ Suggests fixes (manual application)
CategoryQuality
ESLint MCP✅ Optimized for ESLint MCP integration
Best ForLarge 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

ComponentPurposeExample
Risk StandardsSecurity benchmarksCWE-1104 OWASP:A03 CVSS:5.3
Issue DescriptionSpecific vulnerabilityUnmaintainable Third-Party Components detected
Severity & ComplianceImpact assessmentMEDIUM
Fix InstructionActionable remediationFollow the remediation steps below
Technical TruthOfficial referenceOWASP 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

OptionTypeDefaultDescription
minLinesnumber3Minimum lines to consider for duplication
similarityThresholdnumber0.85Similarity threshold (0.5-1.0)
ignoreTestFilesbooleantrueIgnore 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

IssueImpactSolution
🔄 MaintenanceFix bugs in multiple placesSingle source of truth
🐛 Bug PropagationSame bug exists in all copiesFix once, fix everywhere
📈 Code GrowthCodebase grows unnecessarilyExtract common logic
🧪 TestingMust test same logic multiple timesTest once, reuse everywhere
🔍 RefactoringChanges require updating many placesChange in one place

Similarity Calculation

The rule uses normalized AST comparison:

  1. Normalize function bodies (remove variable names, whitespace)
  2. Compare AST structures
  3. Calculate similarity score (0.0 - 1.0)
  4. Report if score > threshold

Comparison with Alternatives

Featureidentical-functionseslint-plugin-sonarjsjscpd (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

Further Reading

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 tracked

Mitigation: 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 tracked

Mitigation: Ensure imported values follow the same constraints. Use TypeScript for type safety.

On this page

No Headings