falsegreen-js 0.4.0 → 0.6.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/CHANGELOG.md +99 -1
- package/README.md +25 -4
- package/dist/cases.js +20 -0
- package/dist/cli.js +14 -2
- package/dist/rules.js +577 -4
- package/dist/scan.d.ts +1 -0
- package/dist/scan.js +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,103 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.0] - 2026-06-29
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `JS25` (high, J1): the only assertion sits inside an array-iterator callback
|
|
13
|
+
(`forEach`/`map`/`filter`/`some`/`every`/`flatMap`). On an empty collection the
|
|
14
|
+
callback runs zero times, so nothing is checked and the test still goes green.
|
|
15
|
+
Fills the verified gap between C2/C2b (whose `hasAssertion` descends into the
|
|
16
|
+
callback and finds the assertion) and C21 (whose own-scope scan stops at the
|
|
17
|
+
callback and sees none). FP guards: an own-scope assertion, a non-empty
|
|
18
|
+
array-literal receiver, or an `expect.assertions`/`hasAssertions` guard suppress it.
|
|
19
|
+
- `JS30` (high, J2): literal-vs-literal assertion such as `expect(2).toBe(3)` or
|
|
20
|
+
chai `expect(x).to.equal(y)`, where both operands are literal nodes through an
|
|
21
|
+
equality matcher (`toBe`/`toEqual`/`toStrictEqual`/`toBeCloseTo`/`equal`/`equals`/
|
|
22
|
+
`eql`/`is`). The comparison is fixed at parse time, independent of any code. The
|
|
23
|
+
same-token case (`expect(1).toBe(1)`) stays with C5; object/array literals
|
|
24
|
+
(reference equality) and template literals with substitutions are excluded.
|
|
25
|
+
- `JS31` (low, J1): a `try/catch` whose try calls code that may throw and whose
|
|
26
|
+
catch neither asserts on the exception, re-raises, nor calls `fail()`. A unit that
|
|
27
|
+
stops throwing (a real regression) still passes green. Complement of JS11, which
|
|
28
|
+
owns the swallowed-assertion case; JS31 fires only when the try has a call but no
|
|
29
|
+
assertion and the catch is harmless.
|
|
30
|
+
- `JS27` (low, J3): `toHaveBeenCalled*` is the sole oracle on a locally-created
|
|
31
|
+
double (`jest.fn`/`vi.fn`/`spyOn`). The test confirms it called the double it set
|
|
32
|
+
up, not the unit's output or state. Gated to the unit level (a logger-spy call
|
|
33
|
+
check is legitimate at integration/e2e) and suppressed when any non-call-tracking
|
|
34
|
+
assertion is present. Sibling of JS8.
|
|
35
|
+
- `JS26` (low, J1): fake timers installed but never advanced. A `setTimeout`/
|
|
36
|
+
`setInterval` is armed under frozen fake timers and nothing in the test scope (nor
|
|
37
|
+
a sibling `before`/`after` hook) calls `runAllTimers`/`advanceTimersByTime`/`tick`,
|
|
38
|
+
so the scheduled callback never fires and the assertion reads un-mutated state. The
|
|
39
|
+
opposite of C16 (uncontrolled timer). Requires an assertion in scope; a flush in
|
|
40
|
+
the body or an enclosing hook suppresses it.
|
|
41
|
+
- `JS29` (low, J6): an `expect(...).resolves`/`.rejects` chain that is a bare
|
|
42
|
+
statement, not awaited or returned. The matcher settles asynchronously, so a
|
|
43
|
+
floating chain finishes green before it resolves. The statically-provable subset of
|
|
44
|
+
the skipped JS20, keyed on the explicit `.resolves`/`.rejects` marker so no type
|
|
45
|
+
inference is needed.
|
|
46
|
+
- `C8b` (low, J4): `toBeCloseTo` called with no precision argument, so the default
|
|
47
|
+
2-digit tolerance applies. The js analogue of `assertAlmostEqual`/`pytest.approx`
|
|
48
|
+
with no tolerance, ported from `falsegreen`. Implicit-precision only; a
|
|
49
|
+
literal-vs-literal `toBeCloseTo` stays with JS30.
|
|
50
|
+
- `C11a` (low, J2): self-confirming literal. The expected value is bound from the
|
|
51
|
+
same call under test (`const e = foo(); expect(foo()).toBe(e)`), so the oracle
|
|
52
|
+
confirms the code against itself. Ported from `falsegreen`; the bound initializer
|
|
53
|
+
must provably be the SUT call (its source text equals the expect subject's), so a
|
|
54
|
+
literal or a different-call binding stays clean.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
- `C8` (exact float) now fires only when the subject is a real (non-literal) value;
|
|
58
|
+
a literal-vs-literal float (`expect(0.1).toBe(0.3)`) is owned by the stronger JS30
|
|
59
|
+
lane, so the two no longer double-report.
|
|
60
|
+
|
|
61
|
+
## [0.5.0] - 2026-06-28
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
- `JS23` (high, J1): `expect.assertions(N)` with a numeric `N` higher than the unconditional,
|
|
65
|
+
reachable, non-nested `expect()` calls that can run. The guard can never be met, so the test
|
|
66
|
+
passes without ever exercising the count it claims. Fires only when `N` is a numeric literal
|
|
67
|
+
and the shortfall is provable: an `expect` in a loop, a branch, a `.then`/callback, or a
|
|
68
|
+
helper makes the count indeterminate and suppresses the finding. `expect.hasAssertions()`
|
|
69
|
+
carries no count and is skipped. This is the implemented sibling of the still-skipped JS16.
|
|
70
|
+
- `JS24` (low, J4): a Cypress query chain (`cy.get`/`cy.find`/`cy.contains`) used as a statement
|
|
71
|
+
with no terminating `.should`/`.and` and no `expect` inside a `.then` callback. The query
|
|
72
|
+
produces a subject that is never asserted, the cy.* analogue of JS13. Action commands
|
|
73
|
+
(`click`/`type`/`visit`/...) do work rather than just query, so a chain ending in one stays
|
|
74
|
+
clean, as does a chain that ends in `.should`/`.and` or asserts in `.then`.
|
|
75
|
+
- CLI `--enable <codes>` (and `--enable=...`): re-activates listed off or opt-in codes at their
|
|
76
|
+
catalog severity, flipping a default-off code on. It cannot raise a code above catalog
|
|
77
|
+
severity. `--disable` wins over `--enable`, so a code passed to both stays off.
|
|
78
|
+
- `examples/` tree (#47): a worked sample for every emitted code, a BAD test the scanner flags
|
|
79
|
+
paired with a CLEAN look-alike one token away that it leaves alone. Files are grouped by
|
|
80
|
+
RiskGroup (`effectiveness`, `execution`, `nondeterminism`, `dependency`), with `cypress.cy.ts`
|
|
81
|
+
for the Cypress code and `diagnostics.test.ts` for the opt-in maintainability group. C16 keeps
|
|
82
|
+
a separate frozen-clock file because the fake-timer signal is file-wide. `vitest.config.ts`
|
|
83
|
+
excludes `examples/**` from collection, and `test/examples.test.ts` scans each file with
|
|
84
|
+
`analyze(parse(...))` to assert every code fires in its file, with a drift guard that fails if a
|
|
85
|
+
new default-on code lands without an example. The config-audit-only PL series scans Jest/Vitest
|
|
86
|
+
config rather than a test file, so it has no test-file example.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
- `JS8` now also catches the `jest.spyOn`/`vi.spyOn` form: a spy with a canned return
|
|
90
|
+
(`mockReturnValue`/`mockResolvedValue`/`mockImplementation`) whose spied target root is also an
|
|
91
|
+
`expect` subject. The test asserts the canned value, not real behaviour. Conservative
|
|
92
|
+
same-binding guard: spying a collaborator (a different object) stays clean, and asserting on
|
|
93
|
+
the spy handle itself (`expect(spy).toHaveBeenCalled()`) is not treated as the subject.
|
|
94
|
+
- `JS3` gains a distinct detail when the snapshot is an empty inline baseline:
|
|
95
|
+
`toMatchInlineSnapshot()` with no argument, or an empty or whitespace-only string baseline,
|
|
96
|
+
passes by writing itself on the first run. A populated inline snapshot keeps the existing
|
|
97
|
+
detail; the snapshot-only detection logic is unchanged.
|
|
98
|
+
|
|
99
|
+
### Docs
|
|
100
|
+
- `CONTRIBUTING.md` documents the FP-boundary decisions that previously lived only in
|
|
101
|
+
source comments: the admission criteria for a new code (statically provable, FP-guarded,
|
|
102
|
+
ships with a CLEAN look-alike one token from the BAD, carries a catalog entry, clears the
|
|
103
|
+
panel and principal-reviewer gate) and the standing per-code rules for C44, C6, JS5, C16,
|
|
104
|
+
JS23, JS24, and JS8 (#51).
|
|
105
|
+
|
|
9
106
|
## [0.4.0] - 2026-06-28
|
|
10
107
|
|
|
11
108
|
### Fixed
|
|
@@ -176,7 +273,8 @@ All notable changes to this project are documented here. The format is based on
|
|
|
176
273
|
- pre-commit hook (`.pre-commit-hooks.yaml`), CI matrix (Node 18/20/22), and an npm
|
|
177
274
|
trusted-publishing release workflow.
|
|
178
275
|
|
|
179
|
-
[Unreleased]: https://github.com/vinicq/falsegreen-js/compare/v0.
|
|
276
|
+
[Unreleased]: https://github.com/vinicq/falsegreen-js/compare/v0.5.0...HEAD
|
|
277
|
+
[0.5.0]: https://github.com/vinicq/falsegreen-js/compare/v0.4.0...v0.5.0
|
|
180
278
|
[0.4.0]: https://github.com/vinicq/falsegreen-js/compare/v0.3.0...v0.4.0
|
|
181
279
|
[0.3.0]: https://github.com/vinicq/falsegreen-js/compare/v0.2.0...v0.3.0
|
|
182
280
|
[0.2.0]: https://github.com/vinicq/falsegreen-js/compare/v0.1.0...v0.2.0
|
package/README.md
CHANGED
|
@@ -15,7 +15,14 @@ AST scan, no code execution. Sibling of [`falsegreen`](https://github.com/vinicq
|
|
|
15
15
|
|
|
16
16
|
Covers `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.mts`, `.cts`.
|
|
17
17
|
|
|
18
|
-
**The falsegreen family
|
|
18
|
+
**The falsegreen family** (install the one for your stack):
|
|
19
|
+
|
|
20
|
+
| Tool | Stack | Install | Package |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| [falsegreen](https://github.com/vinicq/falsegreen) | Python / pytest | `pip install falsegreen` | [PyPI](https://pypi.org/project/falsegreen/) |
|
|
23
|
+
| **falsegreen-js** | JS / TS | `npm i -D falsegreen-js` (`npx falsegreen-js`) | [npm](https://www.npmjs.com/package/falsegreen-js) |
|
|
24
|
+
| [robotframework-falsegreen](https://github.com/vinicq/robotframework-falsegreen) | Robot Framework | `pip install robotframework-falsegreen` | [PyPI](https://pypi.org/project/robotframework-falsegreen/) |
|
|
25
|
+
| [falsegreen-skill](https://github.com/vinicq/falsegreen-skill) | semantic LLM pass | `npx falsegreen-skill analyze <path>` | [npm](https://www.npmjs.com/package/falsegreen-skill) |
|
|
19
26
|
|
|
20
27
|
## Why
|
|
21
28
|
|
|
@@ -44,6 +51,7 @@ npx falsegreen-js --output report.json # write to a file
|
|
|
44
51
|
npx falsegreen-js --output .falsegreen/ # write report.<ext> into a directory
|
|
45
52
|
npx falsegreen-js --config-audit # audit Jest/Vitest config (project-layer PL codes)
|
|
46
53
|
npx falsegreen-js --disable C7,JS3
|
|
54
|
+
npx falsegreen-js --enable D8,M2 # re-activate off/opt-in codes at catalog severity
|
|
47
55
|
```
|
|
48
56
|
|
|
49
57
|
Each finding is reported with its pyramid level (unit / integration / e2e, read from the file's imports) and a one-line fix hint, and the summary breaks the findings down by level and lists the most common fixes. `--output` takes a file or a directory: an extension-less or trailing-slash path (e.g. `.falsegreen/`) receives `report.<ext>` for the chosen format. Reports are run artifacts; keep the output directory gitignored.
|
|
@@ -129,7 +137,9 @@ line up in the research. `JS*` codes are ecosystem-specific.
|
|
|
129
137
|
| C20 | high | assertion in unreachable code (after a `return`/`throw`/`process.exit`, a `break`, a both-arms-terminating `if`, or an exhaustive `switch`) — it never runs |
|
|
130
138
|
| C23 | low | reads a real file at a literal path, or a hard-coded URL (mystery guest) |
|
|
131
139
|
| C8 | low | exact equality on a float (use `toBeCloseTo`) |
|
|
140
|
+
| C8b | low | `toBeCloseTo` with no precision argument — the default 2-digit tolerance may be too loose |
|
|
132
141
|
| C9 | low | `toThrow()` with no error type or message — accepts any error |
|
|
142
|
+
| C11a | low | self-confirming literal — the expected value is bound from the same call under test (`const e = foo(); expect(foo()).toBe(e)`) |
|
|
133
143
|
| C16 | low | result depends on `Date.now`, `Math.random`, or a fixed timer |
|
|
134
144
|
| C18 | low | compares `String(x)` / `JSON.stringify(x)` / `` `${x}` `` to a literal (formatting, not value) |
|
|
135
145
|
| C21 | low | every assertion is conditional — none runs unconditionally |
|
|
@@ -152,6 +162,14 @@ line up in the research. `JS*` codes are ecosystem-specific.
|
|
|
152
162
|
| JS18 | low | test takes a `done` callback instead of async/await — a mistimed `done` passes early |
|
|
153
163
|
| JS21 | high | matcher referenced but never called (`expect(x).toBe` with no `()`) — the assertion never runs |
|
|
154
164
|
| JS22 | high | empty `it.each`/`test.each` table — generated with zero cases, never runs |
|
|
165
|
+
| JS23 | high | `expect.assertions(N)` with fewer unconditional `expect()` calls than N — the guard can never be met |
|
|
166
|
+
| JS24 | low | Cypress query (`cy.get`/`cy.find`/`cy.contains`) as a loose statement with no terminating `.should`/`.and` and no `expect` in `.then` — its result is never asserted |
|
|
167
|
+
| JS25 | high | the only assertion sits inside an array-iterator callback (`forEach`/`map`/`filter`/`some`/`every`/`flatMap`) — runs zero times on an empty collection |
|
|
168
|
+
| JS26 | low | fake timers installed but never advanced (`runAllTimers`/`advanceTimersByTime`/`tick`) — the scheduled callback never fires, so the assertion reads un-mutated state |
|
|
169
|
+
| JS27 | low | `toHaveBeenCalled*` is the sole oracle on a locally-created double — verifies wiring, not behaviour |
|
|
170
|
+
| JS29 | low | `expect(...).resolves`/`.rejects` chain is a bare statement, not awaited or returned — the test finishes green before the matcher settles |
|
|
171
|
+
| JS30 | high | literal-vs-literal assertion (`expect(2).toBe(3)`, chai `expect(x).to.equal(y)`) — both operands are fixed at parse time |
|
|
172
|
+
| JS31 | low | `try/catch` swallows a possible throw with no assertion on the exception — a unit that stops throwing still passes green |
|
|
155
173
|
|
|
156
174
|
Each code carries a judgment tag (J1-J6) shared with the
|
|
157
175
|
[falsegreen-skill](https://github.com/vinicq/falsegreen-skill) semantic framework.
|
|
@@ -186,8 +204,11 @@ Some catalog codes were reviewed and left out, on purpose:
|
|
|
186
204
|
- **JS20** (a Promise compared without `resolves`/`rejects`): telling that a value is a
|
|
187
205
|
Promise needs type information the AST does not carry, so it would be too noisy.
|
|
188
206
|
- **JS12** (a floating promise whose `expect` is never returned): already covered by JS7.
|
|
189
|
-
- **JS16** (`async` test with no `expect.assertions(n)`): the absence of a guard is not a
|
|
190
|
-
smell on its own; flagging it would fire on most async tests.
|
|
207
|
+
- **JS16** (`async` test with no `expect.assertions(n)`): the *absence* of a guard is not a
|
|
208
|
+
smell on its own; flagging it would fire on most async tests. The implemented sibling is
|
|
209
|
+
`JS23`, which fires on a present-but-unsatisfiable guard: `expect.assertions(N)` with a
|
|
210
|
+
numeric `N` higher than the unconditional `expect()` calls that can run, so the count can
|
|
211
|
+
never be met.
|
|
191
212
|
- **JS14** (a giant inline snapshot): a readability and review-noise concern, not a
|
|
192
213
|
false-green one. The snapshot still protects, so it belongs to the diagnostic group and is
|
|
193
214
|
better served by `eslint-plugin-jest` (`no-large-snapshots`) as an opt-in lint rule.
|
|
@@ -226,7 +247,7 @@ Optional. `falsegreen.json`, `.falsegreenrc.json`, or a `"falsegreen"` key in
|
|
|
226
247
|
}
|
|
227
248
|
```
|
|
228
249
|
|
|
229
|
-
Precedence: CLI `--disable` > config `disable`/`severity` > catalog default.
|
|
250
|
+
Precedence: CLI `--disable` > CLI `--enable` > config `disable`/`severity` > catalog default. `--enable <codes>` re-activates listed off or opt-in codes at their catalog severity (it flips a default-off code on; it cannot raise a code above catalog). A code passed to both `--enable` and `--disable` stays off — `--disable` wins.
|
|
230
251
|
|
|
231
252
|
## Scope and honesty
|
|
232
253
|
|
package/dist/cases.js
CHANGED
|
@@ -59,6 +59,16 @@ export const CASES = {
|
|
|
59
59
|
JS18: { title: "test takes a done callback instead of async/await — a done called too early (or in a floating promise) passes before the assertions run", group: "execution", severity: "low", defaultOn: true, judgment: "J1" },
|
|
60
60
|
JS21: { title: "matcher referenced but never called (expect(x).toBe with no ()) — the assertion never executes", group: "execution", severity: "high", defaultOn: true, judgment: "J1" },
|
|
61
61
|
JS22: { title: "empty it.each/test.each table — the test is generated with zero cases and never runs", group: "execution", severity: "high", defaultOn: true, judgment: "J1" },
|
|
62
|
+
JS23: { title: "expect.assertions(N) with fewer unconditional expect() calls than N — the guard can never be met", group: "execution", severity: "high", defaultOn: true, judgment: "J1" },
|
|
63
|
+
JS24: { title: "Cypress query (cy.get/find/contains) with no terminating .should/.and and no expect in .then — its result is never asserted", group: "effectiveness", severity: "low", defaultOn: true, judgment: "J4" },
|
|
64
|
+
JS25: { title: "the only assertion sits inside an array-iterator callback (forEach/map/filter/some/every/flatMap) — runs zero times on an empty collection", group: "execution", severity: "high", defaultOn: true, judgment: "J1" },
|
|
65
|
+
JS26: { title: "fake timers installed but never advanced — the scheduled callback never fires, so the assertion runs against un-mutated state", group: "execution", severity: "low", defaultOn: true, judgment: "J1" },
|
|
66
|
+
JS27: { title: "toHaveBeenCalled* is the sole oracle on a locally-created double — verifies wiring, not behaviour", group: "dependency", severity: "low", defaultOn: true, judgment: "J3" },
|
|
67
|
+
JS29: { title: "expect(...).resolves/.rejects chain is a bare statement, not awaited or returned — the test finishes green before the matcher settles", group: "execution", severity: "low", defaultOn: true, judgment: "J6" },
|
|
68
|
+
JS30: { title: "literal-vs-literal assertion (expect(1).toBe(1)) — both operands are fixed at parse time, independent of any code", group: "effectiveness", severity: "high", defaultOn: true, judgment: "J2" },
|
|
69
|
+
JS31: { title: "try/catch swallows a SUT throw with no assertion on the exception — a unit that stops throwing still passes green", group: "execution", severity: "low", defaultOn: true, judgment: "J1" },
|
|
70
|
+
C8b: { title: "toBeCloseTo with no precision argument — the default 2-digit tolerance may be too loose for the value under test", group: "effectiveness", severity: "low", defaultOn: true, judgment: "J4" },
|
|
71
|
+
C11a: { title: "self-confirming literal — the expected value is bound from the same call/expression under test (const e = foo(); expect(foo()).toBe(e))", group: "effectiveness", severity: "low", defaultOn: true, judgment: "J2" },
|
|
62
72
|
// --- diagnostic group (maintainability; default off, opt-in via --diagnostics
|
|
63
73
|
// or config severity). These are NOT false-green: the test still protects. They
|
|
64
74
|
// are a "plus" for test-code health, mirroring falsegreen's D/M group. -------
|
|
@@ -155,6 +165,16 @@ export const FIX_HINTS = {
|
|
|
155
165
|
JS18: "use async/await instead of the done callback",
|
|
156
166
|
JS21: "call the matcher (add ()) so the assertion executes",
|
|
157
167
|
JS22: "add at least one row to the it.each/test.each table",
|
|
168
|
+
JS23: "make the unconditional expect() count match expect.assertions(N), or remove the guard",
|
|
169
|
+
JS24: "end the cy query in .should()/.and(), or assert in .then()",
|
|
170
|
+
JS25: "add an assertion outside the iterator callback so it runs even when the collection is empty",
|
|
171
|
+
JS26: "advance the fake timers (runAllTimers/advanceTimersByTime/tick) before asserting",
|
|
172
|
+
JS27: "assert the unit's output or state, not just that the double was called",
|
|
173
|
+
JS29: "await or return the resolves/rejects assertion so the matcher settles",
|
|
174
|
+
JS30: "assert a real value against an independent expected one, not literal against literal",
|
|
175
|
+
JS31: "assert on the caught error, re-throw, call fail(), or wrap the call in expect().toThrow()",
|
|
176
|
+
C8b: "pass a precision argument to toBeCloseTo(), or choose an explicit tolerance",
|
|
177
|
+
C11a: "bind the expected value independently of the call under test",
|
|
158
178
|
D1: "give each assertion a message, or split the test",
|
|
159
179
|
D3: "remove the duplicate assertion",
|
|
160
180
|
D4: "add titled cases to it.each/test.each",
|
package/dist/cli.js
CHANGED
|
@@ -34,6 +34,7 @@ Usage:
|
|
|
34
34
|
falsegreen-js --config-audit audit Jest/Vitest config (project-layer PL codes)
|
|
35
35
|
falsegreen-js --diagnostics also report the opt-in maintainability group (D*/M*)
|
|
36
36
|
falsegreen-js --disable C7,JS3 turn off specific codes
|
|
37
|
+
falsegreen-js --enable D8,M2 re-activate off/opt-in codes at catalog severity (--disable wins)
|
|
37
38
|
falsegreen-js --version
|
|
38
39
|
falsegreen-js --help
|
|
39
40
|
|
|
@@ -52,6 +53,7 @@ function parseArgs(argv) {
|
|
|
52
53
|
let baseline;
|
|
53
54
|
let writeBaselinePath;
|
|
54
55
|
const disable = new Set();
|
|
56
|
+
const enable = new Set();
|
|
55
57
|
// An optional-value flag (--baseline / --write-baseline) consumes the next
|
|
56
58
|
// token only when it is a value, not another flag.
|
|
57
59
|
const optionalValue = (next) => (next !== undefined && !next.startsWith("-")) ? next : undefined;
|
|
@@ -115,6 +117,14 @@ function parseArgs(argv) {
|
|
|
115
117
|
a.slice("--disable=".length).split(",").map((s) => s.trim())
|
|
116
118
|
.filter(Boolean).forEach((c) => disable.add(c));
|
|
117
119
|
}
|
|
120
|
+
else if (a === "--enable") {
|
|
121
|
+
const v = argv[++i] ?? "";
|
|
122
|
+
v.split(",").map((s) => s.trim()).filter(Boolean).forEach((c) => enable.add(c));
|
|
123
|
+
}
|
|
124
|
+
else if (a.startsWith("--enable=")) {
|
|
125
|
+
a.slice("--enable=".length).split(",").map((s) => s.trim())
|
|
126
|
+
.filter(Boolean).forEach((c) => enable.add(c));
|
|
127
|
+
}
|
|
118
128
|
else if (a.startsWith("-")) {
|
|
119
129
|
process.stderr.write(`falsegreen-js: unknown option ${a}\n`);
|
|
120
130
|
process.exit(2);
|
|
@@ -124,7 +134,7 @@ function parseArgs(argv) {
|
|
|
124
134
|
}
|
|
125
135
|
const fmt = format ?? (json ? "json" : "text");
|
|
126
136
|
return {
|
|
127
|
-
paths, fmt, staged, help, version, diagnostics, configAudit, disable,
|
|
137
|
+
paths, fmt, staged, help, version, diagnostics, configAudit, disable, enable,
|
|
128
138
|
output, baseline, writeBaselinePath,
|
|
129
139
|
};
|
|
130
140
|
}
|
|
@@ -234,7 +244,9 @@ export function renderText(findings) {
|
|
|
234
244
|
}
|
|
235
245
|
function scan(opt) {
|
|
236
246
|
const config = loadConfig();
|
|
237
|
-
const scanOpts = {
|
|
247
|
+
const scanOpts = {
|
|
248
|
+
config, cliDisable: opt.disable, cliEnable: opt.enable, diagnostics: opt.diagnostics,
|
|
249
|
+
};
|
|
238
250
|
if (opt.staged)
|
|
239
251
|
return stagedFiles().flatMap((f) => scanFile(f, scanOpts));
|
|
240
252
|
return scanPaths(opt.paths.length ? opt.paths : ["."], scanOpts);
|
package/dist/rules.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DIAGNOSTIC_THRESHOLDS } from "./cases.js";
|
|
|
4
4
|
import { ASSERT_ROOTS, ASSERT_METHODS, SNAPSHOT_MATCHERS, EQUALITY_MATCHERS, VUE_SVELTE_ASYNC, oracleKind, } from "./oracles.js";
|
|
5
5
|
import { lineOf } from "./parse.js";
|
|
6
6
|
import { assertionsInDeadCode, hasUnconditionalAssertion } from "./cfg.js";
|
|
7
|
+
import { detectPyramidLevel } from "./level.js";
|
|
7
8
|
// --- test framework vocabulary (runner-agnostic) ---------------------------
|
|
8
9
|
// it/test/specify (Jest, Vitest, Mocha, Jasmine, AVA, node:test, Cypress,
|
|
9
10
|
// Playwright, tap). describe/context/suite are suites. fit/fdescribe focus and
|
|
@@ -14,6 +15,23 @@ import { assertionsInDeadCode, hasUnconditionalAssertion } from "./cfg.js";
|
|
|
14
15
|
const TEST_BLOCK_ROOTS = new Set(["it", "test", "specify"]);
|
|
15
16
|
const SUITE_ROOTS = new Set(["describe", "context", "suite", "fdescribe", "xdescribe", "fcontext", "xcontext"]);
|
|
16
17
|
const FOCUS_NAMES = new Set(["fit", "fdescribe", "fcontext"]);
|
|
18
|
+
// Array-iterator methods whose callback runs once per element — zero times on an
|
|
19
|
+
// empty collection. An assertion that lives ONLY inside one of these callbacks
|
|
20
|
+
// (JS25) runs zero times when the receiver is empty: green with nothing checked.
|
|
21
|
+
const ARRAY_ITERATOR_METHODS = new Set(["forEach", "map", "filter", "some", "every", "flatMap"]);
|
|
22
|
+
// Equality matchers across runners for the literal-vs-literal (JS30) and
|
|
23
|
+
// self-confirming-literal (C11a) lanes: Jest/Vitest toBe family + toBeCloseTo,
|
|
24
|
+
// plus chai/AVA equal/equals/eql/is. Broader than EQUALITY_MATCHERS (which gates
|
|
25
|
+
// the Jest-only C5/C7/C8 lanes) on purpose.
|
|
26
|
+
const EQ_MATCHERS_ANY = new Set([
|
|
27
|
+
"toBe", "toEqual", "toStrictEqual", "toBeCloseTo", "equal", "equals", "eql", "is",
|
|
28
|
+
]);
|
|
29
|
+
// toHaveBeenCalled* family (JS27): matchers that only assert a double was invoked.
|
|
30
|
+
const CALL_TRACKING_MATCHERS = new Set([
|
|
31
|
+
"toHaveBeenCalled", "toHaveBeenCalledTimes", "toHaveBeenCalledWith",
|
|
32
|
+
"toHaveBeenLastCalledWith", "toHaveBeenNthCalledWith", "toBeCalled",
|
|
33
|
+
"toBeCalledTimes", "toBeCalledWith", "toHaveBeenCalledOnce",
|
|
34
|
+
]);
|
|
17
35
|
const SKIP_NAMES = new Set(["xit", "xdescribe", "xcontext", "xspecify"]);
|
|
18
36
|
// --- C48 dark-patch: a test that flips a known test-mode flag then asserts ---
|
|
19
37
|
// env keys (process.env.<KEY> = <test-mode value>) whose name means "we are under
|
|
@@ -78,6 +96,58 @@ function expectRooted(e) {
|
|
|
78
96
|
}
|
|
79
97
|
return false;
|
|
80
98
|
}
|
|
99
|
+
// --- JS24: Cypress query chain with no terminating assertion ---------------
|
|
100
|
+
const CY_QUERY_COMMANDS = new Set(["get", "find", "contains"]);
|
|
101
|
+
/** True if any `expect(...)` call appears under `scope` (any matcher form, or a
|
|
102
|
+
* bare expect). Used to keep a cy chain clean when it asserts inside a .then. */
|
|
103
|
+
function containsExpectCall(scope) {
|
|
104
|
+
let found = false;
|
|
105
|
+
const walk = (n) => {
|
|
106
|
+
if (found)
|
|
107
|
+
return;
|
|
108
|
+
if (ts.isCallExpression(n) && expectRooted(n)) {
|
|
109
|
+
found = true;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
ts.forEachChild(n, walk);
|
|
113
|
+
};
|
|
114
|
+
walk(scope);
|
|
115
|
+
return found;
|
|
116
|
+
}
|
|
117
|
+
/** True if a call chain rooted at `cy` ends in a query command (get/find/contains)
|
|
118
|
+
* and carries no terminating `.should`/`.and` and no `expect(...)` inside a `.then`
|
|
119
|
+
* callback — so it produces a subject that is never asserted. Action commands
|
|
120
|
+
* (click/type/visit/...) as the outermost call do something, so they stay clean.
|
|
121
|
+
* `expr` is the outermost call of the statement. */
|
|
122
|
+
function isUnassertedCyQuery(expr) {
|
|
123
|
+
if (rootIdent(expr) !== "cy")
|
|
124
|
+
return false;
|
|
125
|
+
// outermost command must be a query, not an action (action does work, not just query)
|
|
126
|
+
if (!ts.isPropertyAccessExpression(expr.expression))
|
|
127
|
+
return false;
|
|
128
|
+
if (!CY_QUERY_COMMANDS.has(expr.expression.name.text))
|
|
129
|
+
return false;
|
|
130
|
+
// scan the whole chain for a terminating assertion: any .should/.and, or an
|
|
131
|
+
// expect(...) inside a .then(cb) callback. Either keeps it clean.
|
|
132
|
+
let cur = expr;
|
|
133
|
+
const visit = (n) => {
|
|
134
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
135
|
+
const m = n.expression.name.text;
|
|
136
|
+
if (m === "should" || m === "and")
|
|
137
|
+
return true;
|
|
138
|
+
if (m === "then") {
|
|
139
|
+
const cb = n.arguments.find((a) => ts.isArrowFunction(a) || ts.isFunctionExpression(a));
|
|
140
|
+
if (cb && containsExpectCall(cb))
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let asserted = false;
|
|
145
|
+
ts.forEachChild(n, (c) => { if (visit(c))
|
|
146
|
+
asserted = true; });
|
|
147
|
+
return asserted;
|
|
148
|
+
};
|
|
149
|
+
return !visit(cur);
|
|
150
|
+
}
|
|
81
151
|
function literalTruthiness(e) {
|
|
82
152
|
if (!e)
|
|
83
153
|
return null;
|
|
@@ -151,6 +221,23 @@ function expectChain(call) {
|
|
|
151
221
|
}
|
|
152
222
|
return null;
|
|
153
223
|
}
|
|
224
|
+
/** True if `call` is the terminal matcher call of an `expect(...).resolves`/
|
|
225
|
+
* `.rejects` chain (e.g. `expect(p).resolves.toBe(1)`). Walks the callee base for a
|
|
226
|
+
* `.resolves`/`.rejects` property access that bottoms out in an `expect(...)` call.
|
|
227
|
+
* Only the explicit resolves/rejects marker counts (JS29); a plain promise does not. */
|
|
228
|
+
function expectIsResolvesRejects(call) {
|
|
229
|
+
if (!ts.isPropertyAccessExpression(call.expression))
|
|
230
|
+
return false;
|
|
231
|
+
let base = call.expression.expression;
|
|
232
|
+
let sawSettle = false;
|
|
233
|
+
while (ts.isPropertyAccessExpression(base) || ts.isCallExpression(base)) {
|
|
234
|
+
if (ts.isPropertyAccessExpression(base) &&
|
|
235
|
+
(base.name.text === "resolves" || base.name.text === "rejects"))
|
|
236
|
+
sawSettle = true;
|
|
237
|
+
base = base.expression;
|
|
238
|
+
}
|
|
239
|
+
return sawSettle && expectRooted(call.expression);
|
|
240
|
+
}
|
|
154
241
|
/** True if a call's result is observed: awaited, returned, assigned, the
|
|
155
242
|
* implicit-return body of an arrow, or explicitly discarded with `void` (an
|
|
156
243
|
* author signalling "I am dropping this on purpose"). A bare floating call (its
|
|
@@ -314,6 +401,22 @@ function isHarmlessCatch(block) {
|
|
|
314
401
|
}
|
|
315
402
|
return true;
|
|
316
403
|
}
|
|
404
|
+
/** Stricter than isHarmlessCatch, for JS31: the catch truly swallows the throw,
|
|
405
|
+
* doing NOTHING with it. Only an empty body or `console.*` log-only statements
|
|
406
|
+
* qualify. An assignment (recovery flag like `supported = false`), a return, a
|
|
407
|
+
* fail()/throw, an assertion, or any other call is meaningful handling, not a
|
|
408
|
+
* silent swallow, so JS31 must not fire. Keeps JS31 precision-first. */
|
|
409
|
+
function catchSilentlySwallows(block) {
|
|
410
|
+
for (const stmt of block.statements) {
|
|
411
|
+
if (!ts.isExpressionStatement(stmt))
|
|
412
|
+
return false; // return/var/if/etc: handling
|
|
413
|
+
if (!ts.isCallExpression(stmt.expression))
|
|
414
|
+
return false; // a bare assignment/expr
|
|
415
|
+
if (!calleeName(stmt.expression.expression).startsWith("console."))
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
317
420
|
/** Matcher names of every expect-chain assertion under scope (for snapshot-only). */
|
|
318
421
|
function matchersUnder(scope) {
|
|
319
422
|
const out = [];
|
|
@@ -328,6 +431,309 @@ function matchersUnder(scope) {
|
|
|
328
431
|
ts.forEachChild(scope, walk);
|
|
329
432
|
return out;
|
|
330
433
|
}
|
|
434
|
+
/** True if any snapshot matcher under `scope` is an inline snapshot with no
|
|
435
|
+
* baseline yet: `toMatchInlineSnapshot()` / `toThrowErrorMatchingInlineSnapshot()`
|
|
436
|
+
* with no argument, or an empty/whitespace-only string-literal baseline. On the
|
|
437
|
+
* first run the runner writes the snapshot from the output itself, so it passes
|
|
438
|
+
* by construction. A populated inline snapshot has a real baseline and is not this. */
|
|
439
|
+
function hasEmptyInlineSnapshot(scope) {
|
|
440
|
+
let found = false;
|
|
441
|
+
const walk = (n) => {
|
|
442
|
+
if (found)
|
|
443
|
+
return;
|
|
444
|
+
if (ts.isCallExpression(n)) {
|
|
445
|
+
const chain = expectChain(n);
|
|
446
|
+
if (chain && (chain.matcher === "toMatchInlineSnapshot" ||
|
|
447
|
+
chain.matcher === "toThrowErrorMatchingInlineSnapshot")) {
|
|
448
|
+
const a0 = chain.args[0];
|
|
449
|
+
if (a0 === undefined) {
|
|
450
|
+
found = true;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if ((ts.isStringLiteral(a0) || ts.isNoSubstitutionTemplateLiteral(a0)) &&
|
|
454
|
+
a0.text.trim() === "") {
|
|
455
|
+
found = true;
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
ts.forEachChild(n, walk);
|
|
461
|
+
};
|
|
462
|
+
ts.forEachChild(scope, walk);
|
|
463
|
+
return found;
|
|
464
|
+
}
|
|
465
|
+
/** The numeric N of an `expect.assertions(N)` call (N a numeric literal), else
|
|
466
|
+
* null. `expect.hasAssertions()` carries no count and is not this. */
|
|
467
|
+
function expectAssertionsCount(call) {
|
|
468
|
+
if (calleeName(call.expression) !== "expect.assertions")
|
|
469
|
+
return null;
|
|
470
|
+
const a0 = call.arguments[0];
|
|
471
|
+
if (a0 && ts.isNumericLiteral(a0))
|
|
472
|
+
return Number(a0.text);
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
/** JS23 expect accounting. `unconditional` is the count of expect-chain matcher
|
|
476
|
+
* calls guaranteed to run: a direct ExpressionStatement on the test body's spine
|
|
477
|
+
* (optionally awaited). `indeterminate` is true when any other expect-chain call
|
|
478
|
+
* exists in the body (in a loop, branch, try, switch, callback, .then, or an
|
|
479
|
+
* expression operand) — its run count cannot be proven, so a shortfall is not
|
|
480
|
+
* provable and JS23 must be suppressed (FP-averse: a false positive is worse than
|
|
481
|
+
* a miss). Stops at nested functions only for the spine count, but the
|
|
482
|
+
* indeterminate scan walks the whole body (a .then callback IS indeterminate). */
|
|
483
|
+
function expectAccounting(body) {
|
|
484
|
+
const spine = new Set();
|
|
485
|
+
let unconditional = 0;
|
|
486
|
+
for (const st of body.statements) {
|
|
487
|
+
if (!ts.isExpressionStatement(st))
|
|
488
|
+
continue;
|
|
489
|
+
let e = st.expression;
|
|
490
|
+
if (ts.isAwaitExpression(e))
|
|
491
|
+
e = e.expression;
|
|
492
|
+
if (ts.isCallExpression(e) && expectChain(e)) {
|
|
493
|
+
unconditional++;
|
|
494
|
+
spine.add(e);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Anything that is not the proven-unconditional expect spine makes the count
|
|
498
|
+
// indeterminate: an off-spine expect chain (loop/branch/.then/operand), or any
|
|
499
|
+
// other call — a helper or setup call may carry assertions the count cannot see.
|
|
500
|
+
// Do not descend into a spine chain (its inner expect()/matcher calls are
|
|
501
|
+
// accounted, not separate helpers).
|
|
502
|
+
let indeterminate = false;
|
|
503
|
+
const walk = (n) => {
|
|
504
|
+
if (indeterminate)
|
|
505
|
+
return;
|
|
506
|
+
if (spine.has(n))
|
|
507
|
+
return; // whole spine chain already counted
|
|
508
|
+
if (ts.isCallExpression(n)) {
|
|
509
|
+
const nm = calleeName(n.expression);
|
|
510
|
+
if (nm !== "expect.assertions" && nm !== "expect.hasAssertions") {
|
|
511
|
+
indeterminate = true;
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
ts.forEachChild(n, walk);
|
|
516
|
+
};
|
|
517
|
+
walk(body);
|
|
518
|
+
return { unconditional, indeterminate };
|
|
519
|
+
}
|
|
520
|
+
/** JS8 (spyOn form): collect `{root -> line}` for every jest.spyOn/vi.spyOn target
|
|
521
|
+
* whose return was canned (mockReturnValue/mockResolvedValue/mockRejectedValue/
|
|
522
|
+
* mockImplementation) under `scope`. Test-local on purpose: a spyOn is hoisted only
|
|
523
|
+
* within its own test body, unlike the module-wide jest.mock form. */
|
|
524
|
+
function cannedSpyTargets(scope) {
|
|
525
|
+
const out = new Map();
|
|
526
|
+
const sf = scope.getSourceFile();
|
|
527
|
+
const walk = (n) => {
|
|
528
|
+
if (ts.isCallExpression(n) &&
|
|
529
|
+
/^(mockReturnValue|mockResolvedValue|mockRejectedValue|mockImplementation)$/.test(calleeName(n.expression).split(".").pop() ?? "")) {
|
|
530
|
+
let base = n.expression;
|
|
531
|
+
while (ts.isPropertyAccessExpression(base) || ts.isCallExpression(base)) {
|
|
532
|
+
if (ts.isCallExpression(base)) {
|
|
533
|
+
const cn = calleeName(base.expression);
|
|
534
|
+
if (cn === "jest.spyOn" || cn === "vi.spyOn") {
|
|
535
|
+
const tr = base.arguments[0] && rootIdent(base.arguments[0]);
|
|
536
|
+
if (tr)
|
|
537
|
+
out.set(tr, lineOf(sf, n));
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
base = ts.isCallExpression(base) ? base.expression
|
|
542
|
+
: base.expression;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
ts.forEachChild(n, walk);
|
|
546
|
+
};
|
|
547
|
+
walk(scope);
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
/** Root identifiers used as an expect subject under `scope` (`expect(a.b).m()` -> "a"). */
|
|
551
|
+
function expectSubjectRoots(scope) {
|
|
552
|
+
const out = new Set();
|
|
553
|
+
const walk = (n) => {
|
|
554
|
+
if (ts.isCallExpression(n)) {
|
|
555
|
+
const chain = expectChain(n);
|
|
556
|
+
if (chain && chain.subject) {
|
|
557
|
+
const r = rootIdent(chain.subject);
|
|
558
|
+
if (r)
|
|
559
|
+
out.add(r);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
ts.forEachChild(n, walk);
|
|
563
|
+
};
|
|
564
|
+
walk(scope);
|
|
565
|
+
return out;
|
|
566
|
+
}
|
|
567
|
+
/** JS25: every assertion under `scope` sits inside an array-iterator callback
|
|
568
|
+
* (arr.forEach/map/filter/some/every/flatMap(cb)), and at least one such
|
|
569
|
+
* assertion exists. Walk the body; when entering an iterator callback, any
|
|
570
|
+
* assertion inside counts as "iterator-bound"; an assertion reached outside one
|
|
571
|
+
* is an own-scope assertion that disproves JS25. Returns whether the only
|
|
572
|
+
* assertions are iterator-bound (and there is at least one). FP-averse: a single
|
|
573
|
+
* own-scope assertion anywhere suppresses it. */
|
|
574
|
+
function assertionsOnlyInArrayIterator(scope) {
|
|
575
|
+
let iteratorBound = 0;
|
|
576
|
+
let ownScope = 0;
|
|
577
|
+
// The callback nodes of array-iterator calls under scope, so the walk knows when
|
|
578
|
+
// an assertion is inside one without re-deriving the call shape per node.
|
|
579
|
+
const iterCallbacks = new Set();
|
|
580
|
+
const collect = (n) => {
|
|
581
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) &&
|
|
582
|
+
ARRAY_ITERATOR_METHODS.has(n.expression.name.text)) {
|
|
583
|
+
for (const a of n.arguments) {
|
|
584
|
+
if (ts.isArrowFunction(a) || ts.isFunctionExpression(a))
|
|
585
|
+
iterCallbacks.add(a);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
ts.forEachChild(n, collect);
|
|
589
|
+
};
|
|
590
|
+
collect(scope);
|
|
591
|
+
const walk = (n, insideIter) => {
|
|
592
|
+
const nowInside = insideIter || iterCallbacks.has(n);
|
|
593
|
+
if (isAssertionNode(n)) {
|
|
594
|
+
if (nowInside)
|
|
595
|
+
iteratorBound++;
|
|
596
|
+
else
|
|
597
|
+
ownScope++;
|
|
598
|
+
}
|
|
599
|
+
ts.forEachChild(n, (c) => walk(c, nowInside));
|
|
600
|
+
};
|
|
601
|
+
ts.forEachChild(scope, (c) => walk(c, false));
|
|
602
|
+
return ownScope === 0 && iteratorBound > 0;
|
|
603
|
+
}
|
|
604
|
+
/** True if `scope` carries an expect.assertions(N) / expect.hasAssertions() guard
|
|
605
|
+
* (JS23 territory) — used as a JS25 FP guard: the author already declared a count. */
|
|
606
|
+
function hasAssertionCountGuard(scope) {
|
|
607
|
+
let found = false;
|
|
608
|
+
const walk = (n) => {
|
|
609
|
+
if (found)
|
|
610
|
+
return;
|
|
611
|
+
if (ts.isCallExpression(n)) {
|
|
612
|
+
const nm = calleeName(n.expression);
|
|
613
|
+
if (nm === "expect.assertions" || nm === "expect.hasAssertions") {
|
|
614
|
+
found = true;
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
ts.forEachChild(n, walk);
|
|
619
|
+
};
|
|
620
|
+
walk(scope);
|
|
621
|
+
return found;
|
|
622
|
+
}
|
|
623
|
+
/** True if any array-iterator call under `scope` iterates a non-empty array
|
|
624
|
+
* literal receiver (arr `[1, 2].forEach(...)`) — JS25 FP guard: a non-empty
|
|
625
|
+
* literal always runs the callback at least once, so the assertion does run. */
|
|
626
|
+
function iteratesNonEmptyArrayLiteral(scope) {
|
|
627
|
+
let found = false;
|
|
628
|
+
const walk = (n) => {
|
|
629
|
+
if (found)
|
|
630
|
+
return;
|
|
631
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) &&
|
|
632
|
+
ARRAY_ITERATOR_METHODS.has(n.expression.name.text)) {
|
|
633
|
+
const recv = n.expression.expression;
|
|
634
|
+
if (ts.isArrayLiteralExpression(recv) && recv.elements.length > 0) {
|
|
635
|
+
found = true;
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
ts.forEachChild(n, walk);
|
|
640
|
+
};
|
|
641
|
+
walk(scope);
|
|
642
|
+
return found;
|
|
643
|
+
}
|
|
644
|
+
/** JS27: the matcher names of every expect-chain assertion under `scope`, each
|
|
645
|
+
* paired with the root identifier of its subject. Used to decide whether the ONLY
|
|
646
|
+
* oracle is a toHaveBeenCalled* check on a locally-created double. */
|
|
647
|
+
function expectMatcherSubjects(scope) {
|
|
648
|
+
const out = [];
|
|
649
|
+
const walk = (n) => {
|
|
650
|
+
if (ts.isCallExpression(n)) {
|
|
651
|
+
const chain = expectChain(n);
|
|
652
|
+
if (chain)
|
|
653
|
+
out.push({ matcher: chain.matcher, root: chain.subject ? rootIdent(chain.subject) : null });
|
|
654
|
+
}
|
|
655
|
+
ts.forEachChild(n, walk);
|
|
656
|
+
};
|
|
657
|
+
walk(scope);
|
|
658
|
+
return out;
|
|
659
|
+
}
|
|
660
|
+
/** Root identifiers in `scope` bound to a freshly-created test double:
|
|
661
|
+
* jest.fn()/vi.fn()/jest.spyOn()/vi.spyOn() in a const/let, or a mockReturnValue/
|
|
662
|
+
* mockImplementation-decorated handle. Used by JS27 to confirm the call-tracking
|
|
663
|
+
* subject is a local double, not a real collaborator. */
|
|
664
|
+
function localDoubleRoots(scope) {
|
|
665
|
+
const out = new Set();
|
|
666
|
+
const isDoubleInit = (e) => {
|
|
667
|
+
let cur = e;
|
|
668
|
+
while (ts.isCallExpression(cur) || ts.isPropertyAccessExpression(cur)) {
|
|
669
|
+
if (ts.isCallExpression(cur)) {
|
|
670
|
+
const nm = calleeName(cur.expression);
|
|
671
|
+
if (nm === "jest.fn" || nm === "vi.fn" || nm === "jest.spyOn" || nm === "vi.spyOn" ||
|
|
672
|
+
nm === "sinon.spy" || nm === "sinon.stub")
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
cur = ts.isCallExpression(cur) ? cur.expression : cur.expression;
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
};
|
|
679
|
+
const walk = (n) => {
|
|
680
|
+
if (ts.isVariableDeclaration(n) && n.initializer && ts.isIdentifier(n.name) &&
|
|
681
|
+
isDoubleInit(n.initializer)) {
|
|
682
|
+
out.add(n.name.text);
|
|
683
|
+
}
|
|
684
|
+
ts.forEachChild(n, walk);
|
|
685
|
+
};
|
|
686
|
+
walk(scope);
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
689
|
+
/** JS26: a fake-timer install and a setTimeout/setInterval arm both live under
|
|
690
|
+
* `scope`, but no flush/advance does (in scope or a sibling lifecycle hook). The
|
|
691
|
+
* scheduled callback never fires, so any assertion runs against un-mutated state. */
|
|
692
|
+
function timerInstalledNeverAdvanced(scope, timer, sf) {
|
|
693
|
+
// install present in scope, no flush in scope — order does not matter (a frozen
|
|
694
|
+
// timer that is never advanced is vacuous regardless of where the install sits).
|
|
695
|
+
if (!callMatchesUnder(scope, sf, FAKE_TIMER_INSTALL))
|
|
696
|
+
return false;
|
|
697
|
+
if (callMatchesUnder(scope, sf, FAKE_TIMER_FLUSH))
|
|
698
|
+
return false;
|
|
699
|
+
// a sibling hook may install or flush; reuse the JS-wide hook scan. If a hook
|
|
700
|
+
// flushes, the callback fires, so suppress.
|
|
701
|
+
if (hookControlsTimerFlush(timer, sf))
|
|
702
|
+
return false;
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
/** True if an enclosing describe/top-level afterEach/afterAll flushes timers — the
|
|
706
|
+
* JS26 suppression twin of hookControlsTimer (which also accepts a before-install).
|
|
707
|
+
* Here only a teardown flush matters: an install in a hook does not advance. */
|
|
708
|
+
function hookControlsTimerFlush(timer, sf) {
|
|
709
|
+
const scan = (statements) => {
|
|
710
|
+
for (const stmt of statements) {
|
|
711
|
+
if (!ts.isExpressionStatement(stmt) || !ts.isCallExpression(stmt.expression))
|
|
712
|
+
continue;
|
|
713
|
+
const hook = calleeName(stmt.expression.expression).split(".")[0];
|
|
714
|
+
const cb = getTestCallback(stmt.expression);
|
|
715
|
+
if (!cb)
|
|
716
|
+
continue;
|
|
717
|
+
if (callMatchesUnder(cb, sf, FAKE_TIMER_FLUSH) &&
|
|
718
|
+
(SETUP_HOOKS.has(hook) || TEARDOWN_HOOKS.has(hook)))
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
return false;
|
|
722
|
+
};
|
|
723
|
+
if (scan(sf.statements))
|
|
724
|
+
return true;
|
|
725
|
+
let pn = timer.parent;
|
|
726
|
+
while (pn) {
|
|
727
|
+
if ((ts.isArrowFunction(pn) || ts.isFunctionExpression(pn) || ts.isFunctionDeclaration(pn)) &&
|
|
728
|
+
pn.parent && ts.isCallExpression(pn.parent) && pn.parent.arguments.includes(pn)) {
|
|
729
|
+
const suiteRoot = calleeName(pn.parent.expression).split(".")[0];
|
|
730
|
+
if (SUITE_ROOTS.has(suiteRoot) && pn.body && ts.isBlock(pn.body) && scan(pn.body.statements))
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
pn = pn.parent;
|
|
734
|
+
}
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
331
737
|
function getTestCallback(call) {
|
|
332
738
|
for (const arg of call.arguments) {
|
|
333
739
|
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))
|
|
@@ -480,6 +886,7 @@ export function analyze(sf) {
|
|
|
480
886
|
const findings = [];
|
|
481
887
|
const file = sf.fileName;
|
|
482
888
|
const text = sf.getFullText();
|
|
889
|
+
const level = detectPyramidLevel(sf);
|
|
483
890
|
// Fake-timer / flush presence suppresses the JS7 timer arm: if the test fakes
|
|
484
891
|
// or drives timers anywhere, a setTimeout/setInterval callback is flushed
|
|
485
892
|
// synchronously and its assertion does run. Covers Jest, Vitest and Sinon
|
|
@@ -578,7 +985,9 @@ export function analyze(sf) {
|
|
|
578
985
|
else if (hasAssertion(cb)) {
|
|
579
986
|
const ms = matchersUnder(cb);
|
|
580
987
|
if (ms.length > 0 && ms.every((m) => SNAPSHOT_MATCHERS.has(m))) {
|
|
581
|
-
push(line, "JS3",
|
|
988
|
+
push(line, "JS3", hasEmptyInlineSnapshot(cb)
|
|
989
|
+
? "the only assertion is an empty inline snapshot — it passes by writing itself on first run"
|
|
990
|
+
: "the only assertion is a snapshot");
|
|
582
991
|
}
|
|
583
992
|
// C21: the test has at least one assertion in its own scope and none of
|
|
584
993
|
// them is guaranteed to run unconditionally (all behind a condition, a
|
|
@@ -596,6 +1005,104 @@ export function analyze(sf) {
|
|
|
596
1005
|
push(line, "C21", "every assertion is guarded by a condition");
|
|
597
1006
|
}
|
|
598
1007
|
}
|
|
1008
|
+
// JS23: expect.assertions(N) with N a numeric literal, but fewer
|
|
1009
|
+
// unconditional reachable non-nested expect() matcher calls than N — the
|
|
1010
|
+
// guard can never be satisfied. FP-averse: any expect in a loop, branch,
|
|
1011
|
+
// callback, or helper is indeterminate, so the count is undercounted and a
|
|
1012
|
+
// shortfall is only reported when it is provable. expect.hasAssertions()
|
|
1013
|
+
// carries no count and is skipped. Distinct from the deliberately-skipped JS16.
|
|
1014
|
+
let assertionsGuard = null;
|
|
1015
|
+
forEachNoNesting(cb.body, (n) => {
|
|
1016
|
+
if (ts.isCallExpression(n)) {
|
|
1017
|
+
const cnt = expectAssertionsCount(n);
|
|
1018
|
+
if (cnt !== null)
|
|
1019
|
+
assertionsGuard = { line: lineOf(sf, n), n: cnt };
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
if (assertionsGuard !== null) {
|
|
1023
|
+
const guard = assertionsGuard;
|
|
1024
|
+
const acc = expectAccounting(cb.body);
|
|
1025
|
+
if (!acc.indeterminate && acc.unconditional < guard.n) {
|
|
1026
|
+
push(guard.line, "JS23", `expect.assertions(${guard.n}) but only ${acc.unconditional} unconditional expect() call(s) run`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
// JS8 (spyOn form): test-local. A spyOn target with a canned return that is
|
|
1030
|
+
// also an expect subject IN THE SAME test body is a self-mock — the test
|
|
1031
|
+
// asserts the canned value. Scoped to cb.body so a spy in one test never
|
|
1032
|
+
// matches an assertion in another (the jest.mock module form, hoisted
|
|
1033
|
+
// file-wide, is handled separately after the walk).
|
|
1034
|
+
const spied = cannedSpyTargets(cb.body);
|
|
1035
|
+
if (spied.size > 0) {
|
|
1036
|
+
const subjects = expectSubjectRoots(cb.body);
|
|
1037
|
+
for (const [tr, ln] of spied) {
|
|
1038
|
+
if (subjects.has(tr)) {
|
|
1039
|
+
push(ln, "JS8", `${tr} is spied with a canned return and asserted directly`);
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
// JS25: every assertion lives inside an array-iterator callback
|
|
1045
|
+
// (forEach/map/filter/some/every/flatMap) and none on the test's own
|
|
1046
|
+
// spine — so on an empty collection the callback never runs and zero
|
|
1047
|
+
// assertions execute. The verified hole between C2/C2b (hasAssertion
|
|
1048
|
+
// descends into callbacks, so it finds one) and C21 (its ownAsserts stop
|
|
1049
|
+
// at callbacks, so it sees none). FP guards: any own-scope assertion, a
|
|
1050
|
+
// non-empty array-literal receiver, or an expect.assertions/hasAssertions
|
|
1051
|
+
// guard suppress it.
|
|
1052
|
+
if (hasAssertion(cb) && assertionsOnlyInArrayIterator(cb.body) &&
|
|
1053
|
+
!iteratesNonEmptyArrayLiteral(cb.body) && !hasAssertionCountGuard(cb.body)) {
|
|
1054
|
+
push(line, "JS25", "the only assertion is inside an array-iterator callback; it runs zero times on an empty collection");
|
|
1055
|
+
}
|
|
1056
|
+
// JS27: every expect-chain matcher is in the toHaveBeenCalled* family AND
|
|
1057
|
+
// each such subject root is a locally-created double (jest.fn/vi.fn/spyOn).
|
|
1058
|
+
// The test confirms it called the double it set up, never the unit's output
|
|
1059
|
+
// or state (J3). FP guards: any non-call-tracking assertion suppresses it,
|
|
1060
|
+
// and it is gated to the unit level (a logger-spy call check is legit at
|
|
1061
|
+
// integration/e2e). Sibling of JS8.
|
|
1062
|
+
if (level === "unit") {
|
|
1063
|
+
const ms27 = expectMatcherSubjects(cb.body);
|
|
1064
|
+
if (ms27.length > 0 && ms27.every((m) => CALL_TRACKING_MATCHERS.has(m.matcher))) {
|
|
1065
|
+
const doubles = localDoubleRoots(cb.body);
|
|
1066
|
+
if (ms27.every((m) => m.root !== null && doubles.has(m.root))) {
|
|
1067
|
+
push(line, "JS27", "the only oracle is a toHaveBeenCalled* check on a local double; assert the unit's output or state");
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
// C11a: self-confirming literal — the expected value is bound from the
|
|
1072
|
+
// same call/expression under test. `const e = foo(); expect(foo()).toBe(e)`
|
|
1073
|
+
// can never fail: both sides evaluate the SUT, so the oracle confirms the
|
|
1074
|
+
// code against itself (J2). Static, low-FP corner of the circular-oracle
|
|
1075
|
+
// family. FP guard: the bound initializer must be provably the SUT call,
|
|
1076
|
+
// i.e. its source text equals the expect subject's source text exactly, and
|
|
1077
|
+
// it contains a call (a plain literal binding is not self-confirming).
|
|
1078
|
+
{
|
|
1079
|
+
const inits = new Map(); // var name -> initializer text
|
|
1080
|
+
forEachNoNesting(cb.body, (n) => {
|
|
1081
|
+
if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.initializer &&
|
|
1082
|
+
containsCall(n.initializer)) {
|
|
1083
|
+
inits.set(n.name.text, n.initializer.getText(sf).replace(/\s+/g, " ").trim());
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
if (inits.size > 0) {
|
|
1087
|
+
forEachNoNesting(cb.body, (n) => {
|
|
1088
|
+
if (!ts.isCallExpression(n))
|
|
1089
|
+
return;
|
|
1090
|
+
const ch = expectChain(n);
|
|
1091
|
+
if (!ch || ch.negated || !ch.subject)
|
|
1092
|
+
return;
|
|
1093
|
+
if (!EQ_MATCHERS_ANY.has(ch.matcher))
|
|
1094
|
+
return;
|
|
1095
|
+
const a0 = ch.args[0];
|
|
1096
|
+
if (!a0 || !ts.isIdentifier(a0))
|
|
1097
|
+
return;
|
|
1098
|
+
const initText = inits.get(a0.text);
|
|
1099
|
+
if (initText && containsCall(ch.subject) &&
|
|
1100
|
+
ch.subject.getText(sf).replace(/\s+/g, " ").trim() === initText) {
|
|
1101
|
+
push(lineOf(sf, n), "C11a", "expected value is bound from the same call under test");
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
599
1106
|
// D7: anonymous test (empty or missing description)
|
|
600
1107
|
const desc = node.arguments[0];
|
|
601
1108
|
const emptyStr = (d) => d !== undefined &&
|
|
@@ -737,8 +1244,9 @@ export function analyze(sf) {
|
|
|
737
1244
|
// C7 self-compare
|
|
738
1245
|
push(lineOf(sf, node), "C7", "expected value is the same expression as the subject");
|
|
739
1246
|
}
|
|
740
|
-
else if (EQUALITY_MATCHERS.has(chain.matcher) && arg && ts.isNumericLiteral(arg)) {
|
|
741
|
-
// C8 exact float
|
|
1247
|
+
else if (EQUALITY_MATCHERS.has(chain.matcher) && arg && ts.isNumericLiteral(arg) && !isLiteral(subj)) {
|
|
1248
|
+
// C8 exact float — on a real (non-literal) subject. A literal-vs-literal
|
|
1249
|
+
// float (expect(0.1).toBe(0.3)) is JS30, the stronger both-literals lane.
|
|
742
1250
|
const v = Number(arg.text);
|
|
743
1251
|
if (!Number.isInteger(v) && v !== 0 && v !== 1) {
|
|
744
1252
|
push(lineOf(sf, node), "C8", "exact equality on a float; use toBeCloseTo");
|
|
@@ -748,6 +1256,25 @@ export function analyze(sf) {
|
|
|
748
1256
|
// C18 sensitive equality: compares the stringified form to a literal
|
|
749
1257
|
push(lineOf(sf, node), "C18", "compares the stringified form of a value to a literal");
|
|
750
1258
|
}
|
|
1259
|
+
// JS30: literal-vs-literal through an equality matcher — both operands are
|
|
1260
|
+
// fixed at parse time, so the comparison is independent of any production
|
|
1261
|
+
// code. Different-token only (the same-token case is C5's "both sides are
|
|
1262
|
+
// the same literal"); object/array literals (reference-equality, false-red)
|
|
1263
|
+
// and template literals with substitutions (C18 lane) are excluded by
|
|
1264
|
+
// isLiteral. Broader matcher set than C5/C7 (adds toBeCloseTo + chai/AVA
|
|
1265
|
+
// equal/equals/eql/is). Negation already filtered (chain.negated).
|
|
1266
|
+
if (EQ_MATCHERS_ANY.has(chain.matcher) && isLiteral(subj) && isLiteral(arg) &&
|
|
1267
|
+
subj.getText(sf) !== arg.getText(sf)) {
|
|
1268
|
+
push(lineOf(sf, node), "JS30", "both operands are literals; the comparison is fixed at parse time");
|
|
1269
|
+
}
|
|
1270
|
+
// C8b: toBeCloseTo called with no precision argument — only the expected
|
|
1271
|
+
// value, so the default 2-digit tolerance applies. The js analogue of
|
|
1272
|
+
// assertAlmostEqual/pytest.approx with no tolerance: implicit-precision only.
|
|
1273
|
+
// A literal-vs-literal toBeCloseTo is JS30's stronger lane, so skip it here.
|
|
1274
|
+
if (chain.matcher === "toBeCloseTo" && chain.args.length === 1 &&
|
|
1275
|
+
!(isLiteral(subj) && isLiteral(arg))) {
|
|
1276
|
+
push(lineOf(sf, node), "C8b", "toBeCloseTo with no precision; the default 2-digit tolerance may be too loose");
|
|
1277
|
+
}
|
|
751
1278
|
}
|
|
752
1279
|
// C16 nondeterminism
|
|
753
1280
|
if (!fakeTimers) {
|
|
@@ -809,8 +1336,32 @@ export function analyze(sf) {
|
|
|
809
1336
|
push(lineOf(sf, node), "JS7", `assertion deferred into a floating .${leaf}(); may not run before the test ends`);
|
|
810
1337
|
}
|
|
811
1338
|
}
|
|
1339
|
+
// JS26: fake timers installed but never advanced. The setTimeout/setInterval
|
|
1340
|
+
// is armed, fake timers freeze it, and nothing in the same test scope (nor a
|
|
1341
|
+
// sibling before/after hook) calls runAllTimers/advanceTimersByTime/tick — so
|
|
1342
|
+
// the scheduled callback never fires and any assertion runs against the
|
|
1343
|
+
// un-mutated initial state. Opposite of C16 (uncontrolled timer): here the
|
|
1344
|
+
// timer is controlled but not advanced. Requires an assertion in scope so a
|
|
1345
|
+
// pure setup arm with no oracle is not flagged. Kept low (legit "assert
|
|
1346
|
+
// nothing happened yet" exists). FP guard: any flush in the body or a
|
|
1347
|
+
// before/afterEach of the enclosing describe suppresses it.
|
|
1348
|
+
if (isTimer) {
|
|
1349
|
+
const scope = enclosingTestScope(node);
|
|
1350
|
+
if (timerInstalledNeverAdvanced(scope, node, sf) && hasAssertion(scope)) {
|
|
1351
|
+
push(lineOf(sf, node), "JS26", `${name} scheduled under fake timers that are never advanced; the callback never fires`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
812
1354
|
}
|
|
813
1355
|
}
|
|
1356
|
+
// JS29: an expect(...).resolves/.rejects chain that is a bare statement, not
|
|
1357
|
+
// awaited, returned, or collected. The matcher only settles asynchronously, so
|
|
1358
|
+
// a floating chain finishes green before it resolves. The statically-provable
|
|
1359
|
+
// subset of JS20 (no type inference needed: the explicit .resolves/.rejects
|
|
1360
|
+
// marker is the signal). FP guard: only the explicit resolves/rejects member
|
|
1361
|
+
// (a plain promise stays JS20-out); awaited/returned/collected suppresses it.
|
|
1362
|
+
if (expectIsResolvesRejects(node) && !isObservedAsync(node)) {
|
|
1363
|
+
push(lineOf(sf, node), "JS29", "resolves/rejects assertion is not awaited or returned; it settles after the test ends");
|
|
1364
|
+
}
|
|
814
1365
|
// JS13: a sync query used as a loose statement (result never asserted).
|
|
815
1366
|
// Testing Library getBy*/queryBy*, and Vue Test Utils findComponent /
|
|
816
1367
|
// findAllComponents always, or find/findAll with a string selector (which
|
|
@@ -821,9 +1372,18 @@ export function analyze(sf) {
|
|
|
821
1372
|
const isVueComponentQuery = qleaf === "findComponent" || qleaf === "findAllComponents";
|
|
822
1373
|
const isVueSelectorQuery = (qleaf === "find" || qleaf === "findAll") &&
|
|
823
1374
|
node.arguments.length > 0 && ts.isStringLiteral(node.arguments[0]);
|
|
824
|
-
|
|
1375
|
+
// A cy-rooted chain belongs to JS24, not JS13: cy.get("ul").find("li") ends in
|
|
1376
|
+
// .find("li") and would otherwise trip the Vue-selector heuristic (double-report).
|
|
1377
|
+
if ((isRtlQuery || isVueComponentQuery || isVueSelectorQuery) && rootIdent(node) !== "cy") {
|
|
825
1378
|
push(lineOf(sf, node), "JS13", `${qleaf}() result is not asserted`);
|
|
826
1379
|
}
|
|
1380
|
+
// JS24: the cy.* analogue of JS13 — a Cypress query chain (cy.get/find/
|
|
1381
|
+
// contains) as a statement with no terminating .should/.and and no expect
|
|
1382
|
+
// in a .then callback. Only query commands produce a subject; action
|
|
1383
|
+
// commands (click/type/visit/...) do work and stay clean.
|
|
1384
|
+
if (isUnassertedCyQuery(node)) {
|
|
1385
|
+
push(lineOf(sf, node), "JS24", `cy query (${qleaf}) result is not asserted`);
|
|
1386
|
+
}
|
|
827
1387
|
}
|
|
828
1388
|
// A runner `.each` table: it.each / test.each / describe.each (and fit/xit
|
|
829
1389
|
// variants). Gate the each-table codes on a runner root so a plain helper
|
|
@@ -877,6 +1437,19 @@ export function analyze(sf) {
|
|
|
877
1437
|
if (containsAssertion(node.tryBlock) && isHarmlessCatch(node.catchClause.block)) {
|
|
878
1438
|
push(lineOf(sf, node), "JS11", "a failing assertion in try is swallowed by catch");
|
|
879
1439
|
}
|
|
1440
|
+
else if (
|
|
1441
|
+
// JS31: the try calls production code that may throw, the catch neither
|
|
1442
|
+
// asserts on the exception, re-raises, nor calls fail() — so a unit that
|
|
1443
|
+
// STOPS throwing (a real regression) still passes green. Complement of JS11
|
|
1444
|
+
// (which owns the swallowed-assertion case): JS31 fires only when the try
|
|
1445
|
+
// has a call but NO assertion (otherwise JS11), and the catch is harmless.
|
|
1446
|
+
// FP guard: a catch that asserts on e / re-throws / calls fail() makes
|
|
1447
|
+
// isHarmlessCatch false; a toThrow/assert.throws oracle in the try would be
|
|
1448
|
+
// an assertion (so JS31 is skipped, the throw is covered).
|
|
1449
|
+
containsCall(node.tryBlock) &&
|
|
1450
|
+
catchSilentlySwallows(node.catchClause.block)) {
|
|
1451
|
+
push(lineOf(sf, node), "JS31", "try/catch swallows a possible throw with no assertion on the exception");
|
|
1452
|
+
}
|
|
880
1453
|
}
|
|
881
1454
|
// JS21: a matcher referenced but never called — `expect(x).toBe;` with no (),
|
|
882
1455
|
// so the assertion object is built and dropped; nothing executes. The chain
|
package/dist/scan.d.ts
CHANGED
package/dist/scan.js
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "falsegreen-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Find JavaScript/TypeScript unit tests that give false positives: green tests that protect nothing, and tests that pass while asserting the wrong thing. Deterministic AST scanner, sibling of falsegreen (Python).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jest",
|