@eduardbar/drift 0.6.0 → 0.8.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/CHANGELOG.md +22 -0
- package/assets/og.svg +105 -105
- package/dist/analyzer.d.ts +5 -1
- package/dist/analyzer.js +143 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/packages/eslint-plugin-drift/README.md +88 -0
- package/packages/eslint-plugin-drift/package-lock.json +1250 -0
- package/packages/eslint-plugin-drift/package.json +47 -0
- package/packages/eslint-plugin-drift/src/index.ts +170 -0
- package/packages/eslint-plugin-drift/tsconfig.json +17 -0
- package/src/analyzer.ts +164 -1
- package/src/index.ts +2 -2
- package/src/printer.ts +60 -60
- package/src/types.ts +15 -15
- package/src/utils.ts +35 -35
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,28 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
+
## [0.8.0] - 2026-02-24
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `semantic-duplication` rule — Type-2 AST clone detection via SHA-256 fingerprinting
|
|
25
|
+
- Normalizes parameter names, local variable names, and literals before hashing — detects identical logic with different variable names
|
|
26
|
+
- Runs cross-file across the entire project; reports each duplicate pointing to all other locations
|
|
27
|
+
- Minimum threshold: functions with ≥ 8 body lines (reduces noise from trivial helpers)
|
|
28
|
+
- Skips test framework helpers (describe, it, test, beforeEach, afterEach)
|
|
29
|
+
- RULE_WEIGHTS entry: severity `warning`, weight `12`
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## [0.7.0] - 2026-02-24
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- `eslint-plugin-drift` — separate npm package exposing all 26 drift rules as ESLint 9 flat config rules
|
|
37
|
+
- Each rule wraps drift's AST engine via `analyzeFile()` with a shared ts-morph `Project` instance
|
|
38
|
+
- Per-file result cache (max 100 entries) to prevent redundant analysis in watch mode
|
|
39
|
+
- `recommended` config array enabling all 26 rules at their canonical drift severity
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
21
43
|
## [0.5.0] — 2026-02-24
|
|
22
44
|
|
|
23
45
|
### Added
|
package/assets/og.svg
CHANGED
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
-
<stop offset="0%" style="stop-color:#0a0a0f"/>
|
|
5
|
-
<stop offset="100%" style="stop-color:#0f0f1a"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
8
|
-
<stop offset="0%" style="stop-color:#6366f1"/>
|
|
9
|
-
<stop offset="100%" style="stop-color:#8b5cf6"/>
|
|
10
|
-
</linearGradient>
|
|
11
|
-
<linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
12
|
-
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:0.15"/>
|
|
13
|
-
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:0.05"/>
|
|
14
|
-
</linearGradient>
|
|
15
|
-
<filter id="blur-glow">
|
|
16
|
-
<feGaussianBlur stdDeviation="40" result="blur"/>
|
|
17
|
-
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
18
|
-
</filter>
|
|
19
|
-
<filter id="soft-glow">
|
|
20
|
-
<feGaussianBlur stdDeviation="3" result="blur"/>
|
|
21
|
-
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
22
|
-
</filter>
|
|
23
|
-
</defs>
|
|
24
|
-
|
|
25
|
-
<!-- Background -->
|
|
26
|
-
<rect width="1200" height="630" fill="url(#bg)"/>
|
|
27
|
-
|
|
28
|
-
<!-- Ambient glow top-left -->
|
|
29
|
-
<ellipse cx="200" cy="200" rx="300" ry="250" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.8"/>
|
|
30
|
-
|
|
31
|
-
<!-- Ambient glow bottom-right -->
|
|
32
|
-
<ellipse cx="1050" cy="480" rx="280" ry="220" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.6"/>
|
|
33
|
-
|
|
34
|
-
<!-- Grid lines subtle -->
|
|
35
|
-
<line x1="0" y1="210" x2="1200" y2="210" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
36
|
-
<line x1="0" y1="420" x2="1200" y2="420" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
37
|
-
<line x1="300" y1="0" x2="300" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
38
|
-
<line x1="900" y1="0" x2="900" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
39
|
-
|
|
40
|
-
<!-- Left accent bar -->
|
|
41
|
-
<rect x="72" y="180" width="4" height="270" rx="2" fill="url(#accent)"/>
|
|
42
|
-
|
|
43
|
-
<!-- Main title "drift" -->
|
|
44
|
-
<text x="102" y="310" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', monospace" font-size="120" font-weight="700" fill="#ffffff" letter-spacing="-4">drift</text>
|
|
45
|
-
|
|
46
|
-
<!-- Subtitle -->
|
|
47
|
-
<text x="104" y="370" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="24" font-weight="400" fill="#94a3b8" letter-spacing="0.5">Detect silent technical debt left by AI-generated code.</text>
|
|
48
|
-
|
|
49
|
-
<!-- Terminal block right side -->
|
|
50
|
-
<rect x="680" y="165" width="448" height="300" rx="12" fill="#0d0d18" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
|
|
51
|
-
|
|
52
|
-
<!-- Terminal header bar -->
|
|
53
|
-
<rect x="680" y="165" width="448" height="40" rx="12" fill="#1a1a2e"/>
|
|
54
|
-
<rect x="680" y="185" width="448" height="20" fill="#1a1a2e"/>
|
|
55
|
-
|
|
56
|
-
<!-- Terminal dots -->
|
|
57
|
-
<circle cx="710" cy="185" r="6" fill="#ff5f57"/>
|
|
58
|
-
<circle cx="730" cy="185" r="6" fill="#febc2e"/>
|
|
59
|
-
<circle cx="750" cy="185" r="6" fill="#28c840"/>
|
|
60
|
-
|
|
61
|
-
<!-- Terminal text -->
|
|
62
|
-
<text x="700" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#6366f1">$</text>
|
|
63
|
-
<text x="716" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#e2e8f0"> npx @eduardbar/drift scan ./src</text>
|
|
64
|
-
|
|
65
|
-
<text x="700" y="265" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> drift — vibe coding debt detector</text>
|
|
66
|
-
|
|
67
|
-
<text x="700" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> Score </text>
|
|
68
|
-
<text x="755" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" font-weight="700" fill="#ef4444"> 67</text>
|
|
69
|
-
<text x="779" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b">/100 HIGH</text>
|
|
70
|
-
|
|
71
|
-
<text x="700" y="320" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> 4 file(s) · 5 errors · 12 warnings</text>
|
|
72
|
-
|
|
73
|
-
<text x="700" y="352" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b"> src/api/users.ts</text>
|
|
74
|
-
<text x="700" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444"> ✖ </text>
|
|
75
|
-
<text x="720" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444">large-file</text>
|
|
76
|
-
<text x="800" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> 412 lines detected</text>
|
|
77
|
-
|
|
78
|
-
<text x="700" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
|
|
79
|
-
<text x="720" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">catch-swallow</text>
|
|
80
|
-
<text x="820" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> empty catch block</text>
|
|
81
|
-
|
|
82
|
-
<text x="700" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
|
|
83
|
-
<text x="720" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">any-abuse</text>
|
|
84
|
-
<text x="793" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> explicit any type</text>
|
|
85
|
-
|
|
86
|
-
<text x="700" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#22c55e"> ◦ </text>
|
|
87
|
-
<text x="720" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b">no-return-type</text>
|
|
88
|
-
<text x="830" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> missing return type</text>
|
|
89
|
-
|
|
90
|
-
<!-- Badges row -->
|
|
91
|
-
<rect x="104" y="405" width="100" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
92
|
-
<text x="154" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">TypeScript</text>
|
|
93
|
-
|
|
94
|
-
<rect x="214" y="405" width="72" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
95
|
-
<text x="250" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">ts-morph</text>
|
|
96
|
-
|
|
97
|
-
<rect x="296" y="405" width="52" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
98
|
-
<text x="322" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">MIT</text>
|
|
99
|
-
|
|
100
|
-
<!-- Author -->
|
|
101
|
-
<text x="104" y="520" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="16" fill="#334155">github.com/eduardbar/drift</text>
|
|
102
|
-
|
|
103
|
-
<!-- Bottom accent line -->
|
|
104
|
-
<rect x="0" y="622" width="1200" height="8" fill="url(#accent)" opacity="0.7"/>
|
|
105
|
-
</svg>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#0a0a0f"/>
|
|
5
|
+
<stop offset="100%" style="stop-color:#0f0f1a"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
8
|
+
<stop offset="0%" style="stop-color:#6366f1"/>
|
|
9
|
+
<stop offset="100%" style="stop-color:#8b5cf6"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
12
|
+
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:0.15"/>
|
|
13
|
+
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:0.05"/>
|
|
14
|
+
</linearGradient>
|
|
15
|
+
<filter id="blur-glow">
|
|
16
|
+
<feGaussianBlur stdDeviation="40" result="blur"/>
|
|
17
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
18
|
+
</filter>
|
|
19
|
+
<filter id="soft-glow">
|
|
20
|
+
<feGaussianBlur stdDeviation="3" result="blur"/>
|
|
21
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
22
|
+
</filter>
|
|
23
|
+
</defs>
|
|
24
|
+
|
|
25
|
+
<!-- Background -->
|
|
26
|
+
<rect width="1200" height="630" fill="url(#bg)"/>
|
|
27
|
+
|
|
28
|
+
<!-- Ambient glow top-left -->
|
|
29
|
+
<ellipse cx="200" cy="200" rx="300" ry="250" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.8"/>
|
|
30
|
+
|
|
31
|
+
<!-- Ambient glow bottom-right -->
|
|
32
|
+
<ellipse cx="1050" cy="480" rx="280" ry="220" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.6"/>
|
|
33
|
+
|
|
34
|
+
<!-- Grid lines subtle -->
|
|
35
|
+
<line x1="0" y1="210" x2="1200" y2="210" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
36
|
+
<line x1="0" y1="420" x2="1200" y2="420" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
37
|
+
<line x1="300" y1="0" x2="300" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
38
|
+
<line x1="900" y1="0" x2="900" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
39
|
+
|
|
40
|
+
<!-- Left accent bar -->
|
|
41
|
+
<rect x="72" y="180" width="4" height="270" rx="2" fill="url(#accent)"/>
|
|
42
|
+
|
|
43
|
+
<!-- Main title "drift" -->
|
|
44
|
+
<text x="102" y="310" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', monospace" font-size="120" font-weight="700" fill="#ffffff" letter-spacing="-4">drift</text>
|
|
45
|
+
|
|
46
|
+
<!-- Subtitle -->
|
|
47
|
+
<text x="104" y="370" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="24" font-weight="400" fill="#94a3b8" letter-spacing="0.5">Detect silent technical debt left by AI-generated code.</text>
|
|
48
|
+
|
|
49
|
+
<!-- Terminal block right side -->
|
|
50
|
+
<rect x="680" y="165" width="448" height="300" rx="12" fill="#0d0d18" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
|
|
51
|
+
|
|
52
|
+
<!-- Terminal header bar -->
|
|
53
|
+
<rect x="680" y="165" width="448" height="40" rx="12" fill="#1a1a2e"/>
|
|
54
|
+
<rect x="680" y="185" width="448" height="20" fill="#1a1a2e"/>
|
|
55
|
+
|
|
56
|
+
<!-- Terminal dots -->
|
|
57
|
+
<circle cx="710" cy="185" r="6" fill="#ff5f57"/>
|
|
58
|
+
<circle cx="730" cy="185" r="6" fill="#febc2e"/>
|
|
59
|
+
<circle cx="750" cy="185" r="6" fill="#28c840"/>
|
|
60
|
+
|
|
61
|
+
<!-- Terminal text -->
|
|
62
|
+
<text x="700" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#6366f1">$</text>
|
|
63
|
+
<text x="716" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#e2e8f0"> npx @eduardbar/drift scan ./src</text>
|
|
64
|
+
|
|
65
|
+
<text x="700" y="265" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> drift — vibe coding debt detector</text>
|
|
66
|
+
|
|
67
|
+
<text x="700" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> Score </text>
|
|
68
|
+
<text x="755" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" font-weight="700" fill="#ef4444"> 67</text>
|
|
69
|
+
<text x="779" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b">/100 HIGH</text>
|
|
70
|
+
|
|
71
|
+
<text x="700" y="320" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> 4 file(s) · 5 errors · 12 warnings</text>
|
|
72
|
+
|
|
73
|
+
<text x="700" y="352" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b"> src/api/users.ts</text>
|
|
74
|
+
<text x="700" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444"> ✖ </text>
|
|
75
|
+
<text x="720" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444">large-file</text>
|
|
76
|
+
<text x="800" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> 412 lines detected</text>
|
|
77
|
+
|
|
78
|
+
<text x="700" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
|
|
79
|
+
<text x="720" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">catch-swallow</text>
|
|
80
|
+
<text x="820" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> empty catch block</text>
|
|
81
|
+
|
|
82
|
+
<text x="700" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
|
|
83
|
+
<text x="720" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">any-abuse</text>
|
|
84
|
+
<text x="793" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> explicit any type</text>
|
|
85
|
+
|
|
86
|
+
<text x="700" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#22c55e"> ◦ </text>
|
|
87
|
+
<text x="720" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b">no-return-type</text>
|
|
88
|
+
<text x="830" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> missing return type</text>
|
|
89
|
+
|
|
90
|
+
<!-- Badges row -->
|
|
91
|
+
<rect x="104" y="405" width="100" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
92
|
+
<text x="154" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">TypeScript</text>
|
|
93
|
+
|
|
94
|
+
<rect x="214" y="405" width="72" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
95
|
+
<text x="250" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">ts-morph</text>
|
|
96
|
+
|
|
97
|
+
<rect x="296" y="405" width="52" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
98
|
+
<text x="322" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">MIT</text>
|
|
99
|
+
|
|
100
|
+
<!-- Author -->
|
|
101
|
+
<text x="104" y="520" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="16" fill="#334155">github.com/eduardbar/drift</text>
|
|
102
|
+
|
|
103
|
+
<!-- Bottom accent line -->
|
|
104
|
+
<rect x="0" y="622" width="1200" height="8" fill="url(#accent)" opacity="0.7"/>
|
|
105
|
+
</svg>
|
package/dist/analyzer.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { SourceFile } from 'ts-morph';
|
|
2
|
-
import type { FileReport, DriftConfig } from './types.js';
|
|
2
|
+
import type { DriftIssue, FileReport, DriftConfig } from './types.js';
|
|
3
|
+
export declare const RULE_WEIGHTS: Record<string, {
|
|
4
|
+
severity: DriftIssue['severity'];
|
|
5
|
+
weight: number;
|
|
6
|
+
}>;
|
|
3
7
|
export declare function analyzeFile(file: SourceFile): FileReport;
|
|
4
8
|
export declare function analyzeProject(targetPath: string, config?: DriftConfig): FileReport[];
|
|
5
9
|
//# sourceMappingURL=analyzer.d.ts.map
|
package/dist/analyzer.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
import { Project, SyntaxKind, } from 'ts-morph';
|
|
4
5
|
// Rules and their drift score weight
|
|
5
|
-
const RULE_WEIGHTS = {
|
|
6
|
+
export const RULE_WEIGHTS = {
|
|
6
7
|
'large-file': { severity: 'error', weight: 20 },
|
|
7
8
|
'large-function': { severity: 'error', weight: 15 },
|
|
8
9
|
'debug-leftover': { severity: 'warning', weight: 10 },
|
|
@@ -34,6 +35,8 @@ const RULE_WEIGHTS = {
|
|
|
34
35
|
'inconsistent-error-handling': { severity: 'warning', weight: 8 },
|
|
35
36
|
'unnecessary-abstraction': { severity: 'warning', weight: 7 },
|
|
36
37
|
'naming-inconsistency': { severity: 'warning', weight: 6 },
|
|
38
|
+
// Phase 8: semantic duplication
|
|
39
|
+
'semantic-duplication': { severity: 'warning', weight: 12 },
|
|
37
40
|
};
|
|
38
41
|
function hasIgnoreComment(file, line) {
|
|
39
42
|
const lines = file.getFullText().split('\n');
|
|
@@ -786,6 +789,105 @@ function calculateScore(issues) {
|
|
|
786
789
|
}
|
|
787
790
|
return Math.min(100, raw);
|
|
788
791
|
}
|
|
792
|
+
/** Normalize a function body to a canonical string (Type-2 clone detection).
|
|
793
|
+
* Variable names, parameter names, and numeric/string literals are replaced
|
|
794
|
+
* with canonical tokens so that two functions with identical logic but
|
|
795
|
+
* different identifiers produce the same fingerprint.
|
|
796
|
+
*/
|
|
797
|
+
function normalizeFunctionBody(fn) {
|
|
798
|
+
// Build a substitution map: localName → canonical token
|
|
799
|
+
const subst = new Map();
|
|
800
|
+
// Map parameters first
|
|
801
|
+
for (const [i, param] of fn.getParameters().entries()) {
|
|
802
|
+
const name = param.getName();
|
|
803
|
+
if (name && name !== '_')
|
|
804
|
+
subst.set(name, `P${i}`);
|
|
805
|
+
}
|
|
806
|
+
// Map locally declared variables (VariableDeclaration)
|
|
807
|
+
let varIdx = 0;
|
|
808
|
+
fn.forEachDescendant(node => {
|
|
809
|
+
if (node.getKind() === SyntaxKind.VariableDeclaration) {
|
|
810
|
+
const nameNode = node.getNameNode();
|
|
811
|
+
// Support destructuring — getNameNode() may be a BindingPattern
|
|
812
|
+
if (nameNode.getKind() === SyntaxKind.Identifier) {
|
|
813
|
+
const name = nameNode.getText();
|
|
814
|
+
if (!subst.has(name))
|
|
815
|
+
subst.set(name, `V${varIdx++}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
function serializeNode(node) {
|
|
820
|
+
const kind = node.getKindName();
|
|
821
|
+
switch (node.getKind()) {
|
|
822
|
+
case SyntaxKind.Identifier: {
|
|
823
|
+
const text = node.getText();
|
|
824
|
+
return subst.get(text) ?? text; // external refs (Math, console) kept as-is
|
|
825
|
+
}
|
|
826
|
+
case SyntaxKind.NumericLiteral:
|
|
827
|
+
return 'NL';
|
|
828
|
+
case SyntaxKind.StringLiteral:
|
|
829
|
+
case SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
830
|
+
return 'SL';
|
|
831
|
+
case SyntaxKind.TrueKeyword:
|
|
832
|
+
return 'TRUE';
|
|
833
|
+
case SyntaxKind.FalseKeyword:
|
|
834
|
+
return 'FALSE';
|
|
835
|
+
case SyntaxKind.NullKeyword:
|
|
836
|
+
return 'NULL';
|
|
837
|
+
}
|
|
838
|
+
const children = node.getChildren();
|
|
839
|
+
if (children.length === 0)
|
|
840
|
+
return kind;
|
|
841
|
+
const childStr = children.map(serializeNode).join('|');
|
|
842
|
+
return `${kind}(${childStr})`;
|
|
843
|
+
}
|
|
844
|
+
const body = fn.getBody();
|
|
845
|
+
if (!body)
|
|
846
|
+
return '';
|
|
847
|
+
return serializeNode(body);
|
|
848
|
+
}
|
|
849
|
+
/** Return a SHA-256 fingerprint for a function body (normalized). */
|
|
850
|
+
function fingerprintFunction(fn) {
|
|
851
|
+
const normalized = normalizeFunctionBody(fn);
|
|
852
|
+
return crypto.createHash('sha256').update(normalized).digest('hex');
|
|
853
|
+
}
|
|
854
|
+
/** Return all function-like nodes from a SourceFile that are worth comparing:
|
|
855
|
+
* - At least MIN_LINES lines in their body
|
|
856
|
+
* - Not test helpers (describe/it/test/beforeEach/afterEach)
|
|
857
|
+
*/
|
|
858
|
+
const MIN_LINES = 8;
|
|
859
|
+
function collectFunctions(sf) {
|
|
860
|
+
const results = [];
|
|
861
|
+
const kinds = [
|
|
862
|
+
SyntaxKind.FunctionDeclaration,
|
|
863
|
+
SyntaxKind.FunctionExpression,
|
|
864
|
+
SyntaxKind.ArrowFunction,
|
|
865
|
+
SyntaxKind.MethodDeclaration,
|
|
866
|
+
];
|
|
867
|
+
for (const kind of kinds) {
|
|
868
|
+
for (const node of sf.getDescendantsOfKind(kind)) {
|
|
869
|
+
const body = node.getBody();
|
|
870
|
+
if (!body)
|
|
871
|
+
continue;
|
|
872
|
+
const start = body.getStartLineNumber();
|
|
873
|
+
const end = body.getEndLineNumber();
|
|
874
|
+
if (end - start + 1 < MIN_LINES)
|
|
875
|
+
continue;
|
|
876
|
+
// Skip test-framework helpers
|
|
877
|
+
const name = node.getKind() === SyntaxKind.FunctionDeclaration
|
|
878
|
+
? node.getName() ?? '<anonymous>'
|
|
879
|
+
: node.getKind() === SyntaxKind.MethodDeclaration
|
|
880
|
+
? node.getName()
|
|
881
|
+
: '<anonymous>';
|
|
882
|
+
if (['describe', 'it', 'test', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll'].includes(name))
|
|
883
|
+
continue;
|
|
884
|
+
const pos = node.getStart();
|
|
885
|
+
const lineInfo = sf.getLineAndColumnAtPos(pos);
|
|
886
|
+
results.push({ fn: node, name, line: lineInfo.line, col: lineInfo.column });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return results;
|
|
890
|
+
}
|
|
789
891
|
// ---------------------------------------------------------------------------
|
|
790
892
|
// Public API
|
|
791
893
|
// ---------------------------------------------------------------------------
|
|
@@ -1162,6 +1264,46 @@ export function analyzeProject(targetPath, config) {
|
|
|
1162
1264
|
}
|
|
1163
1265
|
}
|
|
1164
1266
|
}
|
|
1267
|
+
// ── Phase 8: semantic-duplication ────────────────────────────────────────
|
|
1268
|
+
// Build a fingerprint → [{filePath, fnName, line, col}] map across all files
|
|
1269
|
+
const fingerprintMap = new Map();
|
|
1270
|
+
for (const sf of sourceFiles) {
|
|
1271
|
+
const sfPath = sf.getFilePath();
|
|
1272
|
+
for (const { fn, name, line, col } of collectFunctions(sf)) {
|
|
1273
|
+
const fp = fingerprintFunction(fn);
|
|
1274
|
+
if (!fingerprintMap.has(fp))
|
|
1275
|
+
fingerprintMap.set(fp, []);
|
|
1276
|
+
fingerprintMap.get(fp).push({ filePath: sfPath, name, line, col });
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
// For each fingerprint with 2+ functions: report each as a duplicate of the others
|
|
1280
|
+
for (const [, entries] of fingerprintMap) {
|
|
1281
|
+
if (entries.length < 2)
|
|
1282
|
+
continue;
|
|
1283
|
+
for (const entry of entries) {
|
|
1284
|
+
const report = reportByPath.get(entry.filePath);
|
|
1285
|
+
if (!report)
|
|
1286
|
+
continue;
|
|
1287
|
+
// Build the "duplicated in" list (all other locations)
|
|
1288
|
+
const others = entries
|
|
1289
|
+
.filter(e => e !== entry)
|
|
1290
|
+
.map(e => {
|
|
1291
|
+
const rel = path.relative(targetPath, e.filePath).replace(/\\/g, '/');
|
|
1292
|
+
return `${rel}:${e.line} (${e.name})`;
|
|
1293
|
+
})
|
|
1294
|
+
.join(', ');
|
|
1295
|
+
const weight = RULE_WEIGHTS['semantic-duplication']?.weight ?? 12;
|
|
1296
|
+
report.issues.push({
|
|
1297
|
+
rule: 'semantic-duplication',
|
|
1298
|
+
severity: 'warning',
|
|
1299
|
+
message: `Function '${entry.name}' is semantically identical to: ${others}`,
|
|
1300
|
+
line: entry.line,
|
|
1301
|
+
column: entry.col,
|
|
1302
|
+
snippet: `function ${entry.name} — duplicated in ${entries.length - 1} other location${entries.length > 2 ? 's' : ''}`,
|
|
1303
|
+
});
|
|
1304
|
+
report.score = Math.min(100, report.score + weight);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1165
1307
|
return reports;
|
|
1166
1308
|
}
|
|
1167
1309
|
//# sourceMappingURL=analyzer.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { analyzeProject, analyzeFile } from './analyzer.js';
|
|
1
|
+
export { analyzeProject, analyzeFile, RULE_WEIGHTS } from './analyzer.js';
|
|
2
2
|
export { buildReport, formatMarkdown } from './reporter.js';
|
|
3
3
|
export { computeDiff } from './diff.js';
|
|
4
|
-
export type { DriftReport, FileReport, DriftIssue, DriftDiff, FileDiff } from './types.js';
|
|
4
|
+
export type { DriftReport, FileReport, DriftIssue, DriftDiff, FileDiff, DriftConfig } from './types.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { analyzeProject, analyzeFile } from './analyzer.js';
|
|
1
|
+
export { analyzeProject, analyzeFile, RULE_WEIGHTS } from './analyzer.js';
|
|
2
2
|
export { buildReport, formatMarkdown } from './reporter.js';
|
|
3
3
|
export { computeDiff } from './diff.js';
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# eslint-plugin-drift
|
|
2
|
+
|
|
3
|
+
ESLint plugin that exposes [drift](https://github.com/eduardbar/drift)'s 26 technical debt rules as standard ESLint rules, compatible with ESLint 9 flat config.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev eslint-plugin-drift eslint
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Recommended config (all 26 rules)
|
|
14
|
+
|
|
15
|
+
In your `eslint.config.js` (or `eslint.config.mjs`):
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import drift from 'eslint-plugin-drift'
|
|
19
|
+
|
|
20
|
+
export default [
|
|
21
|
+
...drift.configs.recommended,
|
|
22
|
+
]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Manual config (pick individual rules)
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import drift from 'eslint-plugin-drift'
|
|
29
|
+
|
|
30
|
+
export default [
|
|
31
|
+
{
|
|
32
|
+
plugins: { drift },
|
|
33
|
+
rules: {
|
|
34
|
+
'drift/large-file': 'error',
|
|
35
|
+
'drift/large-function': 'error',
|
|
36
|
+
'drift/any-abuse': 'warn',
|
|
37
|
+
'drift/magic-number': 'warn',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Rules
|
|
44
|
+
|
|
45
|
+
| Rule | Severity | Description |
|
|
46
|
+
|------|----------|-------------|
|
|
47
|
+
| `drift/large-file` | error | Files over 300 lines |
|
|
48
|
+
| `drift/large-function` | error | Functions over 50 lines |
|
|
49
|
+
| `drift/duplicate-function-name` | error | Near-identical function names |
|
|
50
|
+
| `drift/high-complexity` | error | Cyclomatic complexity > 10 |
|
|
51
|
+
| `drift/circular-dependency` | error | Circular import chains |
|
|
52
|
+
| `drift/layer-violation` | error | Prohibited architectural layer imports |
|
|
53
|
+
| `drift/debug-leftover` | warn | console.log, TODO, FIXME in production |
|
|
54
|
+
| `drift/dead-code` | warn | Unused imports |
|
|
55
|
+
| `drift/any-abuse` | warn | Explicit `any` type |
|
|
56
|
+
| `drift/catch-swallow` | warn | Empty catch blocks |
|
|
57
|
+
| `drift/comment-contradiction` | warn | Comments that restate the code |
|
|
58
|
+
| `drift/deep-nesting` | warn | Nesting depth > 3 |
|
|
59
|
+
| `drift/too-many-params` | warn | Functions with more than 4 parameters |
|
|
60
|
+
| `drift/high-coupling` | warn | Files importing from more than 10 modules |
|
|
61
|
+
| `drift/promise-style-mix` | warn | Mixed async/await and .then() |
|
|
62
|
+
| `drift/unused-export` | warn | Exports never imported anywhere |
|
|
63
|
+
| `drift/dead-file` | warn | Files never imported |
|
|
64
|
+
| `drift/unused-dependency` | warn | Packages in package.json never used |
|
|
65
|
+
| `drift/cross-boundary-import` | warn | Cross-module boundary imports |
|
|
66
|
+
| `drift/hardcoded-config` | warn | Hardcoded URLs, IPs, connection strings |
|
|
67
|
+
| `drift/inconsistent-error-handling` | warn | Mixed try/catch and .catch() |
|
|
68
|
+
| `drift/unnecessary-abstraction` | warn | Single-use interfaces and abstract classes |
|
|
69
|
+
| `drift/naming-inconsistency` | warn | Mixed camelCase and snake_case |
|
|
70
|
+
| `drift/no-return-type` | warn | Missing explicit return types |
|
|
71
|
+
| `drift/magic-number` | warn | Numeric literals in logic |
|
|
72
|
+
| `drift/over-commented` | warn | Comments exceed 40% of function lines |
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
The plugin runs drift's AST analysis engine on each `.ts`/`.tsx` file when ESLint processes it. Results are cached per file so each file is analyzed once regardless of how many rules are enabled.
|
|
77
|
+
|
|
78
|
+
> **Note:** This plugin analyzes files individually. Cross-file rules (`unused-export`, `dead-file`, `unused-dependency`) work best when running `drift scan` on the full project for comprehensive cross-file analysis.
|
|
79
|
+
|
|
80
|
+
## Requirements
|
|
81
|
+
|
|
82
|
+
- ESLint 9+
|
|
83
|
+
- Node.js 18+
|
|
84
|
+
- TypeScript or JavaScript project
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT — [Eduard Barrera](https://github.com/eduardbar)
|