vibecodedshit 0.0.0-stage → 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 (236) hide show
  1. package/LICENSE +95 -0
  2. package/README.md +73 -2
  3. package/assets/fonts/NotoEmoji-OFL.txt +93 -0
  4. package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
  5. package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
  6. package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
  7. package/dist/bin.js +11 -0
  8. package/dist/card/previous.js +38 -0
  9. package/dist/card/render.js +71 -0
  10. package/dist/cli.js +138 -0
  11. package/dist/commands/card.js +69 -0
  12. package/dist/commands/common.js +1 -0
  13. package/dist/commands/diff.js +54 -0
  14. package/dist/commands/footer.js +28 -0
  15. package/dist/commands/report.js +49 -0
  16. package/dist/commands/route.js +62 -0
  17. package/dist/commands/submit.js +125 -0
  18. package/dist/commands/summary.js +77 -0
  19. package/dist/diff/compare.js +73 -0
  20. package/dist/diff/conditions.js +87 -0
  21. package/dist/diff/format.js +23 -0
  22. package/dist/exec/docker.js +158 -0
  23. package/dist/exec/findings.js +73 -0
  24. package/dist/exec/plan.js +50 -0
  25. package/dist/exec/reports.js +90 -0
  26. package/dist/exec/roots.js +49 -0
  27. package/dist/exec/run.js +182 -0
  28. package/dist/exec/steps/build.js +45 -0
  29. package/dist/exec/steps/coverage-install.js +12 -0
  30. package/dist/exec/steps/install.js +41 -0
  31. package/dist/exec/steps/knip.js +28 -0
  32. package/dist/exec/steps/lint.js +25 -0
  33. package/dist/exec/steps/lockfiles.js +25 -0
  34. package/dist/exec/steps/test.js +17 -0
  35. package/dist/exec/steps/typecheck.js +40 -0
  36. package/dist/exec/test-reports/coverage-py.js +23 -0
  37. package/dist/exec/test-reports/coverage-ranges.js +65 -0
  38. package/dist/exec/test-reports/coverage-summary.js +23 -0
  39. package/dist/exec/test-reports/jest-json.js +31 -0
  40. package/dist/exec/test-reports/junit.js +24 -0
  41. package/dist/exec/test-reports/mocha-json.js +18 -0
  42. package/dist/exec/test-reports/vitest-json.js +5 -0
  43. package/dist/exec/test-runners/detect.js +99 -0
  44. package/dist/exec/test-runners/index.js +6 -0
  45. package/dist/exec/test-runners/jest.js +27 -0
  46. package/dist/exec/test-runners/mocha.js +28 -0
  47. package/dist/exec/test-runners/node-test.js +24 -0
  48. package/dist/exec/test-runners/pytest.js +18 -0
  49. package/dist/exec/test-runners/script.js +48 -0
  50. package/dist/exec/test-runners/spec.js +1 -0
  51. package/dist/exec/test-runners/vitest.js +28 -0
  52. package/dist/exec/tests.js +120 -0
  53. package/dist/exec/types.js +1 -0
  54. package/dist/interactive/after-scan.js +121 -0
  55. package/dist/interactive/docker-setup.js +97 -0
  56. package/dist/interactive/prompt.js +17 -0
  57. package/dist/interactive/system.js +10 -0
  58. package/dist/manifest/package-json.js +36 -0
  59. package/dist/manifest/python.js +106 -0
  60. package/dist/measure/d3/index.js +45 -0
  61. package/dist/measure/d3/presets.js +100 -0
  62. package/dist/measure/d3/py/adapter.js +67 -0
  63. package/dist/measure/d3/py/config.js +80 -0
  64. package/dist/measure/d3/py/contracts.js +57 -0
  65. package/dist/measure/d3/py/domain.js +57 -0
  66. package/dist/measure/d3/py/graph.js +214 -0
  67. package/dist/measure/d3/py/interpreter.js +31 -0
  68. package/dist/measure/d3/py/layers.js +120 -0
  69. package/dist/measure/d3/py/roots.js +58 -0
  70. package/dist/measure/d3/py/types.js +1 -0
  71. package/dist/measure/d3/scc.js +64 -0
  72. package/dist/measure/d3/ts/adapter.js +371 -0
  73. package/dist/measure/d3/ts/classify.js +107 -0
  74. package/dist/measure/d3/ts/config.js +318 -0
  75. package/dist/measure/d3/ts/domain.js +23 -0
  76. package/dist/measure/d3/ts/rules.js +65 -0
  77. package/dist/measure/d3/types.js +5 -0
  78. package/dist/measure/deps/cvss.js +66 -0
  79. package/dist/measure/deps/declared.js +195 -0
  80. package/dist/measure/deps/files.js +40 -0
  81. package/dist/measure/deps/lockfiles.js +308 -0
  82. package/dist/measure/deps/osv.js +122 -0
  83. package/dist/measure/deps/registry.js +142 -0
  84. package/dist/measure/deps/resolved.js +78 -0
  85. package/dist/measure/deps/units.js +5 -0
  86. package/dist/measure/eslint/health-plugin.js +19 -0
  87. package/dist/measure/ops/docs-commands.js +73 -0
  88. package/dist/measure/ops/docs-resolve.js +217 -0
  89. package/dist/measure/ops/entry-points.js +49 -0
  90. package/dist/measure/ops/env-example.js +20 -0
  91. package/dist/measure/ops/env-reads.js +46 -0
  92. package/dist/measure/ops/runtime.js +73 -0
  93. package/dist/measure/p1-1.js +8 -0
  94. package/dist/measure/p1-2.js +8 -0
  95. package/dist/measure/p1-3.js +8 -0
  96. package/dist/measure/p1-4.js +41 -0
  97. package/dist/measure/p1-5.js +59 -0
  98. package/dist/measure/p2-1.js +8 -0
  99. package/dist/measure/p2-2.js +44 -0
  100. package/dist/measure/p2-3.js +81 -0
  101. package/dist/measure/p2-4.js +288 -0
  102. package/dist/measure/p2-5.js +8 -0
  103. package/dist/measure/p3-1.js +16 -0
  104. package/dist/measure/p3-2.js +26 -0
  105. package/dist/measure/p3-3.js +37 -0
  106. package/dist/measure/p3-4.js +26 -0
  107. package/dist/measure/p3-5.js +26 -0
  108. package/dist/measure/p4-1.js +184 -0
  109. package/dist/measure/p4-2.js +27 -0
  110. package/dist/measure/p4-3.js +106 -0
  111. package/dist/measure/p4-4.js +309 -0
  112. package/dist/measure/p5-1.js +117 -0
  113. package/dist/measure/p5-2.js +80 -0
  114. package/dist/measure/p5-3.js +21 -0
  115. package/dist/measure/p5-4.js +161 -0
  116. package/dist/measure/p5-5.js +158 -0
  117. package/dist/measure/p6-1.js +20 -0
  118. package/dist/measure/p6-2.js +20 -0
  119. package/dist/measure/p6-3.js +40 -0
  120. package/dist/measure/p6-4.js +31 -0
  121. package/dist/measure/p6-5.js +60 -0
  122. package/dist/measure/p6-6.js +54 -0
  123. package/dist/measure/py/typecheck-config.js +95 -0
  124. package/dist/measure/registry.js +31 -0
  125. package/dist/measure/safety/py-tests.js +124 -0
  126. package/dist/measure/safety/ts-barrels.js +68 -0
  127. package/dist/measure/safety/ts-cases.js +111 -0
  128. package/dist/measure/safety/ts-credits.js +101 -0
  129. package/dist/measure/safety/ts-imports.js +63 -0
  130. package/dist/measure/safety/ts-resolve.js +43 -0
  131. package/dist/measure/ts/directives.js +37 -0
  132. package/dist/measure/ts/exports.js +60 -0
  133. package/dist/measure/ts/tsconfig.js +179 -0
  134. package/dist/measure/types.js +1 -0
  135. package/dist/measure/units.js +41 -0
  136. package/dist/methodology/load.js +159 -0
  137. package/dist/methodology/schema.js +105 -0
  138. package/dist/python/deadcode.js +27 -0
  139. package/dist/python/env.js +13 -0
  140. package/dist/python/helper.js +24 -0
  141. package/dist/python/imports.js +40 -0
  142. package/dist/python/nox.js +16 -0
  143. package/dist/python/scope.js +35 -0
  144. package/dist/python/tests.js +19 -0
  145. package/dist/python/typing.js +27 -0
  146. package/dist/report/escape.js +5 -0
  147. package/dist/report/format.js +75 -0
  148. package/dist/report/render.js +213 -0
  149. package/dist/report/roadmap.js +58 -0
  150. package/dist/report/scares.js +83 -0
  151. package/dist/report/style.js +27 -0
  152. package/dist/report-only/hotspots.js +48 -0
  153. package/dist/route/check.js +37 -0
  154. package/dist/route/files.js +74 -0
  155. package/dist/route/order.js +22 -0
  156. package/dist/route/select.js +114 -0
  157. package/dist/route/write.js +13 -0
  158. package/dist/run/read.js +89 -0
  159. package/dist/run/recompute.js +19 -0
  160. package/dist/run/scan.js +196 -0
  161. package/dist/run/schema.js +1 -0
  162. package/dist/run/score-run.js +42 -0
  163. package/dist/run/write.js +4 -0
  164. package/dist/scope/build.js +173 -0
  165. package/dist/scope/packages.js +87 -0
  166. package/dist/scope/rules.js +21 -0
  167. package/dist/scope/tracked.js +34 -0
  168. package/dist/scope/ts-lines.js +63 -0
  169. package/dist/scope/types.js +7 -0
  170. package/dist/scoring/aggregate.js +38 -0
  171. package/dist/scoring/pool.js +65 -0
  172. package/dist/scoring/score.js +81 -0
  173. package/dist/scoring/what-if.js +38 -0
  174. package/dist/scripts/checks.js +53 -0
  175. package/dist/scripts/ci.js +139 -0
  176. package/dist/scripts/entry-points/find.js +31 -0
  177. package/dist/scripts/entry-points/index.js +18 -0
  178. package/dist/scripts/entry-points/justfile.js +62 -0
  179. package/dist/scripts/entry-points/makefile.js +66 -0
  180. package/dist/scripts/entry-points/nox.js +29 -0
  181. package/dist/scripts/entry-points/package-json.js +21 -0
  182. package/dist/scripts/entry-points/pyproject.js +73 -0
  183. package/dist/scripts/entry-points/tox.js +130 -0
  184. package/dist/scripts/expand.js +300 -0
  185. package/dist/scripts/triggers.js +151 -0
  186. package/dist/scripts/types.js +1 -0
  187. package/dist/submit/constants.js +10 -0
  188. package/dist/submit/payload.js +42 -0
  189. package/dist/submit/repo-identity.js +46 -0
  190. package/dist/submit/send.js +29 -0
  191. package/dist/tools/catalog.js +46 -0
  192. package/dist/tools/image.js +180 -0
  193. package/dist/tools/kit.js +71 -0
  194. package/dist/tools/knip.js +169 -0
  195. package/dist/tools/outcome.js +21 -0
  196. package/dist/tools/resolve.js +66 -0
  197. package/dist/tools/workcopy.js +27 -0
  198. package/dist/util/ini.js +37 -0
  199. package/dist/util/pep440.js +163 -0
  200. package/dist/util/toml.js +27 -0
  201. package/docker/Dockerfile +142 -0
  202. package/docker/bin/health-capture +5 -0
  203. package/docker/bin/health-coverage-install +5 -0
  204. package/docker/bin/health-eslint +66 -0
  205. package/docker/bin/health-pytest +10 -0
  206. package/docker/bin/health-tsc +10 -0
  207. package/docker/bin/health-venv +11 -0
  208. package/docker/config/eslint.config.mjs +4 -0
  209. package/docker/config/eslint.rules.mjs +65 -0
  210. package/docker/config/eslint.untyped.config.mjs +4 -0
  211. package/docker/config/knip.json +1 -0
  212. package/docker/config/ruff.toml +7 -0
  213. package/methodology/LICENSE +396 -0
  214. package/methodology/v1.0/METHODOLOGY.md +1176 -0
  215. package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
  216. package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
  217. package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
  218. package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
  219. package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
  220. package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
  221. package/methodology/v1.0/methodology.yaml +242 -0
  222. package/methodology/v1.0/presets/django.yaml +43 -0
  223. package/methodology/v1.0/presets/express.yaml +47 -0
  224. package/methodology/v1.0/presets/fastapi.yaml +52 -0
  225. package/methodology/v1.0/presets/generic.yaml +41 -0
  226. package/methodology/v1.0/presets/nextjs.yaml +50 -0
  227. package/methodology/v1.0/scope.yaml +115 -0
  228. package/package.json +61 -4
  229. package/python/dead_code_helper.py +265 -0
  230. package/python/env_helper.py +71 -0
  231. package/python/graph_helper.py +277 -0
  232. package/python/imports_helper.py +132 -0
  233. package/python/nox_helper.py +79 -0
  234. package/python/scope_helper.py +96 -0
  235. package/python/tests_helper.py +171 -0
  236. package/python/typing_helper.py +235 -0
@@ -0,0 +1,31 @@
1
+ import { p11 } from './p1-1.js';
2
+ import { p12 } from './p1-2.js';
3
+ import { p13 } from './p1-3.js';
4
+ import { p14 } from './p1-4.js';
5
+ import { p15 } from './p1-5.js';
6
+ import { p21 } from './p2-1.js';
7
+ import { p22 } from './p2-2.js';
8
+ import { p23 } from './p2-3.js';
9
+ import { p24 } from './p2-4.js';
10
+ import { p25 } from './p2-5.js';
11
+ import { p31 } from './p3-1.js';
12
+ import { p32 } from './p3-2.js';
13
+ import { p33 } from './p3-3.js';
14
+ import { p34 } from './p3-4.js';
15
+ import { p35 } from './p3-5.js';
16
+ import { p41 } from './p4-1.js';
17
+ import { p42 } from './p4-2.js';
18
+ import { p43 } from './p4-3.js';
19
+ import { p44 } from './p4-4.js';
20
+ import { p51 } from './p5-1.js';
21
+ import { p52 } from './p5-2.js';
22
+ import { p53 } from './p5-3.js';
23
+ import { p54 } from './p5-4.js';
24
+ import { p55 } from './p5-5.js';
25
+ import { p61 } from './p6-1.js';
26
+ import { p62 } from './p6-2.js';
27
+ import { p63 } from './p6-3.js';
28
+ import { p64 } from './p6-4.js';
29
+ import { p65 } from './p6-5.js';
30
+ import { p66 } from './p6-6.js';
31
+ export const measurers = [p11, p12, p13, p14, p15, p21, p22, p23, p24, p25, p31, p32, p33, p34, p35, p41, p42, p43, p44, p51, p52, p53, p54, p55, p61, p62, p63, p64, p65, p66];
@@ -0,0 +1,124 @@
1
+ import path from 'node:path';
2
+ import { pythonTestFacts } from '../../python/tests.js';
3
+ import { trackedPaths } from '../../scope/tracked.js';
4
+ /** Resolution of Python imports to tracked files, for 1.4. Third-party and stdlib imports resolve to nothing. */
5
+ export class PyImportGraph {
6
+ tracked;
7
+ facts;
8
+ packageOf = new Map();
9
+ constructor(repoPath, files) {
10
+ this.tracked = new Set(trackedPaths(repoPath));
11
+ this.facts = pythonTestFacts(repoPath);
12
+ for (const file of files)
13
+ this.packageOf.set(file.path, file.package);
14
+ }
15
+ factsOf(file) {
16
+ return this.facts[file];
17
+ }
18
+ /** The directories an absolute import is tried from, in order: the importer's own directory, `<P>/src`, `<P>`, `src`, `.`. */
19
+ roots(from) {
20
+ const pkg = this.packageOf.get(from) ?? '.';
21
+ const candidates = [path.posix.dirname(from), pkg === '.' ? 'src' : `${pkg}/src`, pkg, 'src', '.'];
22
+ return [...new Set(candidates)];
23
+ }
24
+ moduleIn(dir, parts) {
25
+ const base = parts.length === 0 ? dir : path.posix.join(dir, ...parts);
26
+ for (const candidate of parts.length === 0 ? [path.posix.join(base, '__init__.py')] : [`${base}.py`, path.posix.join(base, '__init__.py')]) {
27
+ if (this.tracked.has(path.posix.normalize(candidate)))
28
+ return path.posix.normalize(candidate);
29
+ }
30
+ return undefined;
31
+ }
32
+ /** The file of `module` as imported from `from` (a relative import uses its `level`; an absolute one tries each root, first hit wins). */
33
+ moduleFile(from, level, module) {
34
+ const parts = module === null ? [] : module.split('.');
35
+ if (level > 0) {
36
+ let dir = path.posix.dirname(from);
37
+ for (let i = 1; i < level; i += 1)
38
+ dir = path.posix.dirname(dir);
39
+ return this.moduleIn(dir, parts);
40
+ }
41
+ if (parts.length === 0)
42
+ return undefined;
43
+ for (const root of this.roots(from)) {
44
+ const found = this.moduleIn(root, parts);
45
+ if (found)
46
+ return found;
47
+ }
48
+ return undefined;
49
+ }
50
+ lookupIn(file, name, seen) {
51
+ const key = `${file}\0${name}`;
52
+ if (seen.has(key))
53
+ return undefined;
54
+ seen.add(key);
55
+ const facts = this.facts[file];
56
+ if (!facts?.ok)
57
+ return undefined;
58
+ if (facts.defined.includes(name))
59
+ return file;
60
+ const binding = facts.reexports.find((r) => r.name === name);
61
+ return binding ? this.fromImport(file, binding.level, binding.module, binding.orig, seen) : undefined;
62
+ }
63
+ /** The file that defines `name` imported by `from <module> import name` in `from`, following `__init__.py` re-exports; a submodule is credited itself. */
64
+ fromImport(from, level, module, name, seen = new Set()) {
65
+ const target = this.moduleFile(from, level, module);
66
+ if (target) {
67
+ const found = this.lookupIn(target, name, seen);
68
+ if (found)
69
+ return found;
70
+ }
71
+ const dir = target === undefined ? (module === null && level > 0 ? this.levelDir(from, level) : undefined) : target.endsWith('/__init__.py') || target === '__init__.py' ? path.posix.dirname(target) : undefined;
72
+ if (dir !== undefined) {
73
+ const sub = this.moduleIn(dir, [name]);
74
+ if (sub)
75
+ return sub;
76
+ }
77
+ return target;
78
+ }
79
+ levelDir(from, level) {
80
+ let dir = path.posix.dirname(from);
81
+ for (let i = 1; i < level; i += 1)
82
+ dir = path.posix.dirname(dir);
83
+ return dir;
84
+ }
85
+ /** Every file a star import of `file` reaches: the module itself and the targets of all its re-export bindings. */
86
+ starTargets(file) {
87
+ const facts = this.facts[file];
88
+ const out = [file];
89
+ if (facts?.ok) {
90
+ for (const binding of facts.reexports) {
91
+ const found = this.fromImport(file, binding.level, binding.module, binding.orig);
92
+ if (found !== undefined)
93
+ out.push(found);
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+ /** The files a test file credits through its imports. */
99
+ credits(test) {
100
+ const facts = this.facts[test];
101
+ if (!facts?.ok)
102
+ return [];
103
+ const out = [];
104
+ for (const imp of facts.imports) {
105
+ if (imp.names.length === 0 && !imp.star) {
106
+ const file = this.moduleFile(test, imp.level, imp.module);
107
+ if (file !== undefined)
108
+ out.push(file);
109
+ continue;
110
+ }
111
+ for (const { name } of imp.names) {
112
+ const found = this.fromImport(test, imp.level, imp.module, name);
113
+ if (found !== undefined)
114
+ out.push(found);
115
+ }
116
+ if (imp.star) {
117
+ const file = this.moduleFile(test, imp.level, imp.module);
118
+ if (file !== undefined)
119
+ out.push(...this.starTargets(file));
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ }
@@ -0,0 +1,68 @@
1
+ import ts from 'typescript';
2
+ const hasModifier = (node, kind) => ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((m) => m.kind === kind);
3
+ function bindingNames(name, out) {
4
+ if (ts.isIdentifier(name))
5
+ out.push(name.text);
6
+ else
7
+ for (const el of name.elements)
8
+ if (ts.isBindingElement(el))
9
+ bindingNames(el.name, out);
10
+ }
11
+ export function summarize(sf) {
12
+ const summary = { defined: new Set(), named: new Map(), stars: [], namespaces: new Map() };
13
+ const imports = new Map();
14
+ for (const stmt of sf.statements) {
15
+ if (ts.isImportDeclaration(stmt) && ts.isStringLiteralLike(stmt.moduleSpecifier) && stmt.importClause) {
16
+ const specifier = stmt.moduleSpecifier.text;
17
+ const clause = stmt.importClause;
18
+ if (clause.name)
19
+ imports.set(clause.name.text, { specifier, orig: 'default' });
20
+ if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
21
+ for (const el of clause.namedBindings.elements)
22
+ imports.set(el.name.text, { specifier, orig: (el.propertyName ?? el.name).text });
23
+ }
24
+ }
25
+ }
26
+ for (const stmt of sf.statements) {
27
+ if (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt) || ts.isEnumDeclaration(stmt) || ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) {
28
+ if (stmt.name)
29
+ summary.defined.add(stmt.name.text);
30
+ if (hasModifier(stmt, ts.SyntaxKind.DefaultKeyword))
31
+ summary.defined.add('default');
32
+ }
33
+ else if (ts.isVariableStatement(stmt)) {
34
+ const names = [];
35
+ for (const decl of stmt.declarationList.declarations)
36
+ bindingNames(decl.name, names);
37
+ for (const name of names)
38
+ summary.defined.add(name);
39
+ }
40
+ else if (ts.isExportAssignment(stmt)) {
41
+ summary.defined.add('default');
42
+ }
43
+ else if (ts.isExportDeclaration(stmt)) {
44
+ const from = stmt.moduleSpecifier && ts.isStringLiteralLike(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : undefined;
45
+ const clause = stmt.exportClause;
46
+ if (from !== undefined) {
47
+ if (!clause)
48
+ summary.stars.push(from);
49
+ else if (ts.isNamespaceExport(clause))
50
+ summary.namespaces.set(clause.name.text, from);
51
+ else
52
+ for (const el of clause.elements)
53
+ summary.named.set(el.name.text, { specifier: from, orig: (el.propertyName ?? el.name).text });
54
+ }
55
+ else if (clause && ts.isNamedExports(clause)) {
56
+ for (const el of clause.elements) {
57
+ const local = (el.propertyName ?? el.name).text;
58
+ const imported = imports.get(local);
59
+ if (imported)
60
+ summary.named.set(el.name.text, imported);
61
+ else
62
+ summary.defined.add(el.name.text);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ return summary;
68
+ }
@@ -0,0 +1,111 @@
1
+ import ts from 'typescript';
2
+ const CASE_ROOTS = new Set(['it', 'test']);
3
+ const CASE_MODIFIERS = new Set(['only', 'concurrent']);
4
+ const EACH_MODIFIERS = new Set(['only', 'concurrent', 'each']);
5
+ /** `it`, `test` or `it.only.each` as a root identifier and the property names after it, or undefined for anything else. */
6
+ function chainOf(expr) {
7
+ const mods = [];
8
+ let current = expr;
9
+ while (ts.isPropertyAccessExpression(current)) {
10
+ mods.unshift(current.name.text);
11
+ current = current.expression;
12
+ }
13
+ return ts.isIdentifier(current) ? { root: current.text, mods } : undefined;
14
+ }
15
+ /**
16
+ * A test case is a call to `it` or `test`, or to `it`/`test` followed by a chain of `.only` and `.concurrent`, or `.each(...)` (call or
17
+ * tagged template) and then the call itself. `.skip`, `.todo`, `.fails`, `.skipIf` and `.runIf` are not counted; `describe` is not a case.
18
+ */
19
+ function isCaseCall(call) {
20
+ const callee = call.expression;
21
+ const direct = chainOf(callee);
22
+ if (direct)
23
+ return CASE_ROOTS.has(direct.root) && direct.mods.every((m) => CASE_MODIFIERS.has(m));
24
+ const each = ts.isCallExpression(callee) ? callee.expression : ts.isTaggedTemplateExpression(callee) ? callee.tag : undefined;
25
+ const chain = each ? chainOf(each) : undefined;
26
+ return chain !== undefined && CASE_ROOTS.has(chain.root) && chain.mods.at(-1) === 'each' && chain.mods.every((m) => EACH_MODIFIERS.has(m));
27
+ }
28
+ const isFunctionLike = (node) => ts.isArrowFunction(node) || ts.isFunctionExpression(node);
29
+ function isAssertionCall(call, testParam) {
30
+ const callee = call.expression;
31
+ if (ts.isIdentifier(callee))
32
+ return ['expect', 'assert', 'expectTypeOf'].includes(callee.text);
33
+ if (!ts.isPropertyAccessExpression(callee))
34
+ return false;
35
+ const props = [];
36
+ let root = callee;
37
+ while (ts.isPropertyAccessExpression(root)) {
38
+ props.unshift(root.name.text);
39
+ root = root.expression;
40
+ }
41
+ if (!ts.isIdentifier(root))
42
+ return false;
43
+ if (root.text === 'expect')
44
+ return props.length === 1 && (props[0] === 'soft' || props[0] === 'poll');
45
+ if (root.text === 'assert')
46
+ return true;
47
+ if (testParam !== undefined && root.text === testParam) {
48
+ const [first] = props;
49
+ return first !== undefined && (first.startsWith('assert') || (props.length === 1 && (first === 'is' || first === 'deepEqual')));
50
+ }
51
+ return false;
52
+ }
53
+ function containsAssertion(node, testParam, calls) {
54
+ let found = false;
55
+ const visit = (child) => {
56
+ if (found && calls === undefined)
57
+ return;
58
+ if (ts.isCallExpression(child)) {
59
+ if (isAssertionCall(child, testParam))
60
+ found = true;
61
+ calls?.push(child);
62
+ }
63
+ ts.forEachChild(child, visit);
64
+ };
65
+ visit(node);
66
+ return found;
67
+ }
68
+ /** Functions declared in the file by name: function declarations and `const x = <arrow or function expression>`. */
69
+ function helpersOf(sf) {
70
+ const helpers = new Map();
71
+ const visit = (node) => {
72
+ if (ts.isFunctionDeclaration(node) && node.name && node.body)
73
+ helpers.set(node.name.text, node.body);
74
+ else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isFunctionLike(node.initializer))
75
+ helpers.set(node.name.text, node.initializer.body);
76
+ ts.forEachChild(node, visit);
77
+ };
78
+ visit(sf);
79
+ return helpers;
80
+ }
81
+ /**
82
+ * The test cases of a file. Assertions: `expect(...)`, `expect.soft`, `expect.poll`, `assert(...)`, `assert.*`, `expectTypeOf(...)` and
83
+ * `t.assert*`, `t.is`, `t.deepEqual` where `t` is the callback's first parameter, searched in the whole body. A call to a helper declared in
84
+ * the same file counts when the helper's body directly contains an assertion (one level). A case with no function argument has no assertion.
85
+ */
86
+ export function testCases(sf) {
87
+ const helpers = helpersOf(sf);
88
+ const cases = [];
89
+ const visit = (node) => {
90
+ if (ts.isCallExpression(node) && isCaseCall(node)) {
91
+ const callback = [...node.arguments].reverse().find(isFunctionLike);
92
+ let asserts = false;
93
+ if (callback) {
94
+ const first = callback.parameters[0];
95
+ const testParam = first && ts.isIdentifier(first.name) ? first.name.text : undefined;
96
+ const calls = [];
97
+ asserts = containsAssertion(callback.body, testParam, calls);
98
+ if (!asserts) {
99
+ asserts = calls.some((call) => {
100
+ const helper = ts.isIdentifier(call.expression) ? helpers.get(call.expression.text) : undefined;
101
+ return helper !== undefined && containsAssertion(helper, undefined);
102
+ });
103
+ }
104
+ }
105
+ cases.push({ line: sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1, asserts });
106
+ }
107
+ ts.forEachChild(node, visit);
108
+ };
109
+ visit(sf);
110
+ return cases;
111
+ }
@@ -0,0 +1,101 @@
1
+ import { readTracked } from '../../scope/tracked.js';
2
+ import { parseTsFile } from '../../scope/ts-lines.js';
3
+ import { summarize } from './ts-barrels.js';
4
+ import { importFacts, namespaceAccesses } from './ts-imports.js';
5
+ /** Logic files of the TS/JS group credited by the imports of one test file, with barrels followed to the defining file. */
6
+ export class TsCredits {
7
+ repoPath;
8
+ scope;
9
+ resolver;
10
+ summaries = new Map();
11
+ parsed = new Map();
12
+ constructor(repoPath, scope, resolver) {
13
+ this.repoPath = repoPath;
14
+ this.scope = scope;
15
+ this.resolver = resolver;
16
+ }
17
+ /** A scoped file from the scope's cache; a barrel or other out-of-scope tracked file is parsed from its text. */
18
+ source(file) {
19
+ const cached = this.parsed.get(file);
20
+ if (cached !== undefined)
21
+ return cached ?? undefined;
22
+ let sf = null;
23
+ try {
24
+ sf = this.scope.getTsSourceFile(file);
25
+ }
26
+ catch {
27
+ const text = readTracked(this.repoPath, file);
28
+ sf = text === null ? null : parseTsFile(file, text);
29
+ }
30
+ this.parsed.set(file, sf);
31
+ return sf ?? undefined;
32
+ }
33
+ summary(file) {
34
+ const cached = this.summaries.get(file);
35
+ if (cached !== undefined)
36
+ return cached ?? undefined;
37
+ const sf = this.source(file);
38
+ const summary = sf ? summarize(sf) : null;
39
+ this.summaries.set(file, summary);
40
+ return summary ?? undefined;
41
+ }
42
+ /** The file defining `name` as exported by `start`: breadth first through `export *` sources, first definition wins. */
43
+ define(start, name, seen) {
44
+ const queue = [start];
45
+ while (queue.length > 0) {
46
+ const file = queue.shift();
47
+ const key = `${file}\0${name}`;
48
+ if (seen.has(key))
49
+ continue;
50
+ seen.add(key);
51
+ const summary = this.summary(file);
52
+ if (!summary)
53
+ continue;
54
+ if (summary.defined.has(name))
55
+ return file;
56
+ const named = summary.named.get(name);
57
+ if (named) {
58
+ const target = this.resolver.resolve(named.specifier, file);
59
+ if (target !== undefined)
60
+ return this.define(target, named.orig, seen) ?? target;
61
+ }
62
+ const namespace = summary.namespaces.get(name);
63
+ if (namespace !== undefined) {
64
+ const target = this.resolver.resolve(namespace, file);
65
+ if (target !== undefined)
66
+ return target;
67
+ }
68
+ for (const star of summary.stars) {
69
+ const target = this.resolver.resolve(star, file);
70
+ if (target !== undefined)
71
+ queue.push(target);
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+ /** Every file the test file credits (callers keep only logic-role files). */
77
+ credits(test) {
78
+ const out = new Set();
79
+ const sf = this.source(test);
80
+ if (!sf)
81
+ return out;
82
+ const facts = importFacts(sf);
83
+ const namespaces = namespaceAccesses(sf, new Set(facts.flatMap((f) => (f.local === undefined ? [] : [f.local]))));
84
+ for (const fact of facts) {
85
+ const target = this.resolver.resolve(fact.specifier, test);
86
+ if (target === undefined)
87
+ continue;
88
+ if (fact.name === undefined || fact.name === '*') {
89
+ out.add(target);
90
+ for (const name of fact.local === undefined ? [] : [...(namespaces.get(fact.local) ?? [])]) {
91
+ const defining = this.define(target, name, new Set());
92
+ if (defining !== undefined)
93
+ out.add(defining);
94
+ }
95
+ }
96
+ else
97
+ out.add(this.define(target, fact.name, new Set()) ?? target);
98
+ }
99
+ return out;
100
+ }
101
+ }
@@ -0,0 +1,63 @@
1
+ import ts from 'typescript';
2
+ const isStringLiteral = (node) => node !== undefined && ts.isStringLiteralLike(node);
3
+ /**
4
+ * What a test file imports: static `import` declarations of every form (`import type` and `import { type X }` included), `export ... from`,
5
+ * `require("<literal>")` and dynamic `import("<literal>")`. `vi.mock`, `jest.mock`, `require.resolve` and non-literal paths do not count.
6
+ */
7
+ export function importFacts(sf) {
8
+ const facts = [];
9
+ const visit = (node) => {
10
+ if (ts.isImportDeclaration(node) && isStringLiteral(node.moduleSpecifier)) {
11
+ const specifier = node.moduleSpecifier.text;
12
+ const clause = node.importClause;
13
+ if (!clause)
14
+ facts.push({ specifier });
15
+ else {
16
+ if (clause.name)
17
+ facts.push({ specifier, name: 'default' });
18
+ const bindings = clause.namedBindings;
19
+ if (bindings && ts.isNamespaceImport(bindings))
20
+ facts.push({ specifier, name: '*', local: bindings.name.text });
21
+ else if (bindings)
22
+ for (const el of bindings.elements)
23
+ facts.push({ specifier, name: (el.propertyName ?? el.name).text });
24
+ if (!clause.name && !bindings)
25
+ facts.push({ specifier });
26
+ }
27
+ }
28
+ else if (ts.isExportDeclaration(node) && isStringLiteral(node.moduleSpecifier)) {
29
+ const specifier = node.moduleSpecifier.text;
30
+ const clause = node.exportClause;
31
+ if (clause && ts.isNamedExports(clause))
32
+ for (const el of clause.elements)
33
+ facts.push({ specifier, name: (el.propertyName ?? el.name).text });
34
+ else
35
+ facts.push({ specifier });
36
+ }
37
+ else if (ts.isCallExpression(node) && isStringLiteral(node.arguments[0])) {
38
+ const callee = node.expression;
39
+ if ((ts.isIdentifier(callee) && callee.text === 'require') || callee.kind === ts.SyntaxKind.ImportKeyword) {
40
+ facts.push({ specifier: node.arguments[0].text });
41
+ }
42
+ }
43
+ ts.forEachChild(node, visit);
44
+ };
45
+ visit(sf);
46
+ return facts;
47
+ }
48
+ /** Names accessed as `m.x` for each namespace-import binding `m` of a file. */
49
+ export function namespaceAccesses(sf, locals) {
50
+ const out = new Map();
51
+ if (locals.size === 0)
52
+ return out;
53
+ const visit = (node) => {
54
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && locals.has(node.expression.text)) {
55
+ const set = out.get(node.expression.text) ?? new Set();
56
+ set.add(node.name.text);
57
+ out.set(node.expression.text, set);
58
+ }
59
+ ts.forEachChild(node, visit);
60
+ };
61
+ visit(sf);
62
+ return out;
63
+ }
@@ -0,0 +1,43 @@
1
+ import path from 'node:path';
2
+ import ts from 'typescript';
3
+ import { trackedPaths } from '../../scope/tracked.js';
4
+ import { compilerOptionsFor } from '../ts/tsconfig.js';
5
+ const posix = (p) => p.split(path.sep).join('/');
6
+ /**
7
+ * `ts.resolveModuleName` with the options of the nearest tsconfig and a host backed by the tracked files, so `node_modules`
8
+ * is never read and a package import resolves to nothing. NodeNext `.js` specifiers that map to `.ts` files resolve through the compiler.
9
+ */
10
+ export function createTsResolver(repoPath) {
11
+ const root = posix(path.resolve(repoPath));
12
+ const files = new Set(trackedPaths(repoPath));
13
+ const dirs = new Set(['']);
14
+ for (const file of files)
15
+ for (let d = path.posix.dirname(file); d !== '.' && !dirs.has(d); d = path.posix.dirname(d))
16
+ dirs.add(d);
17
+ const rel = (abs) => path.posix.relative(root, posix(abs));
18
+ const host = {
19
+ fileExists: (abs) => files.has(rel(abs)),
20
+ readFile: (abs) => (files.has(rel(abs)) ? ts.sys.readFile(abs) : undefined),
21
+ directoryExists: (abs) => dirs.has(rel(abs) === '.' ? '' : rel(abs)),
22
+ realpath: (abs) => abs,
23
+ getCurrentDirectory: () => root,
24
+ useCaseSensitiveFileNames: true,
25
+ };
26
+ const optionsByDir = new Map();
27
+ return {
28
+ resolve(specifier, fromFile) {
29
+ const dir = path.posix.dirname(fromFile);
30
+ let options = optionsByDir.get(dir);
31
+ if (!options) {
32
+ options = compilerOptionsFor(repoPath, dir);
33
+ optionsByDir.set(dir, options);
34
+ }
35
+ const containing = path.posix.join(root, fromFile);
36
+ const result = ts.resolveModuleName(specifier, containing, options, host).resolvedModule;
37
+ if (!result)
38
+ return undefined;
39
+ const found = rel(result.resolvedFileName);
40
+ return found.startsWith('..') || !files.has(found) ? undefined : found;
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,37 @@
1
+ import ts from 'typescript';
2
+ /** True when the file's leading comments hold `@<name>` at the start of a comment (after `//`, `///` or `/*`). */
3
+ export function hasLeadingDirective(sourceFile, name) {
4
+ const text = sourceFile.text;
5
+ const pattern = new RegExp(`^(?:\\/\\/\\/?|\\/\\*+)\\s*@${name}\\b`);
6
+ return (ts.getLeadingCommentRanges(text, 0) ?? []).some((range) => pattern.test(text.slice(range.pos, range.end)));
7
+ }
8
+ const LINE_DIRECTIVE = /^\/\/\/?\s*@(?:ts-expect-error|ts-ignore)/;
9
+ const BLOCK_DIRECTIVE = /^(?:\/|\*)*\s*@(?:ts-expect-error|ts-ignore)/;
10
+ /** Number of `@ts-ignore` and `@ts-expect-error` comments, found through the AST so strings, templates, regexes and JSX text never count. */
11
+ export function countSuppressionComments(sourceFile) {
12
+ const text = sourceFile.text;
13
+ const seen = new Set();
14
+ let count = 0;
15
+ const check = (ranges) => {
16
+ for (const range of ranges ?? []) {
17
+ if (seen.has(range.pos))
18
+ continue;
19
+ seen.add(range.pos);
20
+ const comment = text.slice(range.pos, range.end);
21
+ const matches = range.kind === ts.SyntaxKind.SingleLineCommentTrivia
22
+ ? LINE_DIRECTIVE.test(comment)
23
+ : BLOCK_DIRECTIVE.test(comment.slice(comment.lastIndexOf('\n') + 1));
24
+ if (matches)
25
+ count += 1;
26
+ }
27
+ };
28
+ const visit = (node) => {
29
+ if (node.kind !== ts.SyntaxKind.JsxText) {
30
+ check(ts.getLeadingCommentRanges(text, node.pos));
31
+ check(ts.getTrailingCommentRanges(text, node.end));
32
+ }
33
+ ts.forEachChild(node, visit);
34
+ };
35
+ ts.forEachChild(sourceFile, visit);
36
+ return count;
37
+ }
@@ -0,0 +1,60 @@
1
+ import ts from 'typescript';
2
+ function hasModifier(node, kind) {
3
+ return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((m) => m.kind === kind) ?? false);
4
+ }
5
+ function bindingNames(name, out) {
6
+ if (ts.isIdentifier(name)) {
7
+ out.add(name.text);
8
+ return;
9
+ }
10
+ for (const element of name.elements) {
11
+ if (ts.isBindingElement(element))
12
+ bindingNames(element.name, out);
13
+ }
14
+ }
15
+ /**
16
+ * Exported symbols of one file, the denominator of 4.4:
17
+ * each exported declaration by name (overloads and merged declarations once, each binding of a
18
+ * destructured `const` once), each specifier of `export { a, b as c }` and `export { x } from`,
19
+ * `export default` once, `export =` once, `export * as ns from` once, `export * from` zero.
20
+ */
21
+ export function countExportedSymbols(sourceFile) {
22
+ const names = new Set();
23
+ let extra = 0;
24
+ for (const statement of sourceFile.statements) {
25
+ if (ts.isExportAssignment(statement)) {
26
+ extra += 1; // `export default expr` and `export = expr`
27
+ continue;
28
+ }
29
+ if (ts.isExportDeclaration(statement)) {
30
+ const clause = statement.exportClause;
31
+ if (!clause)
32
+ continue; // export * from '...'
33
+ if (ts.isNamespaceExport(clause))
34
+ extra += 1;
35
+ else
36
+ extra += clause.elements.length;
37
+ continue;
38
+ }
39
+ if (!hasModifier(statement, ts.SyntaxKind.ExportKeyword))
40
+ continue;
41
+ if (hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
42
+ extra += 1;
43
+ continue;
44
+ }
45
+ if (ts.isVariableStatement(statement)) {
46
+ for (const declaration of statement.declarationList.declarations)
47
+ bindingNames(declaration.name, names);
48
+ }
49
+ else if (ts.isFunctionDeclaration(statement) ||
50
+ ts.isClassDeclaration(statement) ||
51
+ ts.isInterfaceDeclaration(statement) ||
52
+ ts.isTypeAliasDeclaration(statement) ||
53
+ ts.isEnumDeclaration(statement) ||
54
+ ts.isModuleDeclaration(statement)) {
55
+ if (statement.name)
56
+ names.add(statement.name.getText(sourceFile));
57
+ }
58
+ }
59
+ return names.size + extra;
60
+ }