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,79 @@
1
+ import chalk from 'chalk';
2
+ import type { ScanSummary, Finding, Severity } from './types';
3
+
4
+ const SEVERITY_COLORS: Record<Severity, (s: string) => string> = {
5
+ info: chalk.blue,
6
+ warning: chalk.yellow,
7
+ error: chalk.red,
8
+ };
9
+
10
+ const SEVERITY_ICONS: Record<Severity, string> = {
11
+ info: 'i',
12
+ warning: '!',
13
+ error: 'x',
14
+ };
15
+
16
+ function formatFinding(finding: Finding): string {
17
+ const color = SEVERITY_COLORS[finding.severity];
18
+ const icon = SEVERITY_ICONS[finding.severity];
19
+ const location = chalk.gray(`${finding.file}:${finding.line}:${finding.column}`);
20
+ const rule = chalk.gray(`(${finding.rule})`);
21
+ let line = ` ${color(icon)} ${location} ${finding.message} ${rule}`;
22
+ if (finding.fix) {
23
+ line += `\n ${chalk.cyan('Fix:')} ${finding.fix.description}`;
24
+ }
25
+ return line;
26
+ }
27
+
28
+ export function formatText(summary: ScanSummary): string {
29
+ const lines: string[] = [];
30
+
31
+ for (const result of summary.results) {
32
+ if (result.findings.length === 0) continue;
33
+ lines.push('');
34
+ lines.push(chalk.underline(result.file));
35
+ for (const finding of result.findings) {
36
+ lines.push(formatFinding(finding));
37
+ }
38
+ }
39
+
40
+ lines.push('');
41
+ lines.push(formatScoreLine(summary.score));
42
+ lines.push(
43
+ chalk.gray(
44
+ `${summary.totalFindings} finding${summary.totalFindings === 1 ? '' : 's'} across ${summary.results.length} file${summary.results.length === 1 ? '' : 's'}`
45
+ )
46
+ );
47
+
48
+ return lines.join('\n');
49
+ }
50
+
51
+ function formatScoreLine(score: number): string {
52
+ const label = 'Slop Score';
53
+ if (score <= 20) return chalk.green(`${label}: ${score}/100 - Clean`);
54
+ if (score <= 50) return chalk.yellow(`${label}: ${score}/100 - Moderate`);
55
+ return chalk.red(`${label}: ${score}/100 - High`);
56
+ }
57
+
58
+ export function formatJson(summary: ScanSummary): string {
59
+ return JSON.stringify(
60
+ {
61
+ score: summary.score,
62
+ totalFindings: summary.totalFindings,
63
+ files: summary.results.map((r) => ({
64
+ file: r.file,
65
+ loc: r.loc,
66
+ findings: r.findings.map((f) => ({
67
+ line: f.line,
68
+ column: f.column,
69
+ rule: f.rule,
70
+ severity: f.severity,
71
+ message: f.message,
72
+ ...(f.fix ? { fix: { description: f.fix.description, replacement: f.fix.replacement } } : {}),
73
+ })),
74
+ })),
75
+ },
76
+ null,
77
+ 2
78
+ );
79
+ }
package/src/git.ts ADDED
@@ -0,0 +1,115 @@
1
+ import simpleGit, { SimpleGit } from 'simple-git';
2
+ import * as path from 'path';
3
+
4
+ export interface DiffHunk {
5
+ file: string;
6
+ addedLines: number[];
7
+ }
8
+
9
+ export interface GitContext {
10
+ git: SimpleGit;
11
+ root: string;
12
+ }
13
+
14
+ export async function getGitContext(cwd?: string): Promise<GitContext> {
15
+ const git = simpleGit(cwd ?? process.cwd());
16
+
17
+ let root: string;
18
+ try {
19
+ root = (await git.revparse(['--show-toplevel'])).trim();
20
+ } catch {
21
+ throw new Error(
22
+ 'Not a git repository. Run this command from inside a git repo, or use "distyll scan" instead.'
23
+ );
24
+ }
25
+
26
+ return { git, root };
27
+ }
28
+
29
+ export async function getDiffFiles(
30
+ ctx: GitContext,
31
+ options: { staged?: boolean; ref?: string }
32
+ ): Promise<DiffHunk[]> {
33
+ const args = ['--no-color', '-U0', '--diff-filter=ACMR'];
34
+
35
+ if (options.staged) {
36
+ args.push('--cached');
37
+ } else if (options.ref) {
38
+ args.push(options.ref);
39
+ }
40
+
41
+ const diffOutput = await ctx.git.diff(args);
42
+ return parseDiff(diffOutput, ctx.root);
43
+ }
44
+
45
+ export function parseDiff(diffOutput: string, repoRoot: string): DiffHunk[] {
46
+ const hunks: DiffHunk[] = [];
47
+ const lines = diffOutput.split('\n');
48
+
49
+ let currentFile: string | null = null;
50
+ let addedLines: number[] = [];
51
+
52
+ for (const line of lines) {
53
+ // Match file header: +++ b/path/to/file
54
+ if (line.startsWith('+++ b/')) {
55
+ if (currentFile && addedLines.length > 0) {
56
+ hunks.push({ file: currentFile, addedLines: [...addedLines] });
57
+ }
58
+ currentFile = path.resolve(repoRoot, line.slice(6));
59
+ addedLines = [];
60
+ continue;
61
+ }
62
+
63
+ // Match hunk header: @@ -old,count +new,count @@
64
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
65
+ if (hunkMatch && currentFile) {
66
+ const start = parseInt(hunkMatch[1], 10);
67
+ const count = hunkMatch[2] !== undefined ? parseInt(hunkMatch[2], 10) : 1;
68
+ for (let i = 0; i < count; i++) {
69
+ addedLines.push(start + i);
70
+ }
71
+ }
72
+ }
73
+
74
+ // Push last file
75
+ if (currentFile && addedLines.length > 0) {
76
+ hunks.push({ file: currentFile, addedLines: [...addedLines] });
77
+ }
78
+
79
+ return hunks;
80
+ }
81
+
82
+ export async function getChangedFiles(
83
+ ctx: GitContext,
84
+ options: { staged?: boolean; ref?: string }
85
+ ): Promise<string[]> {
86
+ const args = ['--name-only', '--diff-filter=ACMR'];
87
+
88
+ if (options.staged) {
89
+ args.push('--cached');
90
+ } else if (options.ref) {
91
+ args.push(options.ref);
92
+ }
93
+
94
+ const output = await ctx.git.diff(args);
95
+ return output
96
+ .split('\n')
97
+ .filter((f) => f.trim().length > 0)
98
+ .map((f) => path.resolve(ctx.root, f.trim()));
99
+ }
100
+
101
+ export async function getCurrentBranch(ctx: GitContext): Promise<string> {
102
+ try {
103
+ return (await ctx.git.revparse(['--abbrev-ref', 'HEAD'])).trim();
104
+ } catch {
105
+ return 'unknown';
106
+ }
107
+ }
108
+
109
+ export async function getHeadRef(ctx: GitContext): Promise<string | null> {
110
+ try {
111
+ return (await ctx.git.revparse(['HEAD'])).trim();
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export { scanPaths, scanFile } from './scanner';
2
+ export { allRules, getRuleByName } from './rules';
3
+ export { computeScore } from './scorer';
4
+ export { loadConfig } from './config';
5
+ export { getGitContext, getDiffFiles, parseDiff, getChangedFiles } from './git';
6
+ export { loadCache, saveScore, getTrend } from './cache';
7
+ export { analyzeCodebase } from './fingerprint/analyzer';
8
+ export { saveProfile, loadProfile } from './fingerprint/profile';
9
+ export { compareFileToProfile } from './fingerprint/comparator';
10
+ export { attachFixes } from './fixes';
11
+ export { DistyllError, NotAGitRepoError, UnsupportedLanguageError, FileAccessError, isBinaryBuffer, safeReadFile, formatError } from './errors';
12
+ export type { Finding, Rule, ScanResult, ScanSummary, Severity, Language, StyleProfile, FixSuggestion } from './types';
13
+ export type { DistyllConfig } from './config';
14
+ export type { DiffHunk, GitContext } from './git';
15
+ export type { TrendSummary } from './cache';
@@ -0,0 +1,50 @@
1
+ import type { Language } from '../types';
2
+ import { jsLanguages, jsExtensions, JS_STDLIB_MODULES } from './javascript';
3
+ import { pythonLanguages, pythonExtensions, PYTHON_STDLIB_MODULES } from './python';
4
+
5
+ /** Map from Language to tree-sitter grammar */
6
+ const grammars: Record<string, any> = {
7
+ ...jsLanguages,
8
+ ...pythonLanguages,
9
+ };
10
+
11
+ /** Map from file extension to Language */
12
+ const extensionMap: Record<string, Language> = {
13
+ ...jsExtensions,
14
+ ...pythonExtensions,
15
+ };
16
+
17
+ /** Map from Language to set of known standard library module names */
18
+ const stdlibModules: Partial<Record<Language, Set<string>>> = {
19
+ javascript: JS_STDLIB_MODULES,
20
+ typescript: JS_STDLIB_MODULES,
21
+ tsx: JS_STDLIB_MODULES,
22
+ python: PYTHON_STDLIB_MODULES,
23
+ };
24
+
25
+ export function getGrammar(language: Language): any {
26
+ const grammar = grammars[language];
27
+ if (!grammar) throw new Error(`Unsupported language: ${language}`);
28
+ return grammar;
29
+ }
30
+
31
+ export function detectLanguageFromExt(filePath: string): Language | null {
32
+ const ext = filePath.slice(filePath.lastIndexOf('.'));
33
+ return extensionMap[ext] ?? null;
34
+ }
35
+
36
+ export function getStdlibModules(language: Language): Set<string> {
37
+ return stdlibModules[language] ?? new Set();
38
+ }
39
+
40
+ export function getSupportedExtensions(): string[] {
41
+ return Object.keys(extensionMap).map((ext) => ext.slice(1));
42
+ }
43
+
44
+ export function isJSLike(language: Language): boolean {
45
+ return language === 'javascript' || language === 'typescript' || language === 'tsx';
46
+ }
47
+
48
+ export function isPython(language: Language): boolean {
49
+ return language === 'python';
50
+ }
@@ -0,0 +1,36 @@
1
+ import JavaScript from 'tree-sitter-javascript';
2
+ import TypeScriptLang from 'tree-sitter-typescript';
3
+ import type { Language } from '../types';
4
+
5
+ export const jsLanguages: Record<string, any> = {
6
+ javascript: JavaScript,
7
+ typescript: TypeScriptLang.typescript,
8
+ tsx: TypeScriptLang.tsx,
9
+ };
10
+
11
+ export const jsExtensions: Record<string, Language> = {
12
+ '.js': 'javascript',
13
+ '.jsx': 'javascript',
14
+ '.mjs': 'javascript',
15
+ '.cjs': 'javascript',
16
+ '.ts': 'typescript',
17
+ '.tsx': 'tsx',
18
+ };
19
+
20
+ /** Known Node.js / browser built-in modules */
21
+ export const JS_STDLIB_MODULES = new Set([
22
+ 'fs', 'path', 'os', 'http', 'https', 'url', 'util', 'stream', 'events',
23
+ 'buffer', 'crypto', 'child_process', 'cluster', 'dgram', 'dns', 'domain',
24
+ 'net', 'readline', 'repl', 'tls', 'tty', 'v8', 'vm', 'zlib',
25
+ 'assert', 'async_hooks', 'console', 'constants', 'diagnostics_channel',
26
+ 'inspector', 'module', 'perf_hooks', 'process', 'punycode', 'querystring',
27
+ 'string_decoder', 'timers', 'trace_events', 'worker_threads', 'wasi',
28
+ 'node:fs', 'node:path', 'node:os', 'node:http', 'node:https', 'node:url',
29
+ 'node:util', 'node:stream', 'node:events', 'node:buffer', 'node:crypto',
30
+ 'node:child_process', 'node:cluster', 'node:dgram', 'node:dns', 'node:net',
31
+ 'node:readline', 'node:repl', 'node:tls', 'node:tty', 'node:v8', 'node:vm',
32
+ 'node:zlib', 'node:assert', 'node:async_hooks', 'node:console',
33
+ 'node:diagnostics_channel', 'node:inspector', 'node:module',
34
+ 'node:perf_hooks', 'node:process', 'node:querystring', 'node:string_decoder',
35
+ 'node:timers', 'node:trace_events', 'node:worker_threads', 'node:test',
36
+ ]);
@@ -0,0 +1,47 @@
1
+ import PythonLang from 'tree-sitter-python';
2
+ import type { Language } from '../types';
3
+
4
+ export const pythonLanguages: Record<string, any> = {
5
+ python: PythonLang,
6
+ };
7
+
8
+ export const pythonExtensions: Record<string, Language> = {
9
+ '.py': 'python',
10
+ '.pyi': 'python',
11
+ };
12
+
13
+ /** Python standard library module names (3.10+) */
14
+ export const PYTHON_STDLIB_MODULES = new Set([
15
+ 'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio',
16
+ 'asyncore', 'atexit', 'audioop', 'base64', 'bdb', 'binascii', 'binhex',
17
+ 'bisect', 'builtins', 'bz2', 'calendar', 'cgi', 'cgitb', 'chunk',
18
+ 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', 'colorsys',
19
+ 'compileall', 'concurrent', 'configparser', 'contextlib', 'contextvars',
20
+ 'copy', 'copyreg', 'cProfile', 'crypt', 'csv', 'ctypes', 'curses',
21
+ 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis',
22
+ 'distutils', 'doctest', 'email', 'encodings', 'enum', 'errno',
23
+ 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'fractions',
24
+ 'ftplib', 'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob',
25
+ 'graphlib', 'grp', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http',
26
+ 'idlelib', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io',
27
+ 'ipaddress', 'itertools', 'json', 'keyword', 'lib2to3', 'linecache',
28
+ 'locale', 'logging', 'lzma', 'mailbox', 'mailcap', 'marshal', 'math',
29
+ 'mimetypes', 'mmap', 'modulefinder', 'multiprocessing', 'netrc', 'nis',
30
+ 'nntplib', 'numbers', 'operator', 'optparse', 'os', 'ossaudiodev',
31
+ 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil',
32
+ 'platform', 'plistlib', 'poplib', 'posix', 'posixpath', 'pprint',
33
+ 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr',
34
+ 'pydoc', 'queue', 'quopri', 'random', 're', 'readline', 'reprlib',
35
+ 'resource', 'rlcompleter', 'runpy', 'sched', 'secrets', 'select',
36
+ 'selectors', 'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtpd',
37
+ 'smtplib', 'sndhdr', 'socket', 'socketserver', 'sqlite3', 'ssl',
38
+ 'stat', 'statistics', 'string', 'stringprep', 'struct', 'subprocess',
39
+ 'sunau', 'symtable', 'sys', 'sysconfig', 'syslog', 'tabnanny',
40
+ 'tarfile', 'telnetlib', 'tempfile', 'termios', 'test', 'textwrap',
41
+ 'threading', 'time', 'timeit', 'tkinter', 'token', 'tokenize',
42
+ 'tomllib', 'trace', 'traceback', 'tracemalloc', 'tty', 'turtle',
43
+ 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib',
44
+ 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref', 'webbrowser',
45
+ 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc',
46
+ 'zipapp', 'zipfile', 'zipimport', 'zlib', '_thread',
47
+ ]);
package/src/parser.ts ADDED
@@ -0,0 +1,52 @@
1
+ import Parser from 'tree-sitter';
2
+ import type { Language } from './types';
3
+ import { getGrammar, detectLanguageFromExt } from './languages';
4
+
5
+ const parsers = new Map<Language, Parser>();
6
+
7
+ function getParser(language: Language): Parser {
8
+ let parser = parsers.get(language);
9
+ if (parser) return parser;
10
+
11
+ parser = new Parser();
12
+ parser.setLanguage(getGrammar(language));
13
+ parsers.set(language, parser);
14
+ return parser;
15
+ }
16
+
17
+ export function parse(source: string, language: Language): Parser.Tree {
18
+ const parser = getParser(language);
19
+ return parser.parse(source);
20
+ }
21
+
22
+ export function detectLanguage(filePath: string): Language | null {
23
+ return detectLanguageFromExt(filePath);
24
+ }
25
+
26
+ export function walkTree(node: Parser.SyntaxNode, callback: (node: Parser.SyntaxNode) => void): void {
27
+ callback(node);
28
+ for (const child of node.children) {
29
+ walkTree(child, callback);
30
+ }
31
+ }
32
+
33
+ export function findNodes(root: Parser.SyntaxNode, type: string): Parser.SyntaxNode[] {
34
+ const results: Parser.SyntaxNode[] = [];
35
+ walkTree(root, (node) => {
36
+ if (node.type === type) {
37
+ results.push(node);
38
+ }
39
+ });
40
+ return results;
41
+ }
42
+
43
+ export function findNodesMulti(root: Parser.SyntaxNode, types: string[]): Parser.SyntaxNode[] {
44
+ const typeSet = new Set(types);
45
+ const results: Parser.SyntaxNode[] = [];
46
+ walkTree(root, (node) => {
47
+ if (typeSet.has(node.type)) {
48
+ results.push(node);
49
+ }
50
+ });
51
+ return results;
52
+ }
@@ -0,0 +1,75 @@
1
+ import type { ScanSummary, Severity, Finding } from '../types';
2
+
3
+ const SEVERITY_MAP: Record<Severity, string> = {
4
+ info: 'notice',
5
+ warning: 'warning',
6
+ error: 'error',
7
+ };
8
+
9
+ export function formatGitHubAnnotations(summary: ScanSummary): string {
10
+ const lines: string[] = [];
11
+
12
+ for (const result of summary.results) {
13
+ for (const finding of result.findings) {
14
+ const level = SEVERITY_MAP[finding.severity];
15
+ const file = finding.file;
16
+ const line = finding.line;
17
+ lines.push(`::${level} file=${file},line=${line}::${finding.message} (${finding.rule})`);
18
+ }
19
+ }
20
+
21
+ return lines.join('\n');
22
+ }
23
+
24
+ export function formatGitHubSummary(summary: ScanSummary): string {
25
+ const lines: string[] = [];
26
+
27
+ lines.push('## Distyll Slop Report');
28
+ lines.push('');
29
+
30
+ const emoji = summary.score <= 20 ? '🟢' : summary.score <= 50 ? '🟡' : '🔴';
31
+ lines.push(`**Slop Score: ${emoji} ${summary.score}/100**`);
32
+ lines.push('');
33
+
34
+ const findingCount = summary.totalFindings;
35
+ const fileCount = summary.results.filter((r) => r.findings.length > 0).length;
36
+ lines.push(`Found ${findingCount} issue${findingCount === 1 ? '' : 's'} across ${fileCount} file${fileCount === 1 ? '' : 's'}.`);
37
+ lines.push('');
38
+
39
+ // Group findings by severity
40
+ const allFindings = summary.results.flatMap((r) => r.findings);
41
+ const errors = allFindings.filter((f) => f.severity === 'error').length;
42
+ const warnings = allFindings.filter((f) => f.severity === 'warning').length;
43
+ const infos = allFindings.filter((f) => f.severity === 'info').length;
44
+
45
+ if (errors > 0 || warnings > 0 || infos > 0) {
46
+ lines.push('| Severity | Count |');
47
+ lines.push('|----------|-------|');
48
+ if (errors > 0) lines.push(`| 🔴 Error | ${errors} |`);
49
+ if (warnings > 0) lines.push(`| 🟡 Warning | ${warnings} |`);
50
+ if (infos > 0) lines.push(`| 🔵 Info | ${infos} |`);
51
+ lines.push('');
52
+ }
53
+
54
+ // List top findings (max 20)
55
+ const topFindings = allFindings.slice(0, 20);
56
+ if (topFindings.length > 0) {
57
+ lines.push('<details>');
58
+ lines.push('<summary>Findings</summary>');
59
+ lines.push('');
60
+ lines.push('| File | Line | Rule | Message |');
61
+ lines.push('|------|------|------|---------|');
62
+ for (const f of topFindings) {
63
+ const shortFile = f.file.replace(/.*\//, '');
64
+ lines.push(`| \`${shortFile}\` | ${f.line} | ${f.rule} | ${f.message} |`);
65
+ }
66
+ if (allFindings.length > 20) {
67
+ lines.push('');
68
+ lines.push(`_...and ${allFindings.length - 20} more findings._`);
69
+ }
70
+ lines.push('');
71
+ lines.push('</details>');
72
+ }
73
+
74
+ return lines.join('\n');
75
+ }
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import type { ScanSummary, Severity } from '../types';
3
+ import type { TrendSummary } from '../cache';
4
+
5
+ const SEVERITY_COLORS: Record<Severity, (s: string) => string> = {
6
+ info: chalk.blue,
7
+ warning: chalk.yellow,
8
+ error: chalk.red,
9
+ };
10
+
11
+ const SEVERITY_ICONS: Record<Severity, string> = {
12
+ info: 'i',
13
+ warning: '!',
14
+ error: 'x',
15
+ };
16
+
17
+ export function formatTerminalReport(summary: ScanSummary, trend?: TrendSummary): string {
18
+ const lines: string[] = [];
19
+
20
+ for (const result of summary.results) {
21
+ if (result.findings.length === 0) continue;
22
+ lines.push('');
23
+ lines.push(chalk.underline(result.file));
24
+ for (const finding of result.findings) {
25
+ const color = SEVERITY_COLORS[finding.severity];
26
+ const icon = SEVERITY_ICONS[finding.severity];
27
+ const location = chalk.gray(`${finding.file}:${finding.line}:${finding.column}`);
28
+ const rule = chalk.gray(`(${finding.rule})`);
29
+ lines.push(` ${color(icon)} ${location} ${finding.message} ${rule}`);
30
+ }
31
+ }
32
+
33
+ lines.push('');
34
+ lines.push(formatScoreLine(summary.score));
35
+ lines.push(
36
+ chalk.gray(
37
+ `${summary.totalFindings} finding${summary.totalFindings === 1 ? '' : 's'} across ${summary.results.length} file${summary.results.length === 1 ? '' : 's'}`
38
+ )
39
+ );
40
+
41
+ if (trend) {
42
+ lines.push(formatTrendLine(trend));
43
+ }
44
+
45
+ return lines.join('\n');
46
+ }
47
+
48
+ function formatScoreLine(score: number): string {
49
+ const label = 'Slop Score';
50
+ if (score <= 20) return chalk.green(`${label}: ${score}/100 - Clean`);
51
+ if (score <= 50) return chalk.yellow(`${label}: ${score}/100 - Moderate`);
52
+ return chalk.red(`${label}: ${score}/100 - High`);
53
+ }
54
+
55
+ function formatTrendLine(trend: TrendSummary): string {
56
+ if (trend.direction === 'first-scan') {
57
+ return chalk.gray('First scan — no trend data yet.');
58
+ }
59
+
60
+ const arrow =
61
+ trend.direction === 'improving' ? chalk.green('↓') :
62
+ trend.direction === 'worsening' ? chalk.red('↑') :
63
+ chalk.gray('→');
64
+
65
+ const delta = trend.delta !== null ? (trend.delta > 0 ? `+${trend.delta}` : `${trend.delta}`) : '';
66
+ return `${arrow} Trend: ${delta} from previous score of ${trend.previous}`;
67
+ }
@@ -0,0 +1,62 @@
1
+ import type Parser from 'tree-sitter';
2
+ import { walkTree } from '../parser';
3
+ import type { Finding, Rule } from '../types';
4
+
5
+ const TERMINATING_STATEMENTS = new Set([
6
+ 'return_statement',
7
+ 'throw_statement',
8
+ 'break_statement',
9
+ 'continue_statement',
10
+ ]);
11
+
12
+ function isBlockLike(node: Parser.SyntaxNode): boolean {
13
+ return node.type === 'statement_block' || node.type === 'block';
14
+ }
15
+
16
+ export const deadCodePaths: Rule = {
17
+ name: 'dead-code-paths',
18
+ description: 'Flags unreachable code after unconditional return, throw, break, or continue statements',
19
+ severity: 'warning',
20
+
21
+ check(tree: Parser.Tree, source: string, filePath: string): Finding[] {
22
+ const findings: Finding[] = [];
23
+ const flagged = new Set<number>();
24
+
25
+ walkTree(tree.rootNode, (node) => {
26
+ if (!isBlockLike(node)) return;
27
+
28
+ const children = node.namedChildren;
29
+ for (let i = 0; i < children.length - 1; i++) {
30
+ const stmt = children[i];
31
+
32
+ // Check if this statement is a terminating statement
33
+ if (!TERMINATING_STATEMENTS.has(stmt.type)) continue;
34
+
35
+ // Everything after a terminating statement in the same block is dead code
36
+ for (let j = i + 1; j < children.length; j++) {
37
+ const dead = children[j];
38
+ // Skip comments — they're not executable code
39
+ if (dead.type === 'comment') continue;
40
+
41
+ const lineKey = dead.startPosition.row;
42
+ if (flagged.has(lineKey)) continue;
43
+ flagged.add(lineKey);
44
+
45
+ findings.push({
46
+ file: filePath,
47
+ line: dead.startPosition.row + 1,
48
+ column: dead.startPosition.column + 1,
49
+ endLine: dead.endPosition.row + 1,
50
+ endColumn: dead.endPosition.column + 1,
51
+ rule: 'dead-code-paths',
52
+ severity: 'warning',
53
+ message: `Unreachable code after ${stmt.type.replace('_statement', '')} on line ${stmt.startPosition.row + 1}`,
54
+ });
55
+ }
56
+ break; // No need to check further in this block
57
+ }
58
+ });
59
+
60
+ return findings;
61
+ },
62
+ };