Skip to content

About

Cyclomatic and cognitive complexity rules for oxlint with actionable error messages. Also available as a standalone library for programmatic complexity analysis.

Topics

Resources

Contributing

Stars

47 stars

Watchers

0 watching

Forks

Repository files navigation

oxlint-plugin-complexity

Cyclomatic and cognitive complexity rules for oxlint with actionable error messages. Also available as a standalone library for programmatic complexity analysis.

Features:

  • Cyclomatic and cognitive complexity analysis.
  • Actionable error messages with complexity breakdown.
  • Programmatic API for custom tooling
  • Framework support: React, Vue, Angular, Svelte, Astro, Solid, Qwik
  • File types: .js .mjs .cjs .ts .tsx .jsx .vue .svelte .astro

Note: Refactoring tips require cognitive complexity (only it tracks nesting depth).

Quick Start

npm install oxlint-plugin-complexity --save-dev
// .oxlintrc.json
{
  "jsPlugins": ["oxlint-plugin-complexity"],
  "rules": {
    "complexity/complexity": [
      "error",
      {
        "cyclomatic": 20,
        "cognitive": 15
      }
    ]
  }
}

Actionable Error Messages

Error messages show a summary, line-by-line breakdown, and refactoring tips for deep nesting:

complexity(complexity): Function 'processData' has Cognitive Complexity of 15.
Maximum allowed is 10. [if: +14, for: +1]

Breakdown:
   Line 2: +1 for 'for'
   Line 3: +2 for 'if' (incl. +1 nesting)
   Line 4: +3 for 'if' (incl. +2 nesting)
   Line 5: +4 for 'if' (incl. +3 nesting)
>>> Line 6: +5 for 'if' (incl. +4 nesting) [top offender]
    ↳ Tip: Extract inner loops into helper functions - each extraction removes one nesting level
function processData(items, mode, config) {
  for (const item of items) {
    // Line 2: +1
    if (item.active) {
      // Line 3: +2 (nesting=1)
      if (mode === 'strict') {
        // Line 4: +3 (nesting=2)
        if (config.validate) {
          // Line 5: +4 (nesting=3)
          if (item.required) {
            // Line 6: +5 (nesting=4) <- top offender
          }
        }
      }
    }
  }
}

Rule Configuration

{
  "complexity/complexity": [
    "error",
    {
      // Complexity thresholds
      "cyclomatic": 20, // Default: 20
      "cognitive": 15, // Default: 15

      // Performance optimization (optional)
      "minLines": 10, // Default: 10 (skip functions <10 lines like getters; 0 = analyze all; counts comments/blanks)

      // Extraction suggestions (optional)
      "enableExtraction": true, // Default: true
      "extractionMultiplier": 1.5, // Default: 1.5 (triggers at 1.5× cognitive threshold)
      "minExtractionPercentage": 30, // Default: 30 (min % of total complexity to suggest)

      // Refactoring tip thresholds (optional, set to 0 to disable)
      "nestingTipThreshold": 3, // Default: 3
      "elseIfChainThreshold": 4, // Default: 4
      "logicalOperatorThreshold": 3, // Default: 3
    },
  ],
}

TypeScript users can import ComplexityOptions from oxlint-plugin-complexity to type the rule options.

Cyclomatic Complexity

Counts decision points in code. Learn more

+1 for: if, for, for...in, for...of, while, do...while, case, catch, ? :, &&, ||, ??, ||=/&&=/??=, ?. (each optional member/call), default values (fn(x = 1), { x = 1 } = y)

Cognitive Complexity

Measures how difficult code is to understand by penalizing nesting. Learn more

  • +1 for: if/for/while/switch/catch/? : (+nesting), else, logical sequence changes, recursion
  • Nested scopes: Functions, class field initializers, and static {} blocks are scored independently (top-level start at nesting 0; nested units start at the enclosing scope nesting +1. nested function adds no points to the enclosing scope).
  • Excluded: React components (PascalCase + returns JSX), default value patterns (a || [])

Refactoring Tips

Detects common complexity patterns and provides actionable tips:

  • Deep nesting (nestingTipThreshold): Suggests extracting inner loops/conditions
  • Long else-if chains (elseIfChainThreshold): Recommends lookup tables or strategy pattern
  • Logical operator sequences (logicalOperatorThreshold): Suggests extracting boolean expressions

Extraction Suggestions

Analyzes variable flow to identify extractable code blocks (enabled by default, disable with enableExtraction: false):

Example output:

Smart extraction suggestions:

  Lines 9-22: Extractable with some refactoring
    Complexity: +11 (55% of total)
    Inputs: order, config, processedItems
    Suggested: processOrder(order, config, processedItems): void

  Lines 25-33: Requires significant refactoring
    Complexity: +6 (30% of total)
    Inputs: config, totalCount, processedItems
    Issue: Mutates external variable 'totalCount' (line 27)
    Suggestion: Consider returning 'totalCount' instead of mutating it

TypeScript support: Preserves type annotations in suggested signatures:

Inputs: config: Config, results: number[]
Suggested: processBlock(config: Config, results: number[]): void

Known Limitations

Extraction suggestions use static analysis heuristics and may miss:

  • Globals/module variables (not tracked by variable flow analysis)
  • Complex flows (closures, dynamic properties, indirect mutations)

Always review suggestions before applying, even when marked "high confidence".

Standalone Library Usage

You can use this package as a standalone library to analyze complexity programmatically - no oxlint runtime needed.

npm install oxlint-plugin-complexity oxc-parser estree-walker diff

Analyzing a file

import { analyzeFileComplexity } from 'oxlint-plugin-complexity/standalone';

const code = `
function processOrder(order, config) {
  if (order.items.length === 0) {
    return null;
  }
  for (const item of order.items) {
    if (item.quantity > config.maxQuantity) {
      if (config.strict) {
        throw new Error('Over limit');
      }
    }
  }
  return order;
}
`;

const result = analyzeFileComplexity(code, 'order.ts');

for (const fn of result.functions) {
  console.log(`${fn.name} (lines ${fn.startLine}-${fn.endLine})`);
  console.log(`  Cyclomatic: ${fn.cyclomatic}`);
  console.log(`  Cognitive:  ${fn.cognitive}`);
}

Each function result includes cyclomaticPoints and cognitivePoints arrays with per-construct breakdowns (line, column, complexity contribution, and message).

Git Diff Analysis

Analyze only the functions touched by a diff - useful for CI gates, pre-commit hooks, and code review tools.

import { analyzeDiffComplexity } from 'oxlint-plugin-complexity/diff';
import { execFileSync } from 'node:child_process';

const diff = execFileSync('git', ['diff', 'HEAD~1']).toString();
const result = analyzeDiffComplexity(diff);

for (const file of result.files) {
  for (const fn of file.functions) {
    if (fn.cognitive > 15) {
      console.log(`${file.path}:${fn.startLine} ${fn.name} - cognitive: ${fn.cognitive}`);
    }
  }
}

Migration to v4 (for library usage)

  • Replace MaxCyclomaticOptions / MaxCognitiveOptions with ComplexityOptions, using cyclomatic and cognitive instead of max.
  • Replace createCognitiveVisitor / createCyclomaticVisitor with createCombinedComplexityVisitor(context, callback). Results contain cyclomatic, cognitive, cyclomaticPoints, and cognitivePoints; both metrics now require an oxlint Context.
  • createComplexityVisitor, VisitorContext, and FunctionScope are now internal.

When migrating individual visitors, loop labels become for-in, for-of, and do-while. Cognitive recursion points now identify the first recursive call in traversal order, rather than the whole function on exit. The /standalone and /diff APIs are unchanged by this cleanup.

Attribution

The cognitive complexity metric is based on G. Ann Campbell's specification (SonarSource, 2016).

License

MIT

About

Cyclomatic and cognitive complexity rules for oxlint with actionable error messages. Also available as a standalone library for programmatic complexity analysis.

Topics

Resources

Contributing

Stars

47 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages