energy-state-analyzer 0.2.0 → 0.4.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/.markdownlint.json +4 -0
- package/.prettierignore +7 -0
- package/.prettierrc.json +7 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +24 -1
- package/README.md +40 -102
- package/action.yml +87 -0
- 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 +32 -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/grammars/tree-sitter-kotlin.wasm +0 -0
- package/package.json +11 -4
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: e7c2d41d1347a4a10f7a921e077a45f745eade4c
|
|
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,29 @@ 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.4.0 - 2026-08-26
|
|
15
|
+
|
|
16
|
+
### 🚀 Features
|
|
17
|
+
|
|
18
|
+
* Add .esaignore to exclude paths from the analyzer (#29) ([bd526b4](https://github.com/cardamomcode/energy-state-analyzer/commit/bd526b45970c72d611e127e2ed7f111f2655f937))
|
|
19
|
+
* Inline esa-ignore suppression directives (#25) ([e7c2d41](https://github.com/cardamomcode/energy-state-analyzer/commit/e7c2d41d1347a4a10f7a921e077a45f745eade4c))
|
|
20
|
+
|
|
21
|
+
### 🐞 Bug Fixes
|
|
22
|
+
|
|
23
|
+
* Remove duplicate activation toasts on startup (#27) ([d144a00](https://github.com/cardamomcode/energy-state-analyzer/commit/d144a00770492252b50870b4cf3cebc1b44ba4c5))
|
|
24
|
+
* Count try/catch blocks toward nesting-depth violations (#28) ([d483c16](https://github.com/cardamomcode/energy-state-analyzer/commit/d483c165b4ba63a2e6a4c6eae5dfd62bc9868ecc))
|
|
25
|
+
|
|
26
|
+
<strong><small>[View changes on Github](https://github.com/cardamomcode/energy-state-analyzer/compare/489d195ffe125b4e3ebcb9905f561f2d94daf1ad..e7c2d41d1347a4a10f7a921e077a45f745eade4c)</small></strong>
|
|
27
|
+
|
|
28
|
+
## 0.3.0 - 2026-08-25
|
|
29
|
+
|
|
30
|
+
### 🚀 Features
|
|
31
|
+
|
|
32
|
+
* Add Kotlin support and lazy-load language grammars (#21) ([3a1078e](https://github.com/cardamomcode/energy-state-analyzer/commit/3a1078ecac31ea1574635cb01f794e767b098183))
|
|
33
|
+
* Repo scanning, PR baseline diffing, and a complexity-scored human report (#20) ([489d195](https://github.com/cardamomcode/energy-state-analyzer/commit/489d195ffe125b4e3ebcb9905f561f2d94daf1ad))
|
|
34
|
+
|
|
35
|
+
<strong><small>[View changes on Github](https://github.com/cardamomcode/energy-state-analyzer/compare/b3dda28dffdd3dc8c6cee2d5b07d5a99d50761cb..489d195ffe125b4e3ebcb9905f561f2d94daf1ad)</small></strong>
|
|
36
|
+
|
|
14
37
|
## 0.2.0 - 2026-08-25
|
|
15
38
|
|
|
16
39
|
### 🚀 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,117 +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`.
|
|
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.
|
|
104
43
|
|
|
105
44
|
## Requirements
|
|
106
45
|
|
|
107
|
-
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.
|
|
108
47
|
|
|
109
48
|
## Extension Settings
|
|
110
49
|
|
|
111
|
-
Detector thresholds are configurable under **Settings → Energy State Analyzer
|
|
112
|
-
|
|
113
|
-
- `energyStateAnalyzer.cyclomaticComplexity.mediumThreshold` / `.highThreshold`
|
|
114
|
-
- `energyStateAnalyzer.cognitiveComplexity.mediumThreshold` / `.highThreshold`
|
|
115
|
-
- `energyStateAnalyzer.coherence.largeFunctionLines`
|
|
116
|
-
- `energyStateAnalyzer.coherence.maxLargeFunctions`
|
|
117
|
-
- `energyStateAnalyzer.coherence.singleDomainNameShare`
|
|
118
|
-
- `energyStateAnalyzer.matchOpportunity.minBranches`
|
|
119
|
-
- `energyStateAnalyzer.magicNumber.enabled`
|
|
120
|
-
- `energyStateAnalyzer.magicNumber.allowlist`
|
|
121
|
-
- `energyStateAnalyzer.magicString.enabled`
|
|
122
|
-
- `energyStateAnalyzer.magicString.minDuplicates`
|
|
123
|
-
- `energyStateAnalyzer.magicString.allowlist`
|
|
124
|
-
- `energyStateAnalyzer.colors.highEnergy` / `.mediumEnergy` / `.lowEnergy`
|
|
125
|
-
- `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`)
|
|
126
65
|
|
|
127
66
|
Changes take effect immediately on the active editor.
|
|
128
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
|
+
|
|
129
70
|
## Commands
|
|
130
71
|
|
|
131
|
-
- **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.
|
|
132
73
|
|
|
133
74
|
## Known Issues
|
|
134
75
|
|
|
135
|
-
- Nesting depth and parameter count thresholds are not yet configurable
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
- 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).
|
|
139
|
-
- 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.
|
|
140
|
-
- 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).
|
package/action.yml
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
name: 'Energy State Report'
|
|
2
|
+
description: 'Run energy-state-analyzer over a path or PR diff and post a Markdown report as a sticky PR comment.'
|
|
3
|
+
author: 'dbrattli'
|
|
4
|
+
branding:
|
|
5
|
+
icon: 'zap'
|
|
6
|
+
color: 'yellow'
|
|
7
|
+
|
|
8
|
+
# Requires the caller's checkout step to use `fetch-depth: 0` when base-ref is
|
|
9
|
+
# set, so both the head and base commits are present locally for `git diff`/
|
|
10
|
+
# `git show` (see src/cliModes.ts's changedFilesFromGit/readAtRef).
|
|
11
|
+
|
|
12
|
+
inputs:
|
|
13
|
+
path:
|
|
14
|
+
description: 'Path(s) to scan, space-separated. Ignored in diff mode (base-ref set), which only scans changed files.'
|
|
15
|
+
required: false
|
|
16
|
+
default: '.'
|
|
17
|
+
base-ref:
|
|
18
|
+
description: 'Git ref to diff the current HEAD against (e.g. origin/main). When set, runs in PR-diff mode instead of a full scan.'
|
|
19
|
+
required: false
|
|
20
|
+
default: ''
|
|
21
|
+
report-format:
|
|
22
|
+
description: 'Report format: md, json, or human. Diff mode (base-ref set) only supports md/json.'
|
|
23
|
+
required: false
|
|
24
|
+
default: 'md'
|
|
25
|
+
version:
|
|
26
|
+
description: 'energy-state-analyzer npm version or dist-tag to run via npx (e.g. 0.2.0, latest).'
|
|
27
|
+
required: false
|
|
28
|
+
default: 'latest'
|
|
29
|
+
fail-on-regression:
|
|
30
|
+
description: 'Fail the step when the analyzer reports a blocking result (scan mode: any medium/high violation; diff mode: any worsened file).'
|
|
31
|
+
required: false
|
|
32
|
+
default: 'true'
|
|
33
|
+
post-comment:
|
|
34
|
+
description: 'Post the report as a sticky PR comment. No-ops outside pull_request events.'
|
|
35
|
+
required: false
|
|
36
|
+
default: 'true'
|
|
37
|
+
comment-header:
|
|
38
|
+
description: 'Header used to identify and update the sticky PR comment across pushes.'
|
|
39
|
+
required: false
|
|
40
|
+
default: 'energy-state-report'
|
|
41
|
+
|
|
42
|
+
outputs:
|
|
43
|
+
report-path:
|
|
44
|
+
description: 'Path to the generated report file.'
|
|
45
|
+
value: ${{ steps.run.outputs.report-path }}
|
|
46
|
+
exit-code:
|
|
47
|
+
description: 'Exit code returned by energy-state-analyzer (0 = clean, 1 = blocking violations/regressions).'
|
|
48
|
+
value: ${{ steps.run.outputs.exit-code }}
|
|
49
|
+
|
|
50
|
+
runs:
|
|
51
|
+
using: composite
|
|
52
|
+
steps:
|
|
53
|
+
- name: Setup Node
|
|
54
|
+
uses: actions/setup-node@v5
|
|
55
|
+
with:
|
|
56
|
+
node-version: 22
|
|
57
|
+
|
|
58
|
+
- name: Run energy-state-analyzer
|
|
59
|
+
id: run
|
|
60
|
+
shell: bash
|
|
61
|
+
run: |
|
|
62
|
+
set +e
|
|
63
|
+
if [ -n "${{ inputs.base-ref }}" ]; then
|
|
64
|
+
npx --yes "energy-state-analyzer@${{ inputs.version }}" --base-ref "${{ inputs.base-ref }}" --report "${{ inputs.report-format }}" > energy-state-report.md
|
|
65
|
+
else
|
|
66
|
+
npx --yes "energy-state-analyzer@${{ inputs.version }}" ${{ inputs.path }} --report "${{ inputs.report-format }}" > energy-state-report.md
|
|
67
|
+
fi
|
|
68
|
+
code=$?
|
|
69
|
+
echo "exit-code=$code" >> "$GITHUB_OUTPUT"
|
|
70
|
+
echo "report-path=energy-state-report.md" >> "$GITHUB_OUTPUT"
|
|
71
|
+
# decision: always exit 0 here regardless of the analyzer's own exit code — a blocking
|
|
72
|
+
# result must still reach the comment step below (marocchino/sticky-pull-request-comment
|
|
73
|
+
# needs the report file to exist), so failure is deferred to the dedicated step at the
|
|
74
|
+
# end instead of short-circuiting the rest of this composite action.
|
|
75
|
+
exit 0
|
|
76
|
+
|
|
77
|
+
- name: Comment on PR
|
|
78
|
+
if: ${{ inputs.post-comment == 'true' && github.event_name == 'pull_request' }}
|
|
79
|
+
uses: marocchino/sticky-pull-request-comment@v2
|
|
80
|
+
with:
|
|
81
|
+
header: ${{ inputs.comment-header }}
|
|
82
|
+
path: ${{ steps.run.outputs.report-path }}
|
|
83
|
+
|
|
84
|
+
- name: Fail on regression
|
|
85
|
+
if: ${{ inputs.fail-on-regression == 'true' && steps.run.outputs.exit-code != '0' }}
|
|
86
|
+
shell: bash
|
|
87
|
+
run: exit ${{ steps.run.outputs.exit-code }}
|