Skip to content

adding module complexity#53

Closed
itaymendel wants to merge 2 commits intomainfrom
feat/module-analysis
Closed

adding module complexity#53
itaymendel wants to merge 2 commits intomainfrom
feat/module-analysis

Conversation

@itaymendel
Copy link
Owner

@itaymendel itaymendel commented Mar 4, 2026

Summary by CodeRabbit

  • New Features

    • Module-level complexity analysis with configurable thresholds and a module complexity score
    • Halstead operator/operand metrics and per-function Halstead reporting
    • New standalone analyzeModule() API for programmatic module analysis
  • Configuration

    • Extended complexity rule options to include module-level settings (moduleComplexity, maxCyclomaticSum, maxCognitiveSum)
    • Updated ignore patterns to include tests/
  • Documentation

    • README expanded with module analysis guide, examples, and API usage
  • Tests

    • Comprehensive tests covering Halstead, module analysis, and integration with the complexity rule

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

Adds module-level complexity analysis: Halstead metrics, module Maintainability Index scoring, and aggregation of per-function metrics. Introduces a standalone analyzeModule API, Halstead visitor and utilities, module analysis visitor, updated rule wiring/options, package exports, docs, and comprehensive tests.

Changes

Cohort / File(s) Summary
Configuration & Docs
\.oxlintrc.json, README.md, package.json
Added moduleComplexity and module-level thresholds to config/examples; ignored tests/ in linter config; expanded README with module analysis docs, programmatic API, migration notes; added ./analyze export and optional peer deps (oxc-parser, estree-walker) in package.json.
Standalone API
src/analyze.ts
New analyzeModule(code, filename?, options?) export: parses via oxc-parser, augments AST (loc/parent) with estree-walker, dispatches to a module analysis visitor, and returns ModuleAnalysisResult.
Halstead core
src/module/halstead.ts, src/module/halstead-visitor.ts
New Halstead types/utilities (HalsteadCounts, HalsteadMetrics, increment/merge/calc helpers) and a visitor that tracks operators/operands with per-function scope and enter/exit callbacks.
Module analysis visitor
src/module/visitor.ts
New types (FunctionMetrics, ModuleAnalysisResult, ModuleComplexityOptions, etc.), calculateModuleComplexity, computeMIInputs, and createModuleAnalysisVisitor that aggregates per-function cyclomatic/cognitive/Halstead metrics and computes module-level MI/decomposition.
Rule integration & exports
src/index.ts, src/rules/complexity.ts, src/rules/shared.ts
Refactored exports to expose Halstead and module analysis APIs; replaced prior combined visitors with createModuleAnalysisVisitor; added module schema props and parseModuleOptions; updated complexity rule to report module-level messages.
Tests & fixtures
tests/*, tests/fixtures/js/module-level.js
Extensive new tests: analyze.test.ts, halstead.test.ts, module-analysis.test.ts, module-complexity.test.ts and fixture module-level.js covering parsing, Halstead counts, per-function/module aggregation, MI scoring, thresholds, and error cases.

Sequence Diagram(s)

sequenceDiagram
    participant Input as Code Input
    participant Parser as oxc-parser
    participant AST as AST
    participant Walker as estree-walker
    participant Visitor as Module Analysis Visitor
    participant Halstead as Halstead Counter
    participant Complexity as Complexity Calculator
    participant Result as ModuleAnalysisResult

    Input->>Parser: parseSync(code, filename)
    Parser->>AST: return Program AST
    AST->>Walker: traverse (attach loc/parent)
    Walker->>Visitor: enter/leave node events
    Visitor->>Halstead: increment operators/operands per node
    Visitor->>Complexity: update cyclomatic/cognitive per-function
    Halstead->>Visitor: provide module & function counts on exits
    Complexity->>Visitor: aggregate per-function metrics
    Visitor->>Result: compute module MI score & decomposition
    Result-->>Input: return ModuleAnalysisResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I counted ops and operands in the night,

Halstead hummed and MI took flight,
Functions lined up, metrics in a row,
Module score brighter than moonlight's glow,
I hop away happy — analysis done right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'adding module complexity' directly and clearly summarizes the main change in the changeset, which introduces module-level complexity analysis with moduleComplexity metrics, Halstead-based analysis, and related configuration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/module-analysis

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/analyze.ts (1)

73-84: Stub context returns empty string for getText() — may affect extraction analysis.

The createLibraryContext() stub returns getText: () => '', but context.sourceCode.getText() is called in combined-visitor.ts (see context snippet 1, line 170). While this shouldn't break module complexity calculation, it may cause extraction analysis or other features relying on source text to silently produce incorrect results.

Consider documenting this limitation or storing the actual code parameter in the stub.

💡 Optional: Include source text in stub context
-function createLibraryContext(): Context {
+function createLibraryContext(code: string): Context {
   return {
     sourceCode: {
-      text: '',
-      getText: () => '',
+      text: code,
+      getText: (node?: ESTreeNode) => {
+        if (!node?.loc) return code;
+        // Simplified: would need proper offset calculation for node ranges
+        return code;
+      },
       scopeManager: null,
       getScope: () => null,
     },

Then update the call site:

   const visitor = createModuleAnalysisVisitor(
-    createLibraryContext(),
+    createLibraryContext(code),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/analyze.ts` around lines 73 - 84, The stub createLibraryContext() returns
getText() as an empty string which can cause extraction/analysis that depends on
source text to misbehave; update createLibraryContext to accept and store the
real source text (e.g., add a parameter like code or sourceText) and set
sourceCode.text and sourceCode.getText to return that provided string, ensuring
Context.sourceCode.getText() returns the actual file contents used by
combined-visitor.ts and other analyzers; update any call sites that construct
the stub to pass the code string or, if not possible, add a comment documenting
the limitation and a TODO to wire real text later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Around line 65-77: The package marks oxc-parser and estree-walker as optional
peer deps but src/analyze.ts imports them at module top-level and src/index.ts
re-exports analyzeModule, causing MODULE_NOT_FOUND on any import; fix by
changing analyzeModule in src/analyze.ts to lazily load these optional libs via
dynamic import() inside the function (or helper) so they are only required when
analyzeModule is invoked, update any type/return handling accordingly, and keep
src/index.ts re-exporting analyzeModule unchanged so other imports no longer
trigger missing-module errors.

In `@README.md`:
- Around line 187-193: The fenced code block that starts with "Module is too
complex (score: 81.5/100, maximum: 80)." is missing a language token and
triggers markdownlint MD040; fix it by adding a language identifier (e.g., text)
after the opening fence (change ``` to ```text) so the block is explicitly
marked (the block containing "Estimated bug risk: ~2.3 defects..." through
"cognitive total: 52, maximum: 40.").

In `@src/rules/complexity.ts`:
- Line 45: The guard currently treats opts.moduleComplexity == 0 as "disabled"
which prevents reporting when a user explicitly sets 0; change the early-return
to only disable for negative values (i.e., treat negative thresholds as
disabled) so the check becomes opts.moduleComplexity < 0 ||
result.moduleComplexity <= opts.moduleComplexity, keeping the existing
comparison against result.moduleComplexity so a configured 0 will still trigger
violations when result.moduleComplexity > 0; update the condition in the same
location where opts.moduleComplexity and result.moduleComplexity are used.

---

Nitpick comments:
In `@src/analyze.ts`:
- Around line 73-84: The stub createLibraryContext() returns getText() as an
empty string which can cause extraction/analysis that depends on source text to
misbehave; update createLibraryContext to accept and store the real source text
(e.g., add a parameter like code or sourceText) and set sourceCode.text and
sourceCode.getText to return that provided string, ensuring
Context.sourceCode.getText() returns the actual file contents used by
combined-visitor.ts and other analyzers; update any call sites that construct
the stub to pass the code string or, if not possible, add a comment documenting
the limitation and a TODO to wire real text later.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 15e213e3-7530-424a-a92c-51e7e8f223b3

📥 Commits

Reviewing files that changed from the base of the PR and between 5c73508 and 3943ebd.

📒 Files selected for processing (15)
  • .oxlintrc.json
  • README.md
  • package.json
  • src/analyze.ts
  • src/index.ts
  • src/module/halstead-visitor.ts
  • src/module/halstead.ts
  • src/module/visitor.ts
  • src/rules/complexity.ts
  • src/rules/shared.ts
  • tests/analyze.test.ts
  • tests/fixtures/js/module-level.js
  • tests/halstead.test.ts
  • tests/module-analysis.test.ts
  • tests/module-complexity.test.ts

Comment on lines 65 to 77
"peerDependencies": {
"@oxlint/plugins": ">=1.43.0"
"@oxlint/plugins": ">=1.43.0",
"oxc-parser": ">=0.60.0",
"estree-walker": ">=3.0.0"
},
"peerDependenciesMeta": {
"oxc-parser": {
"optional": true
},
"estree-walker": {
"optional": true
}
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== Checking package.json peerDependencies ==="
sed -n '65,77p' package.json

echo ""
echo "=== Checking src/analyze.ts imports (first 15 lines) ==="
head -15 src/analyze.ts

echo ""
echo "=== Checking for all imports of oxc-parser ==="
rg "from ['\"]oxc-parser['\"]|import.*oxc-parser" src/ -A 2 -B 2

echo ""
echo "=== Checking for all imports of estree-walker ==="
rg "from ['\"]estree-walker['\"]|import.*estree-walker" src/ -A 2 -B 2

Repository: itaymendel/oxlint-plugin-complexity

Length of output: 1531


🏁 Script executed:

#!/bin/bash

echo "=== Checking package.json main/exports field ==="
rg '"main"|"exports"' package.json -A 5

echo ""
echo "=== Checking how analyzeModule is exported ==="
rg "export.*analyzeModule|function analyzeModule" src/ -B 2 -A 5

echo ""
echo "=== Checking where analyzeModule is defined ==="
grep -n "analyzeModule" src/analyze.ts | head -5

echo ""
echo "=== Checking if there are any try-catch or dynamic imports ==="
rg "try\s*\{|catch|dynamic.*import|import\(" src/analyze.ts

Repository: itaymendel/oxlint-plugin-complexity

Length of output: 1237


🏁 Script executed:

#!/bin/bash

echo "=== Checking module/visitor.js for oxc-parser/estree-walker deps ==="
rg "from ['\"]oxc-parser['\"]|import.*oxc-parser|from ['\"]estree-walker['\"]|import.*estree-walker" src/module/visitor.ts

echo ""
echo "=== Checking all imports in module/visitor.ts (first 20 lines) ==="
head -20 src/module/visitor.ts

echo ""
echo "=== Summary: Does main entry point require optional deps? ==="
echo "Main entry exports from: src/index.ts"
echo "Exports from src/index.ts:"
grep "export" src/index.ts

Repository: itaymendel/oxlint-plugin-complexity

Length of output: 1772


Optional peer dependencies will cause import-time failure if not installed.

oxc-parser and estree-walker are marked as optional peerDependencies, but src/analyze.ts imports them unconditionally at the top level. Since src/index.ts re-exports analyzeModule from src/analyze.ts, importing from the main package entry point will trigger a MODULE_NOT_FOUND error if these optional dependencies are not installed, even when accessing other exports.

Consider one of:

  1. Use dynamic import() in analyzeModule so the dependency is only required when the function is called
  2. Move analyzeModule to a separate entry point (e.g., oxlint-plugin-complexity/analyze) to avoid loading optional deps from the main entry
  3. Remove optional: true if these dependencies are required for core functionality
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 65 - 77, The package marks oxc-parser and
estree-walker as optional peer deps but src/analyze.ts imports them at module
top-level and src/index.ts re-exports analyzeModule, causing MODULE_NOT_FOUND on
any import; fix by changing analyzeModule in src/analyze.ts to lazily load these
optional libs via dynamic import() inside the function (or helper) so they are
only required when analyzeModule is invoked, update any type/return handling
accordingly, and keep src/index.ts re-exporting analyzeModule unchanged so other
imports no longer trigger missing-module errors.

result: ModuleAnalysisResult,
opts: ParsedModuleOptions
): string[] {
if (opts.moduleComplexity <= 0 || result.moduleComplexity <= opts.moduleComplexity) return [];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

moduleComplexity: 0 currently disables score violations instead of enforcing the strictest threshold.

Line 45 returns early when opts.moduleComplexity <= 0, so a configured value of 0 can never trigger the module complexity score report even when result.moduleComplexity > 0.

Suggested fix
-  if (opts.moduleComplexity <= 0 || result.moduleComplexity <= opts.moduleComplexity) return [];
+  if (result.moduleComplexity <= opts.moduleComplexity) return [];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (opts.moduleComplexity <= 0 || result.moduleComplexity <= opts.moduleComplexity) return [];
if (result.moduleComplexity <= opts.moduleComplexity) return [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rules/complexity.ts` at line 45, The guard currently treats
opts.moduleComplexity == 0 as "disabled" which prevents reporting when a user
explicitly sets 0; change the early-return to only disable for negative values
(i.e., treat negative thresholds as disabled) so the check becomes
opts.moduleComplexity < 0 || result.moduleComplexity <= opts.moduleComplexity,
keeping the existing comparison against result.moduleComplexity so a configured
0 will still trigger violations when result.moduleComplexity > 0; update the
condition in the same location where opts.moduleComplexity and
result.moduleComplexity are used.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/analyze.ts (1)

73-84: Consider documenting the minimal context limitations.

The createLibraryContext provides a stub implementation where getText() always returns the full source code rather than a node's text, and scopeManager/getScope return null. This works for the current module analysis visitor but may cause issues if visitors are extended to rely on node-specific text extraction or scope analysis.

Consider adding a brief comment documenting these limitations for future maintainers.

💡 Optional: Add documentation comment
+/**
+ * Creates a minimal Context for standalone analysis (no linting context).
+ * Limitations: getText() returns full source, scopeManager is null.
+ */
 function createLibraryContext(code: string): Context {
   return {
     sourceCode: {
       text: code,
       getText: () => code,
       scopeManager: null,
       getScope: () => null,
     },
     options: [],
     report: () => {},
   } as unknown as Context;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/analyze.ts` around lines 73 - 84, Add a short documentation comment above
the createLibraryContext function explaining its minimal/stub behavior: that
sourceCode.getText() always returns the whole file (not node-specific text),
scopeManager is null and getScope() returns null, and that this is sufficient
only for current module analysis visitors but will break visitors that require
node text or scope analysis; mention callers should replace with a full ESLint
Context when needed (reference createLibraryContext and the Context type).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/analyze.ts`:
- Around line 73-84: Add a short documentation comment above the
createLibraryContext function explaining its minimal/stub behavior: that
sourceCode.getText() always returns the whole file (not node-specific text),
scopeManager is null and getScope() returns null, and that this is sufficient
only for current module analysis visitors but will break visitors that require
node text or scope analysis; mention callers should replace with a full ESLint
Context when needed (reference createLibraryContext and the Context type).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bf4acafa-3871-4e3d-af65-d250658b80ba

📥 Commits

Reviewing files that changed from the base of the PR and between 3943ebd and 31d3ae0.

📒 Files selected for processing (4)
  • README.md
  • package.json
  • src/analyze.ts
  • src/index.ts

@itaymendel
Copy link
Owner Author

not shipping. module level stuff are redundant. don't add anything valuable over function level complexity

@itaymendel itaymendel closed this Mar 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant