@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.
Files changed (272) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +4 -0
  3. package/dist/baseline/compare.d.ts +2 -2
  4. package/dist/baseline/compare.d.ts.map +1 -1
  5. package/dist/baseline/compare.js +5 -3
  6. package/dist/baseline/compare.js.map +1 -1
  7. package/dist/baseline/format.d.ts +5 -5
  8. package/dist/baseline/format.d.ts.map +1 -1
  9. package/dist/baseline/format.js +23 -13
  10. package/dist/baseline/format.js.map +1 -1
  11. package/dist/baseline/legacy.d.ts +2 -2
  12. package/dist/baseline/legacy.d.ts.map +1 -1
  13. package/dist/baseline/legacy.js +4 -4
  14. package/dist/baseline/legacy.js.map +1 -1
  15. package/dist/baseline/rewrite.d.ts +1 -1
  16. package/dist/baseline/rewrite.d.ts.map +1 -1
  17. package/dist/baseline/rewrite.js +2 -2
  18. package/dist/baseline/rewrite.js.map +1 -1
  19. package/dist/baseline/update.d.ts +2 -2
  20. package/dist/baseline/update.d.ts.map +1 -1
  21. package/dist/baseline/update.js +24 -18
  22. package/dist/baseline/update.js.map +1 -1
  23. package/dist/cli/args.d.ts +1 -1
  24. package/dist/cli/args.d.ts.map +1 -1
  25. package/dist/cli/args.js +15 -9
  26. package/dist/cli/args.js.map +1 -1
  27. package/dist/cli/baseline.d.ts +1 -1
  28. package/dist/cli/baseline.d.ts.map +1 -1
  29. package/dist/cli/baseline.js +10 -6
  30. package/dist/cli/baseline.js.map +1 -1
  31. package/dist/cli/fix.d.ts +2 -2
  32. package/dist/cli/fix.d.ts.map +1 -1
  33. package/dist/cli/fix.js +34 -40
  34. package/dist/cli/fix.js.map +1 -1
  35. package/dist/cli/lint.d.ts +1 -1
  36. package/dist/cli/lint.d.ts.map +1 -1
  37. package/dist/cli/lint.js +1 -1
  38. package/dist/cli/lint.js.map +1 -1
  39. package/dist/cli/migrate.d.ts.map +1 -1
  40. package/dist/cli/migrate.js +1 -1
  41. package/dist/cli/migrate.js.map +1 -1
  42. package/dist/cli/run-main.js +2 -2
  43. package/dist/cli/run-main.js.map +1 -1
  44. package/dist/cli/shrink.d.ts +2 -2
  45. package/dist/cli/shrink.d.ts.map +1 -1
  46. package/dist/cli/shrink.js +9 -3
  47. package/dist/cli/shrink.js.map +1 -1
  48. package/dist/config/decode.d.ts +1 -1
  49. package/dist/config/decode.d.ts.map +1 -1
  50. package/dist/config/decode.js +13 -9
  51. package/dist/config/decode.js.map +1 -1
  52. package/dist/config/load.d.ts.map +1 -1
  53. package/dist/config/load.js +26 -20
  54. package/dist/config/load.js.map +1 -1
  55. package/dist/config/resolve.d.ts +22 -2
  56. package/dist/config/resolve.d.ts.map +1 -1
  57. package/dist/config/resolve.js +18 -10
  58. package/dist/config/resolve.js.map +1 -1
  59. package/dist/config.d.ts +1 -1
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +3 -1
  62. package/dist/config.js.map +1 -1
  63. package/dist/decoded.d.ts +2 -2
  64. package/dist/decoded.d.ts.map +1 -1
  65. package/dist/decoded.js +7 -5
  66. package/dist/decoded.js.map +1 -1
  67. package/dist/engine/baseline-file.d.ts +1 -1
  68. package/dist/engine/baseline-file.d.ts.map +1 -1
  69. package/dist/engine/baseline-file.js +4 -2
  70. package/dist/engine/baseline-file.js.map +1 -1
  71. package/dist/engine/evaluate.d.ts +2 -2
  72. package/dist/engine/evaluate.d.ts.map +1 -1
  73. package/dist/engine/evaluate.js +5 -3
  74. package/dist/engine/evaluate.js.map +1 -1
  75. package/dist/engine/run-rules.d.ts +1 -1
  76. package/dist/engine/run-rules.d.ts.map +1 -1
  77. package/dist/engine/run-rules.js +24 -14
  78. package/dist/engine/run-rules.js.map +1 -1
  79. package/dist/engine/session.d.ts.map +1 -1
  80. package/dist/engine/session.js +8 -4
  81. package/dist/engine/session.js.map +1 -1
  82. package/dist/engine/violation.d.ts +7 -7
  83. package/dist/engine/violation.d.ts.map +1 -1
  84. package/dist/engine/violation.js +22 -10
  85. package/dist/engine/violation.js.map +1 -1
  86. package/dist/exceptions/registry.d.ts +2 -2
  87. package/dist/exceptions/registry.d.ts.map +1 -1
  88. package/dist/exceptions/registry.js +10 -6
  89. package/dist/exceptions/registry.js.map +1 -1
  90. package/dist/git/base.d.ts +1 -2
  91. package/dist/git/base.d.ts.map +1 -1
  92. package/dist/git/base.js +22 -10
  93. package/dist/git/base.js.map +1 -1
  94. package/dist/git/command.d.ts +2 -2
  95. package/dist/git/command.d.ts.map +1 -1
  96. package/dist/git/command.js +13 -9
  97. package/dist/git/command.js.map +1 -1
  98. package/dist/git/history.d.ts +1 -1
  99. package/dist/git/history.d.ts.map +1 -1
  100. package/dist/git/history.js +5 -3
  101. package/dist/git/history.js.map +1 -1
  102. package/dist/inventory/collect.d.ts +1 -1
  103. package/dist/inventory/collect.d.ts.map +1 -1
  104. package/dist/inventory/collect.js +7 -5
  105. package/dist/inventory/collect.js.map +1 -1
  106. package/dist/inventory/filesystem.d.ts +1 -1
  107. package/dist/inventory/filesystem.d.ts.map +1 -1
  108. package/dist/inventory/filesystem.js +11 -7
  109. package/dist/inventory/filesystem.js.map +1 -1
  110. package/dist/inventory/ignore-scope.d.ts +4 -4
  111. package/dist/inventory/ignore-scope.d.ts.map +1 -1
  112. package/dist/inventory/ignore-scope.js +18 -9
  113. package/dist/inventory/ignore-scope.js.map +1 -1
  114. package/dist/manifests/sorted.d.ts +1 -1
  115. package/dist/manifests/sorted.d.ts.map +1 -1
  116. package/dist/manifests/sorted.js +14 -16
  117. package/dist/manifests/sorted.js.map +1 -1
  118. package/dist/report/plural.d.ts +1 -1
  119. package/dist/report/plural.d.ts.map +1 -1
  120. package/dist/report/plural.js +3 -1
  121. package/dist/report/plural.js.map +1 -1
  122. package/dist/report/render.d.ts +1 -1
  123. package/dist/report/render.d.ts.map +1 -1
  124. package/dist/report/render.js +29 -21
  125. package/dist/report/render.js.map +1 -1
  126. package/dist/report/stale.d.ts +1 -1
  127. package/dist/report/stale.d.ts.map +1 -1
  128. package/dist/report/stale.js +18 -8
  129. package/dist/report/stale.js.map +1 -1
  130. package/dist/rule.d.ts +13 -13
  131. package/dist/rule.d.ts.map +1 -1
  132. package/dist/rule.js +10 -8
  133. package/dist/rule.js.map +1 -1
  134. package/dist/rules/built-in.d.ts +1 -1
  135. package/dist/rules/built-in.d.ts.map +1 -1
  136. package/dist/rules/built-in.js +2 -0
  137. package/dist/rules/built-in.js.map +1 -1
  138. package/dist/rules/comments/kinds.d.ts +4 -4
  139. package/dist/rules/comments/kinds.d.ts.map +1 -1
  140. package/dist/rules/comments/kinds.js +16 -8
  141. package/dist/rules/comments/kinds.js.map +1 -1
  142. package/dist/rules/comments/max-per-file.d.ts.map +1 -1
  143. package/dist/rules/comments/max-per-file.js +13 -9
  144. package/dist/rules/comments/max-per-file.js.map +1 -1
  145. package/dist/rules/comments/pattern-rule.d.ts +1 -1
  146. package/dist/rules/comments/pattern-rule.d.ts.map +1 -1
  147. package/dist/rules/comments/pattern-rule.js +12 -10
  148. package/dist/rules/comments/pattern-rule.js.map +1 -1
  149. package/dist/rules/comments/scan.d.ts +3 -3
  150. package/dist/rules/comments/scan.d.ts.map +1 -1
  151. package/dist/rules/comments/scan.js +23 -19
  152. package/dist/rules/comments/scan.js.map +1 -1
  153. package/dist/rules/max-lines.d.ts.map +1 -1
  154. package/dist/rules/max-lines.js +12 -10
  155. package/dist/rules/max-lines.js.map +1 -1
  156. package/dist/rules/stories/origins.d.ts +10 -0
  157. package/dist/rules/stories/origins.d.ts.map +1 -0
  158. package/dist/rules/stories/origins.js +129 -0
  159. package/dist/rules/stories/origins.js.map +1 -0
  160. package/dist/rules/stories/prerequisites.d.ts +4 -0
  161. package/dist/rules/stories/prerequisites.d.ts.map +1 -0
  162. package/dist/rules/stories/prerequisites.js +32 -0
  163. package/dist/rules/stories/prerequisites.js.map +1 -0
  164. package/dist/rules/stories/program.d.ts +5 -0
  165. package/dist/rules/stories/program.d.ts.map +1 -0
  166. package/dist/rules/stories/program.js +22 -0
  167. package/dist/rules/stories/program.js.map +1 -0
  168. package/dist/rules/stories/specs.d.ts +2 -0
  169. package/dist/rules/stories/specs.d.ts.map +1 -0
  170. package/dist/rules/stories/specs.js +80 -0
  171. package/dist/rules/stories/specs.js.map +1 -0
  172. package/dist/rules/suppressions/biome/configs.d.ts +1 -1
  173. package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
  174. package/dist/rules/suppressions/biome/configs.js +23 -17
  175. package/dist/rules/suppressions/biome/configs.js.map +1 -1
  176. package/dist/rules/suppressions/biome/declarations.d.ts +4 -4
  177. package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -1
  178. package/dist/rules/suppressions/biome/declarations.js +15 -7
  179. package/dist/rules/suppressions/biome/declarations.js.map +1 -1
  180. package/dist/rules/suppressions/biome/json.d.ts +6 -6
  181. package/dist/rules/suppressions/biome/json.d.ts.map +1 -1
  182. package/dist/rules/suppressions/biome/json.js +27 -13
  183. package/dist/rules/suppressions/biome/json.js.map +1 -1
  184. package/dist/rules/suppressions/biome/resolve.d.ts +1 -1
  185. package/dist/rules/suppressions/biome/resolve.d.ts.map +1 -1
  186. package/dist/rules/suppressions/biome/resolve.js +23 -17
  187. package/dist/rules/suppressions/biome/resolve.js.map +1 -1
  188. package/dist/rules/suppressions/biome/weakenings.d.ts +1 -1
  189. package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
  190. package/dist/rules/suppressions/biome/weakenings.js +78 -52
  191. package/dist/rules/suppressions/biome/weakenings.js.map +1 -1
  192. package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
  193. package/dist/rules/suppressions/biome-overrides.js +45 -33
  194. package/dist/rules/suppressions/biome-overrides.js.map +1 -1
  195. package/dist/rules/suppressions/directives.d.ts +1 -1
  196. package/dist/rules/suppressions/directives.d.ts.map +1 -1
  197. package/dist/rules/suppressions/directives.js +5 -3
  198. package/dist/rules/suppressions/directives.js.map +1 -1
  199. package/dist/rules/suppressions/no-double-cast.d.ts.map +1 -1
  200. package/dist/rules/suppressions/no-double-cast.js +14 -10
  201. package/dist/rules/suppressions/no-double-cast.js.map +1 -1
  202. package/dist/rules/suppressions/no-inline.d.ts.map +1 -1
  203. package/dist/rules/suppressions/no-inline.js +8 -6
  204. package/dist/rules/suppressions/no-inline.js.map +1 -1
  205. package/dist/rules/suppressions/stylesheet-comments.d.ts +1 -1
  206. package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
  207. package/dist/rules/suppressions/stylesheet-comments.js +11 -5
  208. package/dist/rules/suppressions/stylesheet-comments.js.map +1 -1
  209. package/dist/rules/syntax.d.ts +1 -1
  210. package/dist/rules/syntax.d.ts.map +1 -1
  211. package/dist/rules/syntax.js +2 -2
  212. package/dist/rules/syntax.js.map +1 -1
  213. package/dist/standard-schema.d.ts +6 -6
  214. package/dist/standard-schema.d.ts.map +1 -1
  215. package/docs/modules.md +2 -0
  216. package/package.json +1 -1
  217. package/src/baseline/compare.ts +5 -3
  218. package/src/baseline/format.ts +17 -10
  219. package/src/baseline/legacy.ts +4 -4
  220. package/src/baseline/rewrite.ts +2 -2
  221. package/src/baseline/update.ts +20 -16
  222. package/src/cli/args.ts +15 -10
  223. package/src/cli/baseline.ts +36 -35
  224. package/src/cli/fix.ts +39 -42
  225. package/src/cli/lint.ts +17 -19
  226. package/src/cli/migrate.ts +20 -21
  227. package/src/cli/run-main.ts +2 -2
  228. package/src/cli/shrink.ts +11 -8
  229. package/src/config/decode.ts +15 -10
  230. package/src/config/load.ts +44 -37
  231. package/src/config/resolve.ts +32 -27
  232. package/src/config.ts +3 -1
  233. package/src/decoded.ts +5 -4
  234. package/src/engine/baseline-file.ts +14 -12
  235. package/src/engine/evaluate.ts +6 -5
  236. package/src/engine/run-rules.ts +21 -13
  237. package/src/engine/session.ts +32 -25
  238. package/src/engine/violation.ts +22 -11
  239. package/src/exceptions/registry.ts +10 -8
  240. package/src/git/base.ts +45 -37
  241. package/src/git/command.ts +6 -4
  242. package/src/git/history.ts +5 -4
  243. package/src/inventory/collect.ts +39 -34
  244. package/src/inventory/filesystem.ts +38 -30
  245. package/src/inventory/ignore-scope.ts +20 -10
  246. package/src/manifests/sorted.ts +18 -18
  247. package/src/report/plural.ts +3 -1
  248. package/src/report/render.ts +23 -18
  249. package/src/report/stale.ts +33 -25
  250. package/src/rule.ts +25 -19
  251. package/src/rules/built-in.ts +2 -0
  252. package/src/rules/comments/kinds.ts +16 -9
  253. package/src/rules/comments/max-per-file.ts +15 -11
  254. package/src/rules/comments/pattern-rule.ts +5 -4
  255. package/src/rules/comments/scan.ts +18 -15
  256. package/src/rules/max-lines.ts +3 -2
  257. package/src/rules/stories/origins.ts +147 -0
  258. package/src/rules/stories/prerequisites.ts +37 -0
  259. package/src/rules/stories/program.ts +29 -0
  260. package/src/rules/stories/specs.ts +93 -0
  261. package/src/rules/suppressions/biome/configs.ts +23 -17
  262. package/src/rules/suppressions/biome/declarations.ts +15 -10
  263. package/src/rules/suppressions/biome/json.ts +25 -13
  264. package/src/rules/suppressions/biome/resolve.ts +23 -18
  265. package/src/rules/suppressions/biome/weakenings.ts +60 -41
  266. package/src/rules/suppressions/biome-overrides.ts +32 -22
  267. package/src/rules/suppressions/directives.ts +5 -4
  268. package/src/rules/suppressions/no-double-cast.ts +14 -10
  269. package/src/rules/suppressions/no-inline.ts +5 -4
  270. package/src/rules/suppressions/stylesheet-comments.ts +11 -6
  271. package/src/rules/syntax.ts +2 -2
  272. 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<Options> extends RuleInputs {
32
- readonly options: 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<Id extends string = string, Input = unknown> {
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: Input | undefined) => Promise<Configured>>;
41
+ configure: Bivariant<(options: TInput | undefined) => Promise<Configured>>;
42
42
  readonly description: string;
43
43
  readonly family?: boolean;
44
- readonly id: Id;
44
+ readonly id: TId;
45
45
  readonly registrable?: boolean;
46
46
  }
47
47
 
48
- interface RuleDefinition<Id extends string, Options> {
49
- readonly check: (context: RuleContext<Options>) => Findings | PromiseLike<Findings>;
48
+ interface RuleDefinition<TId extends string, TOptions> {
49
+ readonly check: (context: RuleContext<TOptions>) => Findings | PromiseLike<Findings>;
50
50
  readonly description: string;
51
- readonly id: Id;
51
+ readonly id: TId;
52
52
  readonly registrable?: false;
53
53
  }
54
54
 
55
- interface RuleDefinitionWithOptions<Id extends string, Input, Options> extends RuleDefinition<Id, Options> {
56
- readonly options: StandardSchemaV1<Input, Options>;
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<Id extends string> extends RuleDefinition<Id, undefined> {
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
- const configureWith =
72
- <Input, Options>(schema: StandardSchemaV1<Input, Options>, check: RuleDefinition<string, Options>["check"], absent: unknown) =>
73
- async (options: unknown): Promise<Configured> => {
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 Id extends string, Input, Options>(definition: RuleDefinitionWithOptions<Id, Input, Options>): Rule<Id, Input>;
83
- export function defineRule<const Id extends string>(definition: RuleDefinitionWithoutOptions<Id>): Rule<Id, undefined>;
84
- export function defineRule<const Id extends string, Input, Options>(
85
- definition: RuleDefinitionWithOptions<Id, Input, Options> | RuleDefinitionWithoutOptions<Id>,
86
- ): Rule<Id, Input> | Rule<Id, undefined> {
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)
@@ -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
- const contentOf = (comment: SourceComment): readonly string[] => comment.body.filter((line) => line !== "");
18
+ function contentOf(comment: SourceComment): readonly string[] {
19
+ return comment.body.filter((line) => line !== "");
20
+ }
19
21
 
20
- const startsWithTag = (line: string, tags: readonly string[]): boolean =>
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 const textOf = (comment: SourceComment): string => comment.body.join("\n");
26
+ export function textOf(comment: SourceComment): string {
27
+ return comment.body.join("\n");
28
+ }
24
29
 
25
- export const isJsdoc = (comment: SourceComment): boolean => comment.kind === "block" && comment.text.startsWith("/**") && comment.text !== "/**/";
30
+ export function isJsdoc(comment: SourceComment): boolean {
31
+ return comment.kind === "block" && comment.text.startsWith("/**") && comment.text !== "/**/";
32
+ }
26
33
 
27
- export const isPragma = (comment: SourceComment, pragmas: readonly string[]): boolean => {
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 const isDirective = (comment: SourceComment): boolean => {
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
- const continues = (previous: SourceComment | undefined, comment: SourceComment): boolean =>
13
- previous !== undefined
14
- && previous.kind === "line"
15
- && comment.kind === "line"
16
- && previous.ownLine
17
- && comment.ownLine
18
- && comment.line === previous.endLine + 1;
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
- const commentBlocks = (comments: readonly SourceComment[]): readonly SourceComment[] =>
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
- const overCap = (file: SourceFile, max: number, allow: readonly string[]): readonly Finding[] => {
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
- const firstMatch = (comment: SourceComment, patterns: readonly RegExp[]): string | undefined => {
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 const definePatternRule = <const Id extends string>({ description, id, message, patterns }: PatternRule<Id>): Rule<Id, undefined> =>
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
- const commentRanges = (source: ts.SourceFile): readonly ts.CommentRange[] => {
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
- const add = (found: readonly ts.CommentRange[] | undefined) => {
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
- const visit = (node: ts.Node) => {
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 const bodyOf = (kind: SourceComment["kind"], text: string): readonly string[] =>
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
- const toComment = (source: ts.SourceFile, range: ts.CommentRange): SourceComment => {
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
- const scan = (file: SourceFile): readonly SourceComment[] => {
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 const scanComments = (file: SourceFile): readonly SourceComment[] => {
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 const commentsOf = (file: SourceFile): readonly SourceComment[] => (DECLARATION.test(file.path) ? [] : scanComments(file));
81
+ export function commentsOf(file: SourceFile): readonly SourceComment[] {
82
+ return DECLARATION.test(file.path) ? [] : scanComments(file);
83
+ }
@@ -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
- const oversized = (file: SourceFile, limit: number): readonly Finding[] =>
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
+ });