@shivaedev/quality 0.14.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 +13 -0
- package/README.md +36 -4
- package/dist/biome/recommendedRules.d.ts +2 -0
- package/dist/biome/recommendedRules.d.ts.map +1 -0
- package/dist/biome/recommendedRules.js +22 -0
- package/dist/biome/recommendedRules.js.map +1 -0
- 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/biome.d.ts +7 -1
- package/dist/rules/biome.d.ts.map +1 -1
- package/dist/rules/biome.js +6 -4
- package/dist/rules/biome.js.map +1 -1
- package/dist/rules/built-in.d.ts +2 -2
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +4 -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/dist/rules/suppressions/biomeRecommended.d.ts +4 -0
- package/dist/rules/suppressions/biomeRecommended.d.ts.map +1 -0
- package/dist/rules/suppressions/biomeRecommended.js +62 -0
- package/dist/rules/suppressions/biomeRecommended.js.map +1 -0
- package/package.json +1 -1
- package/src/biome/recommendedRules.ts +23 -0
- package/src/naming/testName.ts +5 -1
- package/src/rules/biome.ts +12 -4
- package/src/rules/built-in.ts +4 -0
- package/src/rules/stories/fixtureWrites.ts +102 -0
- package/src/rules/stories/setup.ts +67 -0
- package/src/rules/suppressions/biomeRecommended.ts +74 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.15.0 - 2026-10-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `suppressions/biome-recommended` reports each rule the installed Biome recommends that the shared preset neither sets to `error` nor declares in its `declarations.json`, so a Biome upgrade that adds or changes a recommended rule fails the gate instead of running at Biome's default level. It asks Biome itself for the recommended set, through `biome rage --linter` under the `recommended` preset, and each finding names the rule and says to set it to `error` or declare why not. It takes no registry exceptions. The shipped preset passes it, so a repository that upgrades has nothing to record.
|
|
15
|
+
|
|
3
16
|
## 0.14.0 - 2026-10-05
|
|
4
17
|
|
|
5
18
|
### Added
|
package/README.md
CHANGED
|
@@ -101,13 +101,14 @@ A repository with existing comments adopts the rules through the baseline, for e
|
|
|
101
101
|
|
|
102
102
|
### Suppressions
|
|
103
103
|
|
|
104
|
-
A check that is silenced at one site hides the problem instead of fixing it.
|
|
104
|
+
A check that is silenced at one site hides the problem instead of fixing it. Five rules close the escape hatches, and none of them takes registry exceptions: a registry entry that names one fails the gate as stale. A repository with existing suppressions adopts the rules through the baseline.
|
|
105
105
|
|
|
106
106
|
| Rule | Reports | Options |
|
|
107
107
|
| --- | --- | --- |
|
|
108
108
|
| `suppressions/no-inline` | Every comment directive that silences a linter, the compiler or a formatter, except `@ts-expect-error` in a type test | none |
|
|
109
109
|
| `suppressions/no-double-cast` | A cast through `unknown`, `any` or `never`: `x as unknown as T`, `x as any as T`, `x as never as T`, `<T><unknown>x` | none |
|
|
110
110
|
| `suppressions/biome-overrides` | A Biome setting that turns a check off or down, or keeps files out of it, without a declaration, and a declaration that matches no setting | `declared` |
|
|
111
|
+
| `suppressions/biome-recommended` | A rule the installed Biome recommends that the [Biome preset](#biome-preset) neither sets to `error` nor declares | none |
|
|
111
112
|
| `suppressions/no-ignore-deprecations` | `compilerOptions.ignoreDeprecations` in a tsconfig, which silences TypeScript's errors for deprecated options | none |
|
|
112
113
|
|
|
113
114
|
`suppressions/no-inline` reports these directives, wherever a line of a comment starts with one:
|
|
@@ -197,6 +198,10 @@ A package that ships a preset declares the weakenings the preset makes in a `dec
|
|
|
197
198
|
|
|
198
199
|
A weakening the preset makes without such a declaration is reported at the line of the repository's `extends` entry, and the repository's own declaration covers it. A weakening the repository adds beyond the preset needs its own declaration, and a repository declaration that repeats one the preset ships is reported until it is removed. A preset entry that cannot be resolved or read, or whose `declarations.json` is invalid, is reported at the `extends` entry as well.
|
|
199
200
|
|
|
201
|
+
#### Recommended Biome rules
|
|
202
|
+
|
|
203
|
+
`suppressions/biome-recommended` holds the [Biome preset](#biome-preset) to every rule Biome recommends, so a Biome upgrade that adds a recommended rule, or recommends one the preset leaves below `error`, fails the gate until the preset takes a position on it. It asks the installed Biome, the one `quality` runs, which rules its `recommended` preset enables, through `biome rage --linter`, so the list is never written down. When the root Biome config extends `@shivaedev/quality/biome`, it reads that preset and its `declarations.json`, resolved from the repository root as under [Shared presets](#shared-presets); a repository that does not extend the preset is left to the `biome` rule, which asks for it. A recommended rule is in order when the preset's `linter.rules` sets it to `error`, directly or through its group's severity, or when a declaration names it, whatever the declaration's `includes`. Every other recommended rule is reported at the preset, one finding per rule: set it to `error`, or declare why not.
|
|
204
|
+
|
|
200
205
|
### Imports
|
|
201
206
|
|
|
202
207
|
Four rules read the imports of the TypeScript and JavaScript modules among the sources. They build it once per run with the TypeScript compiler, without a bundler or another dependency.
|
|
@@ -397,7 +402,7 @@ The preset sets:
|
|
|
397
402
|
- **Plugins:** GritQL rules that ban ambient time, randomness, `console` and `process.env` for Effect's services, and the naming plugins under [Naming](#naming). They load from `./node_modules/@shivaedev/quality/biome/plugins`, so the package must be installed at the repository root.
|
|
398
403
|
- Files ignored by git are skipped.
|
|
399
404
|
|
|
400
|
-
The preset turns off `noUnusedVariables` and `noUnusedFunctionParameters`, because the tsconfig presets report them through TypeScript, allows default exports in `*.config.*` files, which tools load through the default export, and turns off `useComponentExportOnlyModules` in `*.test.*` and `*.spec.*` files, which define the helper and harness components they render. It also turns off `noProcessGlobal`, `useJsonImportAttributes`, `noMisusedPromises`, `useExhaustiveSwitchCases`, `useSortedClasses`, `noDelete`, `useConsistentArrayType`, `useConsistentCurlyBraces`, `noEqualsToNull` and `noSkippedTests`, because `quality fix` applies every lint fix and their fixes changed behavior or did not terminate on real code. It keeps `noUndeclaredClasses` off, because the rule cannot resolve a stylesheet imported through an alias and Tailwind utilities are not declared in CSS, and `noInlineStyles` off, because an inline style is the right tool for a value computed at run time. It declares these weakenings in its `declarations.json`, so `suppressions/biome-overrides` takes them as declared. Every other weakening a repository adds is an override it declares with a reason.
|
|
405
|
+
The preset turns off `noUnusedVariables` and `noUnusedFunctionParameters`, because the tsconfig presets report them through TypeScript, allows default exports in `*.config.*` files, which tools load through the default export, and turns off `useComponentExportOnlyModules` in `*.test.*` and `*.spec.*` files, which define the helper and harness components they render. It also turns off `noProcessGlobal`, `useJsonImportAttributes`, `noMisusedPromises`, `useExhaustiveSwitchCases`, `useSortedClasses`, `noDelete`, `useConsistentArrayType`, `useConsistentCurlyBraces`, `noEqualsToNull` and `noSkippedTests`, because `quality fix` applies every lint fix and their fixes changed behavior or did not terminate on real code. It keeps `noUndeclaredClasses` off, because the rule cannot resolve a stylesheet imported through an alias and Tailwind utilities are not declared in CSS, and `noInlineStyles` off, because an inline style is the right tool for a value computed at run time. It declares these weakenings in its `declarations.json`, so `suppressions/biome-overrides` takes them as declared and `suppressions/biome-recommended` accepts the recommended rules among them. Every other weakening a repository adds is an override it declares with a reason.
|
|
401
406
|
|
|
402
407
|
An autofix applies only a change that removes no decision; a fix that can change behavior or delete something written on purpose reports only, and the author decides. The preset keeps `noAccessKey`, `noAriaHiddenOnFocusable`, `noAutofocus`, `noInteractiveElementToNoninteractiveRole`, `noNoninteractiveElementToInteractiveRole`, `noNoninteractiveTabindex`, `noRedundantRoles`, `useValidAriaProps`, `useValidAriaRole`, `noImportantStyles`, `noConstAssign`, `noUnusedPrivateClassMembers`, `useExhaustiveDependencies`, `noFloatingPromises`, `useConsistentTestIt`, `useRegexpTest`, `useUnicodeRegex`, `noNonNullAssertion`, `useAtIndex`, `noParametersOnlyUsedInRecursion` and `useNamingConvention` at `error` with their fixes off, and `quality fix` skips `noDuplicateObjectKeys` and `useSortedKeys`, whose fixes Biome's config cannot turn off.
|
|
403
408
|
|
|
@@ -515,7 +520,34 @@ Both rules take a `suites` option, `.gitignore` patterns of folders that hold te
|
|
|
515
520
|
|
|
516
521
|
### Test code
|
|
517
522
|
|
|
518
|
-
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`.
|
|
519
551
|
|
|
520
552
|
## Vitest projects
|
|
521
553
|
|
|
@@ -582,4 +614,4 @@ A package that type-checks its tests with one config and builds `src` with anoth
|
|
|
582
614
|
|
|
583
615
|
## Validation
|
|
584
616
|
|
|
585
|
-
`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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommendedRules.d.ts","sourceRoot":"","sources":["../../src/biome/recommendedRules.ts"],"names":[],"mappings":"AASA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAanE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { runBiome } from "./run.js";
|
|
5
|
+
const RECOMMENDED_ONLY = '{ "linter": { "rules": { "preset": "recommended" } } }\n';
|
|
6
|
+
const ENABLED_RULE = /^ {4}(?<rule>[a-z0-9]+\/\w+)$/gimu;
|
|
7
|
+
export async function recommendedRules() {
|
|
8
|
+
const directory = await mkdtemp(join(tmpdir(), "quality-biome-recommended-"));
|
|
9
|
+
try {
|
|
10
|
+
await writeFile(join(directory, "biome.json"), RECOMMENDED_ONLY);
|
|
11
|
+
const rage = await runBiome(directory, ["rage", "--linter", `--config-path=${directory}`]);
|
|
12
|
+
const rules = [...rage.stdout.matchAll(ENABLED_RULE)].flatMap((match) => match.groups?.rule ?? []);
|
|
13
|
+
if (rules.length === 0) {
|
|
14
|
+
throw new Error(`biome rage --linter listed no enabled rules under the recommended preset:\n${[rage.stdout, rage.stderr].join("\n").trim()}`);
|
|
15
|
+
}
|
|
16
|
+
return rules;
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
await rm(directory, { force: true, recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=recommendedRules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommendedRules.js","sourceRoot":"","sources":["../../src/biome/recommendedRules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,MAAM,gBAAgB,GAAG,0DAA0D,CAAC;AAEpF,MAAM,YAAY,GAAG,mCAAmC,CAAC;AAEzD,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACrC,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,4BAA4B,CAAC,CAAC,CAAC;IAC9E,IAAI,CAAC;QACJ,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,gBAAgB,CAAC,CAAC;QACjE,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,iBAAiB,SAAS,EAAE,CAAC,CAAC,CAAC;QAC3F,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;QACnG,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC/I,CAAC;QACD,OAAO,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACV,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC;AACF,CAAC"}
|
|
@@ -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/biome.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { type Rule } from "#rule.ts";
|
|
1
|
+
import { type Rule, type RuleInputs } from "#rule.ts";
|
|
2
2
|
export declare const PRESET = "@shivaedev/quality/biome";
|
|
3
|
+
interface RootConfig {
|
|
4
|
+
readonly extendsPreset: boolean;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function rootConfig(readText: RuleInputs["readText"]): Promise<RootConfig>;
|
|
3
8
|
export declare const biome: Rule<"biome", undefined>;
|
|
9
|
+
export {};
|
|
4
10
|
//# sourceMappingURL=biome.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"biome.d.ts","sourceRoot":"","sources":["../../src/rules/biome.ts"],"names":[],"mappings":"AACA,OAAO,EAA4B,KAAK,IAAI,
|
|
1
|
+
{"version":3,"file":"biome.d.ts","sourceRoot":"","sources":["../../src/rules/biome.ts"],"names":[],"mappings":"AACA,OAAO,EAA4B,KAAK,IAAI,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAGhF,eAAO,MAAM,MAAM,6BAA6B,CAAC;AAsBjD,UAAU,UAAU;IACnB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAMtF;AAkBD,eAAO,MAAM,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,CAA+B,CAAC"}
|
package/dist/rules/biome.js
CHANGED
|
@@ -18,14 +18,16 @@ function findingOf(diagnostic) {
|
|
|
18
18
|
subject: diagnostic.category ?? "internal",
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
-
async function
|
|
21
|
+
export async function rootConfig(readText) {
|
|
22
22
|
const texts = await Promise.all(ROOT_CONFIGS.map(async (path) => ({ path, text: await readText(path) })));
|
|
23
23
|
const found = texts.find((candidate) => candidate.text !== undefined);
|
|
24
24
|
const parsed = found?.text === undefined ? undefined : parseJsonc(found.path, found.text);
|
|
25
25
|
const extended = parsed?._tag === "Parsed" ? itemsOf(member(parsed.json, "extends")).map(textOf) : [];
|
|
26
|
-
return extended.includes(PRESET)
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
return { extendsPreset: extended.includes(PRESET), path: found?.path ?? "biome.json" };
|
|
27
|
+
}
|
|
28
|
+
async function presetFinding(readText) {
|
|
29
|
+
const { extendsPreset, path } = await rootConfig(readText);
|
|
30
|
+
return extendsPreset ? [] : [{ file: path, message: `Extend "${PRESET}" from the root Biome config.`, subject: "preset" }];
|
|
29
31
|
}
|
|
30
32
|
const bridge = defineRule({
|
|
31
33
|
check: async ({ readText, root }) => {
|
package/dist/rules/biome.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"biome.js","sourceRoot":"","sources":["../../src/rules/biome.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAmB,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,UAAU,EAA4C,MAAM,UAAU,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAExF,MAAM,CAAC,MAAM,MAAM,GAAG,0BAA0B,CAAC;AAEjD,MAAM,YAAY,GAAsB,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;AAEtE,MAAM,OAAO,GAAwB,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAEjE,SAAS,SAAS,CAAC,UAAsB;IACxC,OAAO,UAAU,CAAC,QAAQ,KAAK,QAAQ,IAAI,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,8BAA8B,CAAC;QACvG,CAAC,CAAC,sCAAsC;QACxC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC;AACvB,CAAC;AAED,SAAS,SAAS,CAAC,UAAsB;IACxC,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,IAAI,CAAC,CAAC;IAClD,OAAO;QACN,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,IAAI,IAAI,GAAG;QACrC,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;QACjC,OAAO,EAAE,SAAS,CAAC,UAAU,CAAC;QAC9B,OAAO,EAAE,UAAU,CAAC,QAAQ,IAAI,UAAU;KAC1C,CAAC;AACH,CAAC;
|
|
1
|
+
{"version":3,"file":"biome.js","sourceRoot":"","sources":["../../src/rules/biome.ts"],"names":[],"mappings":"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"}
|
package/dist/rules/built-in.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare const builtInRules: readonly [import("../rule.ts").Rule<"structur
|
|
|
13
13
|
readonly reason: string;
|
|
14
14
|
readonly rule: string;
|
|
15
15
|
}[];
|
|
16
|
-
}>, import("../rule.ts").Rule<"biome", undefined>, import("../rule.ts").Rule<"imports/cycles", undefined>, import("../rule.ts").Rule<"imports/fences", {
|
|
16
|
+
}>, import("../rule.ts").Rule<"suppressions/biome-recommended", undefined>, import("../rule.ts").Rule<"biome", undefined>, import("../rule.ts").Rule<"imports/cycles", undefined>, import("../rule.ts").Rule<"imports/fences", {
|
|
17
17
|
readonly fences?: readonly import("../imports/fences/model.ts").Fence[];
|
|
18
18
|
}>, import("../rule.ts").Rule<"imports/resolvable", {
|
|
19
19
|
readonly generated?: readonly string[];
|
|
@@ -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,7 +12,9 @@ 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";
|
|
17
|
+
import { biomeRecommended } from "#rules/suppressions/biomeRecommended.ts";
|
|
16
18
|
import { noDoubleCast } from "#rules/suppressions/no-double-cast.ts";
|
|
17
19
|
import { noInline } from "#rules/suppressions/no-inline.ts";
|
|
18
20
|
import { noIgnoreDeprecations } from "#rules/suppressions/noIgnoreDeprecations.ts";
|
|
@@ -34,6 +36,7 @@ export const builtInRules = [
|
|
|
34
36
|
noDoubleCast,
|
|
35
37
|
noIgnoreDeprecations,
|
|
36
38
|
biomeOverrides,
|
|
39
|
+
biomeRecommended,
|
|
37
40
|
biome,
|
|
38
41
|
importCycles,
|
|
39
42
|
importFences,
|
|
@@ -45,5 +48,6 @@ export const builtInRules = [
|
|
|
45
48
|
otherNames,
|
|
46
49
|
testsFollow,
|
|
47
50
|
testsColocated,
|
|
51
|
+
storySetup,
|
|
48
52
|
];
|
|
49
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,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,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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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Rule } from "#rule.ts";
|
|
2
|
+
export declare function biomeRecommendedFrom(recommended: () => Promise<readonly string[]>): Rule<"suppressions/biome-recommended", undefined>;
|
|
3
|
+
export declare const biomeRecommended: Rule<"suppressions/biome-recommended", undefined>;
|
|
4
|
+
//# sourceMappingURL=biomeRecommended.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"biomeRecommended.d.ts","sourceRoot":"","sources":["../../../src/rules/suppressions/biomeRecommended.ts"],"names":[],"mappings":"AAEA,OAAO,EAA4B,KAAK,IAAI,EAAmB,MAAM,UAAU,CAAC;AA6DhF,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,gCAAgC,EAAE,SAAS,CAAC,CAQrI;AAED,eAAO,MAAM,gBAAgB,mDAAyC,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { recommendedRules } from "#biome/recommendedRules.ts";
|
|
3
|
+
import { defineRule } from "#rule.ts";
|
|
4
|
+
import { PRESET, rootConfig } from "#rules/biome.ts";
|
|
5
|
+
import { decodeShipped, PRESET_DECLARATIONS } from "#rules/suppressions/biome/declarations.ts";
|
|
6
|
+
import { member, parseJsonc, textOf } from "#rules/suppressions/biome/json.ts";
|
|
7
|
+
import { resolvePackage } from "#rules/suppressions/biome/resolve.ts";
|
|
8
|
+
function settingOf(rules, rule) {
|
|
9
|
+
const [group = "", name = ""] = rule.split("/");
|
|
10
|
+
const groupNode = member(rules, group);
|
|
11
|
+
const severity = textOf(groupNode);
|
|
12
|
+
if (severity !== undefined) {
|
|
13
|
+
return { level: severity, line: groupNode?.line };
|
|
14
|
+
}
|
|
15
|
+
const node = member(groupNode, name);
|
|
16
|
+
return { level: textOf(node) ?? textOf(member(node, "level")), line: node?.line };
|
|
17
|
+
}
|
|
18
|
+
const FIX = `Set it to "error" in the preset, or declare why not in its ${PRESET_DECLARATIONS}.`;
|
|
19
|
+
function looseRule(file, rule, { level, line }) {
|
|
20
|
+
const state = level === undefined ? "leaves at Biome's default level" : `sets to "${level}"`;
|
|
21
|
+
return { file, line, message: `Biome recommends "lint/${rule}", which the preset ${state}. ${FIX}`, subject: `lint/${rule}` };
|
|
22
|
+
}
|
|
23
|
+
function problem(file, message) {
|
|
24
|
+
return [{ file, message }];
|
|
25
|
+
}
|
|
26
|
+
async function check(readText, recommended) {
|
|
27
|
+
const root = await rootConfig(readText);
|
|
28
|
+
if (!root.extendsPreset) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const path = await resolvePackage(readText, PRESET);
|
|
32
|
+
if (path === undefined) {
|
|
33
|
+
return problem(root.path, `Cannot resolve "${PRESET}" from node_modules. Install @shivaedev/quality at the repository root.`);
|
|
34
|
+
}
|
|
35
|
+
const text = await readText(path);
|
|
36
|
+
const parsed = text === undefined ? undefined : parseJsonc(path, text);
|
|
37
|
+
if (parsed?._tag !== "Parsed") {
|
|
38
|
+
return problem(path, `Cannot read the preset "${PRESET}": ${parsed?.reason ?? "the file does not exist"}.`);
|
|
39
|
+
}
|
|
40
|
+
const declarationsPath = posix.join(posix.dirname(path), PRESET_DECLARATIONS);
|
|
41
|
+
const declarations = await decodeShipped(await readText(declarationsPath));
|
|
42
|
+
if (declarations._tag === "Invalid") {
|
|
43
|
+
return problem(declarationsPath, `Cannot read the declarations shipped with "${PRESET}": ${declarations.issues.join("; ")}.`);
|
|
44
|
+
}
|
|
45
|
+
const declared = new Set(declarations.value.map((declaration) => declaration.rule));
|
|
46
|
+
const rules = member(member(parsed.json, "linter"), "rules");
|
|
47
|
+
return (await recommended())
|
|
48
|
+
.filter((rule) => !declared.has(`lint/${rule}`))
|
|
49
|
+
.map((rule) => ({ rule, setting: settingOf(rules, rule) }))
|
|
50
|
+
.filter(({ setting }) => setting.level !== "error")
|
|
51
|
+
.map(({ rule, setting }) => looseRule(path, rule, setting));
|
|
52
|
+
}
|
|
53
|
+
export function biomeRecommendedFrom(recommended) {
|
|
54
|
+
return defineRule({
|
|
55
|
+
check: ({ readText }) => check(readText, recommended),
|
|
56
|
+
description: "Every rule the installed Biome recommends is at error in the shared preset, or the preset declares why not, so a Biome upgrade that adds or changes a recommended rule cannot slip in at Biome's default level.",
|
|
57
|
+
id: "suppressions/biome-recommended",
|
|
58
|
+
registrable: false,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export const biomeRecommended = biomeRecommendedFrom(recommendedRules);
|
|
62
|
+
//# sourceMappingURL=biomeRecommended.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"biomeRecommended.js","sourceRoot":"","sources":["../../../src/rules/suppressions/biomeRecommended.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": {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { runBiome } from "./run.ts";
|
|
5
|
+
|
|
6
|
+
const RECOMMENDED_ONLY = '{ "linter": { "rules": { "preset": "recommended" } } }\n';
|
|
7
|
+
|
|
8
|
+
const ENABLED_RULE = /^ {4}(?<rule>[a-z0-9]+\/\w+)$/gimu;
|
|
9
|
+
|
|
10
|
+
export async function recommendedRules(): Promise<readonly string[]> {
|
|
11
|
+
const directory = await mkdtemp(join(tmpdir(), "quality-biome-recommended-"));
|
|
12
|
+
try {
|
|
13
|
+
await writeFile(join(directory, "biome.json"), RECOMMENDED_ONLY);
|
|
14
|
+
const rage = await runBiome(directory, ["rage", "--linter", `--config-path=${directory}`]);
|
|
15
|
+
const rules = [...rage.stdout.matchAll(ENABLED_RULE)].flatMap((match) => match.groups?.rule ?? []);
|
|
16
|
+
if (rules.length === 0) {
|
|
17
|
+
throw new Error(`biome rage --linter listed no enabled rules under the recommended preset:\n${[rage.stdout, rage.stderr].join("\n").trim()}`);
|
|
18
|
+
}
|
|
19
|
+
return rules;
|
|
20
|
+
} finally {
|
|
21
|
+
await rm(directory, { force: true, recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
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/biome.ts
CHANGED
|
@@ -24,14 +24,22 @@ function findingOf(diagnostic: Diagnostic): Finding {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
interface RootConfig {
|
|
28
|
+
readonly extendsPreset: boolean;
|
|
29
|
+
readonly path: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function rootConfig(readText: RuleInputs["readText"]): Promise<RootConfig> {
|
|
28
33
|
const texts = await Promise.all(ROOT_CONFIGS.map(async (path) => ({ path, text: await readText(path) })));
|
|
29
34
|
const found = texts.find((candidate) => candidate.text !== undefined);
|
|
30
35
|
const parsed = found?.text === undefined ? undefined : parseJsonc(found.path, found.text);
|
|
31
36
|
const extended = parsed?._tag === "Parsed" ? itemsOf(member(parsed.json, "extends")).map(textOf) : [];
|
|
32
|
-
return extended.includes(PRESET)
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
return { extendsPreset: extended.includes(PRESET), path: found?.path ?? "biome.json" };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function presetFinding(readText: RuleInputs["readText"]): Promise<readonly Finding[]> {
|
|
41
|
+
const { extendsPreset, path } = await rootConfig(readText);
|
|
42
|
+
return extendsPreset ? [] : [{ file: path, message: `Extend "${PRESET}" from the root Biome config.`, subject: "preset" }];
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
const bridge = defineRule({
|
package/src/rules/built-in.ts
CHANGED
|
@@ -12,7 +12,9 @@ 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";
|
|
17
|
+
import { biomeRecommended } from "#rules/suppressions/biomeRecommended.ts";
|
|
16
18
|
import { noDoubleCast } from "#rules/suppressions/no-double-cast.ts";
|
|
17
19
|
import { noInline } from "#rules/suppressions/no-inline.ts";
|
|
18
20
|
import { noIgnoreDeprecations } from "#rules/suppressions/noIgnoreDeprecations.ts";
|
|
@@ -35,6 +37,7 @@ export const builtInRules = [
|
|
|
35
37
|
noDoubleCast,
|
|
36
38
|
noIgnoreDeprecations,
|
|
37
39
|
biomeOverrides,
|
|
40
|
+
biomeRecommended,
|
|
38
41
|
biome,
|
|
39
42
|
importCycles,
|
|
40
43
|
importFences,
|
|
@@ -46,4 +49,5 @@ export const builtInRules = [
|
|
|
46
49
|
otherNames,
|
|
47
50
|
testsFollow,
|
|
48
51
|
testsColocated,
|
|
52
|
+
storySetup,
|
|
49
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
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { recommendedRules } from "#biome/recommendedRules.ts";
|
|
3
|
+
import { defineRule, type Finding, type Rule, type RuleInputs } from "#rule.ts";
|
|
4
|
+
import { PRESET, rootConfig } from "#rules/biome.ts";
|
|
5
|
+
import { decodeShipped, PRESET_DECLARATIONS } from "#rules/suppressions/biome/declarations.ts";
|
|
6
|
+
import { type Json, member, parseJsonc, textOf } from "#rules/suppressions/biome/json.ts";
|
|
7
|
+
import { resolvePackage } from "#rules/suppressions/biome/resolve.ts";
|
|
8
|
+
|
|
9
|
+
interface Setting {
|
|
10
|
+
readonly level: string | undefined;
|
|
11
|
+
readonly line: number | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function settingOf(rules: Json | undefined, rule: string): Setting {
|
|
15
|
+
const [group = "", name = ""] = rule.split("/");
|
|
16
|
+
const groupNode = member(rules, group);
|
|
17
|
+
const severity = textOf(groupNode);
|
|
18
|
+
if (severity !== undefined) {
|
|
19
|
+
return { level: severity, line: groupNode?.line };
|
|
20
|
+
}
|
|
21
|
+
const node = member(groupNode, name);
|
|
22
|
+
return { level: textOf(node) ?? textOf(member(node, "level")), line: node?.line };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const FIX = `Set it to "error" in the preset, or declare why not in its ${PRESET_DECLARATIONS}.`;
|
|
26
|
+
|
|
27
|
+
function looseRule(file: string, rule: string, { level, line }: Setting): Finding {
|
|
28
|
+
const state = level === undefined ? "leaves at Biome's default level" : `sets to "${level}"`;
|
|
29
|
+
return { file, line, message: `Biome recommends "lint/${rule}", which the preset ${state}. ${FIX}`, subject: `lint/${rule}` };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function problem(file: string, message: string): readonly Finding[] {
|
|
33
|
+
return [{ file, message }];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function check(readText: RuleInputs["readText"], recommended: () => Promise<readonly string[]>): Promise<readonly Finding[]> {
|
|
37
|
+
const root = await rootConfig(readText);
|
|
38
|
+
if (!root.extendsPreset) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const path = await resolvePackage(readText, PRESET);
|
|
42
|
+
if (path === undefined) {
|
|
43
|
+
return problem(root.path, `Cannot resolve "${PRESET}" from node_modules. Install @shivaedev/quality at the repository root.`);
|
|
44
|
+
}
|
|
45
|
+
const text = await readText(path);
|
|
46
|
+
const parsed = text === undefined ? undefined : parseJsonc(path, text);
|
|
47
|
+
if (parsed?._tag !== "Parsed") {
|
|
48
|
+
return problem(path, `Cannot read the preset "${PRESET}": ${parsed?.reason ?? "the file does not exist"}.`);
|
|
49
|
+
}
|
|
50
|
+
const declarationsPath = posix.join(posix.dirname(path), PRESET_DECLARATIONS);
|
|
51
|
+
const declarations = await decodeShipped(await readText(declarationsPath));
|
|
52
|
+
if (declarations._tag === "Invalid") {
|
|
53
|
+
return problem(declarationsPath, `Cannot read the declarations shipped with "${PRESET}": ${declarations.issues.join("; ")}.`);
|
|
54
|
+
}
|
|
55
|
+
const declared = new Set(declarations.value.map((declaration) => declaration.rule));
|
|
56
|
+
const rules = member(member(parsed.json, "linter"), "rules");
|
|
57
|
+
return (await recommended())
|
|
58
|
+
.filter((rule) => !declared.has(`lint/${rule}`))
|
|
59
|
+
.map((rule) => ({ rule, setting: settingOf(rules, rule) }))
|
|
60
|
+
.filter(({ setting }) => setting.level !== "error")
|
|
61
|
+
.map(({ rule, setting }) => looseRule(path, rule, setting));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function biomeRecommendedFrom(recommended: () => Promise<readonly string[]>): Rule<"suppressions/biome-recommended", undefined> {
|
|
65
|
+
return defineRule({
|
|
66
|
+
check: ({ readText }) => check(readText, recommended),
|
|
67
|
+
description:
|
|
68
|
+
"Every rule the installed Biome recommends is at error in the shared preset, or the preset declares why not, so a Biome upgrade that adds or changes a recommended rule cannot slip in at Biome's default level.",
|
|
69
|
+
id: "suppressions/biome-recommended",
|
|
70
|
+
registrable: false,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const biomeRecommended = biomeRecommendedFrom(recommendedRules);
|