distyll 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/CONTRIBUTING.md +159 -0
  2. package/POSTMORTEM.json +60 -0
  3. package/README.md +218 -0
  4. package/SETUP.md +79 -0
  5. package/action.yml +37 -0
  6. package/dist/cache.d.ts +26 -0
  7. package/dist/cache.d.ts.map +1 -0
  8. package/dist/cache.js +115 -0
  9. package/dist/cache.js.map +1 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +153 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands/ci.d.ts +7 -0
  15. package/dist/commands/ci.d.ts.map +1 -0
  16. package/dist/commands/ci.js +101 -0
  17. package/dist/commands/ci.js.map +1 -0
  18. package/dist/commands/diff.d.ts +10 -0
  19. package/dist/commands/diff.d.ts.map +1 -0
  20. package/dist/commands/diff.js +95 -0
  21. package/dist/commands/diff.js.map +1 -0
  22. package/dist/commands/fingerprint.d.ts +2 -0
  23. package/dist/commands/fingerprint.d.ts.map +1 -0
  24. package/dist/commands/fingerprint.js +77 -0
  25. package/dist/commands/fingerprint.js.map +1 -0
  26. package/dist/commands/hook.d.ts +3 -0
  27. package/dist/commands/hook.d.ts.map +1 -0
  28. package/dist/commands/hook.js +110 -0
  29. package/dist/commands/hook.js.map +1 -0
  30. package/dist/commands/init.d.ts +2 -0
  31. package/dist/commands/init.d.ts.map +1 -0
  32. package/dist/commands/init.js +75 -0
  33. package/dist/commands/init.js.map +1 -0
  34. package/dist/config.d.ts +7 -0
  35. package/dist/config.d.ts.map +1 -0
  36. package/dist/config.js +100 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/errors.d.ts +30 -0
  39. package/dist/errors.d.ts.map +1 -0
  40. package/dist/errors.js +133 -0
  41. package/dist/errors.js.map +1 -0
  42. package/dist/fingerprint/analyzer.d.ts +3 -0
  43. package/dist/fingerprint/analyzer.d.ts.map +1 -0
  44. package/dist/fingerprint/analyzer.js +230 -0
  45. package/dist/fingerprint/analyzer.js.map +1 -0
  46. package/dist/fingerprint/comparator.d.ts +4 -0
  47. package/dist/fingerprint/comparator.d.ts.map +1 -0
  48. package/dist/fingerprint/comparator.js +78 -0
  49. package/dist/fingerprint/comparator.js.map +1 -0
  50. package/dist/fingerprint/profile.d.ts +5 -0
  51. package/dist/fingerprint/profile.d.ts.map +1 -0
  52. package/dist/fingerprint/profile.js +68 -0
  53. package/dist/fingerprint/profile.js.map +1 -0
  54. package/dist/fixes/index.d.ts +12 -0
  55. package/dist/fixes/index.d.ts.map +1 -0
  56. package/dist/fixes/index.js +42 -0
  57. package/dist/fixes/index.js.map +1 -0
  58. package/dist/fixes/single-use-wrapper.d.ts +8 -0
  59. package/dist/fixes/single-use-wrapper.d.ts.map +1 -0
  60. package/dist/fixes/single-use-wrapper.js +54 -0
  61. package/dist/fixes/single-use-wrapper.js.map +1 -0
  62. package/dist/fixes/unnecessary-try-catch.d.ts +8 -0
  63. package/dist/fixes/unnecessary-try-catch.d.ts.map +1 -0
  64. package/dist/fixes/unnecessary-try-catch.js +37 -0
  65. package/dist/fixes/unnecessary-try-catch.js.map +1 -0
  66. package/dist/fixes/unused-imports.d.ts +7 -0
  67. package/dist/fixes/unused-imports.d.ts.map +1 -0
  68. package/dist/fixes/unused-imports.js +41 -0
  69. package/dist/fixes/unused-imports.js.map +1 -0
  70. package/dist/fixes/verbose-comments.d.ts +7 -0
  71. package/dist/fixes/verbose-comments.d.ts.map +1 -0
  72. package/dist/fixes/verbose-comments.js +29 -0
  73. package/dist/fixes/verbose-comments.js.map +1 -0
  74. package/dist/formatter.d.ts +4 -0
  75. package/dist/formatter.d.ts.map +1 -0
  76. package/dist/formatter.js +72 -0
  77. package/dist/formatter.js.map +1 -0
  78. package/dist/git.d.ts +22 -0
  79. package/dist/git.d.ts.map +1 -0
  80. package/dist/git.js +130 -0
  81. package/dist/git.js.map +1 -0
  82. package/dist/index.d.ts +16 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +40 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/languages/index.d.ts +8 -0
  87. package/dist/languages/index.d.ts.map +1 -0
  88. package/dist/languages/index.js +50 -0
  89. package/dist/languages/index.js.map +1 -0
  90. package/dist/languages/javascript.d.ts +6 -0
  91. package/dist/languages/javascript.d.ts.map +1 -0
  92. package/dist/languages/javascript.js +39 -0
  93. package/dist/languages/javascript.js.map +1 -0
  94. package/dist/languages/python.d.ts +6 -0
  95. package/dist/languages/python.d.ts.map +1 -0
  96. package/dist/languages/python.js +50 -0
  97. package/dist/languages/python.js.map +1 -0
  98. package/dist/parser.d.ts +8 -0
  99. package/dist/parser.d.ts.map +1 -0
  100. package/dist/parser.js +55 -0
  101. package/dist/parser.js.map +1 -0
  102. package/dist/reporters/github.d.ts +4 -0
  103. package/dist/reporters/github.d.ts.map +1 -0
  104. package/dist/reporters/github.js +70 -0
  105. package/dist/reporters/github.js.map +1 -0
  106. package/dist/reporters/terminal.d.ts +4 -0
  107. package/dist/reporters/terminal.d.ts.map +1 -0
  108. package/dist/reporters/terminal.js +59 -0
  109. package/dist/reporters/terminal.js.map +1 -0
  110. package/dist/rules/dead-code-paths.d.ts +3 -0
  111. package/dist/rules/dead-code-paths.d.ts.map +1 -0
  112. package/dist/rules/dead-code-paths.js +57 -0
  113. package/dist/rules/dead-code-paths.js.map +1 -0
  114. package/dist/rules/excessive-comments.d.ts +3 -0
  115. package/dist/rules/excessive-comments.d.ts.map +1 -0
  116. package/dist/rules/excessive-comments.js +86 -0
  117. package/dist/rules/excessive-comments.js.map +1 -0
  118. package/dist/rules/hallucinated-imports.d.ts +3 -0
  119. package/dist/rules/hallucinated-imports.d.ts.map +1 -0
  120. package/dist/rules/hallucinated-imports.js +228 -0
  121. package/dist/rules/hallucinated-imports.js.map +1 -0
  122. package/dist/rules/index.d.ts +4 -0
  123. package/dist/rules/index.d.ts.map +1 -0
  124. package/dist/rules/index.js +34 -0
  125. package/dist/rules/index.js.map +1 -0
  126. package/dist/rules/magic-values.d.ts +3 -0
  127. package/dist/rules/magic-values.d.ts.map +1 -0
  128. package/dist/rules/magic-values.js +168 -0
  129. package/dist/rules/magic-values.js.map +1 -0
  130. package/dist/rules/near-duplicate-functions.d.ts +3 -0
  131. package/dist/rules/near-duplicate-functions.d.ts.map +1 -0
  132. package/dist/rules/near-duplicate-functions.js +78 -0
  133. package/dist/rules/near-duplicate-functions.js.map +1 -0
  134. package/dist/rules/over-defensive-nulls.d.ts +3 -0
  135. package/dist/rules/over-defensive-nulls.d.ts.map +1 -0
  136. package/dist/rules/over-defensive-nulls.js +129 -0
  137. package/dist/rules/over-defensive-nulls.js.map +1 -0
  138. package/dist/rules/redundant-else-return.d.ts +3 -0
  139. package/dist/rules/redundant-else-return.d.ts.map +1 -0
  140. package/dist/rules/redundant-else-return.js +57 -0
  141. package/dist/rules/redundant-else-return.js.map +1 -0
  142. package/dist/rules/single-option-object.d.ts +3 -0
  143. package/dist/rules/single-option-object.d.ts.map +1 -0
  144. package/dist/rules/single-option-object.js +88 -0
  145. package/dist/rules/single-option-object.js.map +1 -0
  146. package/dist/rules/single-use-wrapper.d.ts +3 -0
  147. package/dist/rules/single-use-wrapper.d.ts.map +1 -0
  148. package/dist/rules/single-use-wrapper.js +172 -0
  149. package/dist/rules/single-use-wrapper.js.map +1 -0
  150. package/dist/rules/unnecessary-try-catch.d.ts +3 -0
  151. package/dist/rules/unnecessary-try-catch.d.ts.map +1 -0
  152. package/dist/rules/unnecessary-try-catch.js +116 -0
  153. package/dist/rules/unnecessary-try-catch.js.map +1 -0
  154. package/dist/rules/unused-imports.d.ts +3 -0
  155. package/dist/rules/unused-imports.d.ts.map +1 -0
  156. package/dist/rules/unused-imports.js +103 -0
  157. package/dist/rules/unused-imports.js.map +1 -0
  158. package/dist/rules/verbose-comments.d.ts +3 -0
  159. package/dist/rules/verbose-comments.d.ts.map +1 -0
  160. package/dist/rules/verbose-comments.js +100 -0
  161. package/dist/rules/verbose-comments.js.map +1 -0
  162. package/dist/scanner.d.ts +11 -0
  163. package/dist/scanner.d.ts.map +1 -0
  164. package/dist/scanner.js +196 -0
  165. package/dist/scanner.js.map +1 -0
  166. package/dist/scorer.d.ts +3 -0
  167. package/dist/scorer.d.ts.map +1 -0
  168. package/dist/scorer.js +23 -0
  169. package/dist/scorer.js.map +1 -0
  170. package/dist/types.d.ts +62 -0
  171. package/dist/types.d.ts.map +1 -0
  172. package/dist/types.js +3 -0
  173. package/dist/types.js.map +1 -0
  174. package/hn_post.md +13 -0
  175. package/marketing/COMPETITIVE_ANALYSIS.md +62 -0
  176. package/marketing/EMAIL_ANNOUNCEMENT.md +91 -0
  177. package/marketing/LANDING_PAGE_COPY.md +123 -0
  178. package/marketing/LAUNCH_POST.md +68 -0
  179. package/marketing/PRODUCT_HUNT.md +39 -0
  180. package/marketing/TWITTER_THREAD.md +70 -0
  181. package/package.json +44 -0
  182. package/producthunt.md +52 -0
  183. package/reddit_post.md +39 -0
  184. package/site/favicon.svg +10 -0
  185. package/site/index.html +281 -0
  186. package/site/script.js +82 -0
  187. package/site/style.css +516 -0
  188. package/src/cache.ts +114 -0
  189. package/src/cli.ts +169 -0
  190. package/src/commands/ci.ts +111 -0
  191. package/src/commands/diff.ts +108 -0
  192. package/src/commands/fingerprint.ts +47 -0
  193. package/src/commands/hook.ts +85 -0
  194. package/src/commands/init.ts +42 -0
  195. package/src/config.ts +75 -0
  196. package/src/errors.ts +105 -0
  197. package/src/fingerprint/analyzer.ts +214 -0
  198. package/src/fingerprint/comparator.ts +93 -0
  199. package/src/fingerprint/profile.ts +32 -0
  200. package/src/fixes/index.ts +58 -0
  201. package/src/fixes/single-use-wrapper.ts +60 -0
  202. package/src/fixes/unnecessary-try-catch.ts +43 -0
  203. package/src/fixes/unused-imports.ts +53 -0
  204. package/src/fixes/verbose-comments.ts +35 -0
  205. package/src/formatter.ts +79 -0
  206. package/src/git.ts +115 -0
  207. package/src/index.ts +15 -0
  208. package/src/languages/index.ts +50 -0
  209. package/src/languages/javascript.ts +36 -0
  210. package/src/languages/python.ts +47 -0
  211. package/src/parser.ts +52 -0
  212. package/src/reporters/github.ts +75 -0
  213. package/src/reporters/terminal.ts +67 -0
  214. package/src/rules/dead-code-paths.ts +62 -0
  215. package/src/rules/excessive-comments.ts +94 -0
  216. package/src/rules/hallucinated-imports.ts +195 -0
  217. package/src/rules/index.ts +32 -0
  218. package/src/rules/magic-values.ts +167 -0
  219. package/src/rules/near-duplicate-functions.ts +89 -0
  220. package/src/rules/over-defensive-nulls.ts +137 -0
  221. package/src/rules/redundant-else-return.ts +61 -0
  222. package/src/rules/single-option-object.ts +97 -0
  223. package/src/rules/single-use-wrapper.ts +184 -0
  224. package/src/rules/unnecessary-try-catch.ts +121 -0
  225. package/src/rules/unused-imports.ts +115 -0
  226. package/src/rules/verbose-comments.ts +105 -0
  227. package/src/scanner.ts +184 -0
  228. package/src/scorer.ts +26 -0
  229. package/src/types.ts +70 -0
  230. package/tests/commands/diff.test.ts +107 -0
  231. package/tests/config.test.ts +69 -0
  232. package/tests/e2e.test.ts +163 -0
  233. package/tests/edge-cases.test.ts +167 -0
  234. package/tests/fingerprint/analyzer.test.ts +131 -0
  235. package/tests/fixes/unnecessary-try-catch.test.ts +62 -0
  236. package/tests/git.test.ts +79 -0
  237. package/tests/rules/hallucinated-imports.test.ts +59 -0
  238. package/tests/rules/near-duplicate-functions.test.ts +90 -0
  239. package/tests/rules/unnecessary-try-catch.test.ts +81 -0
  240. package/tests/scanner.test.ts +88 -0
  241. package/tsconfig.json +20 -0
  242. package/twitter_thread.md +46 -0
  243. package/vitest.config.ts +7 -0
@@ -0,0 +1,94 @@
1
+ import type Parser from 'tree-sitter';
2
+ import type { Finding, Rule } from '../types';
3
+
4
+ const COMMENT_DENSITY_THRESHOLD = 0.5; // >50% of lines have trailing comments
5
+ const MIN_LINES_TO_CHECK = 10; // Don't flag very short files
6
+
7
+ export const excessiveComments: Rule = {
8
+ name: 'excessive-comments',
9
+ description: 'Flags files or functions with excessive inline comment density (>50% of lines)',
10
+ severity: 'info',
11
+
12
+ check(tree: Parser.Tree, source: string, filePath: string): Finding[] {
13
+ const findings: Finding[] = [];
14
+ const lines = source.split('\n');
15
+
16
+ if (lines.length < MIN_LINES_TO_CHECK) return findings;
17
+
18
+ // Count lines that have trailing inline comments (not standalone comment lines)
19
+ let codeLines = 0;
20
+ let trailingCommentLines = 0;
21
+
22
+ for (const line of lines) {
23
+ const trimmed = line.trim();
24
+ if (trimmed.length === 0) continue;
25
+
26
+ // Skip standalone comment lines — those are fine
27
+ if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
28
+ continue;
29
+ }
30
+
31
+ codeLines++;
32
+
33
+ // Check for trailing comments on code lines
34
+ // This is a heuristic — we look for // or # after code content
35
+ // Being careful to not match // inside strings
36
+ if (hasTrailingComment(trimmed)) {
37
+ trailingCommentLines++;
38
+ }
39
+ }
40
+
41
+ if (codeLines < MIN_LINES_TO_CHECK) return findings;
42
+
43
+ const density = trailingCommentLines / codeLines;
44
+ if (density > COMMENT_DENSITY_THRESHOLD) {
45
+ findings.push({
46
+ file: filePath,
47
+ line: 1,
48
+ column: 1,
49
+ endLine: lines.length,
50
+ endColumn: 1,
51
+ rule: 'excessive-comments',
52
+ severity: 'info',
53
+ message: `${Math.round(density * 100)}% of code lines have trailing comments (${trailingCommentLines}/${codeLines}) — high comment density is a common AI code pattern`,
54
+ });
55
+ }
56
+
57
+ return findings;
58
+ },
59
+ };
60
+
61
+ function hasTrailingComment(line: string): boolean {
62
+ // Simple heuristic: look for // or # that's not inside a string
63
+ let inSingleQuote = false;
64
+ let inDoubleQuote = false;
65
+ let inTemplate = false;
66
+
67
+ for (let i = 0; i < line.length; i++) {
68
+ const ch = line[i];
69
+ const prev = i > 0 ? line[i - 1] : '';
70
+
71
+ if (prev === '\\') continue;
72
+
73
+ if (ch === "'" && !inDoubleQuote && !inTemplate) inSingleQuote = !inSingleQuote;
74
+ if (ch === '"' && !inSingleQuote && !inTemplate) inDoubleQuote = !inDoubleQuote;
75
+ if (ch === '`' && !inSingleQuote && !inDoubleQuote) inTemplate = !inTemplate;
76
+
77
+ if (inSingleQuote || inDoubleQuote || inTemplate) continue;
78
+
79
+ // JS/TS trailing comment
80
+ if (ch === '/' && i + 1 < line.length && line[i + 1] === '/') {
81
+ // Make sure there's actual code before the comment
82
+ const before = line.slice(0, i).trim();
83
+ if (before.length > 0) return true;
84
+ }
85
+
86
+ // Python trailing comment
87
+ if (ch === '#') {
88
+ const before = line.slice(0, i).trim();
89
+ if (before.length > 0) return true;
90
+ }
91
+ }
92
+
93
+ return false;
94
+ }
@@ -0,0 +1,195 @@
1
+ import type Parser from 'tree-sitter';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { findNodes, walkTree } from '../parser';
5
+ import { getStdlibModules, isJSLike, isPython } from '../languages';
6
+ import type { Finding, Rule, Language } from '../types';
7
+
8
+ function getJSImportSources(tree: Parser.Tree): Array<{ source: string; node: Parser.SyntaxNode }> {
9
+ const imports: Array<{ source: string; node: Parser.SyntaxNode }> = [];
10
+ const importStmts = findNodes(tree.rootNode, 'import_statement');
11
+
12
+ for (const stmt of importStmts) {
13
+ walkTree(stmt, (node) => {
14
+ if (node.type === 'string' && node.parent?.type === 'import_statement') {
15
+ const source = node.text.replace(/['"]/g, '');
16
+ imports.push({ source, node: stmt });
17
+ }
18
+ });
19
+ }
20
+
21
+ // Also check require() calls
22
+ const callNodes = findNodes(tree.rootNode, 'call_expression');
23
+ for (const call of callNodes) {
24
+ const fn = call.childForFieldName('function');
25
+ if (fn?.text === 'require' && call.childForFieldName('arguments')) {
26
+ const args = call.childForFieldName('arguments');
27
+ if (args && args.namedChildren.length === 1) {
28
+ const arg = args.namedChildren[0];
29
+ if (arg.type === 'string') {
30
+ const source = arg.text.replace(/['"]/g, '');
31
+ imports.push({ source, node: call });
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ return imports;
38
+ }
39
+
40
+ function getPythonImportModules(tree: Parser.Tree): Array<{ source: string; node: Parser.SyntaxNode }> {
41
+ const imports: Array<{ source: string; node: Parser.SyntaxNode }> = [];
42
+
43
+ // import foo / import foo.bar
44
+ const importStmts = findNodes(tree.rootNode, 'import_statement');
45
+ for (const stmt of importStmts) {
46
+ walkTree(stmt, (node) => {
47
+ if (node.type === 'dotted_name' && node.parent?.type === 'import_statement') {
48
+ // Get just the top-level module name
49
+ const fullName = node.text;
50
+ const topLevel = fullName.split('.')[0];
51
+ imports.push({ source: topLevel, node: stmt });
52
+ }
53
+ });
54
+ }
55
+
56
+ // from foo import bar / from foo.bar import baz
57
+ const fromStmts = findNodes(tree.rootNode, 'import_from_statement');
58
+ for (const stmt of fromStmts) {
59
+ walkTree(stmt, (node) => {
60
+ if (node.type === 'dotted_name' && node.parent?.type === 'import_from_statement') {
61
+ // Only get the module name (first dotted_name child, not imported names)
62
+ const isModuleName = !node.previousNamedSibling || node.previousNamedSibling.type !== 'dotted_name';
63
+ if (isModuleName) {
64
+ const topLevel = node.text.split('.')[0];
65
+ imports.push({ source: topLevel, node: stmt });
66
+ }
67
+ }
68
+ // relative imports like `from .foo import bar` are always local — skip
69
+ if (node.type === 'relative_import') {
70
+ // Mark as safe — no push
71
+ }
72
+ });
73
+ }
74
+
75
+ return imports;
76
+ }
77
+
78
+ function isRelativeImport(source: string): boolean {
79
+ return source.startsWith('.') || source.startsWith('/');
80
+ }
81
+
82
+ function isNodeModuleInstalled(source: string, filePath: string): boolean {
83
+ // Check node_modules from the file's directory up
84
+ const pkgName = source.startsWith('@') ? source.split('/').slice(0, 2).join('/') : source.split('/')[0];
85
+ let dir = path.dirname(filePath);
86
+ for (let i = 0; i < 50; i++) {
87
+ const candidate = path.join(dir, 'node_modules', pkgName);
88
+ if (fs.existsSync(candidate)) return true;
89
+ const parent = path.dirname(dir);
90
+ if (parent === dir) break;
91
+ dir = parent;
92
+ }
93
+ return false;
94
+ }
95
+
96
+ function resolveJSImport(source: string, filePath: string, language: Language): boolean {
97
+ // Relative imports — check if file exists
98
+ if (isRelativeImport(source)) {
99
+ const dir = path.dirname(filePath);
100
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', ''];
101
+ for (const ext of extensions) {
102
+ if (fs.existsSync(path.join(dir, source + ext))) return true;
103
+ if (fs.existsSync(path.join(dir, source, 'index' + ext))) return true;
104
+ }
105
+ return false;
106
+ }
107
+
108
+ const stdlib = getStdlibModules(language);
109
+ if (stdlib.has(source)) return true;
110
+ if (isNodeModuleInstalled(source, filePath)) return true;
111
+
112
+ return false;
113
+ }
114
+
115
+ function resolvePythonImport(source: string, filePath: string): boolean {
116
+ const stdlib = getStdlibModules('python');
117
+ if (stdlib.has(source)) return true;
118
+
119
+ // Check if it's a local file/package in the project
120
+ const dir = path.dirname(filePath);
121
+ if (fs.existsSync(path.join(dir, source + '.py'))) return true;
122
+ if (fs.existsSync(path.join(dir, source)) && fs.existsSync(path.join(dir, source, '__init__.py'))) return true;
123
+
124
+ // Check parent directories for the module (common in Python projects)
125
+ let searchDir = path.dirname(dir);
126
+ for (let i = 0; i < 5; i++) {
127
+ if (fs.existsSync(path.join(searchDir, source + '.py'))) return true;
128
+ if (fs.existsSync(path.join(searchDir, source, '__init__.py'))) return true;
129
+ const parent = path.dirname(searchDir);
130
+ if (parent === searchDir) break;
131
+ searchDir = parent;
132
+ }
133
+
134
+ // Can't definitively say it's hallucinated — might be an installed package
135
+ // Be conservative: only flag if it looks like a plausible hallucination
136
+ return true; // Default to "resolved" to avoid false positives
137
+ }
138
+
139
+ export const hallucinatedImports: Rule = {
140
+ name: 'hallucinated-imports',
141
+ description: 'Flags imports of modules that do not exist in the project or known standard libraries',
142
+ severity: 'error',
143
+
144
+ check(tree: Parser.Tree, source: string, filePath: string): Finding[] {
145
+ const findings: Finding[] = [];
146
+ const language = detectLang(filePath);
147
+ if (!language) return findings;
148
+
149
+ if (isJSLike(language)) {
150
+ const imports = getJSImportSources(tree);
151
+ for (const { source: src, node } of imports) {
152
+ if (!src) continue;
153
+ if (!resolveJSImport(src, filePath, language)) {
154
+ findings.push({
155
+ file: filePath,
156
+ line: node.startPosition.row + 1,
157
+ column: node.startPosition.column + 1,
158
+ endLine: node.endPosition.row + 1,
159
+ endColumn: node.endPosition.column + 1,
160
+ rule: 'hallucinated-imports',
161
+ severity: 'error',
162
+ message: `Import '${src}' could not be resolved — module not found in project or node_modules`,
163
+ });
164
+ }
165
+ }
166
+ } else if (isPython(language)) {
167
+ const imports = getPythonImportModules(tree);
168
+ for (const { source: src, node } of imports) {
169
+ if (!src) continue;
170
+ if (!resolvePythonImport(src, filePath)) {
171
+ findings.push({
172
+ file: filePath,
173
+ line: node.startPosition.row + 1,
174
+ column: node.startPosition.column + 1,
175
+ endLine: node.endPosition.row + 1,
176
+ endColumn: node.endPosition.column + 1,
177
+ rule: 'hallucinated-imports',
178
+ severity: 'error',
179
+ message: `Import '${src}' could not be resolved — module not found in project or standard library`,
180
+ });
181
+ }
182
+ }
183
+ }
184
+
185
+ return findings;
186
+ },
187
+ };
188
+
189
+ function detectLang(filePath: string): Language | null {
190
+ if (filePath.endsWith('.tsx')) return 'tsx';
191
+ if (filePath.endsWith('.ts')) return 'typescript';
192
+ if (filePath.endsWith('.js') || filePath.endsWith('.jsx') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs')) return 'javascript';
193
+ if (filePath.endsWith('.py') || filePath.endsWith('.pyi')) return 'python';
194
+ return null;
195
+ }
@@ -0,0 +1,32 @@
1
+ import type { Rule } from '../types';
2
+ import { unnecessaryTryCatch } from './unnecessary-try-catch';
3
+ import { singleUseWrapper } from './single-use-wrapper';
4
+ import { verboseComments } from './verbose-comments';
5
+ import { unusedImports } from './unused-imports';
6
+ import { singleOptionObject } from './single-option-object';
7
+ import { redundantElseReturn } from './redundant-else-return';
8
+ import { hallucinatedImports } from './hallucinated-imports';
9
+ import { nearDuplicateFunctions } from './near-duplicate-functions';
10
+ import { overDefensiveNulls } from './over-defensive-nulls';
11
+ import { magicValues } from './magic-values';
12
+ import { deadCodePaths } from './dead-code-paths';
13
+ import { excessiveComments } from './excessive-comments';
14
+
15
+ export const allRules: Rule[] = [
16
+ unnecessaryTryCatch,
17
+ singleUseWrapper,
18
+ verboseComments,
19
+ unusedImports,
20
+ singleOptionObject,
21
+ redundantElseReturn,
22
+ hallucinatedImports,
23
+ nearDuplicateFunctions,
24
+ overDefensiveNulls,
25
+ magicValues,
26
+ deadCodePaths,
27
+ excessiveComments,
28
+ ];
29
+
30
+ export function getRuleByName(name: string): Rule | undefined {
31
+ return allRules.find((r) => r.name === name);
32
+ }
@@ -0,0 +1,167 @@
1
+ import type Parser from 'tree-sitter';
2
+ import { walkTree } from '../parser';
3
+ import type { Finding, Rule } from '../types';
4
+
5
+ // Well-known constants that are NOT magic values — exempted to avoid false positives
6
+ const EXEMPT_NUMBERS = new Set([
7
+ '0', '1', '-1', '2', '0.5',
8
+ // HTTP status codes
9
+ '200', '201', '204', '301', '302', '304', '400', '401', '403', '404', '405', '409', '422', '429', '500', '502', '503', '504',
10
+ // Common bit/byte values
11
+ '8', '16', '32', '64', '128', '256', '512', '1024',
12
+ // Time
13
+ '24', '60', '100', '1000', '3600',
14
+ ]);
15
+
16
+ const EXEMPT_STRINGS = new Set([
17
+ '', 'utf-8', 'utf8', 'ascii', 'base64', 'hex', 'binary',
18
+ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS',
19
+ 'Content-Type', 'Authorization', 'Accept',
20
+ 'application/json', 'text/html', 'text/plain', 'multipart/form-data',
21
+ 'true', 'false', 'null', 'undefined',
22
+ 'development', 'production', 'test', 'staging',
23
+ 'click', 'change', 'submit', 'input', 'keydown', 'keyup',
24
+ 'div', 'span', 'p', 'a', 'button', 'input', 'form',
25
+ ]);
26
+
27
+ const MIN_MAGIC_STRING_LENGTH = 2;
28
+ const MAX_MAGIC_STRING_LENGTH = 50;
29
+
30
+ function isInConstDeclaration(node: Parser.SyntaxNode): boolean {
31
+ let current: Parser.SyntaxNode | null = node;
32
+ while (current) {
33
+ // JS/TS: const x = value
34
+ if (current.type === 'lexical_declaration') {
35
+ const keyword = current.children[0];
36
+ if (keyword?.text === 'const') return true;
37
+ }
38
+ // Python: UPPER_CASE = value (convention)
39
+ if (current.type === 'assignment') {
40
+ const left = current.childForFieldName('left');
41
+ if (left && /^[A-Z_][A-Z0-9_]*$/.test(left.text)) return true;
42
+ }
43
+ // Enum members
44
+ if (current.type === 'enum_declaration' || current.type === 'enum_body') return true;
45
+ current = current.parent;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ function isInImportOrExport(node: Parser.SyntaxNode): boolean {
51
+ let current: Parser.SyntaxNode | null = node;
52
+ while (current) {
53
+ if (current.type === 'import_statement' || current.type === 'import_from_statement' ||
54
+ current.type === 'export_statement') return true;
55
+ current = current.parent;
56
+ }
57
+ return false;
58
+ }
59
+
60
+ function isObjectKey(node: Parser.SyntaxNode): boolean {
61
+ if (!node.parent) return false;
62
+ // String used as object key: { "key": value }
63
+ if (node.parent.type === 'pair') {
64
+ const key = node.parent.childForFieldName('key');
65
+ return key === node;
66
+ }
67
+ return false;
68
+ }
69
+
70
+ function isInComparison(node: Parser.SyntaxNode): boolean {
71
+ return node.parent?.type === 'binary_expression' || node.parent?.type === 'comparison_operator';
72
+ }
73
+
74
+ function isReturnValue(node: Parser.SyntaxNode): boolean {
75
+ return node.parent?.type === 'return_statement';
76
+ }
77
+
78
+ function isDefaultParam(node: Parser.SyntaxNode): boolean {
79
+ return node.parent?.type === 'assignment_pattern' || node.parent?.type === 'default_parameter';
80
+ }
81
+
82
+ function countOccurrences(root: Parser.SyntaxNode, value: string, type: string): number {
83
+ let count = 0;
84
+ walkTree(root, (node) => {
85
+ if (node.type === type && node.text === value) count++;
86
+ });
87
+ return count;
88
+ }
89
+
90
+ export const magicValues: Rule = {
91
+ name: 'magic-values',
92
+ description: 'Flags magic numbers and strings that should be named constants',
93
+ severity: 'info',
94
+
95
+ check(tree: Parser.Tree, source: string, filePath: string): Finding[] {
96
+ const findings: Finding[] = [];
97
+ const seen = new Set<string>();
98
+
99
+ walkTree(tree.rootNode, (node) => {
100
+ // Magic numbers
101
+ if (node.type === 'number' || node.type === 'integer' || node.type === 'float') {
102
+ const value = node.text;
103
+ if (EXEMPT_NUMBERS.has(value)) return;
104
+ if (isInConstDeclaration(node)) return;
105
+ if (isInImportOrExport(node)) return;
106
+ if (isDefaultParam(node)) return;
107
+ if (isReturnValue(node)) return;
108
+
109
+ // Only flag if the same magic number appears 3+ times
110
+ const key = `num:${value}`;
111
+ if (seen.has(key)) return;
112
+
113
+ const count = countOccurrences(tree.rootNode, value, node.type);
114
+ if (count >= 3) {
115
+ seen.add(key);
116
+ findings.push({
117
+ file: filePath,
118
+ line: node.startPosition.row + 1,
119
+ column: node.startPosition.column + 1,
120
+ endLine: node.endPosition.row + 1,
121
+ endColumn: node.endPosition.column + 1,
122
+ rule: 'magic-values',
123
+ severity: 'info',
124
+ message: `Magic number ${value} appears ${count} times — consider extracting to a named constant`,
125
+ });
126
+ }
127
+ }
128
+
129
+ // Magic strings
130
+ if (node.type === 'string' || node.type === 'string_fragment') {
131
+ // Get the actual string content (strip quotes)
132
+ let value = node.text;
133
+ if (node.type === 'string') {
134
+ value = value.slice(1, -1);
135
+ }
136
+
137
+ if (value.length < MIN_MAGIC_STRING_LENGTH || value.length > MAX_MAGIC_STRING_LENGTH) return;
138
+ if (EXEMPT_STRINGS.has(value)) return;
139
+ if (isInConstDeclaration(node)) return;
140
+ if (isInImportOrExport(node)) return;
141
+ if (isObjectKey(node)) return;
142
+ if (isDefaultParam(node)) return;
143
+
144
+ // Only flag strings that appear 3+ times
145
+ const key = `str:${value}`;
146
+ if (seen.has(key)) return;
147
+
148
+ const count = countOccurrences(tree.rootNode, node.text, node.type);
149
+ if (count >= 3) {
150
+ seen.add(key);
151
+ findings.push({
152
+ file: filePath,
153
+ line: node.startPosition.row + 1,
154
+ column: node.startPosition.column + 1,
155
+ endLine: node.endPosition.row + 1,
156
+ endColumn: node.endPosition.column + 1,
157
+ rule: 'magic-values',
158
+ severity: 'info',
159
+ message: `Magic string "${value}" appears ${count} times — consider extracting to a named constant`,
160
+ });
161
+ }
162
+ }
163
+ });
164
+
165
+ return findings;
166
+ },
167
+ };
@@ -0,0 +1,89 @@
1
+ import type Parser from 'tree-sitter';
2
+ import { compareTwoStrings } from 'string-similarity';
3
+ import { findNodesMulti } from '../parser';
4
+ import type { Finding, Rule } from '../types';
5
+
6
+ const FUNCTION_NODE_TYPES = [
7
+ 'function_declaration',
8
+ 'method_definition',
9
+ 'arrow_function',
10
+ 'function',
11
+ // Python
12
+ 'function_definition',
13
+ ];
14
+
15
+ const MIN_BODY_LENGTH = 40; // Ignore very short function bodies
16
+ const SIMILARITY_THRESHOLD = 0.85;
17
+
18
+ function getFunctionName(node: Parser.SyntaxNode): string {
19
+ const nameNode = node.childForFieldName('name');
20
+ if (nameNode) return nameNode.text;
21
+
22
+ // Arrow functions assigned to variables: const foo = () => ...
23
+ if (node.type === 'arrow_function' && node.parent?.type === 'variable_declarator') {
24
+ const varName = node.parent.childForFieldName('name');
25
+ if (varName) return varName.text;
26
+ }
27
+
28
+ return '<anonymous>';
29
+ }
30
+
31
+ function getFunctionBody(node: Parser.SyntaxNode): string | null {
32
+ const body = node.childForFieldName('body');
33
+ if (!body) return null;
34
+ return body.text;
35
+ }
36
+
37
+ interface FunctionInfo {
38
+ name: string;
39
+ body: string;
40
+ node: Parser.SyntaxNode;
41
+ }
42
+
43
+ export const nearDuplicateFunctions: Rule = {
44
+ name: 'near-duplicate-functions',
45
+ description: 'Flags functions with near-identical bodies that suggest copy-paste with subtle variations',
46
+ severity: 'warning',
47
+
48
+ check(tree: Parser.Tree, source: string, filePath: string): Finding[] {
49
+ const findings: Finding[] = [];
50
+ const funcNodes = findNodesMulti(tree.rootNode, FUNCTION_NODE_TYPES);
51
+
52
+ const functions: FunctionInfo[] = [];
53
+ for (const node of funcNodes) {
54
+ const body = getFunctionBody(node);
55
+ if (!body || body.length < MIN_BODY_LENGTH) continue;
56
+ functions.push({
57
+ name: getFunctionName(node),
58
+ body,
59
+ node,
60
+ });
61
+ }
62
+
63
+ // Compare all pairs — O(n^2) but function counts per file are small
64
+ const flagged = new Set<number>();
65
+ for (let i = 0; i < functions.length; i++) {
66
+ for (let j = i + 1; j < functions.length; j++) {
67
+ if (flagged.has(j)) continue;
68
+
69
+ const sim = compareTwoStrings(functions[i].body, functions[j].body);
70
+ if (sim >= SIMILARITY_THRESHOLD) {
71
+ flagged.add(j);
72
+ const nodeJ = functions[j].node;
73
+ findings.push({
74
+ file: filePath,
75
+ line: nodeJ.startPosition.row + 1,
76
+ column: nodeJ.startPosition.column + 1,
77
+ endLine: nodeJ.endPosition.row + 1,
78
+ endColumn: nodeJ.endPosition.column + 1,
79
+ rule: 'near-duplicate-functions',
80
+ severity: 'warning',
81
+ message: `Function '${functions[j].name}' is ${Math.round(sim * 100)}% similar to '${functions[i].name}' (line ${functions[i].node.startPosition.row + 1}) — consider extracting shared logic`,
82
+ });
83
+ }
84
+ }
85
+ }
86
+
87
+ return findings;
88
+ },
89
+ };