@nielspeter/eess-ts 0.4.0 → 0.5.1

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 (267) hide show
  1. package/dist/builders/call-rule-builder.d.ts.map +1 -1
  2. package/dist/builders/call-rule-builder.js +2 -1
  3. package/dist/builders/call-rule-builder.js.map +1 -1
  4. package/dist/builders/class-rule-builder.d.ts.map +1 -1
  5. package/dist/builders/class-rule-builder.js +7 -25
  6. package/dist/builders/class-rule-builder.js.map +1 -1
  7. package/dist/builders/correspondence-builder.d.ts.map +1 -1
  8. package/dist/builders/correspondence-builder.js +6 -14
  9. package/dist/builders/correspondence-builder.js.map +1 -1
  10. package/dist/builders/cross-layer-builder.d.ts.map +1 -1
  11. package/dist/builders/cross-layer-builder.js +7 -6
  12. package/dist/builders/cross-layer-builder.js.map +1 -1
  13. package/dist/builders/function-rule-builder.d.ts.map +1 -1
  14. package/dist/builders/function-rule-builder.js +4 -13
  15. package/dist/builders/function-rule-builder.js.map +1 -1
  16. package/dist/builders/module-rule-builder.d.ts.map +1 -1
  17. package/dist/builders/module-rule-builder.js +4 -16
  18. package/dist/builders/module-rule-builder.js.map +1 -1
  19. package/dist/builders/slice-rule-builder.d.ts.map +1 -1
  20. package/dist/builders/slice-rule-builder.js +4 -3
  21. package/dist/builders/slice-rule-builder.js.map +1 -1
  22. package/dist/builders/type-rule-builder.d.ts.map +1 -1
  23. package/dist/builders/type-rule-builder.js +4 -13
  24. package/dist/builders/type-rule-builder.js.map +1 -1
  25. package/dist/cli/commands/baseline.d.ts +5 -1
  26. package/dist/cli/commands/baseline.d.ts.map +1 -1
  27. package/dist/cli/commands/baseline.js +47 -5
  28. package/dist/cli/commands/baseline.js.map +1 -1
  29. package/dist/cli/commands/check.d.ts.map +1 -1
  30. package/dist/cli/commands/check.js +155 -13
  31. package/dist/cli/commands/check.js.map +1 -1
  32. package/dist/cli/commands/doctor.d.ts.map +1 -1
  33. package/dist/cli/commands/doctor.js +97 -6
  34. package/dist/cli/commands/doctor.js.map +1 -1
  35. package/dist/cli/commands/explain.d.ts.map +1 -1
  36. package/dist/cli/commands/explain.js +21 -2
  37. package/dist/cli/commands/explain.js.map +1 -1
  38. package/dist/cli/commands/init.js +1 -1
  39. package/dist/cli/commands/init.js.map +1 -1
  40. package/dist/cli/import-rule-module.d.ts +23 -4
  41. package/dist/cli/import-rule-module.d.ts.map +1 -1
  42. package/dist/cli/import-rule-module.js +30 -18
  43. package/dist/cli/import-rule-module.js.map +1 -1
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +4 -11
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/cli/load-rules.d.ts.map +1 -1
  48. package/dist/cli/load-rules.js +5 -4
  49. package/dist/cli/load-rules.js.map +1 -1
  50. package/dist/cli/resolve-config.d.ts +0 -8
  51. package/dist/cli/resolve-config.d.ts.map +1 -1
  52. package/dist/cli/resolve-config.js +18 -48
  53. package/dist/cli/resolve-config.js.map +1 -1
  54. package/dist/cli/rule-file-findings.d.ts.map +1 -1
  55. package/dist/cli/rule-file-findings.js +60 -2
  56. package/dist/cli/rule-file-findings.js.map +1 -1
  57. package/dist/cli/watch.d.ts +13 -43
  58. package/dist/cli/watch.d.ts.map +1 -1
  59. package/dist/cli/watch.js +8 -148
  60. package/dist/cli/watch.js.map +1 -1
  61. package/dist/conditions/call.d.ts.map +1 -1
  62. package/dist/conditions/call.js +4 -3
  63. package/dist/conditions/call.js.map +1 -1
  64. package/dist/conditions/cross-layer.js +2 -2
  65. package/dist/conditions/cross-layer.js.map +1 -1
  66. package/dist/conditions/dependency.js +2 -2
  67. package/dist/conditions/dependency.js.map +1 -1
  68. package/dist/conditions/exports.d.ts.map +1 -1
  69. package/dist/conditions/exports.js +31 -37
  70. package/dist/conditions/exports.js.map +1 -1
  71. package/dist/conditions/function.js +1 -1
  72. package/dist/conditions/function.js.map +1 -1
  73. package/dist/conditions/jsx.js +1 -1
  74. package/dist/conditions/jsx.js.map +1 -1
  75. package/dist/conditions/members.d.ts.map +1 -1
  76. package/dist/conditions/members.js +3 -2
  77. package/dist/conditions/members.js.map +1 -1
  78. package/dist/conditions/reverse-dependency.d.ts.map +1 -1
  79. package/dist/conditions/reverse-dependency.js +47 -2
  80. package/dist/conditions/reverse-dependency.js.map +1 -1
  81. package/dist/conditions/slice.js +1 -1
  82. package/dist/conditions/slice.js.map +1 -1
  83. package/dist/conditions/structural.js +1 -1
  84. package/dist/conditions/structural.js.map +1 -1
  85. package/dist/core/check-all.d.ts.map +1 -1
  86. package/dist/core/check-all.js +61 -7
  87. package/dist/core/check-all.js.map +1 -1
  88. package/dist/core/combinators.d.ts.map +1 -1
  89. package/dist/core/combinators.js +20 -27
  90. package/dist/core/combinators.js.map +1 -1
  91. package/dist/core/descendant-cache.d.ts.map +1 -1
  92. package/dist/core/descendant-cache.js +5 -7
  93. package/dist/core/descendant-cache.js.map +1 -1
  94. package/dist/core/diagnose.js +15 -7
  95. package/dist/core/diagnose.js.map +1 -1
  96. package/dist/core/disk-set.js +1 -1
  97. package/dist/core/disk-set.js.map +1 -1
  98. package/dist/core/element-cache.js +1 -1
  99. package/dist/core/element-cache.js.map +1 -1
  100. package/dist/core/exclusion-comments.d.ts +15 -50
  101. package/dist/core/exclusion-comments.d.ts.map +1 -1
  102. package/dist/core/exclusion-comments.js +4 -307
  103. package/dist/core/exclusion-comments.js.map +1 -1
  104. package/dist/core/execute-rule.d.ts +8 -0
  105. package/dist/core/execute-rule.d.ts.map +1 -1
  106. package/dist/core/execute-rule.js +99 -13
  107. package/dist/core/execute-rule.js.map +1 -1
  108. package/dist/core/glob-evaluator.js +2 -2
  109. package/dist/core/glob-evaluator.js.map +1 -1
  110. package/dist/core/invalidate-on-modify.d.ts +20 -0
  111. package/dist/core/invalidate-on-modify.d.ts.map +1 -0
  112. package/dist/core/invalidate-on-modify.js +26 -0
  113. package/dist/core/invalidate-on-modify.js.map +1 -0
  114. package/dist/core/line-index.d.ts.map +1 -1
  115. package/dist/core/line-index.js +6 -8
  116. package/dist/core/line-index.js.map +1 -1
  117. package/dist/core/metric-violation.d.ts +1 -1
  118. package/dist/core/module-edges.d.ts.map +1 -1
  119. package/dist/core/module-edges.js +26 -25
  120. package/dist/core/module-edges.js.map +1 -1
  121. package/dist/core/orphan-exclusions.d.ts.map +1 -1
  122. package/dist/core/orphan-exclusions.js +15 -5
  123. package/dist/core/orphan-exclusions.js.map +1 -1
  124. package/dist/core/path-universe.d.ts +10 -42
  125. package/dist/core/path-universe.d.ts.map +1 -1
  126. package/dist/core/path-universe.js +0 -20
  127. package/dist/core/path-universe.js.map +1 -1
  128. package/dist/core/project.d.ts.map +1 -1
  129. package/dist/core/project.js +6 -5
  130. package/dist/core/project.js.map +1 -1
  131. package/dist/core/rule-builder.d.ts +22 -1
  132. package/dist/core/rule-builder.d.ts.map +1 -1
  133. package/dist/core/rule-builder.js +38 -20
  134. package/dist/core/rule-builder.js.map +1 -1
  135. package/dist/core/rule-declaration.d.ts.map +1 -1
  136. package/dist/core/rule-declaration.js +14 -52
  137. package/dist/core/rule-declaration.js.map +1 -1
  138. package/dist/core/terminal-builder.d.ts +17 -13
  139. package/dist/core/terminal-builder.d.ts.map +1 -1
  140. package/dist/core/terminal-builder.js +41 -21
  141. package/dist/core/terminal-builder.js.map +1 -1
  142. package/dist/core/terminal-execution.d.ts +22 -6
  143. package/dist/core/terminal-execution.d.ts.map +1 -1
  144. package/dist/core/terminal-execution.js +39 -7
  145. package/dist/core/terminal-execution.js.map +1 -1
  146. package/dist/core/ts-comment-mask.d.ts +2 -0
  147. package/dist/core/ts-comment-mask.d.ts.map +1 -0
  148. package/dist/core/ts-comment-mask.js +102 -0
  149. package/dist/core/ts-comment-mask.js.map +1 -0
  150. package/dist/core/vacuity-diagnosis.js +1 -1
  151. package/dist/core/vacuity-diagnosis.js.map +1 -1
  152. package/dist/graphql/graphql-rule-builder.d.ts +95 -0
  153. package/dist/graphql/graphql-rule-builder.d.ts.map +1 -0
  154. package/dist/graphql/graphql-rule-builder.js +127 -0
  155. package/dist/graphql/graphql-rule-builder.js.map +1 -0
  156. package/dist/graphql/index.d.ts +1 -0
  157. package/dist/graphql/index.d.ts.map +1 -1
  158. package/dist/graphql/index.js +1 -0
  159. package/dist/graphql/index.js.map +1 -1
  160. package/dist/graphql/resolver-rule-builder.d.ts +22 -76
  161. package/dist/graphql/resolver-rule-builder.d.ts.map +1 -1
  162. package/dist/graphql/resolver-rule-builder.js +8 -130
  163. package/dist/graphql/resolver-rule-builder.js.map +1 -1
  164. package/dist/graphql/schema-loader.d.ts.map +1 -1
  165. package/dist/graphql/schema-loader.js +4 -3
  166. package/dist/graphql/schema-loader.js.map +1 -1
  167. package/dist/graphql/schema-predicates.d.ts.map +1 -1
  168. package/dist/graphql/schema-predicates.js +17 -12
  169. package/dist/graphql/schema-predicates.js.map +1 -1
  170. package/dist/graphql/schema-rule-builder.d.ts +21 -67
  171. package/dist/graphql/schema-rule-builder.d.ts.map +1 -1
  172. package/dist/graphql/schema-rule-builder.js +6 -120
  173. package/dist/graphql/schema-rule-builder.js.map +1 -1
  174. package/dist/helpers/baseline-generator.d.ts +9 -0
  175. package/dist/helpers/baseline-generator.d.ts.map +1 -1
  176. package/dist/helpers/baseline-generator.js +9 -0
  177. package/dist/helpers/baseline-generator.js.map +1 -1
  178. package/dist/helpers/baseline.d.ts.map +1 -1
  179. package/dist/helpers/baseline.js +4 -4
  180. package/dist/helpers/baseline.js.map +1 -1
  181. package/dist/helpers/body-traversal.d.ts.map +1 -1
  182. package/dist/helpers/body-traversal.js +13 -0
  183. package/dist/helpers/body-traversal.js.map +1 -1
  184. package/dist/helpers/callback-extractor.js +6 -38
  185. package/dist/helpers/callback-extractor.js.map +1 -1
  186. package/dist/helpers/complexity.d.ts +1 -1
  187. package/dist/helpers/complexity.js +1 -1
  188. package/dist/helpers/diff-aware.d.ts +11 -39
  189. package/dist/helpers/diff-aware.d.ts.map +1 -1
  190. package/dist/helpers/diff-aware.js +11 -79
  191. package/dist/helpers/diff-aware.js.map +1 -1
  192. package/dist/helpers/matchers.d.ts.map +1 -1
  193. package/dist/helpers/matchers.js +51 -67
  194. package/dist/helpers/matchers.js.map +1 -1
  195. package/dist/index.d.ts +15 -18
  196. package/dist/index.d.ts.map +1 -1
  197. package/dist/index.js +37 -18
  198. package/dist/index.js.map +1 -1
  199. package/dist/models/arch-function.d.ts +26 -2
  200. package/dist/models/arch-function.d.ts.map +1 -1
  201. package/dist/models/arch-function.js +51 -17
  202. package/dist/models/arch-function.js.map +1 -1
  203. package/dist/models/arch-jsx-element.d.ts.map +1 -1
  204. package/dist/models/arch-jsx-element.js +16 -17
  205. package/dist/models/arch-jsx-element.js.map +1 -1
  206. package/dist/predicates/jsx.d.ts.map +1 -1
  207. package/dist/predicates/jsx.js +2 -1
  208. package/dist/predicates/jsx.js.map +1 -1
  209. package/dist/presets/agent-guardrails.d.ts +29 -3
  210. package/dist/presets/agent-guardrails.d.ts.map +1 -1
  211. package/dist/presets/agent-guardrails.js +185 -1
  212. package/dist/presets/agent-guardrails.js.map +1 -1
  213. package/dist/presets/boundaries.d.ts +2 -2
  214. package/dist/presets/boundaries.d.ts.map +1 -1
  215. package/dist/presets/data-layer.d.ts +2 -2
  216. package/dist/presets/data-layer.d.ts.map +1 -1
  217. package/dist/presets/index.d.ts +4 -1
  218. package/dist/presets/index.d.ts.map +1 -1
  219. package/dist/presets/index.js +11 -1
  220. package/dist/presets/index.js.map +1 -1
  221. package/dist/presets/layered.d.ts +2 -2
  222. package/dist/presets/layered.d.ts.map +1 -1
  223. package/dist/presets/recommended.d.ts +3 -3
  224. package/dist/presets/recommended.d.ts.map +1 -1
  225. package/dist/presets/shared.d.ts +8 -11
  226. package/dist/presets/shared.d.ts.map +1 -1
  227. package/dist/presets/shared.js +57 -28
  228. package/dist/presets/shared.js.map +1 -1
  229. package/dist/rules/metrics-function.d.ts.map +1 -1
  230. package/dist/rules/metrics-function.js +59 -62
  231. package/dist/rules/metrics-function.js.map +1 -1
  232. package/dist/rules/metrics.d.ts.map +1 -1
  233. package/dist/rules/metrics.js +62 -59
  234. package/dist/rules/metrics.js.map +1 -1
  235. package/dist/smells/clusters.d.ts +78 -0
  236. package/dist/smells/clusters.d.ts.map +1 -0
  237. package/dist/smells/clusters.js +124 -0
  238. package/dist/smells/clusters.js.map +1 -0
  239. package/dist/smells/duplicate-bodies.d.ts +38 -4
  240. package/dist/smells/duplicate-bodies.d.ts.map +1 -1
  241. package/dist/smells/duplicate-bodies.js +93 -44
  242. package/dist/smells/duplicate-bodies.js.map +1 -1
  243. package/dist/smells/duplicate-report.d.ts +90 -0
  244. package/dist/smells/duplicate-report.d.ts.map +1 -0
  245. package/dist/smells/duplicate-report.js +344 -0
  246. package/dist/smells/duplicate-report.js.map +1 -0
  247. package/dist/smells/fingerprint.d.ts +12 -0
  248. package/dist/smells/fingerprint.d.ts.map +1 -1
  249. package/dist/smells/fingerprint.js +8 -2
  250. package/dist/smells/fingerprint.js.map +1 -1
  251. package/dist/smells/inconsistent-siblings.js +1 -1
  252. package/dist/smells/inconsistent-siblings.js.map +1 -1
  253. package/dist/smells/similar-pairs.d.ts +9 -8
  254. package/dist/smells/similar-pairs.d.ts.map +1 -1
  255. package/dist/smells/similar-pairs.js +83 -1
  256. package/dist/smells/similar-pairs.js.map +1 -1
  257. package/dist/smells/smell-builder.d.ts.map +1 -1
  258. package/dist/smells/smell-builder.js +3 -2
  259. package/dist/smells/smell-builder.js.map +1 -1
  260. package/dist/smells/variation.d.ts +47 -0
  261. package/dist/smells/variation.d.ts.map +1 -0
  262. package/dist/smells/variation.js +90 -0
  263. package/dist/smells/variation.js.map +1 -0
  264. package/dist/tsconfig/tsconfig-builder.d.ts.map +1 -1
  265. package/dist/tsconfig/tsconfig-builder.js +3 -2
  266. package/dist/tsconfig/tsconfig-builder.js.map +1 -1
  267. package/package.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/rules/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAKjB,MAAM,UAAU,CAAA;AACjB,OAAO,KAAK,EAAE,SAAS,EAAoB,MAAM,kBAAkB,CAAA;AA8BnE;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,gBAAgB,CAAC,CA2BtF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,gBAAgB,CAAC,CA2B5E;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,gBAAgB,CAAC,CA8B7E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,gBAAgB,CAAC,CAwBzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,gBAAgB,CAAC,CA2B5E;AAGD,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,uBAAuB,CAAA"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/rules/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAKjB,MAAM,UAAU,CAAA;AACjB,OAAO,KAAK,EAAE,SAAS,EAAoB,MAAM,kBAAkB,CAAA;AA0FnE;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,gBAAgB,CAAC,CAQtF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,gBAAgB,CAAC,CA2B5E;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,gBAAgB,CAAC,CAU7E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,gBAAgB,CAAC,CAwBzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,gBAAgB,CAAC,CAQ5E;AAGD,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,uBAAuB,CAAA"}
@@ -17,38 +17,69 @@ function getMemberName(cls, member) {
17
17
  return `${clsName}.constructor`;
18
18
  }
19
19
  /**
20
- * No method/constructor/getter/setter in the class may exceed the given
21
- * cyclomatic complexity.
20
+ * A ceiling on one measurement of every callable member of every class.
22
21
  *
23
- * @example
24
- * ```ts
25
- * import { maxCyclomaticComplexity } from '@nielspeter/eess-ts/rules/metrics'
22
+ * `maxCyclomaticComplexity`, `maxMethodLines` and `maxParameters` were the same
23
+ * nested walk three times over — `no-copy-paste` reported them at 98% — and the
24
+ * walk is the part that must not drift: a metric that visited methods but not
25
+ * accessors would report a clean ceiling for a class whose getter breaches it,
26
+ * which is a pass constructed from a partial scan (ADR-010).
26
27
  *
27
- * classes(p).should().satisfy(maxCyclomaticComplexity(15)).check()
28
- * ```
28
+ * `unit` is threaded rather than derived from `metric`: `lines` kept its name
29
+ * when `linesOfCode` stopped counting comments, and the baseline refuses to
30
+ * compare across a unit change precisely so that silent tripling cannot happen
31
+ * again (bug 0171). A metric whose name already says what it counts passes
32
+ * nothing here.
29
33
  */
30
- export function maxCyclomaticComplexity(threshold) {
34
+ function memberCeiling(threshold, spec) {
31
35
  return {
32
- description: `have no method with cyclomatic complexity > ${String(threshold)}`,
36
+ description: spec.description,
33
37
  evaluate(elements, context) {
34
38
  const violations = [];
35
39
  for (const cls of elements) {
36
40
  for (const member of getClassMembers(cls)) {
37
- const cc = cyclomaticComplexity(member.getBody());
38
- if (cc > threshold) {
39
- violations.push(metricViolation(member, {
40
- metric: 'complexity',
41
- measured: cc,
42
- qualifiedName: getMemberName(cls, member),
43
- message: `${getMemberName(cls, member)} has cyclomatic complexity ${String(cc)} (max: ${String(threshold)}) — split into smaller methods`,
44
- }, context));
45
- }
41
+ const value = spec.measure(member);
42
+ if (value <= threshold)
43
+ continue;
44
+ const name = getMemberName(cls, member);
45
+ violations.push(metricViolation(member, {
46
+ metric: spec.metric,
47
+ ...(spec.unit === undefined ? {} : { unit: spec.unit }),
48
+ // Written long-hand deliberately: `measured,` is a
49
+ // ShorthandPropertyAssignment, and the producer census in
50
+ // `every-metric-finding-carries-its-unit.test.ts` walks
51
+ // PropertyAssignment. Shorthand here made this producer
52
+ // invisible to it — the census now sees both kinds, and this
53
+ // spelling keeps the two from having to agree twice.
54
+ measured: value,
55
+ qualifiedName: name,
56
+ message: spec.message(name, value),
57
+ }, context));
46
58
  }
47
59
  }
48
60
  return violations;
49
61
  },
50
62
  };
51
63
  }
64
+ /**
65
+ * No method/constructor/getter/setter in the class may exceed the given
66
+ * cyclomatic complexity.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * import { maxCyclomaticComplexity } from '@nielspeter/eess-ts/rules/metrics'
71
+ *
72
+ * classes(p).should().satisfy(maxCyclomaticComplexity(15)).check()
73
+ * ```
74
+ */
75
+ export function maxCyclomaticComplexity(threshold) {
76
+ return memberCeiling(threshold, {
77
+ description: `have no method with cyclomatic complexity > ${String(threshold)}`,
78
+ metric: 'complexity',
79
+ measure: (member) => cyclomaticComplexity(member.getBody()),
80
+ message: (name, cc) => `${name} has cyclomatic complexity ${String(cc)} (max: ${String(threshold)}) — split into smaller methods`,
81
+ });
82
+ }
52
83
  /**
53
84
  * No class may exceed the given number of CODE lines — comments and blank
54
85
  * lines are not counted (bug 0170).
@@ -93,29 +124,15 @@ export function maxClassLines(threshold) {
93
124
  * ```
94
125
  */
95
126
  export function maxMethodLines(threshold) {
96
- return {
127
+ return memberCeiling(threshold, {
97
128
  description: `have no method longer than ${String(threshold)} code lines`,
98
- evaluate(elements, context) {
99
- const violations = [];
100
- for (const cls of elements) {
101
- for (const member of getClassMembers(cls)) {
102
- const loc = linesOfCode(member);
103
- if (loc > threshold) {
104
- violations.push(metricViolation(member, {
105
- metric: 'lines',
106
- // `code-lines` since bug 0170 — the metric kept its name when it stopped
107
- // counting comments, and the baseline must not compare across that.
108
- unit: 'code-lines',
109
- measured: loc,
110
- qualifiedName: getMemberName(cls, member),
111
- message: `${getMemberName(cls, member)} has ${String(loc)} code lines (max: ${String(threshold)})`,
112
- }, context));
113
- }
114
- }
115
- }
116
- return violations;
117
- },
118
- };
129
+ metric: 'lines',
130
+ // `code-lines` since bug 0170 — the metric kept its name when it stopped
131
+ // counting comments, and the baseline must not compare across that.
132
+ unit: 'code-lines',
133
+ measure: (member) => linesOfCode(member),
134
+ message: (name, loc) => `${name} has ${String(loc)} code lines (max: ${String(threshold)})`,
135
+ });
119
136
  }
120
137
  /**
121
138
  * No class may have more than the given number of methods.
@@ -161,26 +178,12 @@ export function maxMethods(threshold) {
161
178
  * ```
162
179
  */
163
180
  export function maxParameters(threshold) {
164
- return {
181
+ return memberCeiling(threshold, {
165
182
  description: `have no method with more than ${String(threshold)} parameters`,
166
- evaluate(elements, context) {
167
- const violations = [];
168
- for (const cls of elements) {
169
- for (const member of getClassMembers(cls)) {
170
- const params = member.getParameters().length;
171
- if (params > threshold) {
172
- violations.push(metricViolation(member, {
173
- metric: 'parameters',
174
- measured: params,
175
- qualifiedName: getMemberName(cls, member),
176
- message: `${getMemberName(cls, member)} has ${String(params)} parameters (max: ${String(threshold)}) — use an options object`,
177
- }, context));
178
- }
179
- }
180
- }
181
- return violations;
182
- },
183
- };
183
+ metric: 'parameters',
184
+ measure: (member) => member.getParameters().length,
185
+ message: (name, params) => `${name} has ${String(params)} parameters (max: ${String(threshold)}) — use an options object`,
186
+ });
184
187
  }
185
188
  // Re-export function-level metric conditions from the same sub-path
186
189
  export { maxFunctionComplexity, maxFunctionLines, maxFunctionParameters, } from './metrics-function.js';
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/rules/metrics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/rules/metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import type { ArchFunction } from '../models/arch-function.js';
2
+ import type { SimilarPair } from './similar-pairs.js';
3
+ /**
4
+ * At least two bodies — a cluster is built from a pair, so it can never hold
5
+ * fewer.
6
+ *
7
+ * Typed rather than asserted so `clusterViolation` can be TOTAL. It used to
8
+ * return `ArchViolation | undefined` on a lookup that cannot fail, and the
9
+ * caller dropped that with `if (violation)` — a finding vanishing with no
10
+ * diagnostic. Architecture review of bug 0242 pointed out that the file
11
+ * rejected exactly that shape for `pairViolation` one screen above, with the
12
+ * argument that applies here word for word: unreachable today is not a defence,
13
+ * the type is what stops it becoming reachable.
14
+ */
15
+ type NonEmptyMembers = readonly [ArchFunction, ArchFunction, ...ArchFunction[]];
16
+ /** A group of bodies that are mutually reachable through reported similarity. */
17
+ export interface SimilarCluster {
18
+ /**
19
+ * Members, in first-seen order — i.e. the source walk's, which is a property
20
+ * of the filesystem.
21
+ *
22
+ * The first is NOT what the violation anchors to; it was until bug 0242, and
23
+ * this comment said so for as long. `clusterViolation` picks the anchor by
24
+ * path then line, and sorts the members it shows, precisely so neither depends
25
+ * on this order.
26
+ */
27
+ readonly members: NonEmptyMembers;
28
+ /** The pairs that put these members in one group. */
29
+ readonly pairs: readonly SimilarPair[];
30
+ /** Highest similarity among {@link pairs}. */
31
+ readonly peakSimilarity: number;
32
+ }
33
+ /**
34
+ * Group reported pairs into connected components.
35
+ *
36
+ * A pair is the wrong unit of report and it is not a close call. Measured on a
37
+ * ~5,600-file production monorepo: 407 clusters produced **4,770** pair
38
+ * findings, the eight largest clusters alone produced 49% of them, one cluster
39
+ * of 89 members produced 398, and the worst single function was named 29 times.
40
+ *
41
+ * Every one of those findings can be true and the output is still unreadable,
42
+ * because N mutually-similar bodies carry ONE observation and emit N^2/2 lines
43
+ * of it. The observation is "these 89 share a shape", which is also the unit a
44
+ * reader can act on. That is what this returns.
45
+ */
46
+ export declare function clusterPairs(pairs: readonly SimilarPair[]): SimilarCluster[];
47
+ /**
48
+ * How likely a cluster is to be worth acting on, highest first.
49
+ *
50
+ * Not a filter and deliberately not a score change — the similarity number is
51
+ * left exactly as it was. This decides only what a reader sees first, because at
52
+ * 4,770 findings the order IS the product.
53
+ *
54
+ * The signal is the one the fingerprint throws away: a body's identifiers are
55
+ * ignored by design (that is what makes the score a type-2 clone score), and so
56
+ * is the DECLARATION's own name, which is where the evidence was. Measured over
57
+ * that same corpus, bucketed by how the two names relate:
58
+ *
59
+ * | bucket | share | reading |
60
+ * | ----------------------------- | ----- | -------------------------- |
61
+ * | different file, different name | 56% | convergent idiom |
62
+ * | same class | 20% | siblings, often extractable |
63
+ * | different file, SAME name | 14% | a COPY of one function |
64
+ * | same file, different class | 10% | co-located |
65
+ *
66
+ * Two pairs from that run, both scored 100%: `timestampPrefix` in two files (a
67
+ * literal copy) and two unrelated SDK wrapper methods (a shared idiom). Same
68
+ * number on screen, opposite verdicts, and the thing that separates them costs
69
+ * nothing to compute.
70
+ *
71
+ * **It is a ranking and not a filter on purpose.** Dropping the different-name
72
+ * bucket would discard real same-class duplication — one repository in that
73
+ * corpus has six `exists*` methods differing only in a table name. Dropping
74
+ * cross-file would have discarded the finding that led to bug 0227.
75
+ */
76
+ export declare function clusterRank(cluster: SimilarCluster): number;
77
+ export {};
78
+ //# sourceMappingURL=clusters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clusters.d.ts","sourceRoot":"","sources":["../../src/smells/clusters.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAErD;;;;;;;;;;;GAWG;AACH,KAAK,eAAe,GAAG,SAAS,CAAC,YAAY,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE,CAAC,CAAA;AAE/E,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;IACjC,qDAAqD;IACrD,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IACtC,8CAA8C;IAC9C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAChC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,cAAc,EAAE,CAkE5E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAa3D"}
@@ -0,0 +1,124 @@
1
+ import { ArchConfigError } from '@nielspeter/eess';
2
+ /**
3
+ * Group reported pairs into connected components.
4
+ *
5
+ * A pair is the wrong unit of report and it is not a close call. Measured on a
6
+ * ~5,600-file production monorepo: 407 clusters produced **4,770** pair
7
+ * findings, the eight largest clusters alone produced 49% of them, one cluster
8
+ * of 89 members produced 398, and the worst single function was named 29 times.
9
+ *
10
+ * Every one of those findings can be true and the output is still unreadable,
11
+ * because N mutually-similar bodies carry ONE observation and emit N^2/2 lines
12
+ * of it. The observation is "these 89 share a shape", which is also the unit a
13
+ * reader can act on. That is what this returns.
14
+ */
15
+ export function clusterPairs(pairs) {
16
+ const parent = new Map();
17
+ const find = (x) => {
18
+ let root = parent.get(x) ?? x;
19
+ if (root === x) {
20
+ parent.set(x, x);
21
+ return x;
22
+ }
23
+ root = find(root);
24
+ parent.set(x, root);
25
+ return root;
26
+ };
27
+ const union = (a, b) => {
28
+ const ra = find(a);
29
+ const rb = find(b);
30
+ if (ra !== rb)
31
+ parent.set(ra, rb);
32
+ };
33
+ for (const pair of pairs)
34
+ union(pair.a, pair.b);
35
+ // First-seen order throughout, so the report is stable across runs. Map
36
+ // iteration order is insertion order, and `pairs` arrives in the order the
37
+ // comparison found them, which is the source-file walk order.
38
+ const byRoot = new Map();
39
+ const seen = new Set();
40
+ for (const pair of pairs) {
41
+ const root = find(pair.a);
42
+ let entry = byRoot.get(root);
43
+ if (!entry) {
44
+ entry = { members: [], pairs: [] };
45
+ byRoot.set(root, entry);
46
+ }
47
+ entry.pairs.push(pair);
48
+ for (const fn of [pair.a, pair.b]) {
49
+ if (seen.has(fn))
50
+ continue;
51
+ seen.add(fn);
52
+ entry.members.push(fn);
53
+ }
54
+ }
55
+ // Narrowed here, at the one place clusters are constructed, rather than at
56
+ // every place they are read. An entry exists only because a pair created it,
57
+ // and both of that pair's ends are pushed, so `members` always holds at least
58
+ // two — but the compiler cannot see that through `push`. A cluster that
59
+ // somehow held fewer is a broken invariant in THIS function, and dropping it
60
+ // silently at emission is what the type change exists to prevent, so it is
61
+ // reported as a configuration finding rather than filtered away.
62
+ return [...byRoot.values()].map((entry) => {
63
+ const [first, second, ...rest] = entry.members;
64
+ if (!first || !second) {
65
+ throw new ArchConfigError('duplicateBodies', 'a similarity cluster was built with fewer than two members, which cannot happen: ' +
66
+ 'every cluster is created from a pair and both of its ends are recorded. ' +
67
+ 'This is a defect in clusterPairs, not in the code being analysed.');
68
+ }
69
+ // Annotated, never cast (ADR-005): the annotation makes the compiler CHECK
70
+ // the literal is a valid `NonEmptyMembers`, where a cast would only assert
71
+ // it.
72
+ const members = [first, second, ...rest];
73
+ return {
74
+ members,
75
+ pairs: entry.pairs,
76
+ peakSimilarity: entry.pairs.reduce((best, p) => Math.max(best, p.similarity), 0),
77
+ };
78
+ });
79
+ }
80
+ /**
81
+ * How likely a cluster is to be worth acting on, highest first.
82
+ *
83
+ * Not a filter and deliberately not a score change — the similarity number is
84
+ * left exactly as it was. This decides only what a reader sees first, because at
85
+ * 4,770 findings the order IS the product.
86
+ *
87
+ * The signal is the one the fingerprint throws away: a body's identifiers are
88
+ * ignored by design (that is what makes the score a type-2 clone score), and so
89
+ * is the DECLARATION's own name, which is where the evidence was. Measured over
90
+ * that same corpus, bucketed by how the two names relate:
91
+ *
92
+ * | bucket | share | reading |
93
+ * | ----------------------------- | ----- | -------------------------- |
94
+ * | different file, different name | 56% | convergent idiom |
95
+ * | same class | 20% | siblings, often extractable |
96
+ * | different file, SAME name | 14% | a COPY of one function |
97
+ * | same file, different class | 10% | co-located |
98
+ *
99
+ * Two pairs from that run, both scored 100%: `timestampPrefix` in two files (a
100
+ * literal copy) and two unrelated SDK wrapper methods (a shared idiom). Same
101
+ * number on screen, opposite verdicts, and the thing that separates them costs
102
+ * nothing to compute.
103
+ *
104
+ * **It is a ranking and not a filter on purpose.** Dropping the different-name
105
+ * bucket would discard real same-class duplication — one repository in that
106
+ * corpus has six `exists*` methods differing only in a table name. Dropping
107
+ * cross-file would have discarded the finding that led to bug 0227.
108
+ */
109
+ export function clusterRank(cluster) {
110
+ const bare = (fn) => (fn.getName() ?? '<anonymous>').split('.').pop() ?? '';
111
+ const file = (fn) => fn.getSourceFile().getFilePath();
112
+ let best = 0;
113
+ for (const pair of cluster.pairs) {
114
+ const sameFile = file(pair.a) === file(pair.b);
115
+ const sameName = bare(pair.a) === bare(pair.b);
116
+ // A copy of one function into another file is the most actionable thing
117
+ // this detector can find, and the cheapest to verify.
118
+ const score = !sameFile && sameName ? 3 : sameFile ? 2 : 1;
119
+ if (score > best)
120
+ best = score;
121
+ }
122
+ return best;
123
+ }
124
+ //# sourceMappingURL=clusters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clusters.js","sourceRoot":"","sources":["../../src/smells/clusters.ts"],"names":[],"mappings":"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"}
@@ -65,13 +65,47 @@ export declare class DuplicateBodiesBuilder extends SmellBuilder {
65
65
  private collectFilteredFunctions;
66
66
  /** Build violations from similar pairs. */
67
67
  /**
68
- * Report order: by folder when `.groupByFolder()` asked for it, otherwise the
69
- * order the pairs were found in.
68
+ * Report order for pairs — which, measured, never has anything to order.
70
69
  *
71
- * Extracted from {@link buildViolations} so that method is the violation
72
- * shape and nothing else — presentation order is a separate decision.
70
+ * Extracted from {@link buildViolations} so that method is the violation shape
71
+ * and nothing else. Enforcement review of bug 0242 read this as a live defect
72
+ * (keyed on `a`, i.e. walk order, while the finding reports at the anchor).
73
+ * Probing it says otherwise: {@link buildViolations} is reached only from
74
+ * {@link buildClusterViolations} for a cluster of two or fewer members, and
75
+ * such a cluster carries exactly one pair. Instrumented to throw on more than
76
+ * one, the whole `packages/ts` suite (3604 tests) produced **zero** hits.
77
+ *
78
+ * So this cannot reorder anything today, and {@link orderedClusters} is what
79
+ * actually orders the output. The key is corrected anyway — being right if
80
+ * this ever becomes reachable costs one expression — but the correction fixes
81
+ * no observable behaviour, and saying otherwise would be the kind of unmeasured
82
+ * claim this repo keeps catching.
73
83
  */
74
84
  private orderedPairs;
85
+ /**
86
+ * One violation per CLUSTER of mutually-similar bodies, not per pair.
87
+ *
88
+ * A pair is the wrong unit and it is not a close call. Measured on a
89
+ * ~5,600-file production monorepo: 407 clusters emitted **4,770** pair
90
+ * findings — more findings than the 3,810 bodies that produced them. The
91
+ * eight largest clusters were 49% of the output, one cluster of 89 members
92
+ * emitted 398 lines, and the worst single function was named 29 times. Every
93
+ * one of those can be TRUE and the report is still unreadable, because N
94
+ * mutually-similar bodies carry one observation and emit N^2/2 lines of it.
95
+ *
96
+ * Two-member clusters — the common case — keep the exact message and identity
97
+ * they had before, so existing baselines for them are untouched. Only a group
98
+ * of three or more collapses, which is where the inflation lives.
99
+ */
100
+ /**
101
+ * Report order.
102
+ *
103
+ * Rank is presentation only — no pair is dropped and no score changes. At four
104
+ * thousand findings the ORDER is the product: what a reader sees first decides
105
+ * whether they read at all. `.groupByFolder()` still wins when asked for.
106
+ */
107
+ private orderedClusters;
108
+ private buildClusterViolations;
75
109
  private buildViolations;
76
110
  }
77
111
  //# sourceMappingURL=duplicate-bodies.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"duplicate-bodies.d.ts","sourceRoot":"","sources":["../../src/smells/duplicate-bodies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAIvD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAIjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AASrD,qBAAa,sBAAuB,SAAQ,YAAY;IACtD,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,sBAAsB,CAAI;gBAEtB,OAAO,EAAE,WAAW;IAIhC,uDAAuD;IACvD,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAM1C;;;;;;;;;OASG;IACH,qBAAqB,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAMtC;;;;;;;;;;;;OAYG;IACM,YAAY,IAAI,eAAe;IAOxC,SAAS,CAAC,MAAM,IAAI,aAAa,EAAE;IAOnC,SAAS,CAAC,QAAQ,IAAI,MAAM;IAqB5B,0DAA0D;IAC1D;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAoBzB,6DAA6D;IAC7D,OAAO,CAAC,aAAa;IAOrB,+DAA+D;IAC/D;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,QAAQ;IAIhB,gDAAgD;IAChD,aAAa,IAAI,MAAM;IAIvB,OAAO,CAAC,wBAAwB;IAmChC,2CAA2C;IAC3C;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,eAAe;CA2CxB"}
1
+ {"version":3,"file":"duplicate-bodies.d.ts","sourceRoot":"","sources":["../../src/smells/duplicate-bodies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAIvD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAOjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AASrD,qBAAa,sBAAuB,SAAQ,YAAY;IACtD,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,sBAAsB,CAAI;gBAEtB,OAAO,EAAE,WAAW;IAIhC,uDAAuD;IACvD,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAM1C;;;;;;;;;OASG;IACH,qBAAqB,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAMtC;;;;;;;;;;;;OAYG;IACM,YAAY,IAAI,eAAe;IAOxC,SAAS,CAAC,MAAM,IAAI,aAAa,EAAE;IAOnC,SAAS,CAAC,QAAQ,IAAI,MAAM;IAqB5B,0DAA0D;IAC1D;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAoBzB,6DAA6D;IAC7D,OAAO,CAAC,aAAa;IAOrB,+DAA+D;IAC/D;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,QAAQ;IAIhB,gDAAgD;IAChD,aAAa,IAAI,MAAM;IAIvB,OAAO,CAAC,wBAAwB;IAmChC,2CAA2C;IAC3C;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,YAAY;IAepB;;;;;;;;;;;;;;OAcG;IACH;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IA8BvB,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,eAAe;CAIxB"}
@@ -1,9 +1,11 @@
1
1
  import picomatch from 'picomatch';
2
2
  import path from 'node:path';
3
- import { selectionMemo } from '@nielspeter/eess';
3
+ import { selectionMemo } from '@nielspeter/eess/internal';
4
4
  import { SmellBuilder } from './smell-builder.js';
5
5
  import { collectFunctions } from '../models/arch-function.js';
6
6
  import { fingerprintAll, findSimilarPairs } from './similar-pairs.js';
7
+ import { anchorFile, clusterViolation, pairViolation } from './duplicate-report.js';
8
+ import { clusterPairs, clusterRank } from './clusters.js';
7
9
  import { relativeToRoot } from '../core/project-relative.js';
8
10
  /** Test file patterns for ignoreTests(). */
9
11
  const TEST_PATTERNS = ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**'];
@@ -58,7 +60,7 @@ export class DuplicateBodiesBuilder extends SmellBuilder {
58
60
  const functions = this.selected();
59
61
  const fingerprinted = fingerprintAll(functions);
60
62
  const pairs = findSimilarPairs(fingerprinted, this._minSimilarity, this._minDistinctVocabulary);
61
- return this.buildViolations(pairs);
63
+ return this.buildClusterViolations(pairs);
62
64
  }
63
65
  describe() {
64
66
  const scope = this._folders.length > 0 ? this._folders.join(', ') : 'all files';
@@ -155,58 +157,105 @@ export class DuplicateBodiesBuilder extends SmellBuilder {
155
157
  }
156
158
  /** Build violations from similar pairs. */
157
159
  /**
158
- * Report order: by folder when `.groupByFolder()` asked for it, otherwise the
159
- * order the pairs were found in.
160
+ * Report order for pairs — which, measured, never has anything to order.
160
161
  *
161
- * Extracted from {@link buildViolations} so that method is the violation
162
- * shape and nothing else — presentation order is a separate decision.
162
+ * Extracted from {@link buildViolations} so that method is the violation shape
163
+ * and nothing else. Enforcement review of bug 0242 read this as a live defect
164
+ * (keyed on `a`, i.e. walk order, while the finding reports at the anchor).
165
+ * Probing it says otherwise: {@link buildViolations} is reached only from
166
+ * {@link buildClusterViolations} for a cluster of two or fewer members, and
167
+ * such a cluster carries exactly one pair. Instrumented to throw on more than
168
+ * one, the whole `packages/ts` suite (3604 tests) produced **zero** hits.
169
+ *
170
+ * So this cannot reorder anything today, and {@link orderedClusters} is what
171
+ * actually orders the output. The key is corrected anyway — being right if
172
+ * this ever becomes reachable costs one expression — but the correction fixes
173
+ * no observable behaviour, and saying otherwise would be the kind of unmeasured
174
+ * claim this repo keeps catching.
163
175
  */
164
176
  orderedPairs(pairs) {
165
177
  if (!this._groupByFolder)
166
178
  return pairs;
167
- return [...pairs].sort((x, y) => {
168
- const folderA = path.dirname(x.a.getSourceFile().getFilePath());
169
- const folderB = path.dirname(y.a.getSourceFile().getFilePath());
170
- return folderA.localeCompare(folderB);
179
+ // Keyed on the ANCHOR's folder, not on `a`'s. Which endpoint is `a` is the
180
+ // source walk; the anchor is where the finding says it is (bug 0242). While
181
+ // those agreed this was invisible, and after the anchor moved a finding
182
+ // reported in one folder would sort into another folder's group — the one
183
+ // thing this option exists to prevent.
184
+ const keyed = pairs.map((pair) => ({
185
+ pair,
186
+ folder: path.dirname(anchorFile([pair.a, pair.b])),
187
+ }));
188
+ keyed.sort((x, y) => (x.folder < y.folder ? -1 : x.folder > y.folder ? 1 : 0));
189
+ return keyed.map((k) => k.pair);
190
+ }
191
+ /**
192
+ * One violation per CLUSTER of mutually-similar bodies, not per pair.
193
+ *
194
+ * A pair is the wrong unit and it is not a close call. Measured on a
195
+ * ~5,600-file production monorepo: 407 clusters emitted **4,770** pair
196
+ * findings — more findings than the 3,810 bodies that produced them. The
197
+ * eight largest clusters were 49% of the output, one cluster of 89 members
198
+ * emitted 398 lines, and the worst single function was named 29 times. Every
199
+ * one of those can be TRUE and the report is still unreadable, because N
200
+ * mutually-similar bodies carry one observation and emit N^2/2 lines of it.
201
+ *
202
+ * Two-member clusters — the common case — keep the exact message and identity
203
+ * they had before, so existing baselines for them are untouched. Only a group
204
+ * of three or more collapses, which is where the inflation lives.
205
+ */
206
+ /**
207
+ * Report order.
208
+ *
209
+ * Rank is presentation only — no pair is dropped and no score changes. At four
210
+ * thousand findings the ORDER is the product: what a reader sees first decides
211
+ * whether they read at all. `.groupByFolder()` still wins when asked for.
212
+ */
213
+ orderedClusters(clusters) {
214
+ // Decorated once, then sorted. `anchorFile` asks ts-morph for a path and a
215
+ // line per member, and a comparator runs O(n log n) times — on the 407
216
+ // clusters this module's own comments measure, computing it inside the
217
+ // comparator is tens of thousands of avoidable calls.
218
+ const keyed = clusters.map((cluster) => {
219
+ // The ANCHOR's folder, not `members[0]`'s: `members[0]` is the source walk
220
+ // and the finding is reported at the path-then-line minimum (bug 0242), so
221
+ // grouping on the walk would file a finding under a folder it is not
222
+ // reported in — the one thing `.groupByFolder()` exists to prevent.
223
+ const anchor = anchorFile(cluster.members);
224
+ return { cluster, anchor, folder: path.dirname(anchor), rank: clusterRank(cluster) };
225
+ });
226
+ // Plain `<`, not `localeCompare`: the latter's result depends on the
227
+ // runtime's ICU build and default locale, which is this record's own failure
228
+ // class — a report that reads differently on two machines — in the
229
+ // expression fixing it. `comparePositions` next door uses `<` too.
230
+ const text = (x, y) => (x < y ? -1 : x > y ? 1 : 0);
231
+ keyed.sort((x, y) => {
232
+ const primary = this._groupByFolder ? text(x.folder, y.folder) : y.rank - x.rank;
233
+ // The tie-break is what makes the SEQUENCE deterministic, and without it
234
+ // the default path had none: `clusterRank` returns one of four values over
235
+ // hundreds of clusters, and `Array#sort` is stable, so equal ranks kept
236
+ // walk order. Found by architecture review of bug 0242's fix; the branch
237
+ // had disclosed it in a test comment and nowhere else.
238
+ return primary !== 0 ? primary : text(x.anchor, y.anchor);
171
239
  });
240
+ return keyed.map((k) => k.cluster);
172
241
  }
173
- buildViolations(pairs) {
174
- const ruleDescription = this.describe();
242
+ buildClusterViolations(pairs) {
175
243
  const violations = [];
176
- for (const pair of this.orderedPairs(pairs)) {
177
- const nameA = pair.a.getName() ?? '<anonymous>';
178
- const fileA = pair.a.getSourceFile().getFilePath();
179
- const lineA = pair.a.getStartLineNumber();
180
- const nameB = pair.b.getName() ?? '<anonymous>';
181
- const fileB = pair.b.getSourceFile().getFilePath();
182
- const lineB = pair.b.getStartLineNumber();
183
- const pct = Math.round(pair.similarity * 100);
184
- violations.push({
185
- rule: ruleDescription,
186
- element: nameA,
187
- file: fileA,
188
- line: lineA,
189
- message: `${nameA} (${fileA}:${String(lineA)}) is ${String(pct)}% similar to ${nameB} (${fileB}:${String(lineB)})`,
190
- // Which endpoint is "a" comes from the source-file walk order, which is
191
- // a property of the filesystem: the same pair reports A→B on one
192
- // machine and B→A on another, and the reported message alone would
193
- // give them different identities. Sort the endpoints so the pair reads
194
- // the same either way. Qualified by path — a bare function name is not
195
- // unique across files — and without the similarity percentage, which
196
- // drifts as either body is edited.
197
- //
198
- // Limitation: two anonymous functions in one file share an endpoint
199
- // (`<file>#<anonymous>`) and so share an identity. Nothing stable
200
- // distinguishes them — a line number would, and that is the coordinate
201
- // dependence being removed. Measured at 0 collisions over 1006 findings
202
- // on a real codebase; the collision guard in
203
- // `tests/integration/baseline-portability.test.ts` is what would catch
204
- // it becoming common.
205
- identity: `duplicate-pair::${[`${fileA}#${nameA}`, `${fileB}#${nameB}`].sort().join('::')}`,
206
- because: this._reason,
207
- });
244
+ for (const cluster of this.orderedClusters(clusterPairs(pairs))) {
245
+ // A two-member cluster IS the pair, so it keeps the message and the
246
+ // baseline identity that already shipped. Only three-or-more collapses,
247
+ // which is where the inflation lives.
248
+ if (cluster.members.length <= 2) {
249
+ violations.push(...this.buildViolations(cluster.pairs));
250
+ continue;
251
+ }
252
+ violations.push(clusterViolation(cluster, { rule: this.describe(), because: this._reason }));
208
253
  }
209
254
  return violations;
210
255
  }
256
+ buildViolations(pairs) {
257
+ const context = { rule: this.describe(), because: this._reason };
258
+ return this.orderedPairs([...pairs]).map((pair) => pairViolation(pair, context));
259
+ }
211
260
  }
212
261
  //# sourceMappingURL=duplicate-bodies.js.map