@shivaedev/quality 0.4.0 → 0.5.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 (387) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +119 -29
  3. package/biome/declarations.json +17 -0
  4. package/biome/plugins/ambient-runtime.grit +18 -0
  5. package/biome/plugins/effect-fn-spans.grit +7 -0
  6. package/biome/preset.json +602 -0
  7. package/dist/baseline/compare.d.ts +8 -9
  8. package/dist/baseline/compare.d.ts.map +1 -1
  9. package/dist/baseline/compare.js +4 -4
  10. package/dist/baseline/compare.js.map +1 -1
  11. package/dist/baseline/format.d.ts +4 -4
  12. package/dist/baseline/format.d.ts.map +1 -1
  13. package/dist/baseline/format.js +3 -2
  14. package/dist/baseline/format.js.map +1 -1
  15. package/dist/baseline/guard.d.ts +3 -3
  16. package/dist/baseline/guard.d.ts.map +1 -1
  17. package/dist/baseline/guard.js +6 -5
  18. package/dist/baseline/guard.js.map +1 -1
  19. package/dist/baseline/legacy.d.ts +3 -3
  20. package/dist/baseline/legacy.d.ts.map +1 -1
  21. package/dist/baseline/legacy.js.map +1 -1
  22. package/dist/baseline/rewrite.d.ts +1 -1
  23. package/dist/baseline/rewrite.d.ts.map +1 -1
  24. package/dist/baseline/rewrite.js.map +1 -1
  25. package/dist/baseline/update.d.ts +8 -9
  26. package/dist/baseline/update.d.ts.map +1 -1
  27. package/dist/baseline/update.js +10 -9
  28. package/dist/baseline/update.js.map +1 -1
  29. package/dist/biome/report.d.ts +33 -0
  30. package/dist/biome/report.d.ts.map +1 -0
  31. package/dist/biome/report.js +25 -0
  32. package/dist/biome/report.js.map +1 -0
  33. package/dist/biome/run.d.ts +7 -0
  34. package/dist/biome/run.d.ts.map +1 -0
  35. package/dist/biome/run.js +16 -0
  36. package/dist/biome/run.js.map +1 -0
  37. package/dist/cli/args.d.ts +6 -3
  38. package/dist/cli/args.d.ts.map +1 -1
  39. package/dist/cli/args.js +9 -4
  40. package/dist/cli/args.js.map +1 -1
  41. package/dist/cli/baseline.d.ts +1 -1
  42. package/dist/cli/baseline.d.ts.map +1 -1
  43. package/dist/cli/baseline.js +3 -3
  44. package/dist/cli/baseline.js.map +1 -1
  45. package/dist/cli/check.js.map +1 -1
  46. package/dist/cli/fix.d.ts +4 -0
  47. package/dist/cli/fix.d.ts.map +1 -0
  48. package/dist/cli/fix.js +19 -0
  49. package/dist/cli/fix.js.map +1 -0
  50. package/dist/cli/lint.js +1 -1
  51. package/dist/cli/lint.js.map +1 -1
  52. package/dist/cli/run-main.d.ts.map +1 -1
  53. package/dist/cli/run-main.js +7 -3
  54. package/dist/cli/run-main.js.map +1 -1
  55. package/dist/cli.js +3 -0
  56. package/dist/cli.js.map +1 -1
  57. package/dist/config/decode.d.ts +4 -4
  58. package/dist/config/decode.js +12 -12
  59. package/dist/config/decode.js.map +1 -1
  60. package/dist/config/load.d.ts +3 -3
  61. package/dist/config/load.d.ts.map +1 -1
  62. package/dist/config/load.js +1 -1
  63. package/dist/config/load.js.map +1 -1
  64. package/dist/config/resolve.d.ts +3 -5
  65. package/dist/config/resolve.d.ts.map +1 -1
  66. package/dist/config/resolve.js +11 -5
  67. package/dist/config/resolve.js.map +1 -1
  68. package/dist/config.d.ts +9 -9
  69. package/dist/config.d.ts.map +1 -1
  70. package/dist/config.js.map +1 -1
  71. package/dist/decoded.d.ts +1 -1
  72. package/dist/decoded.d.ts.map +1 -1
  73. package/dist/engine/baseline-file.d.ts +1 -1
  74. package/dist/engine/baseline-file.d.ts.map +1 -1
  75. package/dist/engine/evaluate.d.ts +8 -9
  76. package/dist/engine/evaluate.d.ts.map +1 -1
  77. package/dist/engine/evaluate.js +4 -4
  78. package/dist/engine/evaluate.js.map +1 -1
  79. package/dist/engine/run-rules.d.ts +4 -3
  80. package/dist/engine/run-rules.d.ts.map +1 -1
  81. package/dist/engine/run-rules.js +3 -3
  82. package/dist/engine/run-rules.js.map +1 -1
  83. package/dist/engine/session.d.ts +3 -3
  84. package/dist/engine/session.d.ts.map +1 -1
  85. package/dist/engine/violation.d.ts +11 -3
  86. package/dist/engine/violation.d.ts.map +1 -1
  87. package/dist/engine/violation.js +6 -2
  88. package/dist/engine/violation.js.map +1 -1
  89. package/dist/exceptions/registry.d.ts +7 -8
  90. package/dist/exceptions/registry.d.ts.map +1 -1
  91. package/dist/exceptions/registry.js +6 -6
  92. package/dist/exceptions/registry.js.map +1 -1
  93. package/dist/git/base.d.ts +1 -1
  94. package/dist/git/base.d.ts.map +1 -1
  95. package/dist/git/base.js.map +1 -1
  96. package/dist/git/command.d.ts +3 -3
  97. package/dist/git/command.d.ts.map +1 -1
  98. package/dist/git/command.js.map +1 -1
  99. package/dist/git/history.d.ts +1 -1
  100. package/dist/git/history.d.ts.map +1 -1
  101. package/dist/git/history.js +1 -1
  102. package/dist/git/history.js.map +1 -1
  103. package/dist/imports/ambient.d.ts +7 -0
  104. package/dist/imports/ambient.d.ts.map +1 -0
  105. package/dist/imports/ambient.js +44 -0
  106. package/dist/imports/ambient.js.map +1 -0
  107. package/dist/imports/components.d.ts +2 -0
  108. package/dist/imports/components.d.ts.map +1 -0
  109. package/dist/imports/components.js +63 -0
  110. package/dist/imports/components.js.map +1 -0
  111. package/dist/imports/extract.d.ts +10 -0
  112. package/dist/imports/extract.d.ts.map +1 -0
  113. package/dist/imports/extract.js +100 -0
  114. package/dist/imports/extract.js.map +1 -0
  115. package/dist/imports/fences/dsl.d.ts +18 -0
  116. package/dist/imports/fences/dsl.d.ts.map +1 -0
  117. package/dist/imports/fences/dsl.js +14 -0
  118. package/dist/imports/fences/dsl.js.map +1 -0
  119. package/dist/imports/fences/evaluate.d.ts +12 -0
  120. package/dist/imports/fences/evaluate.d.ts.map +1 -0
  121. package/dist/imports/fences/evaluate.js +55 -0
  122. package/dist/imports/fences/evaluate.js.map +1 -0
  123. package/dist/imports/fences/match.d.ts +17 -0
  124. package/dist/imports/fences/match.d.ts.map +1 -0
  125. package/dist/imports/fences/match.js +106 -0
  126. package/dist/imports/fences/match.js.map +1 -0
  127. package/dist/imports/fences/model.d.ts +60 -0
  128. package/dist/imports/fences/model.d.ts.map +1 -0
  129. package/dist/imports/fences/model.js +2 -0
  130. package/dist/imports/fences/model.js.map +1 -0
  131. package/dist/imports/fences/policy.d.ts +5 -0
  132. package/dist/imports/fences/policy.d.ts.map +1 -0
  133. package/dist/imports/fences/policy.js +59 -0
  134. package/dist/imports/fences/policy.js.map +1 -0
  135. package/dist/imports/fences/reach.d.ts +12 -0
  136. package/dist/imports/fences/reach.d.ts.map +1 -0
  137. package/dist/imports/fences/reach.js +39 -0
  138. package/dist/imports/fences/reach.js.map +1 -0
  139. package/dist/imports/fences/selectors.d.ts +11 -0
  140. package/dist/imports/fences/selectors.d.ts.map +1 -0
  141. package/dist/imports/fences/selectors.js +27 -0
  142. package/dist/imports/fences/selectors.js.map +1 -0
  143. package/dist/imports/fences/vocabulary.d.ts +9 -0
  144. package/dist/imports/fences/vocabulary.d.ts.map +1 -0
  145. package/dist/imports/fences/vocabulary.js +40 -0
  146. package/dist/imports/fences/vocabulary.js.map +1 -0
  147. package/dist/imports/generated.d.ts +6 -0
  148. package/dist/imports/generated.d.ts.map +1 -0
  149. package/dist/imports/generated.js +78 -0
  150. package/dist/imports/generated.js.map +1 -0
  151. package/dist/imports/glob.d.ts +2 -0
  152. package/dist/imports/glob.d.ts.map +1 -0
  153. package/dist/imports/glob.js +125 -0
  154. package/dist/imports/glob.js.map +1 -0
  155. package/dist/imports/graph.d.ts +23 -0
  156. package/dist/imports/graph.d.ts.map +1 -0
  157. package/dist/imports/graph.js +56 -0
  158. package/dist/imports/graph.js.map +1 -0
  159. package/dist/imports/paths.d.ts +2 -0
  160. package/dist/imports/paths.d.ts.map +1 -0
  161. package/dist/imports/paths.js +24 -0
  162. package/dist/imports/paths.js.map +1 -0
  163. package/dist/imports/pnpm-workspace.d.ts +2 -0
  164. package/dist/imports/pnpm-workspace.d.ts.map +1 -0
  165. package/dist/imports/pnpm-workspace.js +122 -0
  166. package/dist/imports/pnpm-workspace.js.map +1 -0
  167. package/dist/imports/projects.d.ts +10 -0
  168. package/dist/imports/projects.d.ts.map +1 -0
  169. package/dist/imports/projects.js +83 -0
  170. package/dist/imports/projects.js.map +1 -0
  171. package/dist/imports/resolve.d.ts +16 -0
  172. package/dist/imports/resolve.d.ts.map +1 -0
  173. package/dist/imports/resolve.js +88 -0
  174. package/dist/imports/resolve.js.map +1 -0
  175. package/dist/imports/workspace.d.ts +10 -0
  176. package/dist/imports/workspace.d.ts.map +1 -0
  177. package/dist/imports/workspace.js +55 -0
  178. package/dist/imports/workspace.js.map +1 -0
  179. package/dist/index.d.ts +3 -0
  180. package/dist/index.d.ts.map +1 -1
  181. package/dist/index.js +2 -0
  182. package/dist/index.js.map +1 -1
  183. package/dist/inventory/collect.d.ts +6 -6
  184. package/dist/inventory/collect.d.ts.map +1 -1
  185. package/dist/inventory/collect.js.map +1 -1
  186. package/dist/manifests/sorted.d.ts +14 -0
  187. package/dist/manifests/sorted.d.ts.map +1 -0
  188. package/dist/manifests/sorted.js +39 -0
  189. package/dist/manifests/sorted.js.map +1 -0
  190. package/dist/report/guard.d.ts +1 -1
  191. package/dist/report/guard.d.ts.map +1 -1
  192. package/dist/report/guard.js +1 -1
  193. package/dist/report/guard.js.map +1 -1
  194. package/dist/report/render.d.ts +2 -2
  195. package/dist/report/render.d.ts.map +1 -1
  196. package/dist/report/render.js.map +1 -1
  197. package/dist/report/stale.d.ts +2 -2
  198. package/dist/report/stale.d.ts.map +1 -1
  199. package/dist/report/stale.js.map +1 -1
  200. package/dist/rule.d.ts +12 -11
  201. package/dist/rule.d.ts.map +1 -1
  202. package/dist/rule.js.map +1 -1
  203. package/dist/rules/biome.d.ts +4 -0
  204. package/dist/rules/biome.d.ts.map +1 -0
  205. package/dist/rules/biome.js +40 -0
  206. package/dist/rules/biome.js.map +1 -0
  207. package/dist/rules/built-in.d.ts +8 -10
  208. package/dist/rules/built-in.d.ts.map +1 -1
  209. package/dist/rules/built-in.js +12 -0
  210. package/dist/rules/built-in.js.map +1 -1
  211. package/dist/rules/comments/kinds.d.ts +1 -2
  212. package/dist/rules/comments/kinds.d.ts.map +1 -1
  213. package/dist/rules/comments/kinds.js +7 -16
  214. package/dist/rules/comments/kinds.js.map +1 -1
  215. package/dist/rules/comments/max-per-file.d.ts +1 -1
  216. package/dist/rules/comments/max-per-file.js +11 -11
  217. package/dist/rules/comments/max-per-file.js.map +1 -1
  218. package/dist/rules/comments/no-banner.js +2 -2
  219. package/dist/rules/comments/no-banner.js.map +1 -1
  220. package/dist/rules/comments/no-environment-pragma.d.ts +2 -0
  221. package/dist/rules/comments/no-environment-pragma.d.ts.map +1 -0
  222. package/dist/rules/comments/no-environment-pragma.js +8 -0
  223. package/dist/rules/comments/no-environment-pragma.js.map +1 -0
  224. package/dist/rules/comments/no-jsdoc.js +3 -3
  225. package/dist/rules/comments/no-jsdoc.js.map +1 -1
  226. package/dist/rules/comments/no-line-reference.js +2 -2
  227. package/dist/rules/comments/no-line-reference.js.map +1 -1
  228. package/dist/rules/comments/no-pr-reference.js +6 -6
  229. package/dist/rules/comments/no-pr-reference.js.map +1 -1
  230. package/dist/rules/comments/no-todo.js +2 -2
  231. package/dist/rules/comments/no-todo.js.map +1 -1
  232. package/dist/rules/comments/pattern-rule.d.ts +2 -2
  233. package/dist/rules/comments/pattern-rule.d.ts.map +1 -1
  234. package/dist/rules/comments/pattern-rule.js +2 -2
  235. package/dist/rules/comments/pattern-rule.js.map +1 -1
  236. package/dist/rules/comments/scan.d.ts +6 -6
  237. package/dist/rules/comments/scan.d.ts.map +1 -1
  238. package/dist/rules/comments/scan.js +2 -2
  239. package/dist/rules/comments/scan.js.map +1 -1
  240. package/dist/rules/imports/cycles.d.ts +2 -0
  241. package/dist/rules/imports/cycles.d.ts.map +1 -0
  242. package/dist/rules/imports/cycles.js +38 -0
  243. package/dist/rules/imports/cycles.js.map +1 -0
  244. package/dist/rules/imports/fences.d.ts +5 -0
  245. package/dist/rules/imports/fences.d.ts.map +1 -0
  246. package/dist/rules/imports/fences.js +24 -0
  247. package/dist/rules/imports/fences.js.map +1 -0
  248. package/dist/rules/imports/resolvable.d.ts +4 -0
  249. package/dist/rules/imports/resolvable.d.ts.map +1 -0
  250. package/dist/rules/imports/resolvable.js +22 -0
  251. package/dist/rules/imports/resolvable.js.map +1 -0
  252. package/dist/rules/manifests-sorted.d.ts +2 -0
  253. package/dist/rules/manifests-sorted.d.ts.map +1 -0
  254. package/dist/rules/manifests-sorted.js +22 -0
  255. package/dist/rules/manifests-sorted.js.map +1 -0
  256. package/dist/rules/max-lines.js +4 -4
  257. package/dist/rules/max-lines.js.map +1 -1
  258. package/dist/rules/suppressions/biome/configs.d.ts +6 -6
  259. package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
  260. package/dist/rules/suppressions/biome/configs.js +2 -2
  261. package/dist/rules/suppressions/biome/configs.js.map +1 -1
  262. package/dist/rules/suppressions/biome/declarations.d.ts +4 -4
  263. package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -1
  264. package/dist/rules/suppressions/biome/declarations.js +3 -3
  265. package/dist/rules/suppressions/biome/declarations.js.map +1 -1
  266. package/dist/rules/suppressions/biome/json.d.ts +2 -2
  267. package/dist/rules/suppressions/biome/json.d.ts.map +1 -1
  268. package/dist/rules/suppressions/biome/json.js +1 -1
  269. package/dist/rules/suppressions/biome/json.js.map +1 -1
  270. package/dist/rules/suppressions/biome/resolve.js +3 -3
  271. package/dist/rules/suppressions/biome/resolve.js.map +1 -1
  272. package/dist/rules/suppressions/biome/weakenings.d.ts +4 -4
  273. package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
  274. package/dist/rules/suppressions/biome/weakenings.js.map +1 -1
  275. package/dist/rules/suppressions/biome-overrides.d.ts +1 -1
  276. package/dist/rules/suppressions/biome-overrides.js +4 -4
  277. package/dist/rules/suppressions/biome-overrides.js.map +1 -1
  278. package/dist/rules/suppressions/directives.d.ts +1 -1
  279. package/dist/rules/suppressions/directives.d.ts.map +1 -1
  280. package/dist/rules/suppressions/directives.js +11 -11
  281. package/dist/rules/suppressions/directives.js.map +1 -1
  282. package/dist/rules/suppressions/no-double-cast.js +2 -2
  283. package/dist/rules/suppressions/no-double-cast.js.map +1 -1
  284. package/dist/rules/suppressions/no-inline.d.ts +1 -7
  285. package/dist/rules/suppressions/no-inline.d.ts.map +1 -1
  286. package/dist/rules/suppressions/no-inline.js +7 -30
  287. package/dist/rules/suppressions/no-inline.js.map +1 -1
  288. package/dist/rules/suppressions/stylesheet-comments.d.ts +1 -1
  289. package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
  290. package/dist/rules/suppressions/stylesheet-comments.js +3 -3
  291. package/dist/rules/suppressions/stylesheet-comments.js.map +1 -1
  292. package/dist/rules/syntax.js +4 -4
  293. package/dist/rules/syntax.js.map +1 -1
  294. package/dist/standard-schema.d.ts +1 -1
  295. package/dist/standard-schema.d.ts.map +1 -1
  296. package/dist/vitest.d.ts +3 -0
  297. package/dist/vitest.d.ts.map +1 -0
  298. package/dist/vitest.js +20 -0
  299. package/dist/vitest.js.map +1 -0
  300. package/package.json +46 -31
  301. package/src/baseline/compare.ts +10 -15
  302. package/src/baseline/format.ts +8 -6
  303. package/src/baseline/guard.ts +11 -10
  304. package/src/baseline/legacy.ts +3 -3
  305. package/src/baseline/rewrite.ts +1 -1
  306. package/src/baseline/update.ts +21 -21
  307. package/src/biome/report.ts +35 -0
  308. package/src/biome/run.ts +23 -0
  309. package/src/cli/args.ts +14 -8
  310. package/src/cli/baseline.ts +5 -6
  311. package/src/cli/check.ts +1 -1
  312. package/src/cli/fix.ts +19 -0
  313. package/src/cli/lint.ts +1 -1
  314. package/src/cli/run-main.ts +8 -3
  315. package/src/cli.ts +3 -0
  316. package/src/config/decode.ts +12 -12
  317. package/src/config/load.ts +4 -4
  318. package/src/config/resolve.ts +15 -11
  319. package/src/config.ts +9 -9
  320. package/src/decoded.ts +1 -1
  321. package/src/engine/baseline-file.ts +1 -1
  322. package/src/engine/evaluate.ts +14 -16
  323. package/src/engine/run-rules.ts +8 -7
  324. package/src/engine/session.ts +3 -3
  325. package/src/engine/violation.ts +18 -4
  326. package/src/exceptions/registry.ts +10 -16
  327. package/src/git/base.ts +2 -2
  328. package/src/git/command.ts +3 -3
  329. package/src/git/history.ts +2 -2
  330. package/src/imports/ambient.ts +54 -0
  331. package/src/imports/components.ts +80 -0
  332. package/src/imports/extract.ts +129 -0
  333. package/src/imports/fences/dsl.ts +33 -0
  334. package/src/imports/fences/evaluate.ts +75 -0
  335. package/src/imports/fences/match.ts +148 -0
  336. package/src/imports/fences/model.ts +42 -0
  337. package/src/imports/fences/policy.ts +70 -0
  338. package/src/imports/fences/reach.ts +62 -0
  339. package/src/imports/fences/selectors.ts +37 -0
  340. package/src/imports/fences/vocabulary.ts +51 -0
  341. package/src/imports/generated.ts +95 -0
  342. package/src/imports/glob.ts +142 -0
  343. package/src/imports/graph.ts +81 -0
  344. package/src/imports/paths.ts +28 -0
  345. package/src/imports/pnpm-workspace.ts +134 -0
  346. package/src/imports/projects.ts +106 -0
  347. package/src/imports/resolve.ts +115 -0
  348. package/src/imports/workspace.ts +76 -0
  349. package/src/index.ts +3 -0
  350. package/src/inventory/collect.ts +6 -6
  351. package/src/manifests/sorted.ts +46 -0
  352. package/src/report/guard.ts +2 -2
  353. package/src/report/render.ts +9 -9
  354. package/src/report/stale.ts +3 -3
  355. package/src/rule.ts +13 -12
  356. package/src/rules/biome.ts +47 -0
  357. package/src/rules/built-in.ts +12 -0
  358. package/src/rules/comments/kinds.ts +11 -21
  359. package/src/rules/comments/max-per-file.ts +13 -13
  360. package/src/rules/comments/no-banner.ts +2 -2
  361. package/src/rules/comments/no-environment-pragma.ts +9 -0
  362. package/src/rules/comments/no-jsdoc.ts +3 -3
  363. package/src/rules/comments/no-line-reference.ts +2 -2
  364. package/src/rules/comments/no-pr-reference.ts +6 -6
  365. package/src/rules/comments/no-todo.ts +2 -2
  366. package/src/rules/comments/pattern-rule.ts +6 -6
  367. package/src/rules/comments/scan.ts +13 -13
  368. package/src/rules/imports/cycles.ts +44 -0
  369. package/src/rules/imports/fences.ts +27 -0
  370. package/src/rules/imports/resolvable.ts +23 -0
  371. package/src/rules/manifests-sorted.ts +23 -0
  372. package/src/rules/max-lines.ts +6 -6
  373. package/src/rules/suppressions/biome/configs.ts +12 -12
  374. package/src/rules/suppressions/biome/declarations.ts +7 -7
  375. package/src/rules/suppressions/biome/json.ts +3 -3
  376. package/src/rules/suppressions/biome/resolve.ts +3 -3
  377. package/src/rules/suppressions/biome/weakenings.ts +18 -19
  378. package/src/rules/suppressions/biome-overrides.ts +7 -7
  379. package/src/rules/suppressions/directives.ts +12 -12
  380. package/src/rules/suppressions/no-double-cast.ts +4 -4
  381. package/src/rules/suppressions/no-inline.ts +13 -37
  382. package/src/rules/suppressions/stylesheet-comments.ts +4 -4
  383. package/src/rules/syntax.ts +4 -4
  384. package/src/standard-schema.ts +1 -1
  385. package/src/vitest.ts +22 -0
  386. package/tsconfig/base.json +21 -21
  387. package/tsconfig/package.json +2 -2
@@ -0,0 +1,106 @@
1
+ import { dirname, join, relative } from "node:path";
2
+ import ts from "typescript";
3
+
4
+ export interface Project {
5
+ readonly cache: ts.ModuleResolutionCache;
6
+ readonly files: ReadonlySet<string>;
7
+ readonly options: ts.CompilerOptions;
8
+ readonly typesCache: ts.ModuleResolutionCache;
9
+ readonly typesOptions: ts.CompilerOptions;
10
+ }
11
+
12
+ interface ParsedProject {
13
+ readonly files: ReadonlySet<string>;
14
+ readonly key: string;
15
+ readonly options: ts.CompilerOptions;
16
+ readonly references: readonly string[];
17
+ }
18
+
19
+ const CONFIG = "tsconfig.json";
20
+
21
+ const SOURCE_CONDITION = "source";
22
+
23
+ const quietHost: ts.ParseConfigFileHost = { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => undefined };
24
+
25
+ function resolutionOptions(options: ts.CompilerOptions, declarations: boolean): ts.CompilerOptions {
26
+ return {
27
+ ...options,
28
+ allowArbitraryExtensions: declarations,
29
+ allowImportingTsExtensions: true,
30
+ allowJs: true,
31
+ customConditions: [...new Set([SOURCE_CONDITION, ...(options.customConditions ?? [])])],
32
+ module: ts.ModuleKind.ESNext,
33
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
34
+ noDtsResolution: !declarations,
35
+ resolveJsonModule: true,
36
+ resolvePackageJsonExports: true,
37
+ resolvePackageJsonImports: true,
38
+ };
39
+ }
40
+
41
+ function projectOf(root: string, parsed: ParsedProject | undefined): Project {
42
+ const runtime = resolutionOptions(parsed?.options ?? {}, false);
43
+ const types = resolutionOptions(parsed?.options ?? {}, true);
44
+ return {
45
+ cache: ts.createModuleResolutionCache(root, (file) => file, runtime),
46
+ files: parsed?.files ?? new Set(),
47
+ options: runtime,
48
+ typesCache: ts.createModuleResolutionCache(root, (file) => file, types),
49
+ typesOptions: types,
50
+ };
51
+ }
52
+
53
+ function parse(path: string): ParsedProject {
54
+ const parsed = ts.getParsedCommandLineOfConfigFile(path, {}, quietHost);
55
+ return {
56
+ files: new Set(parsed?.fileNames ?? []),
57
+ key: path,
58
+ options: parsed?.options ?? {},
59
+ references: (parsed?.projectReferences ?? []).map((reference) => ts.resolveProjectReferencePath(reference)),
60
+ };
61
+ }
62
+
63
+ function nearestConfig(root: string, file: string): string | undefined {
64
+ for (let directory = dirname(file); !relative(root, directory).startsWith(".."); directory = dirname(directory)) {
65
+ const candidate = join(directory, CONFIG);
66
+ if (ts.sys.fileExists(candidate)) {
67
+ return candidate;
68
+ }
69
+ if (directory === root) {
70
+ return undefined;
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ function memo<Value>(compute: (key: string) => Value): (key: string) => Value {
77
+ const known = new Map<string, Value>();
78
+ return (key) => {
79
+ const value = known.get(key) ?? compute(key);
80
+ known.set(key, value);
81
+ return value;
82
+ };
83
+ }
84
+
85
+ // A file resolves with the options of the project that includes it: the nearest tsconfig.json, or a project it references.
86
+ export function projectsFor(root: string): (file: string) => Project {
87
+ const parsedAt = memo(parse);
88
+ function withReferences(path: string, seen: Set<string>): readonly ParsedProject[] {
89
+ if (seen.has(path)) {
90
+ return [];
91
+ }
92
+ seen.add(path);
93
+ const project = parsedAt(path);
94
+ return [project, ...project.references.flatMap((reference) => withReferences(reference, seen))];
95
+ }
96
+ const familyAt = memo((config) => withReferences(config, new Set()));
97
+ const projects = new Map<string, Project>();
98
+ return (file) => {
99
+ const config = nearestConfig(root, file);
100
+ const owner = config === undefined ? undefined : (familyAt(config).find((candidate) => candidate.files.has(file)) ?? parsedAt(config));
101
+ const key = owner?.key ?? "";
102
+ const project = projects.get(key) ?? projectOf(root, owner);
103
+ projects.set(key, project);
104
+ return project;
105
+ };
106
+ }
@@ -0,0 +1,115 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { isBuiltin } from "node:module";
3
+ import { dirname, join, relative } from "node:path";
4
+ import ts from "typescript";
5
+ import { posix } from "../inventory/ignore-scope.ts";
6
+ import type { Ambient } from "./ambient.ts";
7
+ import type { ImportRequest } from "./extract.ts";
8
+ import type { Project } from "./projects.ts";
9
+
10
+ export type Endpoint =
11
+ | { readonly kind: "file"; readonly path: string }
12
+ | { readonly kind: "external"; readonly package: string; readonly specifier: string };
13
+
14
+ const NODE_PREFIX = "node:";
15
+
16
+ const NODE_MODULES = "/node_modules/";
17
+
18
+ const ASSET_DECLARATION = /\.d(?<extension>\.[^./]+)\.ts$/u;
19
+
20
+ const DECLARATION = /\.d\.[cm]?ts$/u;
21
+
22
+ const QUERY = /\?.*$/u;
23
+
24
+ export function builtinName(specifier: string): string | undefined {
25
+ if (!isBuiltin(specifier)) {
26
+ return undefined;
27
+ }
28
+ return specifier.startsWith(NODE_PREFIX) ? specifier : `${NODE_PREFIX}${specifier}`;
29
+ }
30
+
31
+ const TYPES_SCOPE = "@types";
32
+
33
+ const TYPES_SCOPE_SEPARATOR = "__";
34
+
35
+ function typedName(name: string): string {
36
+ return name.includes(TYPES_SCOPE_SEPARATOR) ? `@${name.replace(TYPES_SCOPE_SEPARATOR, "/")}` : name;
37
+ }
38
+
39
+ function nameAt(path: string): string {
40
+ const [first = "", second] = path.split("/");
41
+ if (second === undefined || !first.startsWith("@")) {
42
+ return first;
43
+ }
44
+ return first === TYPES_SCOPE ? typedName(second) : `${first}/${second}`;
45
+ }
46
+
47
+ // The example checker and the graph name a package through this one function, so a fence example and a real import cannot disagree.
48
+ export function packageNameOf(specifier: string): string {
49
+ return builtinName(specifier) ?? nameAt(specifier);
50
+ }
51
+
52
+ function assetOf(declaration: string): string {
53
+ return declaration.replace(ASSET_DECLARATION, "$<extension>");
54
+ }
55
+
56
+ // TypeScript resolves an import of a stylesheet or an image once the asset has a declaration beside it, so this host lets every asset have one.
57
+ const assetHost: ts.ModuleResolutionHost = {
58
+ ...ts.sys,
59
+ fileExists: (path) => ts.sys.fileExists(path) || (ASSET_DECLARATION.test(path) && ts.sys.fileExists(assetOf(path))),
60
+ };
61
+
62
+ export function isDeclarationFile(path: string): boolean {
63
+ return DECLARATION.test(path) || ASSET_DECLARATION.test(path);
64
+ }
65
+
66
+ function fitting(resolved: string, type: boolean): string | undefined {
67
+ if (ASSET_DECLARATION.test(resolved) && ts.sys.fileExists(assetOf(resolved))) {
68
+ return assetOf(resolved);
69
+ }
70
+ return type || !isDeclarationFile(resolved) ? resolved : undefined;
71
+ }
72
+
73
+ function located(request: ImportRequest, from: string, project: Project): string | undefined {
74
+ const specifier = request.specifier.replace(QUERY, "");
75
+ if (request.kind === "type-reference") {
76
+ return ts.resolveTypeReferenceDirective(specifier, from, project.typesOptions, ts.sys).resolvedTypeReferenceDirective?.resolvedFileName;
77
+ }
78
+ if (request.kind === "path-reference") {
79
+ const path = join(dirname(from), specifier);
80
+ return ts.sys.fileExists(path) ? path : undefined;
81
+ }
82
+ return (
83
+ ts.resolveModuleName(specifier, from, project.options, ts.sys, project.cache).resolvedModule?.resolvedFileName
84
+ ?? ts.resolveModuleName(specifier, from, project.typesOptions, assetHost, project.typesCache).resolvedModule?.resolvedFileName
85
+ );
86
+ }
87
+
88
+ function resolvedPath(request: ImportRequest, from: string, project: Project): string | undefined {
89
+ const path = located(request, from, project);
90
+ return path === undefined ? undefined : fitting(path, request.type);
91
+ }
92
+
93
+ function endpointAt(root: string, specifier: string, path: string): Endpoint {
94
+ const real = posix(realpathSync(path));
95
+ const installed = real.lastIndexOf(NODE_MODULES);
96
+ if (installed !== -1) {
97
+ return { kind: "external", package: nameAt(real.slice(installed + NODE_MODULES.length)), specifier };
98
+ }
99
+ const inRoot = posix(relative(root, real));
100
+ return inRoot.startsWith("../") ? { kind: "external", package: packageNameOf(specifier), specifier } : { kind: "file", path: inRoot };
101
+ }
102
+
103
+ export function resolveImport(root: string, ambient: Ambient, request: ImportRequest, from: string, project: Project): Endpoint | undefined {
104
+ const builtin = builtinName(request.specifier);
105
+ if (builtin !== undefined) {
106
+ return { kind: "external", package: builtin, specifier: builtin };
107
+ }
108
+ const path = resolvedPath(request, from, project);
109
+ if (path !== undefined) {
110
+ return endpointAt(root, request.specifier, path);
111
+ }
112
+ return ambient(request.specifier, project.files)
113
+ ? { kind: "external", package: packageNameOf(request.specifier), specifier: request.specifier }
114
+ : undefined;
115
+ }
@@ -0,0 +1,76 @@
1
+ import { posix } from "node:path";
2
+ import type { RuleInputs } from "../rule.ts";
3
+ import { globMatcher } from "./glob.ts";
4
+ import { pnpmWorkspacePatterns } from "./pnpm-workspace.ts";
5
+
6
+ export interface WorkspacePackage {
7
+ readonly directory: string;
8
+ readonly name: string;
9
+ }
10
+
11
+ type Reader = Pick<RuleInputs, "files" | "readText">;
12
+
13
+ const MANIFEST = "package.json";
14
+
15
+ const PNPM_WORKSPACE = "pnpm-workspace.yaml";
16
+
17
+ const NEGATION = "!";
18
+
19
+ const LEADING_DOT = /^\.\//u;
20
+
21
+ const TRAILING_SLASHES = /\/+$/u;
22
+
23
+ function parsed(text: string | undefined): unknown {
24
+ if (text === undefined) {
25
+ return undefined;
26
+ }
27
+ try {
28
+ return JSON.parse(text);
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ }
33
+
34
+ function field(value: unknown, key: string): unknown {
35
+ return typeof value === "object" && value !== null && key in value ? Reflect.get(value, key) : undefined;
36
+ }
37
+
38
+ function strings(value: unknown): readonly string[] {
39
+ return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [];
40
+ }
41
+
42
+ function npmPatterns(text: string | undefined): readonly string[] {
43
+ const workspaces = field(parsed(text), "workspaces");
44
+ return Array.isArray(workspaces) ? strings(workspaces) : strings(field(workspaces, "packages"));
45
+ }
46
+
47
+ function normalized(pattern: string): string {
48
+ return pattern.replace(LEADING_DOT, "").replace(TRAILING_SLASHES, "");
49
+ }
50
+
51
+ async function memberOf(inputs: Reader): Promise<(directory: string) => boolean> {
52
+ const pnpm = await inputs.readText(PNPM_WORKSPACE);
53
+ const patterns = [...(pnpm === undefined ? [] : pnpmWorkspacePatterns(pnpm)), ...npmPatterns(await inputs.readText(MANIFEST))];
54
+ const included = patterns.filter((pattern) => !pattern.startsWith(NEGATION)).map((pattern) => globMatcher(normalized(pattern)));
55
+ const excluded = patterns
56
+ .filter((pattern) => pattern.startsWith(NEGATION))
57
+ .map((pattern) => globMatcher(normalized(pattern.slice(NEGATION.length))));
58
+ return (directory) => included.some((matches) => matches(directory)) && !excluded.some((matches) => matches(directory));
59
+ }
60
+
61
+ export async function workspacePackages(inputs: Reader): Promise<readonly WorkspacePackage[]> {
62
+ const member = await memberOf(inputs);
63
+ const directories = inputs.files
64
+ .filter((file) => posix.basename(file) === MANIFEST && file !== MANIFEST)
65
+ .map((manifest) => posix.dirname(manifest))
66
+ .filter(member);
67
+ const named = await Promise.all(
68
+ directories.map(async (directory) => ({ directory, name: field(parsed(await inputs.readText(posix.join(directory, MANIFEST))), "name") })),
69
+ );
70
+ return named.flatMap(({ directory, name }) => (typeof name === "string" ? [{ directory, name }] : []));
71
+ }
72
+
73
+ export function packageOf(packages: readonly WorkspacePackage[], path: string): WorkspacePackage | undefined {
74
+ const owners = packages.filter((candidate) => path.startsWith(`${candidate.directory}/`));
75
+ return owners.toSorted((left, right) => right.directory.length - left.directory.length)[0];
76
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export type { BuiltInRules, Level, QualityConfig, RuleSetting, RuleSettings } from "./config.ts";
2
2
  export { defineConfig } from "./config.ts";
3
+ export { fence } from "./imports/fences/dsl.ts";
4
+ export type { Chain, ExampleStep, Examples, External, Fence, Target } from "./imports/fences/model.ts";
5
+ export { anyOf, anything, external, files, folders, modules, packages, scopes, workspace } from "./imports/fences/selectors.ts";
3
6
  export type { Configured, Finding, Findings, Rule, RuleContext, RuleInputs, SourceFile } from "./rule.ts";
4
7
  export { defineRule } from "./rule.ts";
5
8
  export type { StandardIssue, StandardResult, StandardSchemaV1 } from "./standard-schema.ts";
@@ -7,19 +7,19 @@ import { posix } from "./ignore-scope.ts";
7
7
  import { scopeAbove, walk } from "./walk.ts";
8
8
 
9
9
  export interface InventoryScope {
10
- readonly sources: ReadonlyArray<string>;
11
- readonly exclude: ReadonlyArray<string>;
12
- readonly extensions: ReadonlyArray<string>;
10
+ readonly exclude: readonly string[];
11
+ readonly extensions: readonly string[];
12
+ readonly sources: readonly string[];
13
13
  }
14
14
 
15
15
  export interface Inventory {
16
- readonly files: ReadonlyArray<string>;
17
- readonly sources: ReadonlyArray<SourceFile>;
16
+ readonly files: readonly string[];
17
+ readonly sources: readonly SourceFile[];
18
18
  }
19
19
 
20
20
  const READ_CONCURRENCY = 16;
21
21
 
22
- export const linesOf = (text: string): ReadonlyArray<string> => {
22
+ export const linesOf = (text: string): readonly string[] => {
23
23
  const lines = text.split("\n");
24
24
  return lines.at(-1) === "" ? lines.slice(0, -1) : lines;
25
25
  };
@@ -0,0 +1,46 @@
1
+ import { basename, join, relative } from "node:path";
2
+ import { Effect, FileSystem } from "effect";
3
+ import { sortPackageJson } from "sort-package-json";
4
+ import { FilesystemFailure } from "../inventory/filesystem.ts";
5
+ import { posix } from "../inventory/ignore-scope.ts";
6
+ import { walk } from "../inventory/walk.ts";
7
+
8
+ export type Sorting = { readonly _tag: "Invalid" } | { readonly _tag: "Sorted" } | { readonly _tag: "Unsorted"; readonly sorted: string };
9
+
10
+ export function manifestsIn(root: string): Effect.Effect<readonly string[], FilesystemFailure, FileSystem.FileSystem> {
11
+ return Effect.map(walk(root), (paths) =>
12
+ paths
13
+ .filter((path) => basename(path) === "package.json")
14
+ .map((path) => posix(relative(root, path)))
15
+ .sort(),
16
+ );
17
+ }
18
+
19
+ export function sortingOf(text: string): Sorting {
20
+ try {
21
+ JSON.parse(text);
22
+ } catch {
23
+ return { _tag: "Invalid" };
24
+ }
25
+ const sorted = sortPackageJson(text);
26
+ return sorted === text ? { _tag: "Sorted" } : { _tag: "Unsorted", sorted };
27
+ }
28
+
29
+ export function sortManifests(root: string): Effect.Effect<number, FilesystemFailure, FileSystem.FileSystem> {
30
+ return Effect.gen(function* () {
31
+ const fs = yield* FileSystem.FileSystem;
32
+ const manifests = yield* manifestsIn(root);
33
+ const written = yield* Effect.forEach(manifests, (path) =>
34
+ Effect.gen(function* () {
35
+ const absolute = join(root, path);
36
+ const sorting = sortingOf(yield* fs.readFileString(absolute));
37
+ if (sorting._tag !== "Unsorted") {
38
+ return false;
39
+ }
40
+ yield* fs.writeFileString(absolute, sorting.sorted);
41
+ return true;
42
+ }).pipe(Effect.mapError((error) => new FilesystemFailure({ message: `cannot sort ${path}: ${error.message}`, path }))),
43
+ );
44
+ return written.filter(Boolean).length;
45
+ });
46
+ }
@@ -3,9 +3,9 @@ import type { Base } from "../git/base.ts";
3
3
  import { plural } from "./plural.ts";
4
4
 
5
5
  export interface GuardContext {
6
+ readonly base: Base;
6
7
  readonly baseline: string;
7
8
  readonly config: string;
8
- readonly base: Base;
9
9
  }
10
10
 
11
11
  const problemLine = (problem: GuardProblem, context: GuardContext): string => {
@@ -38,7 +38,7 @@ export const renderGuard = (result: GuardResult, context: GuardContext): string
38
38
  return [
39
39
  [
40
40
  `error ${context.baseline} grew against ${against(context.base)}`,
41
- ` The baseline only shrinks. Fix new violations instead of baselining them; a rule enters the baseline for the first time only while \`adopt\` names it.`,
41
+ " The baseline only shrinks. Fix new violations instead of baselining them; a rule enters the baseline for the first time only while `adopt` names it.",
42
42
  ...result.problems.map((problem) => ` ${problemLine(problem, context)}`),
43
43
  ].join("\n"),
44
44
  `quality: baseline check failed with ${plural(result.problems.length, "problem")}.`,
@@ -7,10 +7,10 @@ import { staleSections } from "./stale.ts";
7
7
  export type WarningDetail = "summary" | "all";
8
8
 
9
9
  export interface ReportContext {
10
- readonly descriptions: ReadonlyMap<string, string>;
11
- readonly registry: string;
12
10
  readonly baseline: string;
13
11
  readonly checked: number;
12
+ readonly descriptions: ReadonlyMap<string, string>;
13
+ readonly registry: string;
14
14
  readonly warnings: WarningDetail;
15
15
  }
16
16
 
@@ -21,12 +21,12 @@ const locate = (violation: Violation): string => (violation.line === undefined ?
21
21
  const regressionNote = (regression: Regression): string =>
22
22
  ` ${regression.entry.file} is over its baseline: ${regression.count} against ${regression.entry.count} baselined.`;
23
23
 
24
- const heading = (label: string, rule: string, violations: ReadonlyArray<Violation>, context: ReportContext): ReadonlyArray<string> => {
24
+ const heading = (label: string, rule: string, violations: readonly Violation[], context: ReportContext): readonly string[] => {
25
25
  const description = context.descriptions.get(rule);
26
26
  return [`${label} ${rule} (${violations.length})`, ...(description === undefined ? [] : [` ${description}`])];
27
27
  };
28
28
 
29
- const detailed = (violations: ReadonlyArray<Violation>, regressions: ReadonlyMap<string, Regression>): ReadonlyArray<string> =>
29
+ const detailed = (violations: readonly Violation[], regressions: ReadonlyMap<string, Regression>): readonly string[] =>
30
30
  [...groupBy(violations, (violation) => violation.file).values()].flatMap((inFile) => {
31
31
  const first = inFile[0];
32
32
  const regression = first === undefined ? undefined : regressions.get(keyOf(first.rule, first.file));
@@ -36,7 +36,7 @@ const detailed = (violations: ReadonlyArray<Violation>, regressions: ReadonlyMap
36
36
  ];
37
37
  });
38
38
 
39
- const summarized = (violations: ReadonlyArray<Violation>): ReadonlyArray<string> => {
39
+ const summarized = (violations: readonly Violation[]): readonly string[] => {
40
40
  const files = [...groupBy(violations, (violation) => violation.file).entries()].sort(([, left], [, right]) => right.length - left.length);
41
41
  const listed = files.slice(0, LISTED_FILES).map(([file, inFile]) => `${file} (${inFile.length})`);
42
42
  const rest = files.length - listed.length;
@@ -45,10 +45,10 @@ const summarized = (violations: ReadonlyArray<Violation>): ReadonlyArray<string>
45
45
 
46
46
  const ruleSections = (
47
47
  label: string,
48
- violations: ReadonlyArray<Violation>,
48
+ violations: readonly Violation[],
49
49
  context: ReportContext,
50
- body: (inRule: ReadonlyArray<Violation>) => ReadonlyArray<string>,
51
- ): ReadonlyArray<string> =>
50
+ body: (inRule: readonly Violation[]) => readonly string[],
51
+ ): readonly string[] =>
52
52
  [...groupBy(violations, (violation) => violation.rule).entries()].map(([rule, inRule]) =>
53
53
  [...heading(label, rule, inRule, context), ...body(inRule)].join("\n"),
54
54
  );
@@ -75,7 +75,7 @@ const summary = (outcome: Outcome, context: ReportContext): string => {
75
75
 
76
76
  export const render = (outcome: Outcome, context: ReportContext): string => {
77
77
  const regressions = new Map(outcome.regressions.map((regression) => [keyOf(regression.entry.rule, regression.entry.file), regression]));
78
- const warningBody = context.warnings === "all" ? (inRule: ReadonlyArray<Violation>) => detailed(inRule, regressions) : summarized;
78
+ const warningBody = context.warnings === "all" ? (inRule: readonly Violation[]) => detailed(inRule, regressions) : summarized;
79
79
  return [
80
80
  ...ruleSections("error", outcome.errors, context, (inRule) => detailed(inRule, regressions)),
81
81
  ...ruleSections("warn", outcome.warnings, context, warningBody),
@@ -4,11 +4,11 @@ import type { StaleRegistryEntry } from "../exceptions/registry.ts";
4
4
  import { plural } from "./plural.ts";
5
5
 
6
6
  interface StaleFiles {
7
- readonly registry: string;
8
7
  readonly baseline: string;
8
+ readonly registry: string;
9
9
  }
10
10
 
11
- const section = (label: string, file: string, guidance: string, lines: ReadonlyArray<string>): ReadonlyArray<string> =>
11
+ const section = (label: string, file: string, guidance: string, lines: readonly string[]): readonly string[] =>
12
12
  lines.length === 0 ? [] : [[`${label} ${file}: ${plural(lines.length, "stale entry", "stale entries")}`, ` ${guidance}`, ...lines].join("\n")];
13
13
 
14
14
  const baselineLine = ({ entry, problem }: StaleBaselineEntry): string => ` ${entry.rule} ${entry.file} ${problem}.`;
@@ -16,7 +16,7 @@ const baselineLine = ({ entry, problem }: StaleBaselineEntry): string => ` ${en
16
16
  const registryLine = ({ entry, problem }: StaleRegistryEntry): string =>
17
17
  ` ${entry.rule} ${entry.file}${entry.subject === undefined ? "" : ` (${entry.subject})`} ${problem}.`;
18
18
 
19
- export const staleSections = (outcome: Outcome, files: StaleFiles): ReadonlyArray<string> => [
19
+ export const staleSections = (outcome: Outcome, files: StaleFiles): readonly string[] => [
20
20
  ...section(
21
21
  "error",
22
22
  files.baseline,
package/src/rule.ts CHANGED
@@ -1,27 +1,27 @@
1
1
  import { describeIssue, type StandardSchemaV1 } from "./standard-schema.ts";
2
2
 
3
3
  export interface Finding {
4
+ readonly count?: number | undefined;
4
5
  readonly file: string;
5
6
  readonly line?: number | undefined;
6
7
  readonly message: string;
7
8
  readonly subject?: string | undefined;
8
- readonly count?: number | undefined;
9
9
  readonly threshold?: number | undefined;
10
10
  }
11
11
 
12
- export type Findings = ReadonlyArray<Finding>;
12
+ export type Findings = readonly Finding[];
13
13
 
14
14
  export interface SourceFile {
15
+ readonly lines: readonly string[];
15
16
  readonly path: string;
16
17
  readonly text: string;
17
- readonly lines: ReadonlyArray<string>;
18
18
  }
19
19
 
20
20
  export interface RuleInputs {
21
- readonly root: string;
22
- readonly files: ReadonlyArray<string>;
23
- readonly sources: ReadonlyArray<SourceFile>;
21
+ readonly files: readonly string[];
24
22
  readonly readText: (path: string) => Promise<string | undefined>;
23
+ readonly root: string;
24
+ readonly sources: readonly SourceFile[];
25
25
  }
26
26
 
27
27
  export interface RuleContext<Options> extends RuleInputs {
@@ -29,22 +29,23 @@ export interface RuleContext<Options> extends RuleInputs {
29
29
  }
30
30
 
31
31
  export type Configured =
32
- | { readonly _tag: "Invalid"; readonly issues: ReadonlyArray<string> }
32
+ | { readonly _tag: "Invalid"; readonly issues: readonly string[] }
33
33
  | { readonly _tag: "Ready"; readonly check: (inputs: RuleInputs) => Promise<Findings> };
34
34
 
35
35
  export interface Rule<Id extends string = string, Input = unknown> {
36
- readonly id: Id;
37
- readonly description: string;
38
- readonly registrable?: boolean;
39
36
  // Method syntax keeps rules with different options in one list; configure validates its input at runtime.
40
37
  configure(options: Input | undefined): Promise<Configured>;
38
+ readonly description: string;
39
+ readonly family?: boolean;
40
+ readonly id: Id;
41
+ readonly registrable?: boolean;
41
42
  }
42
43
 
43
44
  interface RuleDefinition<Id extends string, Options> {
44
- readonly id: Id;
45
+ readonly check: (context: RuleContext<Options>) => Findings | PromiseLike<Findings>;
45
46
  readonly description: string;
47
+ readonly id: Id;
46
48
  readonly registrable?: false;
47
- readonly check: (context: RuleContext<Options>) => Findings | PromiseLike<Findings>;
48
49
  }
49
50
 
50
51
  interface RuleDefinitionWithOptions<Id extends string, Input, Options> extends RuleDefinition<Id, Options> {
@@ -0,0 +1,47 @@
1
+ import { biomeReport, type Diagnostic } from "../biome/report.ts";
2
+ import { defineRule, type Finding, type Rule, type RuleInputs } from "../rule.ts";
3
+ import { itemsOf, member, parseJsonc, textOf } from "./suppressions/biome/json.ts";
4
+
5
+ export const PRESET = "@shivaedev/quality/biome";
6
+
7
+ const ROOT_CONFIGS: readonly string[] = ["biome.json", "biome.jsonc"];
8
+
9
+ const FAILING: ReadonlySet<string> = new Set(["error", "fatal"]);
10
+
11
+ function messageOf(diagnostic: Diagnostic): string {
12
+ return diagnostic.category === "format" && diagnostic.message.startsWith("Formatter would have printed")
13
+ ? "Is not formatted. Run `quality fix`."
14
+ : diagnostic.message;
15
+ }
16
+
17
+ function findingOf(diagnostic: Diagnostic): Finding {
18
+ const line = diagnostic.location.start?.line ?? 0;
19
+ return {
20
+ file: diagnostic.location.path ?? ".",
21
+ line: line > 0 ? line : undefined,
22
+ message: messageOf(diagnostic),
23
+ subject: diagnostic.category ?? "internal",
24
+ };
25
+ }
26
+
27
+ async function presetFinding(readText: RuleInputs["readText"]): Promise<readonly Finding[]> {
28
+ const texts = await Promise.all(ROOT_CONFIGS.map(async (path) => ({ path, text: await readText(path) })));
29
+ const found = texts.find((candidate) => candidate.text !== undefined);
30
+ const parsed = found?.text === undefined ? undefined : parseJsonc(found.path, found.text);
31
+ const extended = parsed?._tag === "Parsed" ? itemsOf(member(parsed.json, "extends")).map(textOf) : [];
32
+ return extended.includes(PRESET)
33
+ ? []
34
+ : [{ file: found?.path ?? "biome.json", message: `Extend "${PRESET}" from the root Biome config.`, subject: "preset" }];
35
+ }
36
+
37
+ const bridge = defineRule({
38
+ check: async ({ readText, root }) => {
39
+ const [preset, report] = await Promise.all([presetFinding(readText), biomeReport(root, ["check"])]);
40
+ return [...preset, ...report.diagnostics.filter((diagnostic) => FAILING.has(diagnostic.severity)).map(findingOf)];
41
+ },
42
+ description: "Biome's lint, format and assist findings under the shared preset. `quality fix` applies the safe fixes and the formatting.",
43
+ id: "biome",
44
+ registrable: false,
45
+ });
46
+
47
+ export const biome: Rule<"biome", undefined> = { ...bridge, family: true };
@@ -1,9 +1,15 @@
1
+ import { biome } from "./biome.ts";
1
2
  import { maxPerFile } from "./comments/max-per-file.ts";
2
3
  import { noBanner } from "./comments/no-banner.ts";
4
+ import { noEnvironmentPragma } from "./comments/no-environment-pragma.ts";
3
5
  import { noJsdoc } from "./comments/no-jsdoc.ts";
4
6
  import { noLineReference } from "./comments/no-line-reference.ts";
5
7
  import { noPrReference } from "./comments/no-pr-reference.ts";
6
8
  import { noTodo } from "./comments/no-todo.ts";
9
+ import { importCycles } from "./imports/cycles.ts";
10
+ import { importFences } from "./imports/fences.ts";
11
+ import { importsResolvable } from "./imports/resolvable.ts";
12
+ import { manifestsSorted } from "./manifests-sorted.ts";
7
13
  import { maxLines } from "./max-lines.ts";
8
14
  import { biomeOverrides } from "./suppressions/biome-overrides.ts";
9
15
  import { noDoubleCast } from "./suppressions/no-double-cast.ts";
@@ -16,8 +22,14 @@ export const builtInRules = [
16
22
  noPrReference,
17
23
  noBanner,
18
24
  noTodo,
25
+ noEnvironmentPragma,
19
26
  maxPerFile,
20
27
  noInline,
21
28
  noDoubleCast,
22
29
  biomeOverrides,
30
+ biome,
31
+ importCycles,
32
+ importFences,
33
+ importsResolvable,
34
+ manifestsSorted,
23
35
  ] as const;