@shivaedev/quality 0.2.0 → 0.3.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 (258) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +207 -35
  3. package/dist/baseline/compare.d.ts +3 -8
  4. package/dist/baseline/compare.d.ts.map +1 -1
  5. package/dist/baseline/compare.js +13 -23
  6. package/dist/baseline/compare.js.map +1 -1
  7. package/dist/baseline/format.d.ts +7 -5
  8. package/dist/baseline/format.d.ts.map +1 -1
  9. package/dist/baseline/format.js +31 -23
  10. package/dist/baseline/format.js.map +1 -1
  11. package/dist/baseline/guard.d.ts +23 -0
  12. package/dist/baseline/guard.d.ts.map +1 -0
  13. package/dist/baseline/guard.js +31 -0
  14. package/dist/baseline/guard.js.map +1 -0
  15. package/dist/baseline/legacy.d.ts +11 -0
  16. package/dist/baseline/legacy.d.ts.map +1 -0
  17. package/dist/baseline/legacy.js +33 -0
  18. package/dist/baseline/legacy.js.map +1 -0
  19. package/dist/baseline/rewrite.d.ts +3 -0
  20. package/dist/baseline/rewrite.d.ts.map +1 -0
  21. package/dist/baseline/rewrite.js +19 -0
  22. package/dist/baseline/rewrite.js.map +1 -0
  23. package/dist/baseline/update.d.ts +7 -6
  24. package/dist/baseline/update.d.ts.map +1 -1
  25. package/dist/baseline/update.js +28 -28
  26. package/dist/baseline/update.js.map +1 -1
  27. package/dist/cli/args.d.ts +14 -1
  28. package/dist/cli/args.d.ts.map +1 -1
  29. package/dist/cli/args.js +54 -29
  30. package/dist/cli/args.js.map +1 -1
  31. package/dist/cli/baseline.d.ts +3 -1
  32. package/dist/cli/baseline.d.ts.map +1 -1
  33. package/dist/cli/baseline.js +19 -10
  34. package/dist/cli/baseline.js.map +1 -1
  35. package/dist/cli/check.d.ts +5 -0
  36. package/dist/cli/check.d.ts.map +1 -0
  37. package/dist/cli/check.js +39 -0
  38. package/dist/cli/check.js.map +1 -0
  39. package/dist/cli/lint.js +1 -1
  40. package/dist/cli/lint.js.map +1 -1
  41. package/dist/cli/migrate.d.ts +4 -0
  42. package/dist/cli/migrate.d.ts.map +1 -0
  43. package/dist/cli/migrate.js +31 -0
  44. package/dist/cli/migrate.js.map +1 -0
  45. package/dist/cli/run-main.d.ts +2 -1
  46. package/dist/cli/run-main.d.ts.map +1 -1
  47. package/dist/cli/run-main.js +5 -2
  48. package/dist/cli/run-main.js.map +1 -1
  49. package/dist/cli/shrink.d.ts +6 -0
  50. package/dist/cli/shrink.d.ts.map +1 -0
  51. package/dist/cli/shrink.js +8 -0
  52. package/dist/cli/shrink.js.map +1 -0
  53. package/dist/cli.js +11 -2
  54. package/dist/cli.js.map +1 -1
  55. package/dist/config/decode.d.ts +1 -0
  56. package/dist/config/decode.d.ts.map +1 -1
  57. package/dist/config/decode.js +1 -0
  58. package/dist/config/decode.js.map +1 -1
  59. package/dist/config/file.d.ts +2 -0
  60. package/dist/config/file.d.ts.map +1 -0
  61. package/dist/config/file.js +2 -0
  62. package/dist/config/file.js.map +1 -0
  63. package/dist/config/load.d.ts +1 -1
  64. package/dist/config/load.d.ts.map +1 -1
  65. package/dist/config/load.js +3 -2
  66. package/dist/config/load.js.map +1 -1
  67. package/dist/config/resolve.d.ts +1 -0
  68. package/dist/config/resolve.d.ts.map +1 -1
  69. package/dist/config/resolve.js +6 -4
  70. package/dist/config/resolve.js.map +1 -1
  71. package/dist/config.d.ts +2 -7
  72. package/dist/config.d.ts.map +1 -1
  73. package/dist/config.js.map +1 -1
  74. package/dist/engine/baseline-file.d.ts +11 -0
  75. package/dist/engine/baseline-file.d.ts.map +1 -0
  76. package/dist/engine/baseline-file.js +18 -0
  77. package/dist/engine/baseline-file.js.map +1 -0
  78. package/dist/engine/evaluate.d.ts +2 -2
  79. package/dist/engine/evaluate.d.ts.map +1 -1
  80. package/dist/engine/evaluate.js +3 -3
  81. package/dist/engine/evaluate.js.map +1 -1
  82. package/dist/engine/session.d.ts +3 -5
  83. package/dist/engine/session.d.ts.map +1 -1
  84. package/dist/engine/session.js +12 -10
  85. package/dist/engine/session.js.map +1 -1
  86. package/dist/exceptions/registry.d.ts +1 -1
  87. package/dist/exceptions/registry.d.ts.map +1 -1
  88. package/dist/exceptions/registry.js +8 -3
  89. package/dist/exceptions/registry.js.map +1 -1
  90. package/dist/failure.d.ts +0 -2
  91. package/dist/failure.d.ts.map +1 -1
  92. package/dist/failure.js +0 -2
  93. package/dist/failure.js.map +1 -1
  94. package/dist/git/base.d.ts +9 -0
  95. package/dist/git/base.d.ts.map +1 -0
  96. package/dist/git/base.js +34 -0
  97. package/dist/git/base.js.map +1 -0
  98. package/dist/git/command.d.ts +12 -0
  99. package/dist/git/command.d.ts.map +1 -0
  100. package/dist/git/command.js +13 -0
  101. package/dist/git/command.js.map +1 -0
  102. package/dist/git/history.d.ts +10 -0
  103. package/dist/git/history.d.ts.map +1 -0
  104. package/dist/git/history.js +28 -0
  105. package/dist/git/history.js.map +1 -0
  106. package/dist/inventory/ignore-scope.d.ts.map +1 -1
  107. package/dist/inventory/ignore-scope.js +0 -1
  108. package/dist/inventory/ignore-scope.js.map +1 -1
  109. package/dist/inventory/walk.d.ts +0 -1
  110. package/dist/inventory/walk.d.ts.map +1 -1
  111. package/dist/inventory/walk.js +1 -1
  112. package/dist/inventory/walk.js.map +1 -1
  113. package/dist/report/guard.d.ts +9 -0
  114. package/dist/report/guard.d.ts.map +1 -0
  115. package/dist/report/guard.js +34 -0
  116. package/dist/report/guard.js.map +1 -0
  117. package/dist/report/render.js +1 -2
  118. package/dist/report/render.js.map +1 -1
  119. package/dist/report/stale.d.ts.map +1 -1
  120. package/dist/report/stale.js +4 -3
  121. package/dist/report/stale.js.map +1 -1
  122. package/dist/rule.d.ts +4 -7
  123. package/dist/rule.d.ts.map +1 -1
  124. package/dist/rule.js +1 -1
  125. package/dist/rule.js.map +1 -1
  126. package/dist/rules/built-in.d.ts +17 -0
  127. package/dist/rules/built-in.d.ts.map +1 -1
  128. package/dist/rules/built-in.js +21 -1
  129. package/dist/rules/built-in.js.map +1 -1
  130. package/dist/rules/comments/kinds.d.ts +9 -0
  131. package/dist/rules/comments/kinds.d.ts.map +1 -0
  132. package/dist/rules/comments/kinds.js +34 -0
  133. package/dist/rules/comments/kinds.js.map +1 -0
  134. package/dist/rules/comments/max-per-file.d.ts +5 -0
  135. package/dist/rules/comments/max-per-file.d.ts.map +1 -0
  136. package/dist/rules/comments/max-per-file.js +39 -0
  137. package/dist/rules/comments/max-per-file.js.map +1 -0
  138. package/dist/rules/comments/no-banner.d.ts +2 -0
  139. package/dist/rules/comments/no-banner.d.ts.map +1 -0
  140. package/dist/rules/comments/no-banner.js +9 -0
  141. package/dist/rules/comments/no-banner.js.map +1 -0
  142. package/dist/rules/comments/no-jsdoc.d.ts +4 -0
  143. package/dist/rules/comments/no-jsdoc.d.ts.map +1 -0
  144. package/dist/rules/comments/no-jsdoc.js +14 -0
  145. package/dist/rules/comments/no-jsdoc.js.map +1 -0
  146. package/dist/rules/comments/no-line-reference.d.ts +2 -0
  147. package/dist/rules/comments/no-line-reference.d.ts.map +1 -0
  148. package/dist/rules/comments/no-line-reference.js +9 -0
  149. package/dist/rules/comments/no-line-reference.js.map +1 -0
  150. package/dist/rules/comments/no-pr-reference.d.ts +2 -0
  151. package/dist/rules/comments/no-pr-reference.d.ts.map +1 -0
  152. package/dist/rules/comments/no-pr-reference.js +13 -0
  153. package/dist/rules/comments/no-pr-reference.js.map +1 -0
  154. package/dist/rules/comments/no-todo.d.ts +2 -0
  155. package/dist/rules/comments/no-todo.d.ts.map +1 -0
  156. package/dist/rules/comments/no-todo.js +8 -0
  157. package/dist/rules/comments/no-todo.js.map +1 -0
  158. package/dist/rules/comments/pattern-rule.d.ts +10 -0
  159. package/dist/rules/comments/pattern-rule.d.ts.map +1 -0
  160. package/dist/rules/comments/pattern-rule.js +16 -0
  161. package/dist/rules/comments/pattern-rule.js.map +1 -0
  162. package/dist/rules/comments/scan.d.ts +13 -0
  163. package/dist/rules/comments/scan.d.ts.map +1 -0
  164. package/dist/rules/comments/scan.js +62 -0
  165. package/dist/rules/comments/scan.js.map +1 -0
  166. package/dist/rules/max-lines.d.ts.map +1 -1
  167. package/dist/rules/max-lines.js +8 -1
  168. package/dist/rules/max-lines.js.map +1 -1
  169. package/dist/rules/suppressions/biome/configs.d.ts +14 -0
  170. package/dist/rules/suppressions/biome/configs.d.ts.map +1 -0
  171. package/dist/rules/suppressions/biome/configs.js +39 -0
  172. package/dist/rules/suppressions/biome/configs.js.map +1 -0
  173. package/dist/rules/suppressions/biome/json.d.ts +27 -0
  174. package/dist/rules/suppressions/biome/json.d.ts.map +1 -0
  175. package/dist/rules/suppressions/biome/json.js +37 -0
  176. package/dist/rules/suppressions/biome/json.js.map +1 -0
  177. package/dist/rules/suppressions/biome/weakenings.d.ts +9 -0
  178. package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -0
  179. package/dist/rules/suppressions/biome/weakenings.js +68 -0
  180. package/dist/rules/suppressions/biome/weakenings.js.map +1 -0
  181. package/dist/rules/suppressions/biome-overrides.d.ts +8 -0
  182. package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -0
  183. package/dist/rules/suppressions/biome-overrides.js +47 -0
  184. package/dist/rules/suppressions/biome-overrides.js.map +1 -0
  185. package/dist/rules/suppressions/directives.d.ts +4 -0
  186. package/dist/rules/suppressions/directives.d.ts.map +1 -0
  187. package/dist/rules/suppressions/directives.js +19 -0
  188. package/dist/rules/suppressions/directives.js.map +1 -0
  189. package/dist/rules/suppressions/no-double-cast.d.ts +2 -0
  190. package/dist/rules/suppressions/no-double-cast.d.ts.map +1 -0
  191. package/dist/rules/suppressions/no-double-cast.js +38 -0
  192. package/dist/rules/suppressions/no-double-cast.js.map +1 -0
  193. package/dist/rules/suppressions/no-inline.d.ts +8 -0
  194. package/dist/rules/suppressions/no-inline.d.ts.map +1 -0
  195. package/dist/rules/suppressions/no-inline.js +48 -0
  196. package/dist/rules/suppressions/no-inline.js.map +1 -0
  197. package/dist/rules/suppressions/stylesheet-comments.d.ts +4 -0
  198. package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -0
  199. package/dist/rules/suppressions/stylesheet-comments.js +20 -0
  200. package/dist/rules/suppressions/stylesheet-comments.js.map +1 -0
  201. package/dist/rules/syntax.d.ts +4 -0
  202. package/dist/rules/syntax.d.ts.map +1 -0
  203. package/dist/rules/syntax.js +12 -0
  204. package/dist/rules/syntax.js.map +1 -0
  205. package/package.json +4 -5
  206. package/src/baseline/compare.ts +15 -39
  207. package/src/baseline/format.ts +35 -32
  208. package/src/baseline/guard.ts +47 -0
  209. package/src/baseline/legacy.ts +46 -0
  210. package/src/baseline/rewrite.ts +24 -0
  211. package/src/baseline/update.ts +47 -30
  212. package/src/cli/args.ts +64 -30
  213. package/src/cli/baseline.ts +27 -13
  214. package/src/cli/check.ts +51 -0
  215. package/src/cli/lint.ts +1 -1
  216. package/src/cli/migrate.ts +36 -0
  217. package/src/cli/run-main.ts +8 -3
  218. package/src/cli/shrink.ts +31 -0
  219. package/src/cli.ts +11 -2
  220. package/src/config/decode.ts +1 -0
  221. package/src/config/file.ts +1 -0
  222. package/src/config/load.ts +4 -3
  223. package/src/config/resolve.ts +7 -4
  224. package/src/config.ts +3 -7
  225. package/src/engine/baseline-file.ts +27 -0
  226. package/src/engine/evaluate.ts +4 -2
  227. package/src/engine/session.ts +14 -12
  228. package/src/exceptions/registry.ts +9 -2
  229. package/src/failure.ts +0 -2
  230. package/src/git/base.ts +54 -0
  231. package/src/git/command.ts +31 -0
  232. package/src/git/history.ts +41 -0
  233. package/src/inventory/ignore-scope.ts +0 -1
  234. package/src/inventory/walk.ts +1 -1
  235. package/src/report/guard.ts +46 -0
  236. package/src/report/render.ts +2 -2
  237. package/src/report/stale.ts +11 -3
  238. package/src/rule.ts +5 -8
  239. package/src/rules/built-in.ts +21 -1
  240. package/src/rules/comments/kinds.ts +45 -0
  241. package/src/rules/comments/max-per-file.ts +46 -0
  242. package/src/rules/comments/no-banner.ts +10 -0
  243. package/src/rules/comments/no-jsdoc.ts +18 -0
  244. package/src/rules/comments/no-line-reference.ts +11 -0
  245. package/src/rules/comments/no-pr-reference.ts +13 -0
  246. package/src/rules/comments/no-todo.ts +8 -0
  247. package/src/rules/comments/pattern-rule.ts +28 -0
  248. package/src/rules/comments/scan.ts +80 -0
  249. package/src/rules/max-lines.ts +8 -1
  250. package/src/rules/suppressions/biome/configs.ts +50 -0
  251. package/src/rules/suppressions/biome/json.ts +55 -0
  252. package/src/rules/suppressions/biome/weakenings.ts +102 -0
  253. package/src/rules/suppressions/biome-overrides.ts +61 -0
  254. package/src/rules/suppressions/directives.ts +25 -0
  255. package/src/rules/suppressions/no-double-cast.ts +47 -0
  256. package/src/rules/suppressions/no-inline.ts +63 -0
  257. package/src/rules/suppressions/stylesheet-comments.ts +27 -0
  258. package/src/rules/syntax.ts +14 -0
@@ -1,3 +1,23 @@
1
+ import { maxPerFile } from "./comments/max-per-file.ts";
2
+ import { noBanner } from "./comments/no-banner.ts";
3
+ import { noJsdoc } from "./comments/no-jsdoc.ts";
4
+ import { noLineReference } from "./comments/no-line-reference.ts";
5
+ import { noPrReference } from "./comments/no-pr-reference.ts";
6
+ import { noTodo } from "./comments/no-todo.ts";
1
7
  import { maxLines } from "./max-lines.ts";
8
+ import { biomeOverrides } from "./suppressions/biome-overrides.ts";
9
+ import { noDoubleCast } from "./suppressions/no-double-cast.ts";
10
+ import { noInline } from "./suppressions/no-inline.ts";
2
11
 
3
- export const builtInRules = [maxLines] as const;
12
+ export const builtInRules = [
13
+ maxLines,
14
+ noJsdoc,
15
+ noLineReference,
16
+ noPrReference,
17
+ noBanner,
18
+ noTodo,
19
+ maxPerFile,
20
+ noInline,
21
+ noDoubleCast,
22
+ biomeOverrides,
23
+ ] as const;
@@ -0,0 +1,45 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { SUPPRESSIONS } from "../suppressions/directives.ts";
3
+ import type { SourceComment } from "./scan.ts";
4
+
5
+ export const DEFAULT_PRAGMAS: ReadonlyArray<string> = [
6
+ "@vitest-environment",
7
+ "@vitest-environment-options",
8
+ "@jest-environment",
9
+ "@jsx",
10
+ "@jsxFrag",
11
+ "@jsxImportSource",
12
+ "@jsxRuntime",
13
+ ];
14
+
15
+ export const Pragmas = Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed(DEFAULT_PRAGMAS)));
16
+
17
+ const DIRECTIVES: ReadonlyArray<RegExp> = [
18
+ ...SUPPRESSIONS,
19
+ /^@ts-/,
20
+ /^biome-ignore/,
21
+ /^(?:eslint|oxlint|stylelint)-(?:disable|enable)/,
22
+ /^[#@]__(?:PURE|NO_SIDE_EFFECTS)__/,
23
+ /^(?:c8|v8|istanbul) ignore/,
24
+ ];
25
+
26
+ const TRIPLE_SLASH = /^\/\/\/\s*</;
27
+
28
+ const contentOf = (comment: SourceComment): ReadonlyArray<string> => comment.body.filter((line) => line !== "");
29
+
30
+ const startsWithTag = (line: string, tags: ReadonlyArray<string>): boolean =>
31
+ tags.some((tag) => line === tag || line.startsWith(`${tag} `) || line.startsWith(`${tag}\t`));
32
+
33
+ export const textOf = (comment: SourceComment): string => comment.body.join("\n");
34
+
35
+ export const isJsdoc = (comment: SourceComment): boolean => comment.kind === "block" && comment.text.startsWith("/**") && comment.text !== "/**/";
36
+
37
+ export const isPragma = (comment: SourceComment, pragmas: ReadonlyArray<string>): boolean => {
38
+ const content = contentOf(comment);
39
+ return content.length > 0 && content.every((line) => startsWithTag(line, pragmas));
40
+ };
41
+
42
+ export const isDirective = (comment: SourceComment): boolean => {
43
+ const first = contentOf(comment)[0] ?? "";
44
+ return TRIPLE_SLASH.test(comment.text) || DIRECTIVES.some((directive) => directive.test(first));
45
+ };
@@ -0,0 +1,46 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { plural } from "../../report/plural.ts";
3
+ import { defineRule, type Finding, type SourceFile } from "../../rule.ts";
4
+ import { isDirective, isPragma, Pragmas } from "./kinds.ts";
5
+ import { commentsOf, type SourceComment } from "./scan.ts";
6
+
7
+ const MaxPerFileOptions = Schema.Struct({
8
+ max: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.withDecodingDefaultKey(Effect.succeed(2))),
9
+ allow: Pragmas,
10
+ });
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;
19
+
20
+ const commentBlocks = (comments: ReadonlyArray<SourceComment>): ReadonlyArray<SourceComment> =>
21
+ comments.filter((comment, index) => !continues(comments[index - 1], comment));
22
+
23
+ const overCap = (file: SourceFile, max: number, allow: ReadonlyArray<string>): ReadonlyArray<Finding> => {
24
+ const counted = commentsOf(file).filter((comment) => !isDirective(comment) && !isPragma(comment, allow));
25
+ const blocks = commentBlocks(counted);
26
+ const first = blocks[max];
27
+ return first === undefined
28
+ ? []
29
+ : [
30
+ {
31
+ file: file.path,
32
+ line: first.line,
33
+ count: blocks.length - max,
34
+ message: `${plural(blocks.length, "comment", "comments")} against a limit of ${max}.`,
35
+ threshold: max,
36
+ },
37
+ ];
38
+ };
39
+
40
+ export const maxPerFile = defineRule({
41
+ id: "comments/max-per-file",
42
+ description:
43
+ "A file that needs more than a few comments does too much, or its comments narrate the code. Delete the comments that restate the code, then split the file.",
44
+ options: Schema.toStandardSchemaV1(MaxPerFileOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
45
+ check: ({ options, sources }) => sources.flatMap((file) => overCap(file, options.max, options.allow)),
46
+ });
@@ -0,0 +1,10 @@
1
+ import { definePatternRule } from "./pattern-rule.ts";
2
+
3
+ const DIVIDER = "[-=*#~_+/\\\\─━═┄┈]{3,}";
4
+
5
+ export const noBanner = definePatternRule({
6
+ id: "comments/no-banner",
7
+ description: "Banners, dividers and regions mark a file that does several jobs. Split the file along those sections instead.",
8
+ patterns: [new RegExp(`^${DIVIDER}|${DIVIDER}$`, "m"), /^#(?:end)?region\b/m],
9
+ message: (match) => `Banner or divider: "${match}".`,
10
+ });
@@ -0,0 +1,18 @@
1
+ import { Schema } from "effect";
2
+ import { defineRule } from "../../rule.ts";
3
+ import { isDirective, isJsdoc, isPragma, Pragmas } from "./kinds.ts";
4
+ import { commentsOf } from "./scan.ts";
5
+
6
+ const NoJsdocOptions = Schema.Struct({ allow: Pragmas });
7
+
8
+ export const noJsdoc = defineRule({
9
+ id: "comments/no-jsdoc",
10
+ description: "Names and types already say what a JSDoc block repeats. Delete it, and keep a reason the code cannot show as a short // comment.",
11
+ options: Schema.toStandardSchemaV1(NoJsdocOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
12
+ check: ({ options, sources }) =>
13
+ sources.flatMap((file) =>
14
+ commentsOf(file)
15
+ .filter((comment) => isJsdoc(comment) && !isPragma(comment, options.allow) && !isDirective(comment))
16
+ .map((comment) => ({ file: file.path, line: comment.line, message: "JSDoc block." })),
17
+ ),
18
+ });
@@ -0,0 +1,11 @@
1
+ import { definePatternRule } from "./pattern-rule.ts";
2
+
3
+ const SOURCE_EXTENSION =
4
+ "(?:[cm]?[jt]sx?|json5?|mdx?|css|scss|less|html?|vue|svelte|astro|ya?ml|toml|sql|prisma|graphql|gql|go|rs|py|rb|java|kt|swift|sh)";
5
+
6
+ export const noLineReference = definePatternRule({
7
+ id: "comments/no-line-reference",
8
+ description: "A line number goes stale with the next edit. Name the function, type, test or setting instead.",
9
+ patterns: [new RegExp(`[\\w./@-]*\\.${SOURCE_EXTENSION}(?::\\d+|#L\\d+)`, "i"), /(?<![\w-])lines?\s+\d+/i],
10
+ message: (match) => `Refers to a line: "${match}".`,
11
+ });
@@ -0,0 +1,13 @@
1
+ import { definePatternRule } from "./pattern-rule.ts";
2
+
3
+ export const noPrReference = definePatternRule({
4
+ id: "comments/no-pr-reference",
5
+ description: "History belongs in version control, not in the code. State the reason itself, so the comment holds without the link.",
6
+ patterns: [
7
+ /\b(?:PR|MR|pull request|merge request|issue|ticket)s?\s*#?\d+\b/i,
8
+ /\/(?:pull|pulls|issues|merge_requests)\/\d+/,
9
+ /\bGH-\d+\b/,
10
+ /(?<![\w&/#-])#\d+\b/,
11
+ ],
12
+ message: (match) => `Refers to a pull request or issue: "${match}".`,
13
+ });
@@ -0,0 +1,8 @@
1
+ import { definePatternRule } from "./pattern-rule.ts";
2
+
3
+ export const noTodo = definePatternRule({
4
+ id: "comments/no-todo",
5
+ description: "A TODO in the code is never done. Do the work now, or track it where the repository tracks work.",
6
+ patterns: [/\b(?:TODO|FIXME|XXX)\b/, /@todo\b/i],
7
+ message: (match) => `Marks unfinished work: "${match}".`,
8
+ });
@@ -0,0 +1,28 @@
1
+ import { defineRule, type Finding, type Rule } from "../../rule.ts";
2
+ import { isDirective, textOf } from "./kinds.ts";
3
+ import { commentsOf, type SourceComment } from "./scan.ts";
4
+
5
+ interface PatternRule<Id extends string> {
6
+ readonly id: Id;
7
+ readonly description: string;
8
+ readonly patterns: ReadonlyArray<RegExp>;
9
+ readonly message: (match: string) => string;
10
+ }
11
+
12
+ const firstMatch = (comment: SourceComment, patterns: ReadonlyArray<RegExp>): string | undefined => {
13
+ const text = textOf(comment);
14
+ return patterns.map((pattern) => pattern.exec(text)?.[0]).find((match) => match !== undefined);
15
+ };
16
+
17
+ export const definePatternRule = <const Id extends string>({ description, id, message, patterns }: PatternRule<Id>): Rule<Id, undefined> =>
18
+ defineRule({
19
+ id,
20
+ description,
21
+ check: ({ sources }) =>
22
+ sources.flatMap((file) =>
23
+ commentsOf(file).flatMap((comment): ReadonlyArray<Finding> => {
24
+ const match = isDirective(comment) ? undefined : firstMatch(comment, patterns);
25
+ return match === undefined ? [] : [{ file: file.path, line: comment.line, message: message(match) }];
26
+ }),
27
+ ),
28
+ });
@@ -0,0 +1,80 @@
1
+ import ts from "typescript";
2
+ import type { SourceFile } from "../../rule.ts";
3
+ import { parse } from "../syntax.ts";
4
+
5
+ export interface SourceComment {
6
+ readonly kind: "block" | "line";
7
+ readonly text: string;
8
+ readonly body: ReadonlyArray<string>;
9
+ readonly line: number;
10
+ readonly endLine: number;
11
+ readonly ownLine: boolean;
12
+ }
13
+
14
+ const DECLARATION = /\.d\.[cm]?ts$/;
15
+
16
+ const commentRanges = (source: ts.SourceFile): ReadonlyArray<ts.CommentRange> => {
17
+ const ranges = new Map<number, ts.CommentRange>();
18
+ const jsxText: Array<ts.Node> = [];
19
+ const add = (found: ReadonlyArray<ts.CommentRange> | undefined) => {
20
+ for (const range of found ?? []) {
21
+ ranges.set(range.pos, range);
22
+ }
23
+ };
24
+ const visit = (node: ts.Node) => {
25
+ if (node.kind === ts.SyntaxKind.JsxText) {
26
+ jsxText.push(node);
27
+ }
28
+ add(ts.getLeadingCommentRanges(source.text, node.pos));
29
+ add(ts.getTrailingCommentRanges(source.text, node.end));
30
+ for (const child of node.getChildren(source)) {
31
+ visit(child);
32
+ }
33
+ };
34
+ visit(source);
35
+ return [...ranges.values()]
36
+ .filter((range) => !jsxText.some((text) => range.pos >= text.pos && range.pos < text.end))
37
+ .sort((left, right) => left.pos - right.pos);
38
+ };
39
+
40
+ export const bodyOf = (kind: SourceComment["kind"], text: string): ReadonlyArray<string> =>
41
+ kind === "line"
42
+ ? [text.slice(2).trim()]
43
+ : text
44
+ .slice(2, -2)
45
+ .split("\n")
46
+ .map((line) => line.trim().replace(/^\*(?=\s|$)\s*/, ""));
47
+
48
+ const toComment = (source: ts.SourceFile, range: ts.CommentRange): SourceComment => {
49
+ const kind = range.kind === ts.SyntaxKind.SingleLineCommentTrivia ? "line" : "block";
50
+ const text = source.text.slice(range.pos, range.end);
51
+ const start = source.getLineAndCharacterOfPosition(range.pos);
52
+ const lineStart = source.getPositionOfLineAndCharacter(start.line, 0);
53
+ return {
54
+ body: bodyOf(kind, text),
55
+ endLine: source.getLineAndCharacterOfPosition(range.end).line + 1,
56
+ kind,
57
+ line: start.line + 1,
58
+ ownLine: source.text.slice(lineStart, range.pos).trim() === "",
59
+ text,
60
+ };
61
+ };
62
+
63
+ const scanned = new WeakMap<SourceFile, ReadonlyArray<SourceComment>>();
64
+
65
+ const scan = (file: SourceFile): ReadonlyArray<SourceComment> => {
66
+ const source = parse(file);
67
+ return source === undefined ? [] : commentRanges(source).map((range) => toComment(source, range));
68
+ };
69
+
70
+ export const scanComments = (file: SourceFile): ReadonlyArray<SourceComment> => {
71
+ const cached = scanned.get(file);
72
+ if (cached !== undefined) {
73
+ return cached;
74
+ }
75
+ const comments = scan(file);
76
+ scanned.set(file, comments);
77
+ return comments;
78
+ };
79
+
80
+ export const commentsOf = (file: SourceFile): ReadonlyArray<SourceComment> => (DECLARATION.test(file.path) ? [] : scanComments(file));
@@ -16,7 +16,14 @@ const MaxLinesOptions = Schema.Struct({
16
16
  const oversized = (file: SourceFile, limit: number): ReadonlyArray<Finding> =>
17
17
  file.lines.length <= limit
18
18
  ? []
19
- : [{ file: file.path, measure: file.lines.length, message: `${file.lines.length} lines exceeds the ${limit}-line limit.` }];
19
+ : [
20
+ {
21
+ count: file.lines.length - limit,
22
+ file: file.path,
23
+ message: `${file.lines.length} lines exceeds the ${limit}-line limit.`,
24
+ threshold: limit,
25
+ },
26
+ ];
20
27
 
21
28
  export const maxLines = defineRule({
22
29
  id: "structure/max-lines",
@@ -0,0 +1,50 @@
1
+ import { posix } from "node:path";
2
+ import type { RuleInputs } from "../../../rule.ts";
3
+ import { itemsOf, type Json, member, parseJsonc, textOf } from "./json.ts";
4
+
5
+ export type Config =
6
+ | { readonly _tag: "Config"; readonly path: string; readonly base: string; readonly json: Json }
7
+ | { readonly _tag: "Unreadable"; readonly path: string; readonly reason: string };
8
+
9
+ const ROOT_CONFIGS: ReadonlyArray<string> = ["biome.json", "biome.jsonc"];
10
+
11
+ const NESTED = /(?:^|\/)biome\.jsonc?$/;
12
+
13
+ const directoryOf = (path: string): string => (posix.dirname(path) === "." ? "" : posix.dirname(path));
14
+
15
+ const localExtends = (path: string, json: Json): ReadonlyArray<string> => {
16
+ const node = member(json, "extends");
17
+ const entries = node?._tag === "Array" ? itemsOf(node).flatMap((item) => textOf(item) ?? []) : [textOf(node) ?? ""];
18
+ return entries.filter((entry) => entry.startsWith(".")).map((entry) => posix.join(posix.dirname(path), entry));
19
+ };
20
+
21
+ const load = async (readText: RuleInputs["readText"], path: string, base: string, seen: Set<string>): Promise<ReadonlyArray<Config>> => {
22
+ if (seen.has(path)) {
23
+ return [];
24
+ }
25
+ seen.add(path);
26
+ const text = await readText(path);
27
+ if (text === undefined) {
28
+ return [{ _tag: "Unreadable", path, reason: "the file does not exist" }];
29
+ }
30
+ const parsed = parseJsonc(path, text);
31
+ if (parsed._tag === "Unreadable") {
32
+ return [{ _tag: "Unreadable", path, reason: parsed.reason }];
33
+ }
34
+ const extended = await Promise.all(localExtends(path, parsed.json).map((target) => load(readText, target, base, seen)));
35
+ return [{ _tag: "Config", base, json: parsed.json, path }, ...extended.flat()];
36
+ };
37
+
38
+ const rootConfig = async (readText: RuleInputs["readText"]): Promise<string | undefined> => {
39
+ const present = await Promise.all(ROOT_CONFIGS.map(async (path) => ((await readText(path)) === undefined ? [] : [path])));
40
+ return present.flat()[0];
41
+ };
42
+
43
+ export const biomeConfigs = async ({ files, readText }: RuleInputs): Promise<ReadonlyArray<Config>> => {
44
+ const seen = new Set<string>();
45
+ const root = await rootConfig(readText);
46
+ const nested = files.filter((path) => NESTED.test(path) && path !== root);
47
+ const roots = root === undefined ? [] : await load(readText, root, "", seen);
48
+ const below = await Promise.all(nested.map((path) => load(readText, path, directoryOf(path), seen)));
49
+ return [...roots, ...below.flat()];
50
+ };
@@ -0,0 +1,55 @@
1
+ import ts from "typescript";
2
+
3
+ export type Json =
4
+ | { readonly _tag: "Object"; readonly line: number; readonly entries: ReadonlyMap<string, Json> }
5
+ | { readonly _tag: "Array"; readonly line: number; readonly items: ReadonlyArray<Json> }
6
+ | { readonly _tag: "Value"; readonly line: number; readonly value: string | boolean | undefined };
7
+
8
+ export type Parsed = { readonly _tag: "Parsed"; readonly json: Json } | { readonly _tag: "Unreadable"; readonly reason: string };
9
+
10
+ const LITERALS: ReadonlyMap<ts.SyntaxKind, boolean> = new Map([
11
+ [ts.SyntaxKind.TrueKeyword, true],
12
+ [ts.SyntaxKind.FalseKeyword, false],
13
+ ]);
14
+
15
+ const scalarOf = (node: ts.Expression): string | boolean | undefined => (ts.isStringLiteral(node) ? node.text : LITERALS.get(node.kind));
16
+
17
+ const entryOf = (source: ts.JsonSourceFile, property: ts.ObjectLiteralElementLike): ReadonlyArray<readonly [string, Json]> =>
18
+ ts.isPropertyAssignment(property) && (ts.isStringLiteral(property.name) || ts.isIdentifier(property.name))
19
+ ? [[property.name.text, toJson(source, property.initializer)]]
20
+ : [];
21
+
22
+ const toJson = (source: ts.JsonSourceFile, node: ts.Expression): Json => {
23
+ const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
24
+ if (ts.isObjectLiteralExpression(node)) {
25
+ return { _tag: "Object", entries: new Map(node.properties.flatMap((property) => entryOf(source, property))), line };
26
+ }
27
+ if (ts.isArrayLiteralExpression(node)) {
28
+ return { _tag: "Array", items: node.elements.map((element) => toJson(source, element)), line };
29
+ }
30
+ return { _tag: "Value", line, value: scalarOf(node) };
31
+ };
32
+
33
+ export const parseJsonc = (path: string, text: string): Parsed => {
34
+ const error = ts.parseConfigFileTextToJson(path, text).error;
35
+ const source = ts.parseJsonText(path, text);
36
+ const root = source.statements[0]?.expression;
37
+ if (error !== undefined || root === undefined) {
38
+ return {
39
+ _tag: "Unreadable",
40
+ reason: error === undefined ? "it is empty" : ts.flattenDiagnosticMessageText(error.messageText, " ").replace(/\.$/, ""),
41
+ };
42
+ }
43
+ return { _tag: "Parsed", json: toJson(source, root) };
44
+ };
45
+
46
+ export const member = (node: Json | undefined, key: string): Json | undefined => (node?._tag === "Object" ? node.entries.get(key) : undefined);
47
+
48
+ export const entriesOf = (node: Json | undefined): ReadonlyArray<readonly [string, Json]> => (node?._tag === "Object" ? [...node.entries] : []);
49
+
50
+ export const itemsOf = (node: Json | undefined): ReadonlyArray<Json> => (node?._tag === "Array" ? node.items : []);
51
+
52
+ export const textOf = (node: Json | undefined): string | undefined =>
53
+ node?._tag === "Value" && typeof node.value === "string" ? node.value : undefined;
54
+
55
+ export const isFalse = (node: Json | undefined): boolean => node?._tag === "Value" && node.value === false;
@@ -0,0 +1,102 @@
1
+ import { entriesOf, isFalse, itemsOf, type Json, member, textOf } from "./json.ts";
2
+
3
+ export interface Weakening {
4
+ readonly rule: string;
5
+ readonly includes: ReadonlyArray<string>;
6
+ readonly file: string;
7
+ readonly line: number;
8
+ }
9
+
10
+ interface Setting {
11
+ readonly rule: string;
12
+ readonly line: number;
13
+ }
14
+
15
+ const LINT_WEAK = new Set(["off", "warn", "info"]);
16
+ const ASSIST_WEAK = new Set(["off"]);
17
+ const PRESETS = new Set(["recommended", "preset"]);
18
+
19
+ const levelOf = (node: Json): string | undefined => textOf(node) ?? textOf(member(node, "level"));
20
+
21
+ const at = (rule: string, node: Json | undefined): ReadonlyArray<Setting> => (node === undefined ? [] : [{ line: node.line, rule }]);
22
+
23
+ const disabled = (rule: string, section: Json | undefined): ReadonlyArray<Setting> =>
24
+ isFalse(member(section, "enabled")) ? at(rule, member(section, "enabled")) : [];
25
+
26
+ const withoutDefaults = (rule: string, set: Json | undefined): ReadonlyArray<Setting> => [
27
+ ...(isFalse(member(set, "recommended")) ? at(rule, member(set, "recommended")) : []),
28
+ ...(textOf(member(set, "preset")) === "none" ? at(rule, member(set, "preset")) : []),
29
+ ];
30
+
31
+ const groupSettings = (prefix: string, weak: ReadonlySet<string>, groups: Json | undefined): ReadonlyArray<Setting> =>
32
+ entriesOf(groups)
33
+ .filter(([group]) => !PRESETS.has(group))
34
+ .flatMap(([group, rules]) => [
35
+ ...(weak.has(textOf(rules) ?? "") ? at(`${prefix}/${group}`, rules) : []),
36
+ ...withoutDefaults(`${prefix}/${group}`, rules),
37
+ ...entriesOf(rules)
38
+ .filter(([rule, setting]) => !PRESETS.has(rule) && weak.has(levelOf(setting) ?? ""))
39
+ .flatMap(([rule, setting]) => at(`${prefix}/${group}/${rule}`, setting)),
40
+ ]);
41
+
42
+ const linter = (section: Json | undefined): ReadonlyArray<Setting> => [
43
+ ...disabled("lint", section),
44
+ ...withoutDefaults("lint/recommended", member(section, "rules")),
45
+ ...groupSettings("lint", LINT_WEAK, member(section, "rules")),
46
+ ...entriesOf(member(section, "domains"))
47
+ .filter(([, value]) => textOf(value) === "none")
48
+ .flatMap(([domain, value]) => at(`lint/domains/${domain}`, value)),
49
+ ];
50
+
51
+ const assist = (section: Json | undefined): ReadonlyArray<Setting> => [
52
+ ...disabled("assist", section),
53
+ ...withoutDefaults("assist/recommended", member(section, "actions")),
54
+ ...groupSettings("assist", ASSIST_WEAK, member(section, "actions")),
55
+ ];
56
+
57
+ const LANGUAGES: ReadonlyArray<string> = ["javascript", "json", "css", "graphql", "grit", "html"];
58
+
59
+ const TOOLS: ReadonlyArray<readonly [string, string]> = [
60
+ ["linter", "lint"],
61
+ ["assist", "assist"],
62
+ ["formatter", "format"],
63
+ ];
64
+
65
+ const SCOPED_SECTIONS: ReadonlyArray<readonly [string, string]> = [["files", "files"], ...TOOLS];
66
+
67
+ const scopeSettings = (scope: Json): ReadonlyArray<Setting> => [
68
+ ...linter(member(scope, "linter")),
69
+ ...assist(member(scope, "assist")),
70
+ ...disabled("format", member(scope, "formatter")),
71
+ ...LANGUAGES.flatMap((language) => TOOLS.flatMap(([key, name]) => disabled(`${language}/${name}`, member(member(scope, language), key)))),
72
+ ];
73
+
74
+ const scoped = (base: string, glob: string): string => {
75
+ const [, negation = "", path = glob] = /^(!*)(.*)$/.exec(glob) ?? [];
76
+ return base === "" ? glob : `${negation}${base}/${path}`;
77
+ };
78
+
79
+ const includesOf = (override: Json): ReadonlyArray<string> => {
80
+ const includes = itemsOf(member(override, "includes")).flatMap((item) => textOf(item) ?? []);
81
+ return includes.length === 0 ? ["**"] : includes;
82
+ };
83
+
84
+ const narrowsScope = (globs: ReadonlyArray<string>): boolean =>
85
+ globs.length > 0 && (globs.some((glob) => glob.startsWith("!")) || !globs.includes("**"));
86
+
87
+ const narrowings = (config: Json, file: string, base: string): ReadonlyArray<Weakening> =>
88
+ SCOPED_SECTIONS.flatMap(([key, name]) => {
89
+ const list = member(member(config, key), "includes");
90
+ const globs = itemsOf(list).flatMap((item) => textOf(item) ?? []);
91
+ return list === undefined || !narrowsScope(globs)
92
+ ? []
93
+ : [{ file, includes: globs.map((glob) => scoped(base, glob)), line: list.line, rule: `${name}/includes` }];
94
+ });
95
+
96
+ export const weakeningsOf = (config: Json, file: string, base: string): ReadonlyArray<Weakening> => [
97
+ ...narrowings(config, file, base),
98
+ ...scopeSettings(config).map((setting) => ({ ...setting, file, includes: [scoped(base, "**")] })),
99
+ ...itemsOf(member(config, "overrides")).flatMap((override) =>
100
+ scopeSettings(override).map((setting) => ({ ...setting, file, includes: includesOf(override).map((glob) => scoped(base, glob)) })),
101
+ ),
102
+ ];
@@ -0,0 +1,61 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { defineRule, type Finding } from "../../rule.ts";
3
+ import { biomeConfigs, type Config } from "./biome/configs.ts";
4
+ import { type Weakening, weakeningsOf } from "./biome/weakenings.ts";
5
+
6
+ const Declaration = Schema.Struct({
7
+ rule: Schema.NonEmptyString,
8
+ includes: Schema.NonEmptyArray(Schema.NonEmptyString),
9
+ reason: Schema.String.check(Schema.isPattern(/\S/, { expected: "a reason that says why the scope needs the rule weakened" })),
10
+ });
11
+
12
+ type Declaration = typeof Declaration.Type;
13
+
14
+ const BiomeOverridesOptions = Schema.Struct({
15
+ declared: Schema.Array(Declaration).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
16
+ });
17
+
18
+ const sameScope = (left: ReadonlyArray<string>, right: ReadonlyArray<string>): boolean =>
19
+ new Set(left).size === new Set(right).size && left.every((glob) => right.includes(glob));
20
+
21
+ const matches = (declaration: Declaration, weakening: Weakening): boolean =>
22
+ declaration.rule === weakening.rule && sameScope(declaration.includes, weakening.includes);
23
+
24
+ const scopeText = (includes: ReadonlyArray<string>): string => includes.map((glob) => `"${glob}"`).join(", ");
25
+
26
+ const undeclared = (weakening: Weakening): Finding => ({
27
+ file: weakening.file,
28
+ line: weakening.line,
29
+ message: `Weakens "${weakening.rule}" for ${scopeText(weakening.includes)} without a declaration.`,
30
+ subject: weakening.rule,
31
+ });
32
+
33
+ const unused = (file: string, declaration: Declaration): Finding => ({
34
+ file,
35
+ message: `Declares "${declaration.rule}" for ${scopeText(declaration.includes)}, which no Biome config weakens. Remove the declaration.`,
36
+ subject: declaration.rule,
37
+ });
38
+
39
+ const unreadable = (config: Config): ReadonlyArray<Finding> =>
40
+ config._tag === "Unreadable" ? [{ file: config.path, message: `Cannot read this Biome config: ${config.reason}.` }] : [];
41
+
42
+ export const biomeOverrides = defineRule({
43
+ id: "suppressions/biome-overrides",
44
+ description:
45
+ "A Biome setting that turns a check off or down, or keeps files out of it, is an exception for every file it covers. Fix the code and remove the setting, or declare its rule, includes and reason under this rule's declared option.",
46
+ options: Schema.toStandardSchemaV1(BiomeOverridesOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
47
+ registrable: false,
48
+ check: async (inputs) => {
49
+ const configs = await biomeConfigs(inputs);
50
+ const { declared } = inputs.options;
51
+ const weakenings = configs.flatMap((config) => (config._tag === "Config" ? weakeningsOf(config.json, config.path, config.base) : []));
52
+ const root = configs.find((config) => !config.path.includes("/"))?.path ?? "biome.json";
53
+ return [
54
+ ...configs.flatMap(unreadable),
55
+ ...weakenings.filter((weakening) => !declared.some((declaration) => matches(declaration, weakening))).map(undeclared),
56
+ ...declared
57
+ .filter((declaration) => !weakenings.some((weakening) => matches(declaration, weakening)))
58
+ .map((declaration) => unused(root, declaration)),
59
+ ];
60
+ },
61
+ });
@@ -0,0 +1,25 @@
1
+ import type { SourceComment } from "../comments/scan.ts";
2
+
3
+ export const SUPPRESSIONS: ReadonlyArray<RegExp> = [
4
+ /^biome-ignore(?:-all|-start)?(?![\w-])/,
5
+ /^@ts-(?:ignore|expect-error|nocheck)(?![\w-])/,
6
+ /^(?:eslint|oxlint|stylelint)-disable(?:-next-line|-line)?(?![\w-])/,
7
+ /^deno-lint-ignore(?:-file)?(?![\w-])/,
8
+ /^tslint:disable/,
9
+ /^prettier-ignore(?![\w-])/,
10
+ /^\$Flow(?:FixMe|Ignore|ExpectedError|Issue)(?!\w)/,
11
+ /^@noflow(?!\w)/,
12
+ ];
13
+
14
+ const INLINE_ESLINT_CONFIG = /^eslint\s+["']?[\w@/-]+["']?\s*:/;
15
+
16
+ const LEADER = /^[\s/*]+/;
17
+
18
+ const directiveOn = (comment: SourceComment, line: string): string | undefined => {
19
+ const content = line.replace(LEADER, "");
20
+ const patterns = comment.kind === "block" ? [...SUPPRESSIONS, INLINE_ESLINT_CONFIG] : SUPPRESSIONS;
21
+ return patterns.map((pattern) => pattern.exec(content)?.[0].replace(/\s*:$/, "")).find((match) => match !== undefined);
22
+ };
23
+
24
+ export const suppressionIn = (comment: SourceComment): string | undefined =>
25
+ comment.body.map((line) => directiveOn(comment, line)).find((directive) => directive !== undefined);
@@ -0,0 +1,47 @@
1
+ import ts from "typescript";
2
+ import { defineRule, type Finding, type SourceFile } from "../../rule.ts";
3
+ import { parse } from "../syntax.ts";
4
+
5
+ const BRIDGES: ReadonlyMap<ts.SyntaxKind, string> = new Map([
6
+ [ts.SyntaxKind.UnknownKeyword, "unknown"],
7
+ [ts.SyntaxKind.AnyKeyword, "any"],
8
+ [ts.SyntaxKind.NeverKeyword, "never"],
9
+ ]);
10
+
11
+ type Assertion = ts.AsExpression | ts.TypeAssertion;
12
+
13
+ const isAssertion = (node: ts.Node): node is Assertion => ts.isAsExpression(node) || ts.isTypeAssertionExpression(node);
14
+
15
+ const unwrapped = (node: ts.Expression): ts.Expression => (ts.isParenthesizedExpression(node) ? unwrapped(node.expression) : node);
16
+
17
+ const bridgeOf = (node: Assertion): string | undefined => {
18
+ const inner = unwrapped(node.expression);
19
+ return isAssertion(inner) ? BRIDGES.get(inner.type.kind) : undefined;
20
+ };
21
+
22
+ const castsIn = (path: string, source: ts.SourceFile): ReadonlyArray<Finding> => {
23
+ const findings: Finding[] = [];
24
+ const visit = (node: ts.Node): void => {
25
+ const bridge = isAssertion(node) ? bridgeOf(node) : undefined;
26
+ if (bridge !== undefined) {
27
+ const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
28
+ findings.push({ file: path, line, message: `Casts through "${bridge}" to reach a type the value does not have.` });
29
+ }
30
+ ts.forEachChild(node, visit);
31
+ };
32
+ visit(source);
33
+ return findings;
34
+ };
35
+
36
+ const doubleCasts = (file: SourceFile): ReadonlyArray<Finding> => {
37
+ const source = parse(file);
38
+ return source === undefined ? [] : castsIn(file.path, source);
39
+ };
40
+
41
+ export const noDoubleCast = defineRule({
42
+ id: "suppressions/no-double-cast",
43
+ description:
44
+ "A cast through unknown, any or never silences the compiler the way a suppression does. Decode the value at its boundary, narrow it, or fix the type that disagrees.",
45
+ registrable: false,
46
+ check: ({ sources }) => sources.flatMap(doubleCasts),
47
+ });