energy-state-analyzer 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.esaignore +6 -0
- package/.prettierignore +7 -0
- package/.prettierrc.json +7 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +27 -1
- package/README.md +40 -201
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/docs/cli.md +136 -0
- package/docs/detectors/README.md +30 -0
- package/docs/detectors/cognitive-complexity.md +45 -0
- package/docs/detectors/cyclomatic-complexity.md +50 -0
- package/docs/detectors/excessive-nesting.md +23 -0
- package/docs/detectors/file-coherence.md +34 -0
- package/docs/detectors/inversion-opportunities.md +39 -0
- package/docs/detectors/logical-operator-control-flow.md +24 -0
- package/docs/detectors/magic-numbers.md +33 -0
- package/docs/detectors/magic-strings.md +37 -0
- package/docs/detectors/match-opportunities.md +26 -0
- package/docs/detectors/opaque-boolean-literal.md +22 -0
- package/docs/detectors/parameter-explosion.md +21 -0
- package/docs/detectors/primitive-obsession.md +29 -0
- package/docs/detectors/suppression.md +60 -0
- package/docs/energy-and-entropy.md +14 -0
- package/package.json +4 -1
package/.esaignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Paths the energy-state analyzer itself should not scan.
|
|
2
|
+
# One pattern per line — a literal path/directory (also matches at any depth if it has no
|
|
3
|
+
# '/'), or a single '*' basename glob. See src/core/esaignore.ts for the exact rules.
|
|
4
|
+
|
|
5
|
+
# Deliberately bad/deeply-nested code used only to exercise the detectors themselves.
|
|
6
|
+
src/test/fixtures
|
package/.prettierignore
ADDED
package/.prettierrc.json
ADDED
package/AGENTS.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to AI coding agents when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
Energy State Analyzer is a VS Code extension that visualizes "energy states" in Python, F#, TypeScript, and Kotlin code via real-time static analysis. It parses source with `web-tree-sitter` (per-language WASM grammars in `grammars/`) and highlights code that is complex, deeply nested, or otherwise hard to maintain using editor decorations, gutter icons, and Problems-panel diagnostics.
|
|
8
|
+
|
|
9
|
+
See `energy-state.md` for the original design doc (energy-state principle, planned "detection agents", and known issues/next steps at project inception).
|
|
10
|
+
|
|
11
|
+
## Build Commands
|
|
12
|
+
|
|
13
|
+
Commands are wrapped in a `Justfile`; run `just --list` to see all of them. Prefer these over calling `npm run` directly.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
just install # npm install
|
|
17
|
+
just build # Build extension bundle via webpack (dev mode)
|
|
18
|
+
just watch # Webpack in watch mode
|
|
19
|
+
just lint # ESLint over src/**/*.ts
|
|
20
|
+
just format # Format src/**/*.ts in place with Prettier
|
|
21
|
+
just format-check # Check formatting without writing changes (used by CI)
|
|
22
|
+
just analyze # Run the CLI's own analyzer over src/ (or `just analyze <path...>` for specific files/dirs)
|
|
23
|
+
just test # compile-tests + compile + lint, then run the VS Code extension test host
|
|
24
|
+
just pack # Production build + package into a .vsix via vsce
|
|
25
|
+
just clean # Remove build artifacts (dist, out, *.vsix)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The underlying `npm run` scripts (`compile`, `watch`, `package`, `lint`, `format`, `format-check`, `compile-tests`, `watch-tests`, `pretest`, `test`, `analyze`) still work directly if you need finer control than the Justfile recipes give you.
|
|
29
|
+
|
|
30
|
+
To run and debug the extension interactively, press `F5` in VS Code — this launches an Extension Development Host with the extension loaded, per `.vscode/launch.json`.
|
|
31
|
+
|
|
32
|
+
There is a single test suite (`src/test/extension.test.ts`); there's no mechanism yet to run a single test by name — use the Extension Test Runner's Testing view in VS Code, or edit the suite temporarily with `.only`.
|
|
33
|
+
|
|
34
|
+
## Architecture
|
|
35
|
+
|
|
36
|
+
Everything lives in one file, `src/extension.ts`, structured as:
|
|
37
|
+
|
|
38
|
+
1. **Activation (`activate`)** — initializes the tree-sitter `Parser`, creates decoration types, registers the `energy-state-analyzer.analyze` command, and wires up editor/document change listeners to re-analyze on the fly. Each language's grammar (`grammars/tree-sitter-<language>.wasm`, path resolved via `context.extensionPath`) is loaded lazily on first use of that language, not up front — see `getOrLoadLanguage`. Activation is gated by the `onLanguage:*` entries in `package.json` (python, fsharp, typescript, kotlin).
|
|
39
|
+
2. **Analysis pipeline (`analyzeDocument`)** — parses the active document's text into a tree-sitter AST, then runs a fixed set of independent detector passes over it, each returning `EnergyViolation[]`:
|
|
40
|
+
- `analyzeNesting` — flags `if`/`for`/`while`/`with` nesting deeper than 3 levels.
|
|
41
|
+
- `analyzeFunctionComplexity` — computes cyclomatic complexity per function, flags >10.
|
|
42
|
+
- `analyzeFileCoherence` — flags files with too many functions or imports (utils/helpers sprawl).
|
|
43
|
+
- `analyzeMagicValues` — flags "magic" numeric/string literals outside constant context.
|
|
44
|
+
- `analyzeParameterCount` — flags functions with >5 parameters.
|
|
45
|
+
- `analyzeInversionOpportunities` — flags large dominant if-blocks, nested validation chains, and deep if-nesting that could be rewritten as guard clauses / early returns.
|
|
46
|
+
- `extractTypeInformation` — walks the AST separately to collect function/class/variable/import type info (currently only logged; scaffolding for future features, not yet used for violations).
|
|
47
|
+
Each detector does its own `traverse(node)` walk of the tree-sitter tree; there's no shared visitor abstraction.
|
|
48
|
+
`src/core/analyze.ts` (the actual current pipeline entry point) runs a larger, up-to-date set of these plus `applySuppressions` (`src/core/suppressions.ts`) as a final pass — it filters out violations covered by an `esa-ignore`/`esa-ignore-file` comment and emits low-severity `suppression` findings for directives that are unused or name an unknown type. See `docs/detectors/suppression.md`.
|
|
49
|
+
3. **Presentation** — `applyDecorations` maps violations to `vscode.TextEditorDecorationType` ranges (color/severity: red=high, yellow=medium, green=low, rendered as background tint + gutter lightning-bolt icon via `createLightningIcon`), and `updateProblemsPanel` mirrors the same violations into a `vscode.DiagnosticCollection` so they also show in the Problems panel.
|
|
50
|
+
4. **Violation model** — `EnergyViolation { line, column, type, severity, message }`, with `type` and `severity` string-literal unions backed by the `VIOLATION_TYPE`/`SEVERITY` constant objects near the top of the file (keep these two in sync when adding a new detector).
|
|
51
|
+
|
|
52
|
+
### Adding a new detector
|
|
53
|
+
|
|
54
|
+
Follow the existing pattern: write an `analyze<Thing>(tree, document): EnergyViolation[]` function that walks `tree.rootNode`, push it into the list in `analyzeDocument`, and add a new `VIOLATION_TYPE` entry if it's a new category. If the violation needs special range highlighting, add a case in `applyDecorations`.
|
|
55
|
+
|
|
56
|
+
### Build/packaging notes
|
|
57
|
+
|
|
58
|
+
- Webpack bundles `src/extension.ts` → `dist/extension.js` (CommonJS, `vscode` module treated as external).
|
|
59
|
+
- `web-tree-sitter`'s own `tree-sitter.wasm` is copied into `dist/` via `CopyWebpackPlugin` (webpack.config.js); the per-language grammar WASMs in `grammars/` ship separately and are loaded at runtime by path, not bundled.
|
|
60
|
+
- `tsconfig.json` targets ES2022/commonjs with `strict: true`.
|
|
61
|
+
|
|
62
|
+
## Before Committing or Opening a PR
|
|
63
|
+
|
|
64
|
+
Run `just format`, `just lint`, and `just analyze` (this project dogfoods its own analyzer over `src/`) before every commit or PR, and fix what they flag. Don't rely on CI to catch formatting, lint, or energy-state violations you could have caught locally.
|
|
65
|
+
|
|
66
|
+
If satisfying `just analyze` on your change requires refactoring existing code (e.g. splitting a file to fix a coherence violation, extracting a function to fix complexity/nesting) rather than just the new code you're adding, do that refactor as its own preceding PR, merged before the PR with the actual change. Don't mix the two in one PR — a refactor bundled with a behavior change makes the diff hard to review and obscures what the change is actually about.
|
|
67
|
+
|
|
68
|
+
## Releasing
|
|
69
|
+
|
|
70
|
+
Release automation runs through EasyBuild.ShipIt (see `RELEASING.md`). Use
|
|
71
|
+
Conventional Commit subjects (`feat:`, `fix:`, `docs:`, `chore:`, `ci:`, etc.)
|
|
72
|
+
for commits and PR titles — CI enforces this on PR titles, and ShipIt uses
|
|
73
|
+
them to generate `CHANGELOG.md` and open release PRs. Do not hand-edit
|
|
74
|
+
generated changelog entries or bump `package.json`'s version manually.
|
|
75
|
+
|
|
76
|
+
## Agent Decision Comments
|
|
77
|
+
|
|
78
|
+
This repository uses Agent Decision Comments.
|
|
79
|
+
See `AGENT_DECISION_COMMENTS.md` for the locally adopted convention.
|
|
80
|
+
Upstream releases: https://github.com/dbrattli/adc/releases
|
|
81
|
+
|
|
82
|
+
Before modifying code, read the ADCs already governing it.
|
|
83
|
+
Treat them as active constraints and justify any change explicitly.
|
|
84
|
+
Add ADCs for non-obvious rationale introduced by your change.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
last_commit_released:
|
|
2
|
+
last_commit_released: c52e34266a2eaeedc6f15e737f593420f3e340f9
|
|
3
3
|
name: energy-state-analyzer
|
|
4
4
|
updaters:
|
|
5
5
|
- command: npm version {version} --no-git-tag-version --allow-same-version
|
|
@@ -11,6 +11,32 @@ All notable changes to the "energy-state-analyzer" extension are generated by
|
|
|
11
11
|
[EasyBuild.ShipIt](https://github.com/easybuild-org/EasyBuild.ShipIt) from
|
|
12
12
|
[Conventional Commits](https://www.conventionalcommits.org/).
|
|
13
13
|
|
|
14
|
+
## 0.5.0 - 2026-08-27
|
|
15
|
+
|
|
16
|
+
### 🚀 Features
|
|
17
|
+
|
|
18
|
+
* Exempt test files from the magic-number detector (#33) ([18023bb](https://github.com/cardamomcode/energy-state-analyzer/commit/18023bb92d74f66f189ac7207c1c9bb40f17aca9))
|
|
19
|
+
|
|
20
|
+
### 🐞 Bug Fixes
|
|
21
|
+
|
|
22
|
+
* Count distinct import sources, not raw lines, for import sprawl (#35) ([c52e342](https://github.com/cardamomcode/energy-state-analyzer/commit/c52e34266a2eaeedc6f15e737f593420f3e340f9))
|
|
23
|
+
|
|
24
|
+
<strong><small>[View changes on Github](https://github.com/cardamomcode/energy-state-analyzer/compare/e7c2d41d1347a4a10f7a921e077a45f745eade4c..c52e34266a2eaeedc6f15e737f593420f3e340f9)</small></strong>
|
|
25
|
+
|
|
26
|
+
## 0.4.0 - 2026-08-26
|
|
27
|
+
|
|
28
|
+
### 🚀 Features
|
|
29
|
+
|
|
30
|
+
* Add .esaignore to exclude paths from the analyzer (#29) ([bd526b4](https://github.com/cardamomcode/energy-state-analyzer/commit/bd526b45970c72d611e127e2ed7f111f2655f937))
|
|
31
|
+
* Inline esa-ignore suppression directives (#25) ([e7c2d41](https://github.com/cardamomcode/energy-state-analyzer/commit/e7c2d41d1347a4a10f7a921e077a45f745eade4c))
|
|
32
|
+
|
|
33
|
+
### 🐞 Bug Fixes
|
|
34
|
+
|
|
35
|
+
* Remove duplicate activation toasts on startup (#27) ([d144a00](https://github.com/cardamomcode/energy-state-analyzer/commit/d144a00770492252b50870b4cf3cebc1b44ba4c5))
|
|
36
|
+
* Count try/catch blocks toward nesting-depth violations (#28) ([d483c16](https://github.com/cardamomcode/energy-state-analyzer/commit/d483c165b4ba63a2e6a4c6eae5dfd62bc9868ecc))
|
|
37
|
+
|
|
38
|
+
<strong><small>[View changes on Github](https://github.com/cardamomcode/energy-state-analyzer/compare/489d195ffe125b4e3ebcb9905f561f2d94daf1ad..e7c2d41d1347a4a10f7a921e077a45f745eade4c)</small></strong>
|
|
39
|
+
|
|
14
40
|
## 0.3.0 - 2026-08-25
|
|
15
41
|
|
|
16
42
|
### 🚀 Features
|
package/README.md
CHANGED
|
@@ -4,19 +4,20 @@ Visualizes "energy states" in Python, F#, and TypeScript code as you edit: parts
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
7
|
+
Real-time analysis of the active Python, F#, or TypeScript file, re-run on every edit and on editor focus change, via these detectors (see [docs/detectors](docs/detectors/README.md) for full detail on each):
|
|
8
|
+
|
|
9
|
+
- [Cyclomatic complexity](docs/detectors/cyclomatic-complexity.md), too many independent execution paths.
|
|
10
|
+
- [Cognitive complexity](docs/detectors/cognitive-complexity.md), too hard to read due to nesting.
|
|
11
|
+
- [Excessive nesting](docs/detectors/excessive-nesting.md), control-flow blocks nested too deep.
|
|
12
|
+
- [File coherence](docs/detectors/file-coherence.md), files that have lost a single responsibility.
|
|
13
|
+
- [Magic numbers](docs/detectors/magic-numbers.md), unnamed numeric literals.
|
|
14
|
+
- [Magic strings](docs/detectors/magic-strings.md), unnamed string literals at decision points.
|
|
15
|
+
- [Parameter explosion](docs/detectors/parameter-explosion.md), functions with too many parameters.
|
|
16
|
+
- [Inversion opportunities](docs/detectors/inversion-opportunities.md), nested conditionals that could be guard clauses.
|
|
17
|
+
- [Primitive obsession](docs/detectors/primitive-obsession.md), strings/numbers standing in for a real type.
|
|
18
|
+
- [Match opportunities](docs/detectors/match-opportunities.md), if/elif chains that could be a match/switch.
|
|
19
|
+
- [Logical operator as control flow](docs/detectors/logical-operator-control-flow.md), an `if` hidden behind `&&`/`||`.
|
|
20
|
+
- [Opaque boolean literal](docs/detectors/opaque-boolean-literal.md), an unlabeled `true`/`false` at a call site.
|
|
20
21
|
|
|
21
22
|
Violations are shown three ways:
|
|
22
23
|
|
|
@@ -24,216 +25,54 @@ Violations are shown three ways:
|
|
|
24
25
|
- A hover tooltip explaining the specific violation.
|
|
25
26
|
- An entry in the Problems panel, sourced as "Energy State Analyzer".
|
|
26
27
|
|
|
27
|
-
For functions flagged as too complex (cyclomatic or cognitive), a progressive heatmap in the configured high-energy color (orange by default) is also painted across the function body: each contributing line (an `if`, `for`, `and`, etc.) is shaded from light to dark based on how much it drives up that function's complexity relative to its own worst line
|
|
28
|
+
For functions flagged as too complex (cyclomatic or cognitive), a progressive heatmap in the configured high-energy color (orange by default) is also painted across the function body: each contributing line (an `if`, `for`, `and`, etc.) is shaded from light to dark based on how much it drives up that function's complexity relative to its own worst line, so you can see exactly which branches to break apart first, instead of just knowing the function as a whole is complex.
|
|
28
29
|
|
|
29
30
|
## Energy and Entropy
|
|
30
31
|
|
|
31
|
-
The name is a deliberate analogy to thermodynamics,
|
|
32
|
-
|
|
33
|
-
In physics, energy constrains which microstates a system can occupy, and entropy counts how many of those microstates are compatible with what we observe: `S(E) = k_B ln Ω(E)`. Adding energy usually increases entropy, because there are more ways to distribute it, but *how* it's distributed matters just as much as how much there is. A hot object next to a cold one has lower entropy than the same total energy spread evenly across both, which is why heat spontaneously flows from hot to cold: the system moves toward the macrostate with more compatible microstates.
|
|
34
|
-
|
|
35
|
-
Code behaves the same way. A function's "energy" here is its cyclomatic/cognitive complexity, nesting depth, parameter count, and so on: the raw amount of decision-making and structure packed into it. Its "entropy" is the number of ways a reader can misunderstand it, the number of code paths a change can silently break, and the number of mental states a maintainer has to hold at once to reason about it correctly. Just as in physics, higher energy tends to raise entropy: a function with more branches and deeper nesting generally has more ways to go wrong. But it's not purely amount, *how* that complexity is arranged matters too:
|
|
36
|
-
|
|
37
|
-
- A long function with 20 sequential, flat `if`s is high cyclomatic complexity but comparatively low entropy: each branch is independent and easy to reason about in isolation (the "evenly spread" case).
|
|
38
|
-
- The same 20 decision points nested five deep inside each other is high *cognitive* complexity: the reader must hold all five levels in mind simultaneously, which is a much higher-entropy (harder to predict, easier to break) arrangement of the same energy.
|
|
39
|
-
|
|
40
|
-
This is why the extension tracks cyclomatic and cognitive complexity as separate metrics rather than one score: they capture the *energy* and its *arrangement* respectively. Guard clauses, extracted functions, and early returns don't necessarily remove energy from a codebase; they redistribute it into a lower-entropy arrangement, the code equivalent of letting a hot and cold object equilibrate: same total energy, fewer surprising configurations, easier to hold a correct mental model of.
|
|
41
|
-
|
|
42
|
-
Entropy here also depends on the observer, not just the code. A function's energy is fixed by what's written, but its entropy, the number of arrangements consistent with what someone currently knows, can grow over time even if the code never changes: the original author forgets the reasoning, or a new developer inherits the file with no context. This detector only measures the static, code-side half of that (the energy and its arrangement); the knowledge-decay half is a reason to keep energy low in the first place, since low-entropy code is cheaper to relearn from scratch.
|
|
43
|
-
|
|
44
|
-
## Cyclomatic Complexity
|
|
45
|
-
|
|
46
|
-
Counts the number of independent paths through a function. Starting from a base of **1**, every decision point adds **+1**, regardless of how deeply it's nested:
|
|
47
|
-
|
|
48
|
-
- `if` / `elif` / `while` / `for` / `except`
|
|
49
|
-
- `and` / `or`
|
|
50
|
-
- ternary (`a if cond else b`)
|
|
51
|
-
|
|
52
|
-
Two functions with the same number of `if`s score the same, whether those `if`s are sequential or nested five deep — it measures *how many paths exist*, not how hard the code is to follow.
|
|
53
|
-
|
|
54
|
-
## Cognitive Complexity
|
|
55
|
-
|
|
56
|
-
Modeled on [SonarSource's metric](https://www.sonarsource.com/resources/cognitive-complexity/): it measures how hard a function is to *read*, so nesting is penalized and straight-line control flow isn't.
|
|
57
|
-
|
|
58
|
-
- Each decision point (`if`, `elif`, `for`, `while`, `except`, ternary, nested `def`/`lambda`) adds **1 + current nesting depth**.
|
|
59
|
-
- `else` adds a flat **+1** — no nesting penalty, since it doesn't add a new branch to reason about.
|
|
60
|
-
- Nesting depth only increases when descending into a block body, so an `if` inside two other `if`s scores higher than three sequential `if`s at the top level, even though both have the same cyclomatic complexity.
|
|
61
|
-
- Chained boolean operators of the same kind (`a and b and c`) count as a **single** increment rather than one per operator; mixing `and`/`or` starts a new increment.
|
|
62
|
-
|
|
63
|
-
This project's implementation is a simplified first pass on the SonarSource spec: `for`/`while` `else` clauses are scored like `if`/`else`, boolean-chain merging only looks at the immediate parent operator, and recursive calls aren't specially detected.
|
|
64
|
-
|
|
65
|
-
### Interpreting the Scores
|
|
66
|
-
|
|
67
|
-
A raw number like "34" doesn't mean much on its own. For cyclomatic complexity, McCabe's original 1976 paper proposed risk bands that are still the closest thing to an industry consensus (echoed by SonarQube, ESLint's `complexity` rule, and NIST guidance):
|
|
68
|
-
|
|
69
|
-
| Score | Risk | Roughly |
|
|
70
|
-
| --- | --- | --- |
|
|
71
|
-
| 1–10 | Low | Simple, easy to test exhaustively |
|
|
72
|
-
| 11–20 | Moderate | Getting harder to cover with tests |
|
|
73
|
-
| 21–50 | High | Complex, testing all paths is impractical |
|
|
74
|
-
| 50+ | Very high | Effectively untestable |
|
|
75
|
-
|
|
76
|
-
Cognitive complexity has no equivalent formal consensus, since it's a newer, vendor-originated metric, but SonarSource's own convention (and this extension's defaults) treat **15** as the point where a function is hard enough to hold in your head that it's worth splitting up, with scores past 25 or so being seriously hard to follow regardless of how testable the underlying paths are.
|
|
77
|
-
|
|
78
|
-
The two scores can diverge on the same function: a flat function with many independent branches can have high cyclomatic complexity but modest cognitive complexity (easy to read, hard to test exhaustively), while deeply nested code can be the reverse. See [Energy and Entropy](#energy-and-entropy) above for why this extension tracks them separately rather than collapsing them into one score.
|
|
32
|
+
The name is a deliberate analogy to thermodynamics: a function's "energy" is its complexity, nesting, and parameter count, while its "entropy" is how many ways a reader can misunderstand it or a change can silently break it. See [docs/energy-and-entropy.md](docs/energy-and-entropy.md) for the full explanation of why cyclomatic and cognitive complexity are tracked as separate metrics rather than one score.
|
|
79
33
|
|
|
80
34
|
## Command-Line Usage
|
|
81
35
|
|
|
82
|
-
The same detectors also run headlessly, without VS Code
|
|
36
|
+
The same detectors also run headlessly, without VS Code, useful for CI or for an AI coding agent that wants to check the complexity of code it just generated and keep refactoring until it's clean:
|
|
83
37
|
|
|
84
38
|
```bash
|
|
85
39
|
npx energy-state-analyzer path/to/file.py # or .fs / .fsx / .ts
|
|
86
40
|
```
|
|
87
41
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
npm install --save-dev energy-state-analyzer
|
|
92
|
-
npx energy-state-analyzer path/to/file.py
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
It prints violations as JSON to stdout and exits `1` if any medium/high-severity violation was found (`0` otherwise), so it can gate a loop:
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
npx energy-state-analyzer path/to/file.py \
|
|
99
|
-
--medium-cyclomatic 8 --high-cyclomatic 12 \
|
|
100
|
-
--medium-cognitive 12 --high-cognitive 20
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
All threshold flags are optional: `--medium-nesting`, `--high-nesting`, `--medium-cyclomatic`, `--high-cyclomatic`, `--medium-cognitive`, `--high-cognitive`.
|
|
104
|
-
|
|
105
|
-
### Scanning a repo or subtree
|
|
106
|
-
|
|
107
|
-
Pass more than one path, a directory, or a `dir/**/*.ext`-style pattern to scan every supported file underneath it (skipping `node_modules`, `.git`, `dist`, `out`, `build`, `.next`, `coverage`, `.vscode-test`) and get an aggregated report instead of a single file's violations:
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
npx energy-state-analyzer src --report md
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
```
|
|
114
|
-
# Energy State Report
|
|
115
|
-
|
|
116
|
-
**3 files scanned** — 2 clean, 1 with violations
|
|
117
|
-
|
|
118
|
-
| File | Score | High | Medium | Low |
|
|
119
|
-
| --- | --- | --- | --- | --- |
|
|
120
|
-
| src/foo.py | 13 | 1 | 1 | 0 |
|
|
121
|
-
| src/bar.ts | 0 | 0 | 0 | 0 |
|
|
122
|
-
| src/baz.fs | 0 | 0 | 0 | 0 |
|
|
123
|
-
|
|
124
|
-
**Total score: 13** (1 high, 1 medium, 0 low)
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
`--report json` prints the same data as a structured `{ files, totalScore, totalCounts }` object instead. The per-file **score** is a simple heuristic — `1×low + 4×medium + 9×high` violation counts — meant for spotting hotspots and tracking direction over time, not a certified complexity metric.
|
|
128
|
-
|
|
129
|
-
Only one glob shape is supported: a trailing `**/*.ext` pattern on an otherwise literal directory prefix (e.g. `src/**/*.py`). There's no brace expansion, negation, or mid-path wildcards — pass explicit directories/files for anything more complex.
|
|
130
|
-
|
|
131
|
-
#### A report for humans: `--report human`
|
|
132
|
-
|
|
133
|
-
`--report md`/`--report json` are compact, built for scripts and PR comments. `--report human` produces a longer, prose-and-tables report meant to be read by a person auditing a repo or subtree: a section per flagged file, each with its findings translated into plain language, followed by a repo-wide "Total evaluation":
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
npx energy-state-analyzer src --report human
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
```
|
|
140
|
-
# Energy State Report
|
|
141
|
-
|
|
142
|
-
## Score legend
|
|
143
|
-
|
|
144
|
-
| Score | Risk | Roughly | Cyclomatic/cognitive complexity |
|
|
145
|
-
| --- | --- | --- | --- |
|
|
146
|
-
| 0.0 | None | No violations found | — |
|
|
147
|
-
| 0.1–3.9 | Low | Simple, easy to test exhaustively | 1–10 |
|
|
148
|
-
| 4.0–6.9 | Medium | Getting harder to cover with tests | 11–20 |
|
|
149
|
-
| 7.0–8.9 | High | Complex, testing all paths is impractical | 21–50 |
|
|
150
|
-
| 9.0–10.0 | Critical | Effectively untestable | 50+ |
|
|
151
|
-
|
|
152
|
-
**25 files scanned** — 8 clean, 17 flagged
|
|
153
|
-
|
|
154
|
-
## src/foo.py — High (score 7.8)
|
|
155
|
-
|
|
156
|
-
- **Cyclomatic complexity**: 1 function scores 34 — score 7.8 (High): complex, testing all paths is impractical.
|
|
157
|
-
- **Primitive obsession**: 2 findings (2 medium) — adjacent same-typed values a caller could silently swap without the compiler noticing.
|
|
158
|
-
|
|
159
|
-
...
|
|
160
|
-
|
|
161
|
-
## Total evaluation
|
|
162
|
-
|
|
163
|
-
**Repo score: 7.8 (High)** — driven by the worst file in the scan, `src/foo.py` (complex, testing all paths is impractical).
|
|
164
|
-
|
|
165
|
-
| Risk | Files |
|
|
166
|
-
| --- | --- |
|
|
167
|
-
| None | 8 |
|
|
168
|
-
| Low | 12 |
|
|
169
|
-
| Medium | 3 |
|
|
170
|
-
| High | 2 |
|
|
171
|
-
| Critical | 0 |
|
|
172
|
-
|
|
173
|
-
**51 total findings** (1 high, 25 medium, 25 low) — breadth of issues across the scan, independent of peak severity.
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Risk is reported on a 0.0–10.0 complexity score, sorted into the same None/Low/Medium/High/Critical levels used elsewhere in this tool, rather than a bespoke label set. The score is a direct re-expression of the McCabe risk table above: cyclomatic/cognitive complexity numbers are converted onto it by linear interpolation anchored at the same 10/20/50 breakpoints (see [Interpreting the Scores](#interpreting-the-scores)), so "High" here means the same thing it always has in this project, just expressed as a single number. Every other detector reports a finding count and severity instead, since it flags a pattern rather than a path count — a file with only non-complexity findings gets a fixed score from its worst one (Low 2.0 / Medium 5.0 / High 7.5), which can never reach Critical (Critical is reserved for genuinely extreme complexity).
|
|
177
|
-
|
|
178
|
-
Both the per-file score and the repo-wide "Repo score" are the **maximum** found, not an average. Averaging a file's (or a repo's) scores lets one severely complex function or file hide behind many trivial ones — nine functions at complexity 2 and one at 60 average to about 8 (which itself would still misleadingly read as "Low"), hiding exactly the function most worth fixing. Total finding counts are reported separately as a breadth indicator, deliberately not folded into the same number. Flagged files are listed worst-first.
|
|
179
|
-
|
|
180
|
-
### Diffing a PR against a base branch
|
|
181
|
-
|
|
182
|
-
`--base-ref <ref>` compares the current working tree against a git ref, so a GitHub Actions job can report whether a PR increased or decreased complexity relative to its base branch:
|
|
183
|
-
|
|
184
|
-
```bash
|
|
185
|
-
npx energy-state-analyzer --base-ref origin/main --report md
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
With no path arguments, changed files are discovered via `git diff --name-only <ref>...HEAD`; pass explicit paths to override that. Each changed file's pre-PR content is read with `git show <ref>:<path>` and re-analyzed in memory — a file that doesn't exist at the base ref (new file, or a rename `git diff` didn't resolve) is reported as `new` rather than erroring out.
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
# Energy State Diff vs `origin/main`
|
|
192
|
-
|
|
193
|
-
| File | Base | Head | Δ | Status |
|
|
194
|
-
| --- | --- | --- | --- | --- |
|
|
195
|
-
| src/foo.py | 4 | 13 | +9 | 🔴 worsened |
|
|
196
|
-
| src/bar.ts | 9 | 0 | -9 | 🟢 improved |
|
|
197
|
-
| src/new.py | — | 5 | — | 🆕 new |
|
|
198
|
-
|
|
199
|
-
_2 files changed, 1 worsened, 1 improved, 1 new._
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
The exit code in every mode (single-file, scan, or diff) follows the same rule: `1` if any medium/high-severity violation exists in the current (head) code, `0` otherwise — whether a diff made things better or worse is visible in the report, not encoded as a separate exit code. `energy-state-cli <single-file>` with no other flags keeps its original behavior (flat JSON violation array, same exit rule) unchanged.
|
|
42
|
+
See [docs/cli.md](docs/cli.md) for scanning a whole repo, aggregated markdown/JSON/human reports, and diffing a PR against a base branch.
|
|
203
43
|
|
|
204
44
|
## Requirements
|
|
205
45
|
|
|
206
|
-
The extension activates automatically when you open a Python, F#, or TypeScript file; it bundles its own grammars for parsing (via `web-tree-sitter`), so no external tools are required. F# files only get a `fsharp` language ID (and so trigger analysis) if you have an F# language extension installed (e.g. [Ionide](https://ionide.io/))
|
|
46
|
+
The extension activates automatically when you open a Python, F#, or TypeScript file; it bundles its own grammars for parsing (via `web-tree-sitter`), so no external tools are required. F# files only get a `fsharp` language ID (and so trigger analysis) if you have an F# language extension installed (e.g. [Ionide](https://ionide.io/)), VS Code otherwise treats `.fs` files as plain text.
|
|
207
47
|
|
|
208
48
|
## Extension Settings
|
|
209
49
|
|
|
210
|
-
Detector thresholds are configurable under **Settings → Energy State Analyzer
|
|
211
|
-
|
|
212
|
-
- `energyStateAnalyzer.cyclomaticComplexity.mediumThreshold` / `.highThreshold`
|
|
213
|
-
- `energyStateAnalyzer.cognitiveComplexity.mediumThreshold` / `.highThreshold`
|
|
214
|
-
- `energyStateAnalyzer.coherence.largeFunctionLines`
|
|
215
|
-
- `energyStateAnalyzer.coherence.maxLargeFunctions`
|
|
216
|
-
- `energyStateAnalyzer.coherence.singleDomainNameShare`
|
|
217
|
-
- `energyStateAnalyzer.matchOpportunity.minBranches`
|
|
218
|
-
- `energyStateAnalyzer.magicNumber.enabled`
|
|
219
|
-
- `energyStateAnalyzer.magicNumber.allowlist`
|
|
220
|
-
- `energyStateAnalyzer.magicString.enabled`
|
|
221
|
-
- `energyStateAnalyzer.magicString.minDuplicates`
|
|
222
|
-
- `energyStateAnalyzer.magicString.allowlist`
|
|
223
|
-
- `energyStateAnalyzer.colors.highEnergy` / `.mediumEnergy` / `.lowEnergy`
|
|
224
|
-
- `energyStateAnalyzer.colors.backgroundOpacity`
|
|
50
|
+
Detector thresholds are configurable under **Settings → Energy State Analyzer**. See each detector's doc (linked under Features above) for what a setting does; the keys and defaults are:
|
|
51
|
+
|
|
52
|
+
- `energyStateAnalyzer.cyclomaticComplexity.mediumThreshold` / `.highThreshold` (`10` / `15`)
|
|
53
|
+
- `energyStateAnalyzer.cognitiveComplexity.mediumThreshold` / `.highThreshold` (`15` / `25`)
|
|
54
|
+
- `energyStateAnalyzer.coherence.largeFunctionLines` (`20`)
|
|
55
|
+
- `energyStateAnalyzer.coherence.maxLargeFunctions` (`5`)
|
|
56
|
+
- `energyStateAnalyzer.coherence.singleDomainNameShare` (`0.7`)
|
|
57
|
+
- `energyStateAnalyzer.matchOpportunity.minBranches` (`3`)
|
|
58
|
+
- `energyStateAnalyzer.magicNumber.enabled` (`true`)
|
|
59
|
+
- `energyStateAnalyzer.magicNumber.allowlist` (`[0, 1, -1, 2]`)
|
|
60
|
+
- `energyStateAnalyzer.magicString.enabled` (`true`)
|
|
61
|
+
- `energyStateAnalyzer.magicString.minDuplicates` (`2`)
|
|
62
|
+
- `energyStateAnalyzer.magicString.allowlist` (`["", "utf-8", "__main__"]`)
|
|
63
|
+
- `energyStateAnalyzer.colors.highEnergy` / `.mediumEnergy` / `.lowEnergy` (`#fb8500` / `#ffb703` / `#99dd99`)
|
|
64
|
+
- `energyStateAnalyzer.colors.backgroundOpacity` (`0.1`)
|
|
225
65
|
|
|
226
66
|
Changes take effect immediately on the active editor.
|
|
227
67
|
|
|
68
|
+
To exclude files/folders (e.g. test fixtures, generated code) from both the extension's live analysis and the CLI, add a `.esaignore` file to your workspace root — see [`docs/cli.md`](docs/cli.md#excluding-files-and-folders-esaignore).
|
|
69
|
+
|
|
228
70
|
## Commands
|
|
229
71
|
|
|
230
|
-
- **Energy State Analyzer: Analyze Energy State** (`energy-state-analyzer.analyze`)
|
|
72
|
+
- **Energy State Analyzer: Analyze Energy State** (`energy-state-analyzer.analyze`), manually re-run analysis on the active editor.
|
|
231
73
|
|
|
232
74
|
## Known Issues
|
|
233
75
|
|
|
234
|
-
- Nesting depth and parameter count thresholds are not yet configurable
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
- The inversion-opportunities detector only fires for Python and TypeScript; F#'s grammar has no block-boundary node to anchor that heuristic on (see Architecture).
|
|
238
|
-
- TypeScript arrow functions aren't analyzed by complexity/parameter-count/coherence (same limitation Python already has for `lambda`) — only named `function` declarations and class methods are.
|
|
239
|
-
- The primitive-obsession detector's `in (a, b, c)`-style membership check only runs on Python; F#'s grammar has no direct equivalent, and TypeScript's idiom (`[...].includes(x)`) is a call expression rather than a comparison node.
|
|
76
|
+
- Nesting depth and parameter count thresholds are not yet configurable via VS Code settings, only cyclomatic complexity, cognitive complexity, the large-function coherence check, the match-opportunity branch count, and the magic-number/magic-string detectors are.
|
|
77
|
+
- TypeScript arrow functions aren't analyzed by complexity/parameter-count/coherence (same limitation Python already has for `lambda`), only named `function` declarations and class methods are.
|
|
78
|
+
- Several detectors have per-language gaps beyond the above, see the "Known limitations" section of the relevant [detector doc](docs/detectors/README.md).
|