@shivaedev/quality 0.3.1 → 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 (391) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +187 -30
  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 +21 -10
  259. package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
  260. package/dist/rules/suppressions/biome/configs.js +58 -22
  261. package/dist/rules/suppressions/biome/configs.js.map +1 -1
  262. package/dist/rules/suppressions/biome/declarations.d.ts +18 -0
  263. package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -0
  264. package/dist/rules/suppressions/biome/declarations.js +20 -0
  265. package/dist/rules/suppressions/biome/declarations.js.map +1 -0
  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.d.ts +3 -0
  271. package/dist/rules/suppressions/biome/resolve.d.ts.map +1 -0
  272. package/dist/rules/suppressions/biome/resolve.js +70 -0
  273. package/dist/rules/suppressions/biome/resolve.js.map +1 -0
  274. package/dist/rules/suppressions/biome/weakenings.d.ts +5 -5
  275. package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
  276. package/dist/rules/suppressions/biome/weakenings.js +41 -33
  277. package/dist/rules/suppressions/biome/weakenings.js.map +1 -1
  278. package/dist/rules/suppressions/biome-overrides.d.ts +1 -1
  279. package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
  280. package/dist/rules/suppressions/biome-overrides.js +35 -27
  281. package/dist/rules/suppressions/biome-overrides.js.map +1 -1
  282. package/dist/rules/suppressions/directives.d.ts +1 -1
  283. package/dist/rules/suppressions/directives.d.ts.map +1 -1
  284. package/dist/rules/suppressions/directives.js +11 -11
  285. package/dist/rules/suppressions/directives.js.map +1 -1
  286. package/dist/rules/suppressions/no-double-cast.js +2 -2
  287. package/dist/rules/suppressions/no-double-cast.js.map +1 -1
  288. package/dist/rules/suppressions/no-inline.d.ts +1 -7
  289. package/dist/rules/suppressions/no-inline.d.ts.map +1 -1
  290. package/dist/rules/suppressions/no-inline.js +7 -30
  291. package/dist/rules/suppressions/no-inline.js.map +1 -1
  292. package/dist/rules/suppressions/stylesheet-comments.d.ts +1 -1
  293. package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
  294. package/dist/rules/suppressions/stylesheet-comments.js +3 -3
  295. package/dist/rules/suppressions/stylesheet-comments.js.map +1 -1
  296. package/dist/rules/syntax.js +4 -4
  297. package/dist/rules/syntax.js.map +1 -1
  298. package/dist/standard-schema.d.ts +1 -1
  299. package/dist/standard-schema.d.ts.map +1 -1
  300. package/dist/vitest.d.ts +3 -0
  301. package/dist/vitest.d.ts.map +1 -0
  302. package/dist/vitest.js +20 -0
  303. package/dist/vitest.js.map +1 -0
  304. package/package.json +48 -31
  305. package/src/baseline/compare.ts +10 -15
  306. package/src/baseline/format.ts +8 -6
  307. package/src/baseline/guard.ts +11 -10
  308. package/src/baseline/legacy.ts +3 -3
  309. package/src/baseline/rewrite.ts +1 -1
  310. package/src/baseline/update.ts +21 -21
  311. package/src/biome/report.ts +35 -0
  312. package/src/biome/run.ts +23 -0
  313. package/src/cli/args.ts +14 -8
  314. package/src/cli/baseline.ts +5 -6
  315. package/src/cli/check.ts +1 -1
  316. package/src/cli/fix.ts +19 -0
  317. package/src/cli/lint.ts +1 -1
  318. package/src/cli/run-main.ts +8 -3
  319. package/src/cli.ts +3 -0
  320. package/src/config/decode.ts +12 -12
  321. package/src/config/load.ts +4 -4
  322. package/src/config/resolve.ts +15 -11
  323. package/src/config.ts +9 -9
  324. package/src/decoded.ts +1 -1
  325. package/src/engine/baseline-file.ts +1 -1
  326. package/src/engine/evaluate.ts +14 -16
  327. package/src/engine/run-rules.ts +8 -7
  328. package/src/engine/session.ts +3 -3
  329. package/src/engine/violation.ts +18 -4
  330. package/src/exceptions/registry.ts +10 -16
  331. package/src/git/base.ts +2 -2
  332. package/src/git/command.ts +3 -3
  333. package/src/git/history.ts +2 -2
  334. package/src/imports/ambient.ts +54 -0
  335. package/src/imports/components.ts +80 -0
  336. package/src/imports/extract.ts +129 -0
  337. package/src/imports/fences/dsl.ts +33 -0
  338. package/src/imports/fences/evaluate.ts +75 -0
  339. package/src/imports/fences/match.ts +148 -0
  340. package/src/imports/fences/model.ts +42 -0
  341. package/src/imports/fences/policy.ts +70 -0
  342. package/src/imports/fences/reach.ts +62 -0
  343. package/src/imports/fences/selectors.ts +37 -0
  344. package/src/imports/fences/vocabulary.ts +51 -0
  345. package/src/imports/generated.ts +95 -0
  346. package/src/imports/glob.ts +142 -0
  347. package/src/imports/graph.ts +81 -0
  348. package/src/imports/paths.ts +28 -0
  349. package/src/imports/pnpm-workspace.ts +134 -0
  350. package/src/imports/projects.ts +106 -0
  351. package/src/imports/resolve.ts +115 -0
  352. package/src/imports/workspace.ts +76 -0
  353. package/src/index.ts +3 -0
  354. package/src/inventory/collect.ts +6 -6
  355. package/src/manifests/sorted.ts +46 -0
  356. package/src/report/guard.ts +2 -2
  357. package/src/report/render.ts +9 -9
  358. package/src/report/stale.ts +3 -3
  359. package/src/rule.ts +13 -12
  360. package/src/rules/biome.ts +47 -0
  361. package/src/rules/built-in.ts +12 -0
  362. package/src/rules/comments/kinds.ts +11 -21
  363. package/src/rules/comments/max-per-file.ts +13 -13
  364. package/src/rules/comments/no-banner.ts +2 -2
  365. package/src/rules/comments/no-environment-pragma.ts +9 -0
  366. package/src/rules/comments/no-jsdoc.ts +3 -3
  367. package/src/rules/comments/no-line-reference.ts +2 -2
  368. package/src/rules/comments/no-pr-reference.ts +6 -6
  369. package/src/rules/comments/no-todo.ts +2 -2
  370. package/src/rules/comments/pattern-rule.ts +6 -6
  371. package/src/rules/comments/scan.ts +13 -13
  372. package/src/rules/imports/cycles.ts +44 -0
  373. package/src/rules/imports/fences.ts +27 -0
  374. package/src/rules/imports/resolvable.ts +23 -0
  375. package/src/rules/manifests-sorted.ts +23 -0
  376. package/src/rules/max-lines.ts +6 -6
  377. package/src/rules/suppressions/biome/configs.ts +99 -30
  378. package/src/rules/suppressions/biome/declarations.ts +37 -0
  379. package/src/rules/suppressions/biome/json.ts +3 -3
  380. package/src/rules/suppressions/biome/resolve.ts +86 -0
  381. package/src/rules/suppressions/biome/weakenings.ts +64 -48
  382. package/src/rules/suppressions/biome-overrides.ts +45 -36
  383. package/src/rules/suppressions/directives.ts +12 -12
  384. package/src/rules/suppressions/no-double-cast.ts +4 -4
  385. package/src/rules/suppressions/no-inline.ts +13 -37
  386. package/src/rules/suppressions/stylesheet-comments.ts +4 -4
  387. package/src/rules/syntax.ts +4 -4
  388. package/src/standard-schema.ts +1 -1
  389. package/src/vitest.ts +22 -0
  390. package/tsconfig/base.json +35 -0
  391. package/tsconfig/package.json +10 -0
@@ -0,0 +1,95 @@
1
+ import { execFile } from "node:child_process";
2
+ import { posix } from "node:path";
3
+ import { promisify } from "node:util";
4
+ import { emptyScope, type IgnoreScope, verdictFor, withIgnoreFile } from "../inventory/ignore-scope.ts";
5
+ import type { RuleInputs } from "../rule.ts";
6
+ import type { ImportGraph, UnresolvedImport } from "./graph.ts";
7
+
8
+ type Reader = Pick<RuleInputs, "readText" | "root">;
9
+
10
+ const IGNORE_FILE = ".gitignore";
11
+
12
+ const LEADING_DOT = /^\.\//u;
13
+
14
+ const TRAILING_SLASHES = /\/+$/u;
15
+
16
+ const NODE_MODULES = "node_modules";
17
+
18
+ const NOT_A_REPOSITORY = "not a git repository";
19
+
20
+ const PLACEHOLDERS = new Set([".gitkeep", ".keep"]);
21
+
22
+ const run = promisify(execFile);
23
+
24
+ async function withIgnoreFileIn(reader: Reader, scope: IgnoreScope, directory: string): Promise<IgnoreScope> {
25
+ const contents = await reader.readText(posix.join(directory, IGNORE_FILE));
26
+ return contents === undefined || contents === "" ? scope : withIgnoreFile(scope, posix.join(reader.root, directory), contents);
27
+ }
28
+
29
+ async function ignored(reader: Reader, file: string): Promise<boolean> {
30
+ const parts = file.split("/");
31
+ let scope = emptyScope;
32
+ for (const index of parts.keys()) {
33
+ scope = await withIgnoreFileIn(reader, scope, parts.slice(0, index).join("/") || ".");
34
+ if (verdictFor(scope, posix.join(reader.root, ...parts.slice(0, index + 1)), index < parts.length - 1) === "ignored") {
35
+ return true;
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+
41
+ async function tracked(root: string, folder: string): Promise<readonly string[]> {
42
+ try {
43
+ const { stdout } = await run("git", ["ls-files", "-z", "--", folder], { cwd: root });
44
+ return stdout.split("\0").filter(Boolean);
45
+ } catch (error) {
46
+ if (error instanceof Error && "stderr" in error && String(error.stderr).includes(NOT_A_REPOSITORY)) {
47
+ return [];
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ function under(folder: string, path: string | undefined): boolean {
54
+ return path?.startsWith(`${folder}/`) === true;
55
+ }
56
+
57
+ async function contentIssue(reader: Reader, folder: string, named: readonly string[]): Promise<string | undefined> {
58
+ const verdicts = await Promise.all(named.map((file) => ignored(reader, file)));
59
+ if (verdicts.includes(false)) {
60
+ return `"${folder}" is not ignored by git, so it holds source, not generated output`;
61
+ }
62
+ const kept = (await tracked(reader.root, folder)).find((file) => !PLACEHOLDERS.has(posix.basename(file)));
63
+ return kept === undefined ? undefined : `"${folder}" holds files git tracks, such as ${kept}, so it is not generated output`;
64
+ }
65
+
66
+ function placeIssue(graph: ImportGraph, folder: string): string | undefined {
67
+ if (folder === "" || folder.startsWith("../") || posix.isAbsolute(folder)) {
68
+ return `"${folder}" is no folder inside the repository`;
69
+ }
70
+ if (folder.split("/").includes(NODE_MODULES)) {
71
+ return `"${folder}" lies in node_modules, which holds installed packages, not generated output`;
72
+ }
73
+ return graph.edges.some((edge) => edge.to.kind === "file" && under(folder, edge.to.path))
74
+ ? undefined
75
+ : `"${folder}" holds no file that an import names`;
76
+ }
77
+
78
+ async function issueOf(reader: Reader, graph: ImportGraph, folder: string): Promise<string | undefined> {
79
+ const misplaced = placeIssue(graph, folder);
80
+ if (misplaced !== undefined) {
81
+ return misplaced;
82
+ }
83
+ const named = [...new Set(graph.edges.flatMap((edge) => (edge.to.kind === "file" && under(folder, edge.to.path) ? [edge.to.path] : [])))];
84
+ return await contentIssue(reader, folder, named);
85
+ }
86
+
87
+ // A generated folder may be empty until its generator runs, so a relative import of a missing file in it resolves.
88
+ export async function withoutGenerated(reader: Reader, graph: ImportGraph, declared: readonly string[]): Promise<readonly UnresolvedImport[]> {
89
+ const folders = declared.map((folder) => posix.normalize(folder).replace(LEADING_DOT, "").replace(TRAILING_SLASHES, ""));
90
+ const issues = (await Promise.all(folders.map((folder) => issueOf(reader, graph, folder)))).flatMap((issue) => issue ?? []);
91
+ if (issues.length > 0) {
92
+ throw new Error(`the generated folders are invalid:\n${issues.map((issue) => ` - ${issue}`).join("\n")}`);
93
+ }
94
+ return graph.unresolved.filter((request) => !folders.some((folder) => under(folder, request.missing)));
95
+ }
@@ -0,0 +1,142 @@
1
+ type Segment = RegExp | typeof GLOBSTAR;
2
+
3
+ const STAR = "*";
4
+
5
+ const GLOBSTAR = "**";
6
+
7
+ const DOT = ".";
8
+
9
+ const NO_DOT = "(?!\\.)";
10
+
11
+ const EXTGLOB = /[?*+@!]\(/u;
12
+
13
+ const BRACE_RANGE = /^[^,]*\.\.[^,]*$/u;
14
+
15
+ const REGEX_SYNTAX = /[\\^$.*+?()[\]{}|/]/u;
16
+
17
+ const CLASS_SYNTAX = /[\\\]^]/gu;
18
+
19
+ const BRACE_DEPTH = new Map([
20
+ ["{", 1],
21
+ ["}", -1],
22
+ ]);
23
+
24
+ function depthChange(char: string | undefined): number {
25
+ return BRACE_DEPTH.get(char ?? "") ?? 0;
26
+ }
27
+
28
+ function charSource(char: string): string {
29
+ if (char === STAR) {
30
+ return "[^/]*";
31
+ }
32
+ if (char === "?") {
33
+ return "[^/]";
34
+ }
35
+ return REGEX_SYNTAX.test(char) ? `\\${char}` : char;
36
+ }
37
+
38
+ function unreadable(pattern: string, problem: string): Error {
39
+ return new Error(`cannot read the workspace pattern "${pattern}": ${problem}`);
40
+ }
41
+
42
+ function closingBrace(text: string, open: number): number {
43
+ let depth = 0;
44
+ for (let index = open; index < text.length; index += 1) {
45
+ depth += depthChange(text[index]);
46
+ if (depth === 0) {
47
+ return index;
48
+ }
49
+ }
50
+ return -1;
51
+ }
52
+
53
+ function choices(body: string): readonly string[] {
54
+ const parts = [""];
55
+ let depth = 0;
56
+ for (const char of body) {
57
+ depth += depthChange(char);
58
+ if (char === "," && depth === 0) {
59
+ parts.push("");
60
+ } else {
61
+ parts[parts.length - 1] += char;
62
+ }
63
+ }
64
+ return parts;
65
+ }
66
+
67
+ function expanded(pattern: string, whole: string): readonly string[] {
68
+ const open = pattern.indexOf("{");
69
+ if (open === -1) {
70
+ return [pattern];
71
+ }
72
+ const close = closingBrace(pattern, open);
73
+ if (close === -1) {
74
+ throw unreadable(whole, "a { never closes");
75
+ }
76
+ const body = pattern.slice(open + 1, close);
77
+ if (BRACE_RANGE.test(body)) {
78
+ throw unreadable(whole, "brace ranges are not supported");
79
+ }
80
+ const options = choices(body);
81
+ if (options.length < 2) {
82
+ throw unreadable(whole, "a { } needs two or more choices");
83
+ }
84
+ return options.flatMap((option) => expanded(`${pattern.slice(0, open)}${option}${pattern.slice(close + 1)}`, whole));
85
+ }
86
+
87
+ function characterClass(segment: string, start: number, whole: string): { readonly end: number; readonly source: string } {
88
+ const negated = segment[start + 1] === "!" || segment[start + 1] === "^";
89
+ const first = start + (negated ? 2 : 1);
90
+ const end = segment.indexOf("]", segment[first] === "]" ? first + 1 : first);
91
+ if (end === -1) {
92
+ throw unreadable(whole, "a [ never closes");
93
+ }
94
+ const body = segment.slice(first, end);
95
+ if (body.includes("[:")) {
96
+ throw unreadable(whole, "character class names are not supported");
97
+ }
98
+ return { end, source: `[${negated ? "^" : ""}${body.replace(CLASS_SYNTAX, "\\$&")}]` };
99
+ }
100
+
101
+ function segmentOf(segment: string, whole: string): Segment {
102
+ if (segment === GLOBSTAR) {
103
+ return GLOBSTAR;
104
+ }
105
+ if (EXTGLOB.test(segment)) {
106
+ throw unreadable(whole, "extended globs are not supported");
107
+ }
108
+ let source = "";
109
+ for (let index = 0; index < segment.length; index += 1) {
110
+ const char = segment[index] ?? "";
111
+ if (char === "[") {
112
+ const found = characterClass(segment, index, whole);
113
+ source += found.source;
114
+ index = found.end;
115
+ } else {
116
+ source += charSource(char);
117
+ }
118
+ }
119
+ return new RegExp(`^${segment.startsWith(DOT) ? "" : NO_DOT}${source}$`, "u");
120
+ }
121
+
122
+ function segmentsMatch(pattern: readonly Segment[], path: readonly string[]): boolean {
123
+ const [first, ...rest] = pattern;
124
+ if (first === undefined) {
125
+ return path.length === 0;
126
+ }
127
+ if (first === GLOBSTAR) {
128
+ const hidden = path.findIndex((segment) => segment.startsWith(DOT));
129
+ const reachable = hidden === -1 ? path.length : hidden;
130
+ return [...path.keys(), path.length].some((skipped) => skipped <= reachable && segmentsMatch(rest, path.slice(skipped)));
131
+ }
132
+ const [head, ...tail] = path;
133
+ return head !== undefined && first.test(head) && segmentsMatch(rest, tail);
134
+ }
135
+
136
+ export function globMatcher(pattern: string): (path: string) => boolean {
137
+ if (pattern.includes("\\")) {
138
+ throw unreadable(pattern, "escapes are not supported");
139
+ }
140
+ const alternatives = expanded(pattern, pattern).map((alternative) => alternative.split("/").map((segment) => segmentOf(segment, pattern)));
141
+ return (path) => alternatives.some((segments) => segmentsMatch(segments, path.split("/")));
142
+ }
@@ -0,0 +1,81 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { join, posix } from "node:path";
3
+ import type { RuleInputs, SourceFile } from "../rule.ts";
4
+ import { parse } from "../rules/syntax.ts";
5
+ import { ambientModules } from "./ambient.ts";
6
+ import { type ImportKind, type ImportRequest, importsOf } from "./extract.ts";
7
+ import { projectsFor } from "./projects.ts";
8
+ import { type Endpoint, isDeclarationFile, resolveImport } from "./resolve.ts";
9
+ import { type WorkspacePackage, workspacePackages } from "./workspace.ts";
10
+
11
+ export interface ImportEdge {
12
+ readonly from: string;
13
+ readonly kind: ImportKind;
14
+ readonly line: number;
15
+ readonly specifier: string;
16
+ readonly to: Endpoint;
17
+ readonly type: boolean;
18
+ }
19
+
20
+ export type UnresolvedImport = Omit<ImportEdge, "to"> & { readonly missing: string | undefined };
21
+
22
+ export interface ImportGraph {
23
+ readonly edges: readonly ImportEdge[];
24
+ readonly modules: readonly string[];
25
+ readonly packages: readonly WorkspacePackage[];
26
+ readonly unresolved: readonly UnresolvedImport[];
27
+ }
28
+
29
+ const QUERY = /\?.*$/u;
30
+
31
+ const NO_MODULES =
32
+ "the import graph covers no modules: the sources hold no TypeScript or JavaScript module. Point `sources` at the code, or turn the imports rules off.";
33
+
34
+ // A relative import of a missing file still names a path, so fences see it before its generator writes it.
35
+ function missingTarget(from: string, request: ImportRequest): string | undefined {
36
+ const relative = request.kind === "path-reference" || request.specifier.startsWith("./") || request.specifier.startsWith("../");
37
+ const target = posix.join(posix.dirname(from), request.specifier.replace(QUERY, ""));
38
+ return relative && !target.startsWith("../") ? target : undefined;
39
+ }
40
+
41
+ function walk(inputs: RuleInputs, root: string): Pick<ImportGraph, "edges" | "modules" | "unresolved"> {
42
+ const projectOf = projectsFor(root);
43
+ const parsed = inputs.sources.flatMap((file) => {
44
+ const syntax = parse(file);
45
+ return syntax === undefined ? [] : [{ file, path: join(root, file.path), syntax }];
46
+ });
47
+ const ambient = ambientModules(parsed);
48
+ const edges: ImportEdge[] = [];
49
+ const unresolved: UnresolvedImport[] = [];
50
+ for (const { file, path, syntax } of parsed) {
51
+ const project = projectOf(path);
52
+ for (const request of importsOf(syntax, isDeclarationFile(file.path))) {
53
+ const to = resolveImport(root, ambient, request, path, project);
54
+ if (to === undefined) {
55
+ const missing = missingTarget(file.path, request);
56
+ unresolved.push({ ...request, from: file.path, missing });
57
+ edges.push(...(missing === undefined ? [] : [{ ...request, from: file.path, to: { kind: "file" as const, path: missing } }]));
58
+ } else {
59
+ edges.push({ ...request, from: file.path, to });
60
+ }
61
+ }
62
+ }
63
+ return { edges, modules: parsed.map(({ file }) => file.path), unresolved };
64
+ }
65
+
66
+ async function build(inputs: RuleInputs): Promise<ImportGraph> {
67
+ const walked = walk(inputs, realpathSync(inputs.root));
68
+ if (walked.modules.length === 0) {
69
+ throw new Error(NO_MODULES);
70
+ }
71
+ return { ...walked, packages: await workspacePackages(inputs) };
72
+ }
73
+
74
+ const graphs = new WeakMap<readonly SourceFile[], Promise<ImportGraph>>();
75
+
76
+ // The three imports rules run on one scan, so they share the graph built from its sources.
77
+ export function importGraph(inputs: RuleInputs): Promise<ImportGraph> {
78
+ const known = graphs.get(inputs.sources) ?? build(inputs);
79
+ graphs.set(inputs.sources, known);
80
+ return known;
81
+ }
@@ -0,0 +1,28 @@
1
+ function trail(previous: ReadonlyMap<string, string | undefined>, node: string): readonly string[] {
2
+ const path: string[] = [];
3
+ for (let step: string | undefined = node; step !== undefined; step = previous.get(step)) {
4
+ path.unshift(step);
5
+ }
6
+ return path;
7
+ }
8
+
9
+ export function shortestPath(
10
+ start: string,
11
+ neighbours: (node: string) => readonly string[],
12
+ goal: (node: string) => boolean,
13
+ ): readonly string[] | undefined {
14
+ const previous = new Map<string, string | undefined>([[start, undefined]]);
15
+ const queue = [start];
16
+ for (const node of queue) {
17
+ for (const next of neighbours(node)) {
18
+ if (goal(next)) {
19
+ return [...trail(previous, node), next];
20
+ }
21
+ if (!previous.has(next)) {
22
+ previous.set(next, node);
23
+ queue.push(next);
24
+ }
25
+ }
26
+ }
27
+ return undefined;
28
+ }
@@ -0,0 +1,134 @@
1
+ const KEY = /^(?:packages|"packages"|'packages')\s*:/u;
2
+
3
+ const ITEM = "-";
4
+
5
+ const SPACE = /\s/u;
6
+
7
+ const LINE_BREAK = /\r?\n/u;
8
+
9
+ function failure(problem: string, cause?: unknown): Error {
10
+ return new Error(`pnpm-workspace.yaml: ${problem}`, { cause });
11
+ }
12
+
13
+ function withoutComment(line: string): string {
14
+ let quote: string | undefined;
15
+ for (let index = 0; index < line.length; index += 1) {
16
+ const char = line[index];
17
+ if (quote !== undefined) {
18
+ quote = char === quote ? undefined : quote;
19
+ } else if (char === '"' || char === "'") {
20
+ quote = char;
21
+ } else if (char === "#" && (index === 0 || SPACE.test(line[index - 1] ?? ""))) {
22
+ return line.slice(0, index).trimEnd();
23
+ }
24
+ }
25
+ return line.trimEnd();
26
+ }
27
+
28
+ function scalar(raw: string): string {
29
+ const value = raw.trim();
30
+ if (value.startsWith('"') && value.endsWith('"') && value.length > 1) {
31
+ try {
32
+ return String(JSON.parse(value));
33
+ } catch (error) {
34
+ throw failure(`cannot read "${value}" in packages`, error);
35
+ }
36
+ }
37
+ if (value.startsWith("'") && value.endsWith("'") && value.length > 1) {
38
+ return value.slice(1, -1).replaceAll("''", "'");
39
+ }
40
+ if (value === "" || value.includes(": ") || value.endsWith(":") || ["[", "{", "'", '"'].some((opening) => value.startsWith(opening))) {
41
+ throw failure(`cannot read "${value}" in packages`);
42
+ }
43
+ return value;
44
+ }
45
+
46
+ const FLOW_INDICATOR = /[{}[\]]/u;
47
+
48
+ interface FlowScan {
49
+ current: string;
50
+ readonly entries: string[];
51
+ quote: string | undefined;
52
+ }
53
+
54
+ function flowScalar(raw: string): string {
55
+ const value = raw.trim();
56
+ const quoted = value.startsWith('"') || value.startsWith("'");
57
+ if (!quoted && FLOW_INDICATOR.test(value)) {
58
+ throw failure(`cannot read "${value}" in packages; quote a pattern that holds { } [ ] or ,`);
59
+ }
60
+ return scalar(value);
61
+ }
62
+
63
+ function closed(scan: FlowScan, rest: string): readonly string[] {
64
+ if (rest.trim() !== "") {
65
+ throw failure(`cannot read "${rest.trim()}" after the packages list`);
66
+ }
67
+ return [...scan.entries, scan.current].filter((entry) => entry.trim() !== "").map(flowScalar);
68
+ }
69
+
70
+ function scanned(scan: FlowScan, char: string): "close" | undefined {
71
+ if (scan.quote !== undefined) {
72
+ scan.quote = char === scan.quote ? undefined : scan.quote;
73
+ } else if (char === "]") {
74
+ return "close";
75
+ } else if (char === ",") {
76
+ scan.entries.push(scan.current);
77
+ scan.current = "";
78
+ return undefined;
79
+ } else if (char === '"' || char === "'") {
80
+ scan.quote = char;
81
+ }
82
+ scan.current += char;
83
+ return undefined;
84
+ }
85
+
86
+ function flowList(lines: readonly string[]): readonly string[] {
87
+ const scan: FlowScan = { current: "", entries: [], quote: undefined };
88
+ for (const [number, line] of lines.map(withoutComment).entries()) {
89
+ for (let index = number === 0 ? 1 : 0; index < line.length; index += 1) {
90
+ if (scanned(scan, line[index] ?? "") === "close") {
91
+ return closed(scan, line.slice(index + 1));
92
+ }
93
+ }
94
+ scan.current += " ";
95
+ }
96
+ throw failure("the packages list never closes");
97
+ }
98
+
99
+ function blockList(lines: readonly string[]): readonly string[] {
100
+ const patterns: string[] = [];
101
+ for (const line of lines.map(withoutComment)) {
102
+ const trimmed = line.trim();
103
+ if (trimmed === "") {
104
+ continue;
105
+ }
106
+ const item = trimmed === ITEM || trimmed.startsWith(`${ITEM} `);
107
+ if (!(item || line.startsWith(" ") || line.startsWith("\t"))) {
108
+ break;
109
+ }
110
+ if (!item) {
111
+ throw failure(`cannot read "${trimmed}" in packages`);
112
+ }
113
+ patterns.push(scalar(trimmed.slice(ITEM.length)));
114
+ }
115
+ return patterns;
116
+ }
117
+
118
+ export function pnpmWorkspacePatterns(text: string): readonly string[] {
119
+ const lines = text.split(LINE_BREAK);
120
+ const start = lines.findIndex((line) => KEY.test(line));
121
+ if (start === -1) {
122
+ return [];
123
+ }
124
+ const rest = withoutComment(lines[start] ?? "")
125
+ .replace(KEY, "")
126
+ .trim();
127
+ if (rest.startsWith("[")) {
128
+ return flowList([rest, ...lines.slice(start + 1)]);
129
+ }
130
+ if (rest !== "") {
131
+ throw failure(`cannot read "${rest}" in packages`);
132
+ }
133
+ return blockList(lines.slice(start + 1));
134
+ }
@@ -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
+ }