@shivaedev/quality 0.23.0 → 0.24.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 +4 -0
- package/dist/baseline/compare.d.ts +2 -2
- package/dist/baseline/compare.d.ts.map +1 -1
- package/dist/baseline/compare.js +5 -3
- package/dist/baseline/compare.js.map +1 -1
- package/dist/baseline/format.d.ts +5 -5
- package/dist/baseline/format.d.ts.map +1 -1
- package/dist/baseline/format.js +23 -13
- package/dist/baseline/format.js.map +1 -1
- package/dist/baseline/legacy.d.ts +2 -2
- package/dist/baseline/legacy.d.ts.map +1 -1
- package/dist/baseline/legacy.js +4 -4
- package/dist/baseline/legacy.js.map +1 -1
- package/dist/baseline/rewrite.d.ts +1 -1
- package/dist/baseline/rewrite.d.ts.map +1 -1
- package/dist/baseline/rewrite.js +2 -2
- package/dist/baseline/rewrite.js.map +1 -1
- package/dist/baseline/update.d.ts +2 -2
- package/dist/baseline/update.d.ts.map +1 -1
- package/dist/baseline/update.js +24 -18
- package/dist/baseline/update.js.map +1 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +15 -9
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/baseline.d.ts +1 -1
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js +10 -6
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/fix.d.ts +2 -2
- package/dist/cli/fix.d.ts.map +1 -1
- package/dist/cli/fix.js +34 -40
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/lint.d.ts +1 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/lint.js +1 -1
- package/dist/cli/lint.js.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +1 -1
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/run-main.js +2 -2
- package/dist/cli/run-main.js.map +1 -1
- package/dist/cli/shrink.d.ts +2 -2
- package/dist/cli/shrink.d.ts.map +1 -1
- package/dist/cli/shrink.js +9 -3
- package/dist/cli/shrink.js.map +1 -1
- package/dist/config/decode.d.ts +1 -1
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +13 -9
- package/dist/config/decode.js.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +26 -20
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts +22 -2
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +18 -10
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/decoded.d.ts +2 -2
- package/dist/decoded.d.ts.map +1 -1
- package/dist/decoded.js +7 -5
- package/dist/decoded.js.map +1 -1
- package/dist/engine/baseline-file.d.ts +1 -1
- package/dist/engine/baseline-file.d.ts.map +1 -1
- package/dist/engine/baseline-file.js +4 -2
- package/dist/engine/baseline-file.js.map +1 -1
- package/dist/engine/evaluate.d.ts +2 -2
- package/dist/engine/evaluate.d.ts.map +1 -1
- package/dist/engine/evaluate.js +5 -3
- package/dist/engine/evaluate.js.map +1 -1
- package/dist/engine/run-rules.d.ts +1 -1
- package/dist/engine/run-rules.d.ts.map +1 -1
- package/dist/engine/run-rules.js +24 -14
- package/dist/engine/run-rules.js.map +1 -1
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/session.js +8 -4
- package/dist/engine/session.js.map +1 -1
- package/dist/engine/violation.d.ts +7 -7
- package/dist/engine/violation.d.ts.map +1 -1
- package/dist/engine/violation.js +22 -10
- package/dist/engine/violation.js.map +1 -1
- package/dist/exceptions/registry.d.ts +2 -2
- package/dist/exceptions/registry.d.ts.map +1 -1
- package/dist/exceptions/registry.js +10 -6
- package/dist/exceptions/registry.js.map +1 -1
- package/dist/git/base.d.ts +1 -2
- package/dist/git/base.d.ts.map +1 -1
- package/dist/git/base.js +22 -10
- package/dist/git/base.js.map +1 -1
- package/dist/git/command.d.ts +2 -2
- package/dist/git/command.d.ts.map +1 -1
- package/dist/git/command.js +13 -9
- package/dist/git/command.js.map +1 -1
- package/dist/git/history.d.ts +1 -1
- package/dist/git/history.d.ts.map +1 -1
- package/dist/git/history.js +5 -3
- package/dist/git/history.js.map +1 -1
- package/dist/inventory/collect.d.ts +1 -1
- package/dist/inventory/collect.d.ts.map +1 -1
- package/dist/inventory/collect.js +7 -5
- package/dist/inventory/collect.js.map +1 -1
- package/dist/inventory/filesystem.d.ts +1 -1
- package/dist/inventory/filesystem.d.ts.map +1 -1
- package/dist/inventory/filesystem.js +11 -7
- package/dist/inventory/filesystem.js.map +1 -1
- package/dist/inventory/ignore-scope.d.ts +4 -4
- package/dist/inventory/ignore-scope.d.ts.map +1 -1
- package/dist/inventory/ignore-scope.js +18 -9
- package/dist/inventory/ignore-scope.js.map +1 -1
- package/dist/manifests/sorted.d.ts +1 -1
- package/dist/manifests/sorted.d.ts.map +1 -1
- package/dist/manifests/sorted.js +14 -16
- package/dist/manifests/sorted.js.map +1 -1
- package/dist/report/plural.d.ts +1 -1
- package/dist/report/plural.d.ts.map +1 -1
- package/dist/report/plural.js +3 -1
- package/dist/report/plural.js.map +1 -1
- package/dist/report/render.d.ts +1 -1
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +29 -21
- package/dist/report/render.js.map +1 -1
- package/dist/report/stale.d.ts +1 -1
- package/dist/report/stale.d.ts.map +1 -1
- package/dist/report/stale.js +18 -8
- package/dist/report/stale.js.map +1 -1
- package/dist/rule.d.ts +13 -13
- package/dist/rule.d.ts.map +1 -1
- package/dist/rule.js +10 -8
- package/dist/rule.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/comments/kinds.d.ts +4 -4
- package/dist/rules/comments/kinds.d.ts.map +1 -1
- package/dist/rules/comments/kinds.js +16 -8
- package/dist/rules/comments/kinds.js.map +1 -1
- package/dist/rules/comments/max-per-file.d.ts.map +1 -1
- package/dist/rules/comments/max-per-file.js +13 -9
- package/dist/rules/comments/max-per-file.js.map +1 -1
- package/dist/rules/comments/pattern-rule.d.ts +1 -1
- package/dist/rules/comments/pattern-rule.d.ts.map +1 -1
- package/dist/rules/comments/pattern-rule.js +12 -10
- package/dist/rules/comments/pattern-rule.js.map +1 -1
- package/dist/rules/comments/scan.d.ts +3 -3
- package/dist/rules/comments/scan.d.ts.map +1 -1
- package/dist/rules/comments/scan.js +23 -19
- package/dist/rules/comments/scan.js.map +1 -1
- package/dist/rules/max-lines.d.ts.map +1 -1
- package/dist/rules/max-lines.js +12 -10
- package/dist/rules/max-lines.js.map +1 -1
- package/dist/rules/stories/origins.d.ts +10 -0
- package/dist/rules/stories/origins.d.ts.map +1 -0
- package/dist/rules/stories/origins.js +129 -0
- package/dist/rules/stories/origins.js.map +1 -0
- package/dist/rules/stories/prerequisites.d.ts +4 -0
- package/dist/rules/stories/prerequisites.d.ts.map +1 -0
- package/dist/rules/stories/prerequisites.js +32 -0
- package/dist/rules/stories/prerequisites.js.map +1 -0
- package/dist/rules/stories/program.d.ts +5 -0
- package/dist/rules/stories/program.d.ts.map +1 -0
- package/dist/rules/stories/program.js +22 -0
- package/dist/rules/stories/program.js.map +1 -0
- package/dist/rules/stories/specs.d.ts +2 -0
- package/dist/rules/stories/specs.d.ts.map +1 -0
- package/dist/rules/stories/specs.js +80 -0
- package/dist/rules/stories/specs.js.map +1 -0
- package/dist/rules/suppressions/biome/configs.d.ts +1 -1
- package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/configs.js +23 -17
- package/dist/rules/suppressions/biome/configs.js.map +1 -1
- package/dist/rules/suppressions/biome/declarations.d.ts +4 -4
- package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/declarations.js +15 -7
- package/dist/rules/suppressions/biome/declarations.js.map +1 -1
- package/dist/rules/suppressions/biome/json.d.ts +6 -6
- package/dist/rules/suppressions/biome/json.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/json.js +27 -13
- package/dist/rules/suppressions/biome/json.js.map +1 -1
- package/dist/rules/suppressions/biome/resolve.d.ts +1 -1
- package/dist/rules/suppressions/biome/resolve.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/resolve.js +23 -17
- package/dist/rules/suppressions/biome/resolve.js.map +1 -1
- package/dist/rules/suppressions/biome/weakenings.d.ts +1 -1
- package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/weakenings.js +78 -52
- package/dist/rules/suppressions/biome/weakenings.js.map +1 -1
- package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
- package/dist/rules/suppressions/biome-overrides.js +45 -33
- package/dist/rules/suppressions/biome-overrides.js.map +1 -1
- package/dist/rules/suppressions/directives.d.ts +1 -1
- package/dist/rules/suppressions/directives.d.ts.map +1 -1
- package/dist/rules/suppressions/directives.js +5 -3
- package/dist/rules/suppressions/directives.js.map +1 -1
- package/dist/rules/suppressions/no-double-cast.d.ts.map +1 -1
- package/dist/rules/suppressions/no-double-cast.js +14 -10
- package/dist/rules/suppressions/no-double-cast.js.map +1 -1
- package/dist/rules/suppressions/no-inline.d.ts.map +1 -1
- package/dist/rules/suppressions/no-inline.js +8 -6
- package/dist/rules/suppressions/no-inline.js.map +1 -1
- package/dist/rules/suppressions/stylesheet-comments.d.ts +1 -1
- package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
- package/dist/rules/suppressions/stylesheet-comments.js +11 -5
- package/dist/rules/suppressions/stylesheet-comments.js.map +1 -1
- package/dist/rules/syntax.d.ts +1 -1
- package/dist/rules/syntax.d.ts.map +1 -1
- package/dist/rules/syntax.js +2 -2
- package/dist/rules/syntax.js.map +1 -1
- package/dist/standard-schema.d.ts +6 -6
- package/dist/standard-schema.d.ts.map +1 -1
- package/docs/modules.md +2 -0
- package/package.json +1 -1
- package/src/baseline/compare.ts +5 -3
- package/src/baseline/format.ts +17 -10
- package/src/baseline/legacy.ts +4 -4
- package/src/baseline/rewrite.ts +2 -2
- package/src/baseline/update.ts +20 -16
- package/src/cli/args.ts +15 -10
- package/src/cli/baseline.ts +36 -35
- package/src/cli/fix.ts +39 -42
- package/src/cli/lint.ts +17 -19
- package/src/cli/migrate.ts +20 -21
- package/src/cli/run-main.ts +2 -2
- package/src/cli/shrink.ts +11 -8
- package/src/config/decode.ts +15 -10
- package/src/config/load.ts +44 -37
- package/src/config/resolve.ts +32 -27
- package/src/config.ts +3 -1
- package/src/decoded.ts +5 -4
- package/src/engine/baseline-file.ts +14 -12
- package/src/engine/evaluate.ts +6 -5
- package/src/engine/run-rules.ts +21 -13
- package/src/engine/session.ts +32 -25
- package/src/engine/violation.ts +22 -11
- package/src/exceptions/registry.ts +10 -8
- package/src/git/base.ts +45 -37
- package/src/git/command.ts +6 -4
- package/src/git/history.ts +5 -4
- package/src/inventory/collect.ts +39 -34
- package/src/inventory/filesystem.ts +38 -30
- package/src/inventory/ignore-scope.ts +20 -10
- package/src/manifests/sorted.ts +18 -18
- package/src/report/plural.ts +3 -1
- package/src/report/render.ts +23 -18
- package/src/report/stale.ts +33 -25
- package/src/rule.ts +25 -19
- package/src/rules/built-in.ts +2 -0
- package/src/rules/comments/kinds.ts +16 -9
- package/src/rules/comments/max-per-file.ts +15 -11
- package/src/rules/comments/pattern-rule.ts +5 -4
- package/src/rules/comments/scan.ts +18 -15
- package/src/rules/max-lines.ts +3 -2
- package/src/rules/stories/origins.ts +147 -0
- package/src/rules/stories/prerequisites.ts +37 -0
- package/src/rules/stories/program.ts +29 -0
- package/src/rules/stories/specs.ts +93 -0
- package/src/rules/suppressions/biome/configs.ts +23 -17
- package/src/rules/suppressions/biome/declarations.ts +15 -10
- package/src/rules/suppressions/biome/json.ts +25 -13
- package/src/rules/suppressions/biome/resolve.ts +23 -18
- package/src/rules/suppressions/biome/weakenings.ts +60 -41
- package/src/rules/suppressions/biome-overrides.ts +32 -22
- package/src/rules/suppressions/directives.ts +5 -4
- package/src/rules/suppressions/no-double-cast.ts +14 -10
- package/src/rules/suppressions/no-inline.ts +5 -4
- package/src/rules/suppressions/stylesheet-comments.ts +11 -6
- package/src/rules/syntax.ts +2 -2
- package/src/standard-schema.ts +4 -4
package/src/rule.ts
CHANGED
|
@@ -28,35 +28,35 @@ export interface RuleInputs {
|
|
|
28
28
|
readonly sources: readonly SourceFile[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export interface RuleContext<
|
|
32
|
-
readonly options:
|
|
31
|
+
export interface RuleContext<TOptions> extends RuleInputs {
|
|
32
|
+
readonly options: TOptions;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export type Configured =
|
|
36
36
|
| { readonly _tag: "Invalid"; readonly issues: readonly string[] }
|
|
37
37
|
| { readonly _tag: "Ready"; readonly check: (inputs: RuleInputs) => Promise<Findings> };
|
|
38
38
|
|
|
39
|
-
export interface Rule<
|
|
39
|
+
export interface Rule<TId extends string = string, TInput = unknown> {
|
|
40
40
|
// Bivariance keeps rules with different options in one list; configure validates its input at runtime.
|
|
41
|
-
configure: Bivariant<(options:
|
|
41
|
+
configure: Bivariant<(options: TInput | undefined) => Promise<Configured>>;
|
|
42
42
|
readonly description: string;
|
|
43
43
|
readonly family?: boolean;
|
|
44
|
-
readonly id:
|
|
44
|
+
readonly id: TId;
|
|
45
45
|
readonly registrable?: boolean;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
interface RuleDefinition<
|
|
49
|
-
readonly check: (context: RuleContext<
|
|
48
|
+
interface RuleDefinition<TId extends string, TOptions> {
|
|
49
|
+
readonly check: (context: RuleContext<TOptions>) => Findings | PromiseLike<Findings>;
|
|
50
50
|
readonly description: string;
|
|
51
|
-
readonly id:
|
|
51
|
+
readonly id: TId;
|
|
52
52
|
readonly registrable?: false;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
interface RuleDefinitionWithOptions<
|
|
56
|
-
readonly options: StandardSchemaV1<
|
|
55
|
+
interface RuleDefinitionWithOptions<TId extends string, TInput, TOptions> extends RuleDefinition<TId, TOptions> {
|
|
56
|
+
readonly options: StandardSchemaV1<TInput, TOptions>;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
interface RuleDefinitionWithoutOptions<
|
|
59
|
+
interface RuleDefinitionWithoutOptions<TId extends string> extends RuleDefinition<TId, undefined> {
|
|
60
60
|
readonly options?: undefined;
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -68,9 +68,12 @@ const noOptions: StandardSchemaV1<undefined, undefined> = {
|
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
function configureWith<TInput, TOptions>(
|
|
72
|
+
schema: StandardSchemaV1<TInput, TOptions>,
|
|
73
|
+
check: RuleDefinition<string, TOptions>["check"],
|
|
74
|
+
absent: unknown,
|
|
75
|
+
) {
|
|
76
|
+
return async (options: unknown): Promise<Configured> => {
|
|
74
77
|
const result = await schema["~standard"].validate(options ?? absent);
|
|
75
78
|
if (result.issues !== undefined) {
|
|
76
79
|
return { _tag: "Invalid", issues: result.issues.map(describeIssue) };
|
|
@@ -78,12 +81,15 @@ const configureWith =
|
|
|
78
81
|
const value = result.value;
|
|
79
82
|
return { _tag: "Ready", check: async (inputs) => check({ ...inputs, options: value }) };
|
|
80
83
|
};
|
|
84
|
+
}
|
|
81
85
|
|
|
82
|
-
export function defineRule<const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
export function defineRule<const TId extends string, TInput, TOptions>(
|
|
87
|
+
definition: RuleDefinitionWithOptions<TId, TInput, TOptions>,
|
|
88
|
+
): Rule<TId, TInput>;
|
|
89
|
+
export function defineRule<const TId extends string>(definition: RuleDefinitionWithoutOptions<TId>): Rule<TId, undefined>;
|
|
90
|
+
export function defineRule<const TId extends string, TInput, TOptions>(
|
|
91
|
+
definition: RuleDefinitionWithOptions<TId, TInput, TOptions> | RuleDefinitionWithoutOptions<TId>,
|
|
92
|
+
): Rule<TId, TInput> | Rule<TId, undefined> {
|
|
87
93
|
const configure =
|
|
88
94
|
definition.options === undefined
|
|
89
95
|
? configureWith(noOptions, definition.check, undefined)
|
package/src/rules/built-in.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { noTestCode } from "#rules/imports/noTestCode.ts";
|
|
|
17
17
|
import { importsResolvable } from "#rules/imports/resolvable.ts";
|
|
18
18
|
import { importsRuntime } from "#rules/imports/runtime.ts";
|
|
19
19
|
import { storySetup } from "#rules/stories/setup.ts";
|
|
20
|
+
import { storySpecs } from "#rules/stories/specs.ts";
|
|
20
21
|
import { biomeOverrides } from "#rules/suppressions/biome-overrides.ts";
|
|
21
22
|
import { biomeRecommended } from "#rules/suppressions/biomeRecommended.ts";
|
|
22
23
|
import { noDoubleCast } from "#rules/suppressions/no-double-cast.ts";
|
|
@@ -62,4 +63,5 @@ export const builtInRules = [
|
|
|
62
63
|
testsFollow,
|
|
63
64
|
testsColocated,
|
|
64
65
|
storySetup,
|
|
66
|
+
storySpecs,
|
|
65
67
|
] as const;
|
|
@@ -15,21 +15,28 @@ const DIRECTIVES: readonly RegExp[] = [
|
|
|
15
15
|
|
|
16
16
|
const TRIPLE_SLASH = /^\/\/\/\s*</u;
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
function contentOf(comment: SourceComment): readonly string[] {
|
|
19
|
+
return comment.body.filter((line) => line !== "");
|
|
20
|
+
}
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
tags.some((tag) => line === tag || line.startsWith(`${tag} `) || line.startsWith(`${tag}\t`));
|
|
22
|
+
function startsWithTag(line: string, tags: readonly string[]): boolean {
|
|
23
|
+
return tags.some((tag) => line === tag || line.startsWith(`${tag} `) || line.startsWith(`${tag}\t`));
|
|
24
|
+
}
|
|
22
25
|
|
|
23
|
-
export
|
|
26
|
+
export function textOf(comment: SourceComment): string {
|
|
27
|
+
return comment.body.join("\n");
|
|
28
|
+
}
|
|
24
29
|
|
|
25
|
-
export
|
|
30
|
+
export function isJsdoc(comment: SourceComment): boolean {
|
|
31
|
+
return comment.kind === "block" && comment.text.startsWith("/**") && comment.text !== "/**/";
|
|
32
|
+
}
|
|
26
33
|
|
|
27
|
-
export
|
|
34
|
+
export function isPragma(comment: SourceComment, pragmas: readonly string[]): boolean {
|
|
28
35
|
const content = contentOf(comment);
|
|
29
36
|
return content.length > 0 && content.every((line) => startsWithTag(line, pragmas));
|
|
30
|
-
}
|
|
37
|
+
}
|
|
31
38
|
|
|
32
|
-
export
|
|
39
|
+
export function isDirective(comment: SourceComment): boolean {
|
|
33
40
|
const first = contentOf(comment)[0] ?? "";
|
|
34
41
|
return TRIPLE_SLASH.test(comment.text) || DIRECTIVES.some((directive) => directive.test(first));
|
|
35
|
-
}
|
|
42
|
+
}
|
|
@@ -9,18 +9,22 @@ const MaxPerFileOptions = Schema.Struct({
|
|
|
9
9
|
max: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(2)).pipe(Schema.withDecodingDefaultKey(Effect.succeed(2))),
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
function continues(previous: SourceComment | undefined, comment: SourceComment): boolean {
|
|
13
|
+
return (
|
|
14
|
+
previous !== undefined
|
|
15
|
+
&& previous.kind === "line"
|
|
16
|
+
&& comment.kind === "line"
|
|
17
|
+
&& previous.ownLine
|
|
18
|
+
&& comment.ownLine
|
|
19
|
+
&& comment.line === previous.endLine + 1
|
|
20
|
+
);
|
|
21
|
+
}
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
comments.filter((comment, index) => !continues(comments[index - 1], comment));
|
|
23
|
+
function commentBlocks(comments: readonly SourceComment[]): readonly SourceComment[] {
|
|
24
|
+
return comments.filter((comment, index) => !continues(comments[index - 1], comment));
|
|
25
|
+
}
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
function overCap(file: SourceFile, max: number, allow: readonly string[]): readonly Finding[] {
|
|
24
28
|
const counted = commentsOf(file).filter((comment) => !(isDirective(comment) || isPragma(comment, allow)));
|
|
25
29
|
const blocks = commentBlocks(counted);
|
|
26
30
|
const first = blocks[max];
|
|
@@ -35,7 +39,7 @@ const overCap = (file: SourceFile, max: number, allow: readonly string[]): reado
|
|
|
35
39
|
threshold: max,
|
|
36
40
|
},
|
|
37
41
|
];
|
|
38
|
-
}
|
|
42
|
+
}
|
|
39
43
|
|
|
40
44
|
export const maxPerFile = defineRule({
|
|
41
45
|
check: ({ options, sources }) => sources.flatMap((file) => overCap(file, options.max, options.allow)),
|
|
@@ -9,13 +9,13 @@ interface PatternRule<Id extends string> {
|
|
|
9
9
|
readonly patterns: readonly RegExp[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
function firstMatch(comment: SourceComment, patterns: readonly RegExp[]): string | undefined {
|
|
13
13
|
const text = textOf(comment);
|
|
14
14
|
return patterns.map((pattern) => pattern.exec(text)?.[0]).find((match) => match !== undefined);
|
|
15
|
-
}
|
|
15
|
+
}
|
|
16
16
|
|
|
17
|
-
export
|
|
18
|
-
defineRule({
|
|
17
|
+
export function definePatternRule<const Id extends string>({ description, id, message, patterns }: PatternRule<Id>): Rule<Id, undefined> {
|
|
18
|
+
return defineRule({
|
|
19
19
|
check: ({ sources }) =>
|
|
20
20
|
sources.flatMap((file) =>
|
|
21
21
|
commentsOf(file).flatMap((comment): readonly Finding[] => {
|
|
@@ -26,3 +26,4 @@ export const definePatternRule = <const Id extends string>({ description, id, me
|
|
|
26
26
|
description,
|
|
27
27
|
id,
|
|
28
28
|
});
|
|
29
|
+
}
|
|
@@ -13,15 +13,15 @@ export interface SourceComment {
|
|
|
13
13
|
|
|
14
14
|
const DECLARATION = /\.d\.[cm]?ts$/u;
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
function commentRanges(source: ts.SourceFile): readonly ts.CommentRange[] {
|
|
17
17
|
const ranges = new Map<number, ts.CommentRange>();
|
|
18
18
|
const jsxText: ts.Node[] = [];
|
|
19
|
-
|
|
19
|
+
function add(found: readonly ts.CommentRange[] | undefined) {
|
|
20
20
|
for (const range of found ?? []) {
|
|
21
21
|
ranges.set(range.pos, range);
|
|
22
22
|
}
|
|
23
|
-
}
|
|
24
|
-
|
|
23
|
+
}
|
|
24
|
+
function visit(node: ts.Node) {
|
|
25
25
|
if (node.kind === ts.SyntaxKind.JsxText) {
|
|
26
26
|
jsxText.push(node);
|
|
27
27
|
}
|
|
@@ -30,22 +30,23 @@ const commentRanges = (source: ts.SourceFile): readonly ts.CommentRange[] => {
|
|
|
30
30
|
for (const child of node.getChildren(source)) {
|
|
31
31
|
visit(child);
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
|
34
34
|
visit(source);
|
|
35
35
|
return [...ranges.values()]
|
|
36
36
|
.filter((range) => !jsxText.some((text) => range.pos >= text.pos && range.pos < text.end))
|
|
37
37
|
.sort((left, right) => left.pos - right.pos);
|
|
38
|
-
}
|
|
38
|
+
}
|
|
39
39
|
|
|
40
|
-
export
|
|
41
|
-
kind === "line"
|
|
40
|
+
export function bodyOf(kind: SourceComment["kind"], text: string): readonly string[] {
|
|
41
|
+
return kind === "line"
|
|
42
42
|
? [text.slice(2).trim()]
|
|
43
43
|
: text
|
|
44
44
|
.slice(2, -2)
|
|
45
45
|
.split("\n")
|
|
46
46
|
.map((line) => line.trim().replace(/^\*(?=\s|$)\s*/u, ""));
|
|
47
|
+
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
function toComment(source: ts.SourceFile, range: ts.CommentRange): SourceComment {
|
|
49
50
|
const kind = range.kind === ts.SyntaxKind.SingleLineCommentTrivia ? "line" : "block";
|
|
50
51
|
const text = source.text.slice(range.pos, range.end);
|
|
51
52
|
const start = source.getLineAndCharacterOfPosition(range.pos);
|
|
@@ -58,16 +59,16 @@ const toComment = (source: ts.SourceFile, range: ts.CommentRange): SourceComment
|
|
|
58
59
|
ownLine: source.text.slice(lineStart, range.pos).trim() === "",
|
|
59
60
|
text,
|
|
60
61
|
};
|
|
61
|
-
}
|
|
62
|
+
}
|
|
62
63
|
|
|
63
64
|
const scanned = new WeakMap<SourceFile, readonly SourceComment[]>();
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
function scan(file: SourceFile): readonly SourceComment[] {
|
|
66
67
|
const source = parse(file);
|
|
67
68
|
return source === undefined ? [] : commentRanges(source).map((range) => toComment(source, range));
|
|
68
|
-
}
|
|
69
|
+
}
|
|
69
70
|
|
|
70
|
-
export
|
|
71
|
+
export function scanComments(file: SourceFile): readonly SourceComment[] {
|
|
71
72
|
const cached = scanned.get(file);
|
|
72
73
|
if (cached !== undefined) {
|
|
73
74
|
return cached;
|
|
@@ -75,6 +76,8 @@ export const scanComments = (file: SourceFile): readonly SourceComment[] => {
|
|
|
75
76
|
const comments = scan(file);
|
|
76
77
|
scanned.set(file, comments);
|
|
77
78
|
return comments;
|
|
78
|
-
}
|
|
79
|
+
}
|
|
79
80
|
|
|
80
|
-
export
|
|
81
|
+
export function commentsOf(file: SourceFile): readonly SourceComment[] {
|
|
82
|
+
return DECLARATION.test(file.path) ? [] : scanComments(file);
|
|
83
|
+
}
|
package/src/rules/max-lines.ts
CHANGED
|
@@ -11,8 +11,8 @@ const MaxLinesOptions = Schema.Struct({
|
|
|
11
11
|
test: Limit.check(Schema.isLessThanOrEqualTo(300)).pipe(Schema.withDecodingDefaultKey(Effect.succeed(300))),
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
file.lines.length <= limit
|
|
14
|
+
function oversized(file: SourceFile, limit: number): readonly Finding[] {
|
|
15
|
+
return file.lines.length <= limit
|
|
16
16
|
? []
|
|
17
17
|
: [
|
|
18
18
|
{
|
|
@@ -22,6 +22,7 @@ const oversized = (file: SourceFile, limit: number): readonly Finding[] =>
|
|
|
22
22
|
threshold: limit,
|
|
23
23
|
},
|
|
24
24
|
];
|
|
25
|
+
}
|
|
25
26
|
|
|
26
27
|
export const maxLines = defineRule({
|
|
27
28
|
check: ({ options, sources }) =>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import type { ImportGraph } from "#imports/graph.ts";
|
|
4
|
+
import type { RuleInputs } from "#rule.ts";
|
|
5
|
+
import { storyProgram } from "./program.ts";
|
|
6
|
+
|
|
7
|
+
type Origin = "factory" | "kit" | "story" | "plain" | "suite" | "vitest" | "stories" | "function" | undefined;
|
|
8
|
+
|
|
9
|
+
function importFrom(node: ts.Node): string | undefined {
|
|
10
|
+
for (let current: ts.Node | undefined = node; current !== undefined; current = current.parent) {
|
|
11
|
+
if (ts.isImportDeclaration(current) && ts.isStringLiteral(current.moduleSpecifier)) {
|
|
12
|
+
return current.moduleSpecifier.text;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function moduleOrigin(from: string | undefined): Origin {
|
|
19
|
+
if (from === "vitest" || from === "@effect/vitest") {
|
|
20
|
+
return "vitest";
|
|
21
|
+
}
|
|
22
|
+
return from === "@shivaedev/test-story" || from === "@shivaedev/test-story/storyKit.ts" ? "stories" : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function imported(name: string, from: string | undefined): Origin {
|
|
26
|
+
const namespace = moduleOrigin(from);
|
|
27
|
+
if (name === "storyKit" && namespace === "stories") {
|
|
28
|
+
return "factory";
|
|
29
|
+
}
|
|
30
|
+
if ((name === "describe" || name === "suite") && namespace === "vitest") {
|
|
31
|
+
return "suite";
|
|
32
|
+
}
|
|
33
|
+
return (name === "it" || name === "test") && namespace === "vitest" ? "plain" : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function storyOrigins(inputs: RuleInputs, graph: ImportGraph) {
|
|
37
|
+
const program = storyProgram(inputs, graph);
|
|
38
|
+
const checker = program.getTypeChecker();
|
|
39
|
+
const visiting = new Set<ts.Node>();
|
|
40
|
+
const known = new Map<ts.Node, Origin>();
|
|
41
|
+
|
|
42
|
+
function binding(node: ts.BindingElement): Origin {
|
|
43
|
+
if (!ts.isVariableDeclaration(node.parent.parent)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const value = node.parent.parent.initializer;
|
|
47
|
+
const name = node.propertyName ?? node.name;
|
|
48
|
+
return value !== undefined && (ts.isIdentifier(name) || ts.isStringLiteral(name)) ? member(value, name.text) : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function declaration(node: ts.Node): Origin {
|
|
52
|
+
if (ts.isImportSpecifier(node)) {
|
|
53
|
+
return imported((node.propertyName ?? node.name).text, importFrom(node));
|
|
54
|
+
}
|
|
55
|
+
if (ts.isNamespaceImport(node)) {
|
|
56
|
+
return moduleOrigin(importFrom(node));
|
|
57
|
+
}
|
|
58
|
+
if (ts.isVariableDeclaration(node) || ts.isPropertyAssignment(node)) {
|
|
59
|
+
return node.initializer === undefined ? undefined : origin(node.initializer);
|
|
60
|
+
}
|
|
61
|
+
return localBinding(node);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function localBinding(node: ts.Node): Origin {
|
|
65
|
+
if (ts.isShorthandPropertyAssignment(node)) {
|
|
66
|
+
return symbol(checker.getShorthandAssignmentValueSymbol(node));
|
|
67
|
+
}
|
|
68
|
+
return ts.isBindingElement(node) ? binding(node) : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function symbol(value: ts.Symbol | undefined): Origin {
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const direct = value.declarations?.map(visit).find((result) => result !== undefined);
|
|
76
|
+
return direct ?? (value.flags & ts.SymbolFlags.Alias ? symbol(checker.getAliasedSymbol(value)) : undefined);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function object(node: ts.ObjectLiteralExpression): Origin {
|
|
80
|
+
for (const property of [...node.properties].reverse()) {
|
|
81
|
+
if (property.name?.getText() === "it") {
|
|
82
|
+
return visit(property) === "story" ? "kit" : undefined;
|
|
83
|
+
}
|
|
84
|
+
if (ts.isSpreadAssignment(property) && visit(property.expression) === "kit") {
|
|
85
|
+
return "kit";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function member(value: ts.Expression, name: string): Origin {
|
|
92
|
+
const receiver = visit(value);
|
|
93
|
+
if (receiver === "kit" && name === "it") {
|
|
94
|
+
return "story";
|
|
95
|
+
}
|
|
96
|
+
if (receiver === "story" || receiver === "plain" || receiver === "suite") {
|
|
97
|
+
return receiver;
|
|
98
|
+
}
|
|
99
|
+
const from = receiver === "vitest" ? "vitest" : undefined;
|
|
100
|
+
return imported(name, receiver === "stories" ? "@shivaedev/test-story/storyKit.ts" : from);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function identifier(node: ts.Identifier): Origin {
|
|
104
|
+
if (node.text === "describe" || node.text === "suite") {
|
|
105
|
+
return symbol(checker.getSymbolAtLocation(node)) ?? "suite";
|
|
106
|
+
}
|
|
107
|
+
const value = checker.getSymbolAtLocation(node);
|
|
108
|
+
return symbol(value) ?? (value === undefined && (node.text === "it" || node.text === "test") ? "plain" : undefined);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function origin(node: ts.Node): Origin {
|
|
112
|
+
if (ts.isFunctionLike(node)) {
|
|
113
|
+
return "function";
|
|
114
|
+
}
|
|
115
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
116
|
+
return object(node);
|
|
117
|
+
}
|
|
118
|
+
return expression(node);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function expression(node: ts.Node): Origin {
|
|
122
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) {
|
|
123
|
+
return visit(node.expression);
|
|
124
|
+
}
|
|
125
|
+
if (ts.isCallExpression(node)) {
|
|
126
|
+
const callee = visit(node.expression);
|
|
127
|
+
return callee === "factory" ? "kit" : callee;
|
|
128
|
+
}
|
|
129
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
130
|
+
return member(node.expression, node.name.text) ?? symbol(checker.getSymbolAtLocation(node.name));
|
|
131
|
+
}
|
|
132
|
+
return ts.isIdentifier(node) ? identifier(node) : declaration(node);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function visit(node: ts.Node): Origin {
|
|
136
|
+
if (visiting.has(node) || known.has(node)) {
|
|
137
|
+
return known.get(node);
|
|
138
|
+
}
|
|
139
|
+
visiting.add(node);
|
|
140
|
+
const result = origin(node);
|
|
141
|
+
visiting.delete(node);
|
|
142
|
+
known.set(node, result);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { origin: visit, source: (path: string) => program.getSourceFile(join(inputs.root, path)) };
|
|
147
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import type { WorkspacePackage } from "#imports/workspace.ts";
|
|
3
|
+
import type { RuleInputs } from "#rule.ts";
|
|
4
|
+
|
|
5
|
+
const DEPENDENCIES = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
6
|
+
|
|
7
|
+
function object(value: unknown): value is Record<string, unknown> {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function manifest(text: string | undefined): Record<string, unknown> {
|
|
12
|
+
const value: unknown = JSON.parse(text ?? "{}");
|
|
13
|
+
return object(value) ? value : {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function storyPrerequisites(inputs: RuleInputs, packages: readonly WorkspacePackage[]): Promise<ReadonlySet<string>> {
|
|
17
|
+
const root = manifest(await inputs.readText("package.json"));
|
|
18
|
+
const members = typeof root.name === "string" ? [...packages, { directory: "", name: root.name }] : packages;
|
|
19
|
+
const byName = new Map(members.map((member) => [member.name, member]));
|
|
20
|
+
const pending = ["@shivaedev/test-story"];
|
|
21
|
+
const directories = new Set<string>();
|
|
22
|
+
while (pending.length > 0) {
|
|
23
|
+
const current = byName.get(pending.pop() ?? "");
|
|
24
|
+
if (current === undefined || directories.has(current.directory)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
directories.add(current.directory);
|
|
28
|
+
const value = manifest(await inputs.readText(posix.join(current.directory, "package.json")));
|
|
29
|
+
for (const key of DEPENDENCIES) {
|
|
30
|
+
const dependencies = value[key];
|
|
31
|
+
if (object(dependencies)) {
|
|
32
|
+
pending.push(...Object.keys(dependencies).filter((name) => byName.has(name)));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return directories;
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import type { ImportGraph } from "#imports/graph.ts";
|
|
4
|
+
import type { RuleInputs } from "#rule.ts";
|
|
5
|
+
|
|
6
|
+
export function storyProgram(inputs: RuleInputs, graph: ImportGraph): ts.Program {
|
|
7
|
+
const sources = new Map(inputs.sources.map((file) => [join(inputs.root, file.path), file.text]));
|
|
8
|
+
const targets = new Map(
|
|
9
|
+
graph.edges.flatMap((edge) =>
|
|
10
|
+
edge.to.kind === "file" ? [[`${join(inputs.root, edge.from)}:${edge.specifier}`, join(inputs.root, edge.to.path)] as const] : [],
|
|
11
|
+
),
|
|
12
|
+
);
|
|
13
|
+
const options: ts.CompilerOptions = { allowJs: true, noLib: true, types: [] };
|
|
14
|
+
const host: ts.CompilerHost = {
|
|
15
|
+
...ts.createCompilerHost(options),
|
|
16
|
+
fileExists: (path) => sources.has(path),
|
|
17
|
+
getSourceFile: (path) => {
|
|
18
|
+
const text = sources.get(path);
|
|
19
|
+
return text === undefined ? undefined : ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
|
|
20
|
+
},
|
|
21
|
+
readFile: (path) => sources.get(path),
|
|
22
|
+
resolveModuleNames: (names, from) =>
|
|
23
|
+
names.map((name) => {
|
|
24
|
+
const path = targets.get(`${from}:${name}`);
|
|
25
|
+
return path === undefined ? undefined : { extension: ts.Extension.Ts, resolvedFileName: path };
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
return ts.createProgram([...sources.keys()], options, host);
|
|
29
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { importGraph } from "#imports/graph.ts";
|
|
3
|
+
import { packageOf } from "#imports/workspace.ts";
|
|
4
|
+
import { testName } from "#naming/testName.ts";
|
|
5
|
+
import { defineRule } from "#rule.ts";
|
|
6
|
+
import { storyOrigins } from "./origins.ts";
|
|
7
|
+
import { storyPrerequisites } from "./prerequisites.ts";
|
|
8
|
+
|
|
9
|
+
function namesTest(node: ts.Expression): boolean {
|
|
10
|
+
if (ts.isIdentifier(node)) {
|
|
11
|
+
return node.text === "it" || node.text === "test";
|
|
12
|
+
}
|
|
13
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
14
|
+
return node.name.text === "it" || namesTest(node.expression);
|
|
15
|
+
}
|
|
16
|
+
return ts.isCallExpression(node) && namesTest(node.expression);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type OriginOf = ReturnType<typeof storyOrigins>["origin"];
|
|
20
|
+
|
|
21
|
+
function callback(node: ts.Node, origin: OriginOf): boolean {
|
|
22
|
+
return (
|
|
23
|
+
ts.isArrowFunction(node)
|
|
24
|
+
|| ts.isFunctionExpression(node)
|
|
25
|
+
|| ((ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) && origin(node) === "function")
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function testShaped(call: ts.CallExpression, origin: OriginOf): boolean {
|
|
30
|
+
const title = call.arguments[0];
|
|
31
|
+
return (
|
|
32
|
+
title !== undefined
|
|
33
|
+
&& (ts.isStringLiteralLike(title) || ts.isTemplateExpression(title))
|
|
34
|
+
&& call.arguments.slice(1).some((argument) => callback(argument, origin))
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function declarationScope(node: ts.Node, origin: OriginOf): boolean {
|
|
39
|
+
for (let current = node.parent; current !== undefined; current = current.parent) {
|
|
40
|
+
if (ts.isFunctionLike(current) && (!ts.isCallExpression(current.parent) || origin(current.parent.expression) !== "suite")) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function uncoveredRegistration(call: ts.CallExpression, originOf: OriginOf): boolean {
|
|
48
|
+
const origin = originOf(call.expression);
|
|
49
|
+
if (origin === "story" || origin === "suite") {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return origin === "plain" || ((namesTest(call.expression) || testShaped(call, originOf)) && declarationScope(call, originOf));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const storySpecs = defineRule({
|
|
56
|
+
check: async (inputs) => {
|
|
57
|
+
const graph = await importGraph(inputs);
|
|
58
|
+
const exempt = await storyPrerequisites(inputs, graph.packages);
|
|
59
|
+
const origins = storyOrigins(inputs, graph);
|
|
60
|
+
return inputs.sources.flatMap((file) => {
|
|
61
|
+
const name = testName(file.path);
|
|
62
|
+
const owner = packageOf(graph.packages, file.path);
|
|
63
|
+
if (name === undefined || name.modifiers.includes("typecheck") || exempt.has(owner?.directory ?? "")) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const source = origins.source(file.path);
|
|
67
|
+
let plain: ts.CallExpression | undefined;
|
|
68
|
+
function visit(node: ts.Node): void {
|
|
69
|
+
if (ts.isCallExpression(node) && uncoveredRegistration(node, origins.origin)) {
|
|
70
|
+
plain ??= node;
|
|
71
|
+
}
|
|
72
|
+
ts.forEachChild(node, visit);
|
|
73
|
+
}
|
|
74
|
+
if (source !== undefined) {
|
|
75
|
+
visit(source);
|
|
76
|
+
}
|
|
77
|
+
if (source === undefined || plain === undefined) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
file: file.path,
|
|
83
|
+
line: source.getLineAndCharacterOfPosition(plain.getStart(source)).line + 1,
|
|
84
|
+
message:
|
|
85
|
+
"Declares tests without a test-story story kit. Build the package's kit with storyKit from @shivaedev/test-story/storyKit.ts in src/test-support, and declare every test through the kit's it. See https://github.com/ShivaeDev/platform/tree/main/packages/quality#story-tests.",
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
description:
|
|
91
|
+
"Every package tests its real engine through its own test-story kit. Typecheck tests and test-story's workspace prerequisites are exempt.",
|
|
92
|
+
id: "tests/story-specs",
|
|
93
|
+
});
|