@nielspeter/eess-ts 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builders/call-rule-builder.d.ts.map +1 -1
- package/dist/builders/call-rule-builder.js +2 -1
- package/dist/builders/call-rule-builder.js.map +1 -1
- package/dist/builders/class-rule-builder.d.ts.map +1 -1
- package/dist/builders/class-rule-builder.js +7 -25
- package/dist/builders/class-rule-builder.js.map +1 -1
- package/dist/builders/correspondence-builder.d.ts.map +1 -1
- package/dist/builders/correspondence-builder.js +6 -14
- package/dist/builders/correspondence-builder.js.map +1 -1
- package/dist/builders/cross-layer-builder.d.ts.map +1 -1
- package/dist/builders/cross-layer-builder.js +7 -6
- package/dist/builders/cross-layer-builder.js.map +1 -1
- package/dist/builders/function-rule-builder.d.ts.map +1 -1
- package/dist/builders/function-rule-builder.js +4 -13
- package/dist/builders/function-rule-builder.js.map +1 -1
- package/dist/builders/module-rule-builder.d.ts.map +1 -1
- package/dist/builders/module-rule-builder.js +4 -16
- package/dist/builders/module-rule-builder.js.map +1 -1
- package/dist/builders/slice-rule-builder.d.ts.map +1 -1
- package/dist/builders/slice-rule-builder.js +4 -3
- package/dist/builders/slice-rule-builder.js.map +1 -1
- package/dist/builders/type-rule-builder.d.ts.map +1 -1
- package/dist/builders/type-rule-builder.js +4 -13
- package/dist/builders/type-rule-builder.js.map +1 -1
- package/dist/cli/commands/baseline.d.ts +5 -1
- package/dist/cli/commands/baseline.d.ts.map +1 -1
- package/dist/cli/commands/baseline.js +47 -5
- package/dist/cli/commands/baseline.js.map +1 -1
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +155 -13
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/explain.js +1 -1
- package/dist/cli/commands/explain.js.map +1 -1
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/import-rule-module.d.ts.map +1 -1
- package/dist/cli/import-rule-module.js +1 -14
- package/dist/cli/import-rule-module.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -11
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/load-rules.d.ts.map +1 -1
- package/dist/cli/load-rules.js +5 -4
- package/dist/cli/load-rules.js.map +1 -1
- package/dist/cli/resolve-config.d.ts +0 -8
- package/dist/cli/resolve-config.d.ts.map +1 -1
- package/dist/cli/resolve-config.js +18 -48
- package/dist/cli/resolve-config.js.map +1 -1
- package/dist/cli/rule-file-findings.d.ts.map +1 -1
- package/dist/cli/rule-file-findings.js +60 -2
- package/dist/cli/rule-file-findings.js.map +1 -1
- package/dist/cli/watch.d.ts +13 -43
- package/dist/cli/watch.d.ts.map +1 -1
- package/dist/cli/watch.js +8 -148
- package/dist/cli/watch.js.map +1 -1
- package/dist/conditions/call.d.ts.map +1 -1
- package/dist/conditions/call.js +4 -3
- package/dist/conditions/call.js.map +1 -1
- package/dist/conditions/cross-layer.js +2 -2
- package/dist/conditions/cross-layer.js.map +1 -1
- package/dist/conditions/dependency.js +2 -2
- package/dist/conditions/dependency.js.map +1 -1
- package/dist/conditions/exports.d.ts.map +1 -1
- package/dist/conditions/exports.js +31 -37
- package/dist/conditions/exports.js.map +1 -1
- package/dist/conditions/function.js +1 -1
- package/dist/conditions/function.js.map +1 -1
- package/dist/conditions/jsx.js +1 -1
- package/dist/conditions/jsx.js.map +1 -1
- package/dist/conditions/members.d.ts.map +1 -1
- package/dist/conditions/members.js +3 -2
- package/dist/conditions/members.js.map +1 -1
- package/dist/conditions/reverse-dependency.d.ts.map +1 -1
- package/dist/conditions/reverse-dependency.js +47 -2
- package/dist/conditions/reverse-dependency.js.map +1 -1
- package/dist/conditions/slice.js +1 -1
- package/dist/conditions/slice.js.map +1 -1
- package/dist/conditions/structural.js +1 -1
- package/dist/conditions/structural.js.map +1 -1
- package/dist/core/check-all.d.ts.map +1 -1
- package/dist/core/check-all.js +61 -7
- package/dist/core/check-all.js.map +1 -1
- package/dist/core/combinators.d.ts.map +1 -1
- package/dist/core/combinators.js +20 -27
- package/dist/core/combinators.js.map +1 -1
- package/dist/core/descendant-cache.d.ts.map +1 -1
- package/dist/core/descendant-cache.js +5 -7
- package/dist/core/descendant-cache.js.map +1 -1
- package/dist/core/diagnose.js +15 -7
- package/dist/core/diagnose.js.map +1 -1
- package/dist/core/disk-set.js +1 -1
- package/dist/core/disk-set.js.map +1 -1
- package/dist/core/element-cache.js +1 -1
- package/dist/core/element-cache.js.map +1 -1
- package/dist/core/exclusion-comments.d.ts +15 -50
- package/dist/core/exclusion-comments.d.ts.map +1 -1
- package/dist/core/exclusion-comments.js +4 -307
- package/dist/core/exclusion-comments.js.map +1 -1
- package/dist/core/execute-rule.d.ts +8 -0
- package/dist/core/execute-rule.d.ts.map +1 -1
- package/dist/core/execute-rule.js +99 -13
- package/dist/core/execute-rule.js.map +1 -1
- package/dist/core/glob-evaluator.js +2 -2
- package/dist/core/glob-evaluator.js.map +1 -1
- package/dist/core/invalidate-on-modify.d.ts +20 -0
- package/dist/core/invalidate-on-modify.d.ts.map +1 -0
- package/dist/core/invalidate-on-modify.js +26 -0
- package/dist/core/invalidate-on-modify.js.map +1 -0
- package/dist/core/line-index.d.ts.map +1 -1
- package/dist/core/line-index.js +6 -8
- package/dist/core/line-index.js.map +1 -1
- package/dist/core/metric-violation.d.ts +1 -1
- package/dist/core/module-edges.d.ts.map +1 -1
- package/dist/core/module-edges.js +26 -25
- package/dist/core/module-edges.js.map +1 -1
- package/dist/core/orphan-exclusions.d.ts.map +1 -1
- package/dist/core/orphan-exclusions.js +15 -5
- package/dist/core/orphan-exclusions.js.map +1 -1
- package/dist/core/path-universe.d.ts +10 -42
- package/dist/core/path-universe.d.ts.map +1 -1
- package/dist/core/path-universe.js +0 -20
- package/dist/core/path-universe.js.map +1 -1
- package/dist/core/project.d.ts.map +1 -1
- package/dist/core/project.js +6 -5
- package/dist/core/project.js.map +1 -1
- package/dist/core/rule-builder.d.ts +22 -1
- package/dist/core/rule-builder.d.ts.map +1 -1
- package/dist/core/rule-builder.js +38 -20
- package/dist/core/rule-builder.js.map +1 -1
- package/dist/core/rule-declaration.d.ts.map +1 -1
- package/dist/core/rule-declaration.js +14 -52
- package/dist/core/rule-declaration.js.map +1 -1
- package/dist/core/terminal-builder.d.ts +17 -13
- package/dist/core/terminal-builder.d.ts.map +1 -1
- package/dist/core/terminal-builder.js +41 -21
- package/dist/core/terminal-builder.js.map +1 -1
- package/dist/core/terminal-execution.d.ts +22 -6
- package/dist/core/terminal-execution.d.ts.map +1 -1
- package/dist/core/terminal-execution.js +39 -7
- package/dist/core/terminal-execution.js.map +1 -1
- package/dist/core/ts-comment-mask.d.ts +2 -0
- package/dist/core/ts-comment-mask.d.ts.map +1 -0
- package/dist/core/ts-comment-mask.js +102 -0
- package/dist/core/ts-comment-mask.js.map +1 -0
- package/dist/core/vacuity-diagnosis.js +1 -1
- package/dist/core/vacuity-diagnosis.js.map +1 -1
- package/dist/graphql/graphql-rule-builder.d.ts +95 -0
- package/dist/graphql/graphql-rule-builder.d.ts.map +1 -0
- package/dist/graphql/graphql-rule-builder.js +127 -0
- package/dist/graphql/graphql-rule-builder.js.map +1 -0
- package/dist/graphql/index.d.ts +1 -0
- package/dist/graphql/index.d.ts.map +1 -1
- package/dist/graphql/index.js +1 -0
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/resolver-rule-builder.d.ts +22 -76
- package/dist/graphql/resolver-rule-builder.d.ts.map +1 -1
- package/dist/graphql/resolver-rule-builder.js +8 -130
- package/dist/graphql/resolver-rule-builder.js.map +1 -1
- package/dist/graphql/schema-loader.d.ts.map +1 -1
- package/dist/graphql/schema-loader.js +4 -3
- package/dist/graphql/schema-loader.js.map +1 -1
- package/dist/graphql/schema-predicates.d.ts.map +1 -1
- package/dist/graphql/schema-predicates.js +17 -12
- package/dist/graphql/schema-predicates.js.map +1 -1
- package/dist/graphql/schema-rule-builder.d.ts +21 -67
- package/dist/graphql/schema-rule-builder.d.ts.map +1 -1
- package/dist/graphql/schema-rule-builder.js +6 -120
- package/dist/graphql/schema-rule-builder.js.map +1 -1
- package/dist/helpers/baseline-generator.d.ts +9 -0
- package/dist/helpers/baseline-generator.d.ts.map +1 -1
- package/dist/helpers/baseline-generator.js +9 -0
- package/dist/helpers/baseline-generator.js.map +1 -1
- package/dist/helpers/baseline.d.ts.map +1 -1
- package/dist/helpers/baseline.js +4 -4
- package/dist/helpers/baseline.js.map +1 -1
- package/dist/helpers/body-traversal.d.ts.map +1 -1
- package/dist/helpers/body-traversal.js +13 -0
- package/dist/helpers/body-traversal.js.map +1 -1
- package/dist/helpers/callback-extractor.js +6 -38
- package/dist/helpers/callback-extractor.js.map +1 -1
- package/dist/helpers/complexity.d.ts +1 -1
- package/dist/helpers/complexity.js +1 -1
- package/dist/helpers/diff-aware.d.ts +11 -39
- package/dist/helpers/diff-aware.d.ts.map +1 -1
- package/dist/helpers/diff-aware.js +11 -79
- package/dist/helpers/diff-aware.js.map +1 -1
- package/dist/helpers/matchers.d.ts.map +1 -1
- package/dist/helpers/matchers.js +51 -67
- package/dist/helpers/matchers.js.map +1 -1
- package/dist/index.d.ts +15 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -18
- package/dist/index.js.map +1 -1
- package/dist/models/arch-function.d.ts +26 -2
- package/dist/models/arch-function.d.ts.map +1 -1
- package/dist/models/arch-function.js +51 -17
- package/dist/models/arch-function.js.map +1 -1
- package/dist/models/arch-jsx-element.d.ts.map +1 -1
- package/dist/models/arch-jsx-element.js +16 -17
- package/dist/models/arch-jsx-element.js.map +1 -1
- package/dist/predicates/jsx.d.ts.map +1 -1
- package/dist/predicates/jsx.js +2 -1
- package/dist/predicates/jsx.js.map +1 -1
- package/dist/presets/agent-guardrails.d.ts +29 -3
- package/dist/presets/agent-guardrails.d.ts.map +1 -1
- package/dist/presets/agent-guardrails.js +185 -1
- package/dist/presets/agent-guardrails.js.map +1 -1
- package/dist/presets/boundaries.d.ts +2 -2
- package/dist/presets/boundaries.d.ts.map +1 -1
- package/dist/presets/data-layer.d.ts +2 -2
- package/dist/presets/data-layer.d.ts.map +1 -1
- package/dist/presets/index.d.ts +4 -1
- package/dist/presets/index.d.ts.map +1 -1
- package/dist/presets/index.js +11 -1
- package/dist/presets/index.js.map +1 -1
- package/dist/presets/layered.d.ts +2 -2
- package/dist/presets/layered.d.ts.map +1 -1
- package/dist/presets/recommended.d.ts +3 -3
- package/dist/presets/recommended.d.ts.map +1 -1
- package/dist/presets/shared.d.ts +8 -11
- package/dist/presets/shared.d.ts.map +1 -1
- package/dist/presets/shared.js +57 -28
- package/dist/presets/shared.js.map +1 -1
- package/dist/rules/metrics-function.d.ts.map +1 -1
- package/dist/rules/metrics-function.js +59 -62
- package/dist/rules/metrics-function.js.map +1 -1
- package/dist/rules/metrics.d.ts.map +1 -1
- package/dist/rules/metrics.js +62 -59
- package/dist/rules/metrics.js.map +1 -1
- package/dist/smells/clusters.d.ts +78 -0
- package/dist/smells/clusters.d.ts.map +1 -0
- package/dist/smells/clusters.js +124 -0
- package/dist/smells/clusters.js.map +1 -0
- package/dist/smells/duplicate-bodies.d.ts +38 -4
- package/dist/smells/duplicate-bodies.d.ts.map +1 -1
- package/dist/smells/duplicate-bodies.js +93 -44
- package/dist/smells/duplicate-bodies.js.map +1 -1
- package/dist/smells/duplicate-report.d.ts +90 -0
- package/dist/smells/duplicate-report.d.ts.map +1 -0
- package/dist/smells/duplicate-report.js +344 -0
- package/dist/smells/duplicate-report.js.map +1 -0
- package/dist/smells/fingerprint.d.ts +12 -0
- package/dist/smells/fingerprint.d.ts.map +1 -1
- package/dist/smells/fingerprint.js +8 -2
- package/dist/smells/fingerprint.js.map +1 -1
- package/dist/smells/inconsistent-siblings.js +1 -1
- package/dist/smells/inconsistent-siblings.js.map +1 -1
- package/dist/smells/similar-pairs.d.ts +9 -8
- package/dist/smells/similar-pairs.d.ts.map +1 -1
- package/dist/smells/similar-pairs.js +83 -1
- package/dist/smells/similar-pairs.js.map +1 -1
- package/dist/smells/smell-builder.d.ts.map +1 -1
- package/dist/smells/smell-builder.js +3 -2
- package/dist/smells/smell-builder.js.map +1 -1
- package/dist/smells/variation.d.ts +47 -0
- package/dist/smells/variation.d.ts.map +1 -0
- package/dist/smells/variation.js +90 -0
- package/dist/smells/variation.js.map +1 -0
- package/dist/tsconfig/tsconfig-builder.d.ts.map +1 -1
- package/dist/tsconfig/tsconfig-builder.js +3 -2
- package/dist/tsconfig/tsconfig-builder.js.map +1 -1
- package/package.json +3 -2
package/dist/rules/metrics.js
CHANGED
|
@@ -17,38 +17,69 @@ function getMemberName(cls, member) {
|
|
|
17
17
|
return `${clsName}.constructor`;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
* cyclomatic complexity.
|
|
20
|
+
* A ceiling on one measurement of every callable member of every class.
|
|
22
21
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
34
|
+
function memberCeiling(threshold, spec) {
|
|
31
35
|
return {
|
|
32
|
-
description:
|
|
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
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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":"AASA,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAS7D,SAAS,eAAe,CAAC,GAAqB;IAC5C,OAAO;QACL,GAAG,GAAG,CAAC,UAAU,EAAE;QACnB,GAAG,GAAG,CAAC,eAAe,EAAE;QACxB,GAAG,GAAG,CAAC,eAAe,EAAE;QACxB,GAAG,GAAG,CAAC,eAAe,EAAE;KACzB,CAAA;AACH,CAAC;AAED,SAAS,aAAa,CAAC,GAAqB,EAAE,MAAmB;IAC/D,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,EAAE,IAAI,aAAa,CAAA;IAC9C,IAAI,SAAS,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QAChE,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;QAC3C,OAAO,GAAG,OAAO,IAAI,UAAU,EAAE,CAAA;IACnC,CAAC;IACD,OAAO,GAAG,OAAO,cAAc,CAAA;AACjC,CAAC;AAED
|
|
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
|
|
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
|
-
*
|
|
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;
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
const ruleDescription = this.describe();
|
|
242
|
+
buildClusterViolations(pairs) {
|
|
175
243
|
const violations = [];
|
|
176
|
-
for (const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|