energy-state-analyzer 0.1.2 → 0.3.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/.markdownlint.json +4 -0
- package/CHANGELOG.md +28 -1
- package/README.md +129 -6
- package/action.yml +87 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/web-tree-sitter.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/package.json +62 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
last_commit_released:
|
|
2
|
+
last_commit_released: 489d195ffe125b4e3ebcb9905f561f2d94daf1ad
|
|
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,33 @@ 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.3.0 - 2026-08-25
|
|
15
|
+
|
|
16
|
+
### 🚀 Features
|
|
17
|
+
|
|
18
|
+
* Add Kotlin support and lazy-load language grammars (#21) ([3a1078e](https://github.com/cardamomcode/energy-state-analyzer/commit/3a1078ecac31ea1574635cb01f794e767b098183))
|
|
19
|
+
* Repo scanning, PR baseline diffing, and a complexity-scored human report (#20) ([489d195](https://github.com/cardamomcode/energy-state-analyzer/commit/489d195ffe125b4e3ebcb9905f561f2d94daf1ad))
|
|
20
|
+
|
|
21
|
+
<strong><small>[View changes on Github](https://github.com/cardamomcode/energy-state-analyzer/compare/b3dda28dffdd3dc8c6cee2d5b07d5a99d50761cb..489d195ffe125b4e3ebcb9905f561f2d94daf1ad)</small></strong>
|
|
22
|
+
|
|
23
|
+
## 0.2.0 - 2026-08-25
|
|
24
|
+
|
|
25
|
+
### 🚀 Features
|
|
26
|
+
|
|
27
|
+
* Add opaque-boolean-literal detector (#16) ([d12447b](https://github.com/cardamomcode/energy-state-analyzer/commit/d12447bb07607c5fdc0b62781fb6df69ffbef1f4))
|
|
28
|
+
|
|
29
|
+
### 🐞 Bug Fixes
|
|
30
|
+
|
|
31
|
+
* Add braces to satisfy eslint curly rule (#10) ([8fd41cd](https://github.com/cardamomcode/energy-state-analyzer/commit/8fd41cde59103455a76ab1cbb504492578bbc3b3))
|
|
32
|
+
* Tailor primitive-obsession advice to each language (#7) ([cf7807b](https://github.com/cardamomcode/energy-state-analyzer/commit/cf7807b55de2efbb0befd8a6a8a282f3a33215f3))
|
|
33
|
+
* Cover primitive-obsession membership check with a test, correct heatmap color docs (#12) ([ba0050c](https://github.com/cardamomcode/energy-state-analyzer/commit/ba0050cb2b1894d0d90f7e80b273defa72f77277))
|
|
34
|
+
* Split magic-value detection into separate number/string detectors (#13) ([4aca127](https://github.com/cardamomcode/energy-state-analyzer/commit/4aca127af06ee4ea2d71a4ccb8d47d26ef22014e))
|
|
35
|
+
* Dogfood the extension's own CLI on its source, fix bugs it found (#14) ([bffadd3](https://github.com/cardamomcode/energy-state-analyzer/commit/bffadd3b2d50e700414ad313afdcc68be385125b))
|
|
36
|
+
* Suppress primitive-obsession swap-risk for Python keyword-only params (#15) ([eb1c592](https://github.com/cardamomcode/energy-state-analyzer/commit/eb1c59211823a6dd2dff152be8bd536531b4648a))
|
|
37
|
+
* Exempt Python module-level constants from magic-number detection (#17) ([0024607](https://github.com/cardamomcode/energy-state-analyzer/commit/0024607849eb0d15e5d5c5925a5e2b804b49616f))
|
|
38
|
+
|
|
39
|
+
<strong><small>[View changes on Github](https://github.com/cardamomcode/energy-state-analyzer/compare/0cbd44479c8048e4313fae49fcb157a52900d5f6..b3dda28dffdd3dc8c6cee2d5b07d5a99d50761cb)</small></strong>
|
|
40
|
+
|
|
14
41
|
## 0.1.2 - 2026-08-24
|
|
15
42
|
|
|
16
43
|
### 🐞 Bug Fixes
|
package/README.md
CHANGED
|
@@ -8,13 +8,15 @@ Visualizes "energy states" in Python, F#, and TypeScript code as you edit: parts
|
|
|
8
8
|
- **Cyclomatic complexity** — flags functions with too many independent execution paths (`if`/`for`/`while`/`except`/boolean operators/ternaries all count equally, regardless of nesting).
|
|
9
9
|
- **Cognitive complexity** — flags functions that are hard to *read*, weighting each decision point by how deeply it's nested and not penalizing early-return guard clauses.
|
|
10
10
|
- **Excessive nesting** — flags `if`/`for`/`while`/`with` blocks nested more than 3 levels deep.
|
|
11
|
-
- **File coherence** — flags files with too many functions or imports (a sign of "utils/helpers sprawl"), and separately flags files with too many large functions (regardless of total function count, so languages like F# with many small functions per module aren't penalized).
|
|
12
|
-
- **Magic
|
|
11
|
+
- **File coherence** — flags files with too many functions or imports (a sign of "utils/helpers sprawl"), and separately flags files with too many large functions (regardless of total function count, so languages like F# with many small functions per module aren't penalized). A file where most functions share a leading name word (e.g. all `extractFoo`/`extractBar`) is treated as one coherent domain and exempted from the function-count check.
|
|
12
|
+
- **Magic numbers** — flags numeric literals used outside of a named binding, an index/key position, or a default parameter value. Numbers get no free pass for "looking like prose" the way strings do, so this stays broad; an `energyStateAnalyzer.magicNumber.allowlist` setting (default `[0, 1, -1, 2]`) exempts the values that recur constantly without carrying hidden meaning.
|
|
13
|
+
- **Magic strings** — flags a string literal only where an unnamed one actually risks a silent typo: compared with `==`/`===`, checked for membership (Python's `x in (...)`), or used as a dict/object key. A message being logged, thrown, or returned isn't a decision point, so it's left alone entirely — as is any f-string/template-literal/`.format()`/`%`-formatted string, since a placeholder is itself evidence the string isn't a stand-in for an enum value. To cut single-use false positives further, a qualifying literal is only flagged once it recurs at a decision point at least `energyStateAnalyzer.magicString.minDuplicates` times (default `2`) across the file.
|
|
13
14
|
- **Parameter explosion** — flags functions with more than 5 parameters.
|
|
14
15
|
- **Inversion opportunities** — flags large dominant `if` blocks and nested validation chains that could be rewritten as guard clauses with early returns.
|
|
15
|
-
- **Primitive obsession** — flags consecutive same-typed primitive parameters (e.g. `lat: float, lon: float`) that callers can silently swap, and variables compared against 3+ distinct string literals (a de facto enum encoded as strings). Runs on Python, F#, and TypeScript; Python additionally flags a variable checked against a literal tuple/list/set in one `in` expression, since F# and TypeScript have no direct equivalent construct.
|
|
16
|
+
- **Primitive obsession** — flags consecutive same-typed primitive parameters (e.g. `lat: float, lon: float`) that callers can silently swap, and variables compared against 3+ distinct string literals (a de facto enum encoded as strings). Runs on Python, F#, and TypeScript; Python additionally flags a variable checked against a literal tuple/list/set in one `in` expression, since F# and TypeScript have no direct equivalent construct. In Python, a same-typed pair is not flagged when both parameters are keyword-only (after a bare `*` or `*args` in the signature) since a caller can no longer pass them positionally — named-parameter naming is still a weaker mitigation than a distinct type (`NewType`, a dataclass, etc.), since nothing stops a future `**kwargs`-splat call from transposing the values. This suppression doesn't apply to TypeScript (no argument-labeling syntax for positional params) or F# (named arguments are optional at the call site, so they don't prevent a positional call).
|
|
16
17
|
- **Match opportunities** — flags an `if`/`elif`/`elif` chain (or TypeScript's nested `else if`) of 3 or more branches that all compare the same single variable to a literal, suggesting a `match`/`switch` statement instead. Runs on Python, F#, and TypeScript.
|
|
17
18
|
- **Logical operator as control flow** — flags a bare `condition && doSomething()` (or `condition || fallback()`) statement, an `if` hidden behind a boolean operator instead of written as one. Runs on Python and TypeScript; not on F#, which has no such statement-level idiom in its grammar.
|
|
19
|
+
- **Opaque boolean literal** — flags a bare `true`/`false` passed positionally into a call (e.g. `configure(true)`), since a reader can't tell what it means without checking the callee's signature. Suppressed when the boolean is labeled at the call site: a Python keyword argument (`configure(retries=True)`), a TypeScript object-literal field (`configure({ retries: true })`), or F#'s named-argument syntax (`configure(retries = true)`) — unlike the primitive-obsession suppression above, F#'s named args count here even though they're optional, since this rule is about reader comprehension at this call site, not about preventing a future misuse. The preferred fix is usually splitting into two clearly named functions (`enableRetries()`/`disableRetries()`) or an enum; naming the argument is an acceptable but weaker mitigation. Deliberately conservative: only literal `true`/`false` are flagged, not bare `0`/`1`, to avoid noise on ordinary numeric arguments.
|
|
18
20
|
|
|
19
21
|
Violations are shown three ways:
|
|
20
22
|
|
|
@@ -22,7 +24,7 @@ Violations are shown three ways:
|
|
|
22
24
|
- A hover tooltip explaining the specific violation.
|
|
23
25
|
- An entry in the Problems panel, sourced as "Energy State Analyzer".
|
|
24
26
|
|
|
25
|
-
For functions flagged as too complex (cyclomatic or cognitive), a progressive
|
|
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 — so you can see exactly which branches to break apart first, instead of just knowing the function as a whole is complex.
|
|
26
28
|
|
|
27
29
|
## Energy and Entropy
|
|
28
30
|
|
|
@@ -60,6 +62,21 @@ Modeled on [SonarSource's metric](https://www.sonarsource.com/resources/cognitiv
|
|
|
60
62
|
|
|
61
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.
|
|
62
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.
|
|
79
|
+
|
|
63
80
|
## Command-Line Usage
|
|
64
81
|
|
|
65
82
|
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. Published to npm, so no clone or install step is required:
|
|
@@ -85,6 +102,105 @@ npx energy-state-analyzer path/to/file.py \
|
|
|
85
102
|
|
|
86
103
|
All threshold flags are optional: `--medium-nesting`, `--high-nesting`, `--medium-cyclomatic`, `--high-cyclomatic`, `--medium-cognitive`, `--high-cognitive`.
|
|
87
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.
|
|
203
|
+
|
|
88
204
|
## Requirements
|
|
89
205
|
|
|
90
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/)) — VS Code otherwise treats `.fs` files as plain text.
|
|
@@ -97,8 +213,13 @@ Detector thresholds are configurable under **Settings → Energy State Analyzer*
|
|
|
97
213
|
- `energyStateAnalyzer.cognitiveComplexity.mediumThreshold` / `.highThreshold`
|
|
98
214
|
- `energyStateAnalyzer.coherence.largeFunctionLines` — line count above which a function counts as "large" (default `20`).
|
|
99
215
|
- `energyStateAnalyzer.coherence.maxLargeFunctions` — number of large functions a file can contain before it's flagged (default `5`).
|
|
216
|
+
- `energyStateAnalyzer.coherence.singleDomainNameShare` — share (0-1) of a file's functions that must share a leading name word (e.g. `extractFoo`/`extractBar`) to be treated as one coherent domain, skipping the function-count sprawl check (default `0.7`).
|
|
100
217
|
- `energyStateAnalyzer.matchOpportunity.minBranches` — number of branches an if/elif chain must have, all keyed on the same variable, before it's flagged as a match/switch opportunity (default `3`).
|
|
101
|
-
- `energyStateAnalyzer.
|
|
218
|
+
- `energyStateAnalyzer.magicNumber.enabled` — whether to flag magic numbers (default `true`).
|
|
219
|
+
- `energyStateAnalyzer.magicNumber.allowlist` — numeric literals that are never flagged, regardless of context (default `[0, 1, -1, 2]`).
|
|
220
|
+
- `energyStateAnalyzer.magicString.enabled` — whether to flag magic strings (default `true`).
|
|
221
|
+
- `energyStateAnalyzer.magicString.minDuplicates` — number of times the same string literal must recur at a decision point before it's flagged (default `2`).
|
|
222
|
+
- `energyStateAnalyzer.magicString.allowlist` — string literals that are never flagged, regardless of context (default `["", "utf-8", "__main__"]`).
|
|
102
223
|
- `energyStateAnalyzer.colors.highEnergy` / `.mediumEnergy` / `.lowEnergy` — hex colors for the high/medium/low severity background tint and gutter icon (defaults `#fb8500` orange, `#ffb703` gold, `#99dd99` green).
|
|
103
224
|
- `energyStateAnalyzer.colors.backgroundOpacity` — opacity of the severity background tint (default `0.1`).
|
|
104
225
|
|
|
@@ -110,7 +231,9 @@ Changes take effect immediately on the active editor.
|
|
|
110
231
|
|
|
111
232
|
## Known Issues
|
|
112
233
|
|
|
113
|
-
- Nesting depth and parameter count thresholds are not yet configurable — only cyclomatic complexity, cognitive complexity, the large-function coherence check, the match-opportunity branch count, and the magic-
|
|
234
|
+
- Nesting depth and parameter count thresholds are not yet configurable — only cyclomatic complexity, cognitive complexity, the large-function coherence check, the match-opportunity branch count, and the magic-number/magic-string detectors are.
|
|
235
|
+
- The magic-string detector's decision-point scan (equality/membership/dict-key) and its formatted-string exemption are fully implemented for Python and partially for TypeScript (no `.includes()` membership support yet) and F# (no dict/subscript node, no interpolated-string exemption) — see the `LanguageAdapter` fields in `src/core/language.ts` for exactly what's modeled per language.
|
|
236
|
+
- The magic-string detector doesn't (yet) special-case enum-like keyword/default arguments (e.g. `mode="fast"`) as a lower-confidence decision point — only equality, membership, and dict/index-key positions count.
|
|
114
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).
|
|
115
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.
|
|
116
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.
|
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 }}
|