@shivaedev/quality 0.15.0 → 0.16.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 +7 -0
- package/README.md +29 -2
- package/dist/naming/testName.d.ts +1 -0
- package/dist/naming/testName.d.ts.map +1 -1
- package/dist/naming/testName.js +4 -1
- package/dist/naming/testName.js.map +1 -1
- package/dist/rules/built-in.d.ts +1 -1
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +2 -0
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/stories/fixtureWrites.d.ts +7 -0
- package/dist/rules/stories/fixtureWrites.d.ts.map +1 -0
- package/dist/rules/stories/fixtureWrites.js +83 -0
- package/dist/rules/stories/fixtureWrites.js.map +1 -0
- package/dist/rules/stories/setup.d.ts +2 -0
- package/dist/rules/stories/setup.d.ts.map +1 -0
- package/dist/rules/stories/setup.js +56 -0
- package/dist/rules/stories/setup.js.map +1 -0
- package/package.json +1 -1
- package/src/naming/testName.ts +5 -1
- package/src/rules/built-in.ts +2 -0
- package/src/rules/stories/fixtureWrites.ts +102 -0
- package/src/rules/stories/setup.ts +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 - 2026-10-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- The README states the story-test convention: a test reads as a story over a domain kit the repository keeps in `test-support/`, names its setup as traits, acts through real entry points and prints the story when it fails.
|
|
8
|
+
- `tests/story-setup` reports, in a test file, a top-level function named `seed…`, `make…`, `build…` or `setup…`, and each call that writes fixture files through Node's `fs` or `fs/promises`, such as `writeFileSync`, `mkdirSync` or `mkdtemp`. Its message asks for the setup as traits of the story kit and points at the convention. Files under `test-support/` are not tests, so the kit itself is not reported. The rule has no fix. A repository that upgrades records its existing findings with `quality baseline write --rule tests/story-setup`.
|
|
9
|
+
|
|
3
10
|
## 0.15.0 - 2026-10-05
|
|
4
11
|
|
|
5
12
|
### Added
|
package/README.md
CHANGED
|
@@ -520,7 +520,34 @@ Both rules take a `suites` option, `.gitignore` patterns of folders that hold te
|
|
|
520
520
|
|
|
521
521
|
### Test code
|
|
522
522
|
|
|
523
|
-
Test code is a test file, as `tests/follow` reads the name (`*.test.ts` or `*.spec.ts`, with any environment), or any file under a folder named `test-support/`. `test-support/` is the one folder name for the fixtures, harnesses and generated clients that tests share. Test code ships nowhere, so the rules that check shipped code skip it: `imports/fences` never holds it, and `structure/max-lines` gives it the test limit. The test rules still read only test files: a file under `test-support/` is not a test, so `tests/follow` and `tests/
|
|
523
|
+
Test code is a test file, as `tests/follow` reads the name (`*.test.ts` or `*.spec.ts`, with any environment), or any file under a folder named `test-support/`. `test-support/` is the one folder name for the fixtures, harnesses and generated clients that tests share. Test code ships nowhere, so the rules that check shipped code skip it: `imports/fences` never holds it, and `structure/max-lines` gives it the test limit. The test rules still read only test files: a file under `test-support/` is not a test, so `tests/follow`, `tests/colocated` and `tests/story-setup` leave it alone.
|
|
524
|
+
|
|
525
|
+
## Story tests
|
|
526
|
+
|
|
527
|
+
A test reads as a short story over the domain: what exists, what someone does, and what is true afterwards. The words come from a story kit that the repository keeps in `test-support/`, so a test says what it needs in domain words and never builds state by hand.
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
it("ships a paid order from stock", () => {
|
|
531
|
+
const { customer, warehouse } = newShop(hasInStock(3, "lamp"), hasInCart(1, "lamp"));
|
|
532
|
+
|
|
533
|
+
customer.checksOut();
|
|
534
|
+
warehouse.settles();
|
|
535
|
+
|
|
536
|
+
expect(warehouse.shipped()).toEqual(["lamp"]);
|
|
537
|
+
expect(warehouse.inStock("lamp")).toBe(2);
|
|
538
|
+
});
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
- **Setup is traits.** A trait is one sentence of setup in domain words and the change it makes, such as `hasInStock(3, "lamp")`. A test hands the traits it needs to the kit's entry point, such as `newShop(...traits)`, and a setup that recurs becomes one named trait made of others. A trait that asks for an impossible state refuses and fails the test, instead of seeding it quietly.
|
|
542
|
+
- **Actions go through real entry points.** The test acts the way a user or caller does: through the command, service, route or UI path they reach, never by writing internal state.
|
|
543
|
+
- **Failures print the story.** Every trait and action adds a line to the story log, and a failing test prints it, so the failure says what happened, not only which values differ.
|
|
544
|
+
|
|
545
|
+
`tests/story-setup` holds test files to this. In a test file, it reports:
|
|
546
|
+
|
|
547
|
+
- a top-level function whose name starts with the word `seed`, `make`, `build` or `setup`, declared as a function or as a variable that holds an arrow function or function expression: `function seedCart()`, `const makeRun = () => …`. The prefix is a whole word, so `builder` and `settings` pass;
|
|
548
|
+
- a call that writes fixture files through Node's `fs` or `fs/promises`: `writeFile`, `appendFile`, `mkdir`, `mkdtemp`, `copyFile`, `cp` and `symlink`, each in its sync form too, whether imported by name, through a namespace or default import, or through `promises`.
|
|
549
|
+
|
|
550
|
+
Each finding names its line and has the helper or the function it calls as its subject. Its message asks for the setup as traits of the story kit in `test-support/` and points here. A file under `test-support/` is not a test, so the kit itself builds state and writes files freely. The rule only reports; nothing rewrites a test. A repository adopts it through the baseline: `quality baseline write --rule tests/story-setup`.
|
|
524
551
|
|
|
525
552
|
## Vitest projects
|
|
526
553
|
|
|
@@ -587,4 +614,4 @@ A package that type-checks its tests with one config and builds `src` with anoth
|
|
|
587
614
|
|
|
588
615
|
## Validation
|
|
589
616
|
|
|
590
|
-
`pnpm ready` checks formatting, TypeScript 7, the rules, the import graph against seeded repositories and the fence policy against its examples, config, discovery, registry, baseline and report behavior, the command line against seeded repositories and git histories, the Biome preset against every rule Biome recommends and against its declarations, its naming rules and plugins against seeded files that break and keep each one, the file, folder, other-file and test naming rules against seeded trees, the `biome` and `manifests/sorted` rules and `quality fix` against seeded repositories, the Vitest projects against a seeded repository that Vitest runs, an installed tarball consumer that type-checks a config and runs the `quality` bin through a baseline that takes in the preset's lint and plugin findings, and installed consumers that extend each tsconfig preset, type-check a fixture with an expected error for each check the base turns on, and run the package preset's build output.
|
|
617
|
+
`pnpm ready` checks formatting, TypeScript 7, the rules, the import graph against seeded repositories and the fence policy against its examples, config, discovery, registry, baseline and report behavior, the command line against seeded repositories and git histories, the Biome preset against every rule Biome recommends and against its declarations, its naming rules and plugins against seeded files that break and keep each one, the file, folder, other-file and test naming rules against seeded trees, `tests/story-setup` against seeded test, source and `test-support/` files, the `biome` and `manifests/sorted` rules and `quality fix` against seeded repositories, the Vitest projects against a seeded repository that Vitest runs, an installed tarball consumer that type-checks a config and runs the `quality` bin through a baseline that takes in the preset's lint and plugin findings, and installed consumers that extend each tsconfig preset, type-check a fixture with an expected error for each check the base turns on, and run the package preset's build output.
|
|
@@ -8,5 +8,6 @@ export interface TestName {
|
|
|
8
8
|
readonly stem: string;
|
|
9
9
|
}
|
|
10
10
|
export declare function testName(path: string): TestName | undefined;
|
|
11
|
+
export declare function isTestSupport(path: string): boolean;
|
|
11
12
|
export declare function isTestCode(path: string): boolean;
|
|
12
13
|
//# sourceMappingURL=testName.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testName.d.ts","sourceRoot":"","sources":["../../src/naming/testName.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,uCAAwC,CAAC;AAElE,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAID,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAQ3D;AAID,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD"}
|
|
1
|
+
{"version":3,"file":"testName.d.ts","sourceRoot":"","sources":["../../src/naming/testName.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,uCAAwC,CAAC;AAElE,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAID,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAQ3D;AAID,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD"}
|
package/dist/naming/testName.js
CHANGED
|
@@ -11,7 +11,10 @@ export function testName(path) {
|
|
|
11
11
|
return { extension: groups.extension, folder: posix.dirname(path), kind, modifiers, stem };
|
|
12
12
|
}
|
|
13
13
|
const TEST_SUPPORT = "test-support";
|
|
14
|
+
export function isTestSupport(path) {
|
|
15
|
+
return posix.dirname(path).split("/").includes(TEST_SUPPORT);
|
|
16
|
+
}
|
|
14
17
|
export function isTestCode(path) {
|
|
15
|
-
return testName(path) !== undefined ||
|
|
18
|
+
return testName(path) !== undefined || isTestSupport(path);
|
|
16
19
|
}
|
|
17
20
|
//# sourceMappingURL=testName.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testName.js","sourceRoot":"","sources":["../../src/naming/testName.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAElC,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAU,CAAC;AAYlE,MAAM,SAAS,GAAG,gEAAgE,CAAC;AAEnF,MAAM,UAAU,QAAQ,CAAC,IAAY;IACpC,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC;IAC5D,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,CAAC;IAC1B,IAAI,MAAM,EAAE,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM,CAAC,EAAE,CAAC;QAC1G,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,MAAM,CAAC,IAAI,GAAG,EAAE,EAAE,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACzD,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AAC5F,CAAC;AAED,MAAM,YAAY,GAAG,cAAc,CAAC;AAEpC,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"testName.js","sourceRoot":"","sources":["../../src/naming/testName.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAElC,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAU,CAAC;AAYlE,MAAM,SAAS,GAAG,gEAAgE,CAAC;AAEnF,MAAM,UAAU,QAAQ,CAAC,IAAY;IACpC,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC;IAC5D,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,CAAC;IAC1B,IAAI,MAAM,EAAE,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM,CAAC,EAAE,CAAC;QAC1G,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,MAAM,CAAC,IAAI,GAAG,EAAE,EAAE,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACzD,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AAC5F,CAAC;AAED,MAAM,YAAY,GAAG,cAAc,CAAC;AAEpC,MAAM,UAAU,aAAa,CAAC,IAAY;IACzC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACtC,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC;AAC5D,CAAC"}
|
package/dist/rules/built-in.d.ts
CHANGED
|
@@ -29,5 +29,5 @@ export declare const builtInRules: readonly [import("../rule.ts").Rule<"structur
|
|
|
29
29
|
readonly suites?: readonly string[];
|
|
30
30
|
}>, import("../rule.ts").Rule<"tests/colocated", {
|
|
31
31
|
readonly suites?: readonly string[];
|
|
32
|
-
}>];
|
|
32
|
+
}>, import("../rule.ts").Rule<"tests/story-setup", undefined>];
|
|
33
33
|
//# sourceMappingURL=built-in.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"built-in.d.ts","sourceRoot":"","sources":["../../src/rules/built-in.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"built-in.d.ts","sourceRoot":"","sources":["../../src/rules/built-in.ts"],"names":[],"mappings":"AA0BA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8DA0Bf,CAAC"}
|
package/dist/rules/built-in.js
CHANGED
|
@@ -12,6 +12,7 @@ import { importsAliased } from "#rules/imports/aliased.ts";
|
|
|
12
12
|
import { importCycles } from "#rules/imports/cycles.ts";
|
|
13
13
|
import { importFences } from "#rules/imports/fences.ts";
|
|
14
14
|
import { importsResolvable } from "#rules/imports/resolvable.ts";
|
|
15
|
+
import { storySetup } from "#rules/stories/setup.ts";
|
|
15
16
|
import { biomeOverrides } from "#rules/suppressions/biome-overrides.ts";
|
|
16
17
|
import { biomeRecommended } from "#rules/suppressions/biomeRecommended.ts";
|
|
17
18
|
import { noDoubleCast } from "#rules/suppressions/no-double-cast.ts";
|
|
@@ -47,5 +48,6 @@ export const builtInRules = [
|
|
|
47
48
|
otherNames,
|
|
48
49
|
testsFollow,
|
|
49
50
|
testsColocated,
|
|
51
|
+
storySetup,
|
|
50
52
|
];
|
|
51
53
|
//# sourceMappingURL=built-in.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"built-in.js","sourceRoot":"","sources":["../../src/rules/built-in.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,6CAA6C,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,CAAC,MAAM,YAAY,GAAG;IAC3B,QAAQ;IACR,OAAO;IACP,eAAe;IACf,aAAa;IACb,QAAQ;IACR,MAAM;IACN,mBAAmB;IACnB,UAAU;IACV,QAAQ;IACR,YAAY;IACZ,oBAAoB;IACpB,cAAc;IACd,gBAAgB;IAChB,KAAK;IACL,YAAY;IACZ,YAAY;IACZ,iBAAiB;IACjB,cAAc;IACd,eAAe;IACf,gBAAgB;IAChB,WAAW;IACX,UAAU;IACV,WAAW;IACX,cAAc;
|
|
1
|
+
{"version":3,"file":"built-in.js","sourceRoot":"","sources":["../../src/rules/built-in.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,6CAA6C,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,CAAC,MAAM,YAAY,GAAG;IAC3B,QAAQ;IACR,OAAO;IACP,eAAe;IACf,aAAa;IACb,QAAQ;IACR,MAAM;IACN,mBAAmB;IACnB,UAAU;IACV,QAAQ;IACR,YAAY;IACZ,oBAAoB;IACpB,cAAc;IACd,gBAAgB;IAChB,KAAK;IACL,YAAY;IACZ,YAAY;IACZ,iBAAiB;IACjB,cAAc;IACd,eAAe;IACf,gBAAgB;IAChB,WAAW;IACX,UAAU;IACV,WAAW;IACX,cAAc;IACd,UAAU;CACD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtureWrites.d.ts","sourceRoot":"","sources":["../../../src/rules/stories/fixtureWrites.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAyB5B,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAwDD,wBAAgB,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,SAAS,YAAY,EAAE,CAiB5E"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
const FS_MODULES = new Set(["fs", "fs/promises", "node:fs", "node:fs/promises"]);
|
|
3
|
+
const FIXTURE_WRITES = new Set([
|
|
4
|
+
"appendFile",
|
|
5
|
+
"appendFileSync",
|
|
6
|
+
"copyFile",
|
|
7
|
+
"copyFileSync",
|
|
8
|
+
"cp",
|
|
9
|
+
"cpSync",
|
|
10
|
+
"mkdir",
|
|
11
|
+
"mkdirSync",
|
|
12
|
+
"mkdtemp",
|
|
13
|
+
"mkdtempSync",
|
|
14
|
+
"symlink",
|
|
15
|
+
"symlinkSync",
|
|
16
|
+
"writeFile",
|
|
17
|
+
"writeFileSync",
|
|
18
|
+
]);
|
|
19
|
+
function fsImportsOf(source) {
|
|
20
|
+
return source.statements.flatMap((statement) => ts.isImportDeclaration(statement)
|
|
21
|
+
&& ts.isStringLiteral(statement.moduleSpecifier)
|
|
22
|
+
&& FS_MODULES.has(statement.moduleSpecifier.text)
|
|
23
|
+
&& statement.importClause !== undefined
|
|
24
|
+
? [statement.importClause]
|
|
25
|
+
: []);
|
|
26
|
+
}
|
|
27
|
+
function addBindings(clause, functions, namespaces) {
|
|
28
|
+
const bindings = clause.namedBindings;
|
|
29
|
+
for (const local of [clause.name, bindings !== undefined && ts.isNamespaceImport(bindings) ? bindings.name : undefined]) {
|
|
30
|
+
if (local !== undefined) {
|
|
31
|
+
namespaces.add(local.text);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const element of bindings !== undefined && ts.isNamedImports(bindings) ? bindings.elements : []) {
|
|
35
|
+
const imported = (element.propertyName ?? element.name).text;
|
|
36
|
+
if (imported === "promises") {
|
|
37
|
+
namespaces.add(element.name.text);
|
|
38
|
+
}
|
|
39
|
+
else if (FIXTURE_WRITES.has(imported)) {
|
|
40
|
+
functions.set(element.name.text, imported);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function fsBindingsOf(source) {
|
|
45
|
+
const functions = new Map();
|
|
46
|
+
const namespaces = new Set();
|
|
47
|
+
for (const clause of fsImportsOf(source)) {
|
|
48
|
+
addBindings(clause, functions, namespaces);
|
|
49
|
+
}
|
|
50
|
+
return { functions, namespaces };
|
|
51
|
+
}
|
|
52
|
+
function isFsNamespace(node, bindings) {
|
|
53
|
+
return ((ts.isIdentifier(node) && bindings.namespaces.has(node.text))
|
|
54
|
+
|| (ts.isPropertyAccessExpression(node) && node.name.text === "promises" && isFsNamespace(node.expression, bindings)));
|
|
55
|
+
}
|
|
56
|
+
function fixtureWriteOf(callee, bindings) {
|
|
57
|
+
if (ts.isIdentifier(callee)) {
|
|
58
|
+
return bindings.functions.get(callee.text);
|
|
59
|
+
}
|
|
60
|
+
if (ts.isPropertyAccessExpression(callee) && FIXTURE_WRITES.has(callee.name.text) && isFsNamespace(callee.expression, bindings)) {
|
|
61
|
+
return callee.name.text;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
export function fixtureWrites(source) {
|
|
66
|
+
const bindings = fsBindingsOf(source);
|
|
67
|
+
if (bindings.functions.size === 0 && bindings.namespaces.size === 0) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const writes = [];
|
|
71
|
+
function visit(node) {
|
|
72
|
+
if (ts.isCallExpression(node)) {
|
|
73
|
+
const name = fixtureWriteOf(node.expression, bindings);
|
|
74
|
+
if (name !== undefined) {
|
|
75
|
+
writes.push({ call: node, name });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
ts.forEachChild(node, visit);
|
|
79
|
+
}
|
|
80
|
+
visit(source);
|
|
81
|
+
return writes;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=fixtureWrites.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtureWrites.js","sourceRoot":"","sources":["../../../src/rules/stories/fixtureWrites.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../src/rules/stories/setup.ts"],"names":[],"mappings":"AA6DA,eAAO,MAAM,UAAU,yDAKrB,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { isTestSupport, testName } from "#naming/testName.ts";
|
|
3
|
+
import { defineRule } from "#rule.ts";
|
|
4
|
+
import { parse } from "#rules/syntax.ts";
|
|
5
|
+
import { fixtureWrites } from "./fixtureWrites.js";
|
|
6
|
+
const SETUP_HELPER = /^(?:seed|make|build|setup)(?![a-z])/u;
|
|
7
|
+
const STORY_KIT = "Name the setup in domain words as traits of the repository's story kit in test-support/, so the test reads as a story: see https://github.com/ShivaeDev/platform/tree/main/packages/quality#story-tests.";
|
|
8
|
+
function isTestFile(path) {
|
|
9
|
+
return testName(path) !== undefined && !isTestSupport(path);
|
|
10
|
+
}
|
|
11
|
+
function lineOf(source, node) {
|
|
12
|
+
return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
13
|
+
}
|
|
14
|
+
function unwrapped(node) {
|
|
15
|
+
return ts.isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
16
|
+
}
|
|
17
|
+
function isFunction(node) {
|
|
18
|
+
return node !== undefined && (ts.isArrowFunction(unwrapped(node)) || ts.isFunctionExpression(unwrapped(node)));
|
|
19
|
+
}
|
|
20
|
+
function helpersOf(statement) {
|
|
21
|
+
if (ts.isFunctionDeclaration(statement)) {
|
|
22
|
+
return statement.name === undefined || statement.body === undefined ? [] : [statement.name];
|
|
23
|
+
}
|
|
24
|
+
if (!ts.isVariableStatement(statement)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return statement.declarationList.declarations.flatMap((declaration) => ts.isIdentifier(declaration.name) && isFunction(declaration.initializer) ? [declaration.name] : []);
|
|
28
|
+
}
|
|
29
|
+
function storySetupIn(file) {
|
|
30
|
+
const source = isTestFile(file.path) ? parse(file) : undefined;
|
|
31
|
+
if (source === undefined) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const helpers = source.statements
|
|
35
|
+
.flatMap(helpersOf)
|
|
36
|
+
.filter((name) => SETUP_HELPER.test(name.text))
|
|
37
|
+
.map((name) => ({
|
|
38
|
+
file: file.path,
|
|
39
|
+
line: lineOf(source, name),
|
|
40
|
+
message: `Declares the setup helper "${name.text}". ${STORY_KIT}`,
|
|
41
|
+
subject: name.text,
|
|
42
|
+
}));
|
|
43
|
+
const writes = fixtureWrites(source).map(({ call, name }) => ({
|
|
44
|
+
file: file.path,
|
|
45
|
+
line: lineOf(source, call),
|
|
46
|
+
message: `Writes a fixture with ${name}. ${STORY_KIT}`,
|
|
47
|
+
subject: name,
|
|
48
|
+
}));
|
|
49
|
+
return [...helpers, ...writes];
|
|
50
|
+
}
|
|
51
|
+
export const storySetup = defineRule({
|
|
52
|
+
check: ({ sources }) => sources.flatMap(storySetupIn),
|
|
53
|
+
description: "A test reads as a story over the domain words of the repository's story kit in test-support/. It names its setup as traits instead of declaring seed, make, build or setup helpers or writing fixture files.",
|
|
54
|
+
id: "tests/story-setup",
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/rules/stories/setup.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/quality",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Repository quality gate with typed rules, a baseline of existing debt and a registry of reasoned exceptions",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/quality#readme",
|
|
6
6
|
"bugs": {
|
package/src/naming/testName.ts
CHANGED
|
@@ -26,6 +26,10 @@ export function testName(path: string): TestName | undefined {
|
|
|
26
26
|
|
|
27
27
|
const TEST_SUPPORT = "test-support";
|
|
28
28
|
|
|
29
|
+
export function isTestSupport(path: string): boolean {
|
|
30
|
+
return posix.dirname(path).split("/").includes(TEST_SUPPORT);
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
export function isTestCode(path: string): boolean {
|
|
30
|
-
return testName(path) !== undefined ||
|
|
34
|
+
return testName(path) !== undefined || isTestSupport(path);
|
|
31
35
|
}
|
package/src/rules/built-in.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { importsAliased } from "#rules/imports/aliased.ts";
|
|
|
12
12
|
import { importCycles } from "#rules/imports/cycles.ts";
|
|
13
13
|
import { importFences } from "#rules/imports/fences.ts";
|
|
14
14
|
import { importsResolvable } from "#rules/imports/resolvable.ts";
|
|
15
|
+
import { storySetup } from "#rules/stories/setup.ts";
|
|
15
16
|
import { biomeOverrides } from "#rules/suppressions/biome-overrides.ts";
|
|
16
17
|
import { biomeRecommended } from "#rules/suppressions/biomeRecommended.ts";
|
|
17
18
|
import { noDoubleCast } from "#rules/suppressions/no-double-cast.ts";
|
|
@@ -48,4 +49,5 @@ export const builtInRules = [
|
|
|
48
49
|
otherNames,
|
|
49
50
|
testsFollow,
|
|
50
51
|
testsColocated,
|
|
52
|
+
storySetup,
|
|
51
53
|
] as const;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
const FS_MODULES: ReadonlySet<string> = new Set(["fs", "fs/promises", "node:fs", "node:fs/promises"]);
|
|
4
|
+
const FIXTURE_WRITES: ReadonlySet<string> = new Set([
|
|
5
|
+
"appendFile",
|
|
6
|
+
"appendFileSync",
|
|
7
|
+
"copyFile",
|
|
8
|
+
"copyFileSync",
|
|
9
|
+
"cp",
|
|
10
|
+
"cpSync",
|
|
11
|
+
"mkdir",
|
|
12
|
+
"mkdirSync",
|
|
13
|
+
"mkdtemp",
|
|
14
|
+
"mkdtempSync",
|
|
15
|
+
"symlink",
|
|
16
|
+
"symlinkSync",
|
|
17
|
+
"writeFile",
|
|
18
|
+
"writeFileSync",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
interface FsBindings {
|
|
22
|
+
readonly functions: ReadonlyMap<string, string>;
|
|
23
|
+
readonly namespaces: ReadonlySet<string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FixtureWrite {
|
|
27
|
+
readonly call: ts.CallExpression;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fsImportsOf(source: ts.SourceFile): readonly ts.ImportClause[] {
|
|
32
|
+
return source.statements.flatMap((statement) =>
|
|
33
|
+
ts.isImportDeclaration(statement)
|
|
34
|
+
&& ts.isStringLiteral(statement.moduleSpecifier)
|
|
35
|
+
&& FS_MODULES.has(statement.moduleSpecifier.text)
|
|
36
|
+
&& statement.importClause !== undefined
|
|
37
|
+
? [statement.importClause]
|
|
38
|
+
: [],
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function addBindings(clause: ts.ImportClause, functions: Map<string, string>, namespaces: Set<string>): void {
|
|
43
|
+
const bindings = clause.namedBindings;
|
|
44
|
+
for (const local of [clause.name, bindings !== undefined && ts.isNamespaceImport(bindings) ? bindings.name : undefined]) {
|
|
45
|
+
if (local !== undefined) {
|
|
46
|
+
namespaces.add(local.text);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const element of bindings !== undefined && ts.isNamedImports(bindings) ? bindings.elements : []) {
|
|
50
|
+
const imported = (element.propertyName ?? element.name).text;
|
|
51
|
+
if (imported === "promises") {
|
|
52
|
+
namespaces.add(element.name.text);
|
|
53
|
+
} else if (FIXTURE_WRITES.has(imported)) {
|
|
54
|
+
functions.set(element.name.text, imported);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fsBindingsOf(source: ts.SourceFile): FsBindings {
|
|
60
|
+
const functions = new Map<string, string>();
|
|
61
|
+
const namespaces = new Set<string>();
|
|
62
|
+
for (const clause of fsImportsOf(source)) {
|
|
63
|
+
addBindings(clause, functions, namespaces);
|
|
64
|
+
}
|
|
65
|
+
return { functions, namespaces };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isFsNamespace(node: ts.Expression, bindings: FsBindings): boolean {
|
|
69
|
+
return (
|
|
70
|
+
(ts.isIdentifier(node) && bindings.namespaces.has(node.text))
|
|
71
|
+
|| (ts.isPropertyAccessExpression(node) && node.name.text === "promises" && isFsNamespace(node.expression, bindings))
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function fixtureWriteOf(callee: ts.Expression, bindings: FsBindings): string | undefined {
|
|
76
|
+
if (ts.isIdentifier(callee)) {
|
|
77
|
+
return bindings.functions.get(callee.text);
|
|
78
|
+
}
|
|
79
|
+
if (ts.isPropertyAccessExpression(callee) && FIXTURE_WRITES.has(callee.name.text) && isFsNamespace(callee.expression, bindings)) {
|
|
80
|
+
return callee.name.text;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function fixtureWrites(source: ts.SourceFile): readonly FixtureWrite[] {
|
|
86
|
+
const bindings = fsBindingsOf(source);
|
|
87
|
+
if (bindings.functions.size === 0 && bindings.namespaces.size === 0) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const writes: FixtureWrite[] = [];
|
|
91
|
+
function visit(node: ts.Node): void {
|
|
92
|
+
if (ts.isCallExpression(node)) {
|
|
93
|
+
const name = fixtureWriteOf(node.expression, bindings);
|
|
94
|
+
if (name !== undefined) {
|
|
95
|
+
writes.push({ call: node, name });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
ts.forEachChild(node, visit);
|
|
99
|
+
}
|
|
100
|
+
visit(source);
|
|
101
|
+
return writes;
|
|
102
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { isTestSupport, testName } from "#naming/testName.ts";
|
|
3
|
+
import { defineRule, type Finding, type SourceFile } from "#rule.ts";
|
|
4
|
+
import { parse } from "#rules/syntax.ts";
|
|
5
|
+
import { fixtureWrites } from "./fixtureWrites.ts";
|
|
6
|
+
|
|
7
|
+
const SETUP_HELPER = /^(?:seed|make|build|setup)(?![a-z])/u;
|
|
8
|
+
const STORY_KIT =
|
|
9
|
+
"Name the setup in domain words as traits of the repository's story kit in test-support/, so the test reads as a story: see https://github.com/ShivaeDev/platform/tree/main/packages/quality#story-tests.";
|
|
10
|
+
|
|
11
|
+
function isTestFile(path: string): boolean {
|
|
12
|
+
return testName(path) !== undefined && !isTestSupport(path);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function lineOf(source: ts.SourceFile, node: ts.Node): number {
|
|
16
|
+
return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function unwrapped(node: ts.Expression): ts.Expression {
|
|
20
|
+
return ts.isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isFunction(node: ts.Expression | undefined): boolean {
|
|
24
|
+
return node !== undefined && (ts.isArrowFunction(unwrapped(node)) || ts.isFunctionExpression(unwrapped(node)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function helpersOf(statement: ts.Statement): readonly ts.Identifier[] {
|
|
28
|
+
if (ts.isFunctionDeclaration(statement)) {
|
|
29
|
+
return statement.name === undefined || statement.body === undefined ? [] : [statement.name];
|
|
30
|
+
}
|
|
31
|
+
if (!ts.isVariableStatement(statement)) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
return statement.declarationList.declarations.flatMap((declaration) =>
|
|
35
|
+
ts.isIdentifier(declaration.name) && isFunction(declaration.initializer) ? [declaration.name] : [],
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function storySetupIn(file: SourceFile): readonly Finding[] {
|
|
40
|
+
const source = isTestFile(file.path) ? parse(file) : undefined;
|
|
41
|
+
if (source === undefined) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const helpers = source.statements
|
|
45
|
+
.flatMap(helpersOf)
|
|
46
|
+
.filter((name) => SETUP_HELPER.test(name.text))
|
|
47
|
+
.map((name) => ({
|
|
48
|
+
file: file.path,
|
|
49
|
+
line: lineOf(source, name),
|
|
50
|
+
message: `Declares the setup helper "${name.text}". ${STORY_KIT}`,
|
|
51
|
+
subject: name.text,
|
|
52
|
+
}));
|
|
53
|
+
const writes = fixtureWrites(source).map(({ call, name }) => ({
|
|
54
|
+
file: file.path,
|
|
55
|
+
line: lineOf(source, call),
|
|
56
|
+
message: `Writes a fixture with ${name}. ${STORY_KIT}`,
|
|
57
|
+
subject: name,
|
|
58
|
+
}));
|
|
59
|
+
return [...helpers, ...writes];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const storySetup = defineRule({
|
|
63
|
+
check: ({ sources }) => sources.flatMap(storySetupIn),
|
|
64
|
+
description:
|
|
65
|
+
"A test reads as a story over the domain words of the repository's story kit in test-support/. It names its setup as traits instead of declaring seed, make, build or setup helpers or writing fixture files.",
|
|
66
|
+
id: "tests/story-setup",
|
|
67
|
+
});
|