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.
- package/CONTRIBUTING.md +159 -0
- package/POSTMORTEM.json +60 -0
- package/README.md +218 -0
- package/SETUP.md +79 -0
- package/action.yml +37 -0
- package/dist/cache.d.ts +26 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +115 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +153 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/ci.d.ts +7 -0
- package/dist/commands/ci.d.ts.map +1 -0
- package/dist/commands/ci.js +101 -0
- package/dist/commands/ci.js.map +1 -0
- package/dist/commands/diff.d.ts +10 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +95 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/fingerprint.d.ts +2 -0
- package/dist/commands/fingerprint.d.ts.map +1 -0
- package/dist/commands/fingerprint.js +77 -0
- package/dist/commands/fingerprint.js.map +1 -0
- package/dist/commands/hook.d.ts +3 -0
- package/dist/commands/hook.d.ts.map +1 -0
- package/dist/commands/hook.js +110 -0
- package/dist/commands/hook.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +75 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +100 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +133 -0
- package/dist/errors.js.map +1 -0
- package/dist/fingerprint/analyzer.d.ts +3 -0
- package/dist/fingerprint/analyzer.d.ts.map +1 -0
- package/dist/fingerprint/analyzer.js +230 -0
- package/dist/fingerprint/analyzer.js.map +1 -0
- package/dist/fingerprint/comparator.d.ts +4 -0
- package/dist/fingerprint/comparator.d.ts.map +1 -0
- package/dist/fingerprint/comparator.js +78 -0
- package/dist/fingerprint/comparator.js.map +1 -0
- package/dist/fingerprint/profile.d.ts +5 -0
- package/dist/fingerprint/profile.d.ts.map +1 -0
- package/dist/fingerprint/profile.js +68 -0
- package/dist/fingerprint/profile.js.map +1 -0
- package/dist/fixes/index.d.ts +12 -0
- package/dist/fixes/index.d.ts.map +1 -0
- package/dist/fixes/index.js +42 -0
- package/dist/fixes/index.js.map +1 -0
- package/dist/fixes/single-use-wrapper.d.ts +8 -0
- package/dist/fixes/single-use-wrapper.d.ts.map +1 -0
- package/dist/fixes/single-use-wrapper.js +54 -0
- package/dist/fixes/single-use-wrapper.js.map +1 -0
- package/dist/fixes/unnecessary-try-catch.d.ts +8 -0
- package/dist/fixes/unnecessary-try-catch.d.ts.map +1 -0
- package/dist/fixes/unnecessary-try-catch.js +37 -0
- package/dist/fixes/unnecessary-try-catch.js.map +1 -0
- package/dist/fixes/unused-imports.d.ts +7 -0
- package/dist/fixes/unused-imports.d.ts.map +1 -0
- package/dist/fixes/unused-imports.js +41 -0
- package/dist/fixes/unused-imports.js.map +1 -0
- package/dist/fixes/verbose-comments.d.ts +7 -0
- package/dist/fixes/verbose-comments.d.ts.map +1 -0
- package/dist/fixes/verbose-comments.js +29 -0
- package/dist/fixes/verbose-comments.js.map +1 -0
- package/dist/formatter.d.ts +4 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +72 -0
- package/dist/formatter.js.map +1 -0
- package/dist/git.d.ts +22 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +130 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/languages/index.d.ts +8 -0
- package/dist/languages/index.d.ts.map +1 -0
- package/dist/languages/index.js +50 -0
- package/dist/languages/index.js.map +1 -0
- package/dist/languages/javascript.d.ts +6 -0
- package/dist/languages/javascript.d.ts.map +1 -0
- package/dist/languages/javascript.js +39 -0
- package/dist/languages/javascript.js.map +1 -0
- package/dist/languages/python.d.ts +6 -0
- package/dist/languages/python.d.ts.map +1 -0
- package/dist/languages/python.js +50 -0
- package/dist/languages/python.js.map +1 -0
- package/dist/parser.d.ts +8 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +55 -0
- package/dist/parser.js.map +1 -0
- package/dist/reporters/github.d.ts +4 -0
- package/dist/reporters/github.d.ts.map +1 -0
- package/dist/reporters/github.js +70 -0
- package/dist/reporters/github.js.map +1 -0
- package/dist/reporters/terminal.d.ts +4 -0
- package/dist/reporters/terminal.d.ts.map +1 -0
- package/dist/reporters/terminal.js +59 -0
- package/dist/reporters/terminal.js.map +1 -0
- package/dist/rules/dead-code-paths.d.ts +3 -0
- package/dist/rules/dead-code-paths.d.ts.map +1 -0
- package/dist/rules/dead-code-paths.js +57 -0
- package/dist/rules/dead-code-paths.js.map +1 -0
- package/dist/rules/excessive-comments.d.ts +3 -0
- package/dist/rules/excessive-comments.d.ts.map +1 -0
- package/dist/rules/excessive-comments.js +86 -0
- package/dist/rules/excessive-comments.js.map +1 -0
- package/dist/rules/hallucinated-imports.d.ts +3 -0
- package/dist/rules/hallucinated-imports.d.ts.map +1 -0
- package/dist/rules/hallucinated-imports.js +228 -0
- package/dist/rules/hallucinated-imports.js.map +1 -0
- package/dist/rules/index.d.ts +4 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +34 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/magic-values.d.ts +3 -0
- package/dist/rules/magic-values.d.ts.map +1 -0
- package/dist/rules/magic-values.js +168 -0
- package/dist/rules/magic-values.js.map +1 -0
- package/dist/rules/near-duplicate-functions.d.ts +3 -0
- package/dist/rules/near-duplicate-functions.d.ts.map +1 -0
- package/dist/rules/near-duplicate-functions.js +78 -0
- package/dist/rules/near-duplicate-functions.js.map +1 -0
- package/dist/rules/over-defensive-nulls.d.ts +3 -0
- package/dist/rules/over-defensive-nulls.d.ts.map +1 -0
- package/dist/rules/over-defensive-nulls.js +129 -0
- package/dist/rules/over-defensive-nulls.js.map +1 -0
- package/dist/rules/redundant-else-return.d.ts +3 -0
- package/dist/rules/redundant-else-return.d.ts.map +1 -0
- package/dist/rules/redundant-else-return.js +57 -0
- package/dist/rules/redundant-else-return.js.map +1 -0
- package/dist/rules/single-option-object.d.ts +3 -0
- package/dist/rules/single-option-object.d.ts.map +1 -0
- package/dist/rules/single-option-object.js +88 -0
- package/dist/rules/single-option-object.js.map +1 -0
- package/dist/rules/single-use-wrapper.d.ts +3 -0
- package/dist/rules/single-use-wrapper.d.ts.map +1 -0
- package/dist/rules/single-use-wrapper.js +172 -0
- package/dist/rules/single-use-wrapper.js.map +1 -0
- package/dist/rules/unnecessary-try-catch.d.ts +3 -0
- package/dist/rules/unnecessary-try-catch.d.ts.map +1 -0
- package/dist/rules/unnecessary-try-catch.js +116 -0
- package/dist/rules/unnecessary-try-catch.js.map +1 -0
- package/dist/rules/unused-imports.d.ts +3 -0
- package/dist/rules/unused-imports.d.ts.map +1 -0
- package/dist/rules/unused-imports.js +103 -0
- package/dist/rules/unused-imports.js.map +1 -0
- package/dist/rules/verbose-comments.d.ts +3 -0
- package/dist/rules/verbose-comments.d.ts.map +1 -0
- package/dist/rules/verbose-comments.js +100 -0
- package/dist/rules/verbose-comments.js.map +1 -0
- package/dist/scanner.d.ts +11 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +196 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scorer.d.ts +3 -0
- package/dist/scorer.d.ts.map +1 -0
- package/dist/scorer.js +23 -0
- package/dist/scorer.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/hn_post.md +13 -0
- package/marketing/COMPETITIVE_ANALYSIS.md +62 -0
- package/marketing/EMAIL_ANNOUNCEMENT.md +91 -0
- package/marketing/LANDING_PAGE_COPY.md +123 -0
- package/marketing/LAUNCH_POST.md +68 -0
- package/marketing/PRODUCT_HUNT.md +39 -0
- package/marketing/TWITTER_THREAD.md +70 -0
- package/package.json +44 -0
- package/producthunt.md +52 -0
- package/reddit_post.md +39 -0
- package/site/favicon.svg +10 -0
- package/site/index.html +281 -0
- package/site/script.js +82 -0
- package/site/style.css +516 -0
- package/src/cache.ts +114 -0
- package/src/cli.ts +169 -0
- package/src/commands/ci.ts +111 -0
- package/src/commands/diff.ts +108 -0
- package/src/commands/fingerprint.ts +47 -0
- package/src/commands/hook.ts +85 -0
- package/src/commands/init.ts +42 -0
- package/src/config.ts +75 -0
- package/src/errors.ts +105 -0
- package/src/fingerprint/analyzer.ts +214 -0
- package/src/fingerprint/comparator.ts +93 -0
- package/src/fingerprint/profile.ts +32 -0
- package/src/fixes/index.ts +58 -0
- package/src/fixes/single-use-wrapper.ts +60 -0
- package/src/fixes/unnecessary-try-catch.ts +43 -0
- package/src/fixes/unused-imports.ts +53 -0
- package/src/fixes/verbose-comments.ts +35 -0
- package/src/formatter.ts +79 -0
- package/src/git.ts +115 -0
- package/src/index.ts +15 -0
- package/src/languages/index.ts +50 -0
- package/src/languages/javascript.ts +36 -0
- package/src/languages/python.ts +47 -0
- package/src/parser.ts +52 -0
- package/src/reporters/github.ts +75 -0
- package/src/reporters/terminal.ts +67 -0
- package/src/rules/dead-code-paths.ts +62 -0
- package/src/rules/excessive-comments.ts +94 -0
- package/src/rules/hallucinated-imports.ts +195 -0
- package/src/rules/index.ts +32 -0
- package/src/rules/magic-values.ts +167 -0
- package/src/rules/near-duplicate-functions.ts +89 -0
- package/src/rules/over-defensive-nulls.ts +137 -0
- package/src/rules/redundant-else-return.ts +61 -0
- package/src/rules/single-option-object.ts +97 -0
- package/src/rules/single-use-wrapper.ts +184 -0
- package/src/rules/unnecessary-try-catch.ts +121 -0
- package/src/rules/unused-imports.ts +115 -0
- package/src/rules/verbose-comments.ts +105 -0
- package/src/scanner.ts +184 -0
- package/src/scorer.ts +26 -0
- package/src/types.ts +70 -0
- package/tests/commands/diff.test.ts +107 -0
- package/tests/config.test.ts +69 -0
- package/tests/e2e.test.ts +163 -0
- package/tests/edge-cases.test.ts +167 -0
- package/tests/fingerprint/analyzer.test.ts +131 -0
- package/tests/fixes/unnecessary-try-catch.test.ts +62 -0
- package/tests/git.test.ts +79 -0
- package/tests/rules/hallucinated-imports.test.ts +59 -0
- package/tests/rules/near-duplicate-functions.test.ts +90 -0
- package/tests/rules/unnecessary-try-catch.test.ts +81 -0
- package/tests/scanner.test.ts +88 -0
- package/tsconfig.json +20 -0
- package/twitter_thread.md +46 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
const CLI_PATH = path.resolve(__dirname, '../dist/cli.js');
|
|
8
|
+
|
|
9
|
+
function run(args: string, opts?: { cwd?: string; expectFail?: boolean }): string {
|
|
10
|
+
try {
|
|
11
|
+
return execSync(`node ${CLI_PATH} ${args}`, {
|
|
12
|
+
cwd: opts?.cwd ?? process.cwd(),
|
|
13
|
+
encoding: 'utf-8',
|
|
14
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
15
|
+
timeout: 30000,
|
|
16
|
+
});
|
|
17
|
+
} catch (err: any) {
|
|
18
|
+
if (opts?.expectFail) {
|
|
19
|
+
return (err.stdout ?? '') + (err.stderr ?? '');
|
|
20
|
+
}
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createTempProject(): string {
|
|
26
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'distyll-e2e-'));
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('CLI end-to-end', () => {
|
|
31
|
+
it('--version outputs version number', () => {
|
|
32
|
+
const output = run('--version');
|
|
33
|
+
expect(output.trim()).toMatch(/^\d+\.\d+\.\d+$/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('--help shows usage', () => {
|
|
37
|
+
const output = run('--help');
|
|
38
|
+
expect(output).toContain('distyll');
|
|
39
|
+
expect(output).toContain('scan');
|
|
40
|
+
expect(output).toContain('diff');
|
|
41
|
+
expect(output).toContain('hook');
|
|
42
|
+
expect(output).toContain('ci');
|
|
43
|
+
expect(output).toContain('init');
|
|
44
|
+
expect(output).toContain('fingerprint');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('scan --help shows scan options', () => {
|
|
48
|
+
const output = run('scan --help');
|
|
49
|
+
expect(output).toContain('--format');
|
|
50
|
+
expect(output).toContain('--threshold');
|
|
51
|
+
expect(output).toContain('--style');
|
|
52
|
+
expect(output).toContain('--verbose');
|
|
53
|
+
expect(output).toContain('--quiet');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('scan command', () => {
|
|
58
|
+
it('scans a clean file and reports score 0', () => {
|
|
59
|
+
const dir = createTempProject();
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(dir, 'clean.js'),
|
|
62
|
+
'function add(a, b) {\n return a + b;\n}\n\nconst result = add(1, 2);\nconsole.log(result);\n'
|
|
63
|
+
);
|
|
64
|
+
const output = run(`scan ${dir}`);
|
|
65
|
+
expect(output).toContain('Slop Score');
|
|
66
|
+
expect(output).toContain('0/100');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('detects slop in a file with wrapper function', () => {
|
|
70
|
+
const dir = createTempProject();
|
|
71
|
+
fs.writeFileSync(
|
|
72
|
+
path.join(dir, 'sloppy.js'),
|
|
73
|
+
'function wrapper(x) {\n return doThing(x);\n}\n'
|
|
74
|
+
);
|
|
75
|
+
const output = run(`scan ${dir}`);
|
|
76
|
+
expect(output).toContain('single-use-wrapper');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('--format json outputs valid JSON', () => {
|
|
80
|
+
const dir = createTempProject();
|
|
81
|
+
fs.writeFileSync(path.join(dir, 'test.js'), 'const x = 1;\n');
|
|
82
|
+
const output = run(`scan ${dir} --format json`);
|
|
83
|
+
const parsed = JSON.parse(output);
|
|
84
|
+
expect(parsed).toHaveProperty('score');
|
|
85
|
+
expect(parsed).toHaveProperty('totalFindings');
|
|
86
|
+
expect(parsed).toHaveProperty('files');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('--quiet outputs only the score number', () => {
|
|
90
|
+
const dir = createTempProject();
|
|
91
|
+
fs.writeFileSync(path.join(dir, 'test.js'), 'const x = 1;\n');
|
|
92
|
+
const output = run(`scan ${dir} --quiet`);
|
|
93
|
+
expect(output.trim()).toMatch(/^\d+$/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('--threshold exits with code 1 when exceeded', () => {
|
|
97
|
+
const dir = createTempProject();
|
|
98
|
+
fs.writeFileSync(
|
|
99
|
+
path.join(dir, 'sloppy.js'),
|
|
100
|
+
'function wrapper(x) {\n return doThing(x);\n}\n'
|
|
101
|
+
);
|
|
102
|
+
const output = run(`scan ${dir} --threshold 0`, { expectFail: true });
|
|
103
|
+
expect(output).toContain('exceeds threshold');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('scans Python files', () => {
|
|
107
|
+
const dir = createTempProject();
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(dir, 'test.py'),
|
|
110
|
+
'def add(a, b):\n return a + b\n\nresult = add(1, 2)\nprint(result)\n'
|
|
111
|
+
);
|
|
112
|
+
const output = run(`scan ${dir}`);
|
|
113
|
+
expect(output).toContain('Slop Score');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('init command', () => {
|
|
118
|
+
it('creates .distyll.json and .distyll/ directory', () => {
|
|
119
|
+
const dir = createTempProject();
|
|
120
|
+
const output = run(`init ${dir}`);
|
|
121
|
+
expect(output).toContain('.distyll.json');
|
|
122
|
+
expect(fs.existsSync(path.join(dir, '.distyll.json'))).toBe(true);
|
|
123
|
+
expect(fs.existsSync(path.join(dir, '.distyll'))).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('does not overwrite existing config', () => {
|
|
127
|
+
const dir = createTempProject();
|
|
128
|
+
const configPath = path.join(dir, '.distyll.json');
|
|
129
|
+
fs.writeFileSync(configPath, '{"custom": true}');
|
|
130
|
+
const output = run(`init ${dir}`);
|
|
131
|
+
expect(output).toContain('already exists');
|
|
132
|
+
expect(fs.readFileSync(configPath, 'utf-8')).toBe('{"custom": true}');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('fingerprint command', () => {
|
|
137
|
+
it('creates a style profile from a codebase', () => {
|
|
138
|
+
const dir = createTempProject();
|
|
139
|
+
fs.writeFileSync(
|
|
140
|
+
path.join(dir, 'sample.js'),
|
|
141
|
+
[
|
|
142
|
+
'function calculateTotal(items) {',
|
|
143
|
+
' let total = 0;',
|
|
144
|
+
' for (const item of items) {',
|
|
145
|
+
' total += item.price;',
|
|
146
|
+
' }',
|
|
147
|
+
' return total;',
|
|
148
|
+
'}',
|
|
149
|
+
'',
|
|
150
|
+
'function formatCurrency(amount) {',
|
|
151
|
+
' return "$" + amount.toFixed(2);',
|
|
152
|
+
'}',
|
|
153
|
+
'',
|
|
154
|
+
].join('\n')
|
|
155
|
+
);
|
|
156
|
+
run(`fingerprint ${dir}`);
|
|
157
|
+
const profilePath = path.join(dir, '.distyll', 'profile.json');
|
|
158
|
+
expect(fs.existsSync(profilePath)).toBe(true);
|
|
159
|
+
const profile = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
|
|
160
|
+
expect(profile).toHaveProperty('metrics');
|
|
161
|
+
expect(profile.metrics).toHaveProperty('medianFunctionLength');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { scanFile, scanPaths } from '../src/scanner';
|
|
6
|
+
import { isBinaryBuffer, safeReadFile, formatError, DistyllError } from '../src/errors';
|
|
7
|
+
|
|
8
|
+
function createTempDir(): string {
|
|
9
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'distyll-edge-'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createTempFile(content: string | Buffer, ext: string, dir?: string): string {
|
|
13
|
+
const d = dir ?? createTempDir();
|
|
14
|
+
const filePath = path.join(d, `test${ext}`);
|
|
15
|
+
fs.writeFileSync(filePath, content);
|
|
16
|
+
return filePath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('binary file detection', () => {
|
|
20
|
+
it('detects binary files with null bytes', () => {
|
|
21
|
+
const buf = Buffer.from([0x48, 0x65, 0x6c, 0x00, 0x6f]);
|
|
22
|
+
expect(isBinaryBuffer(buf)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('does not flag text files as binary', () => {
|
|
26
|
+
const buf = Buffer.from('function hello() { return "world"; }');
|
|
27
|
+
expect(isBinaryBuffer(buf)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('scanFile returns null for binary files', () => {
|
|
31
|
+
const binaryContent = Buffer.alloc(100);
|
|
32
|
+
binaryContent[50] = 0;
|
|
33
|
+
binaryContent.write('function foo() {}', 0);
|
|
34
|
+
const filePath = createTempFile(binaryContent, '.js');
|
|
35
|
+
expect(scanFile(filePath)).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('empty file handling', () => {
|
|
40
|
+
it('scanFile returns null for empty files', () => {
|
|
41
|
+
const filePath = createTempFile('', '.js');
|
|
42
|
+
expect(scanFile(filePath)).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('scanPaths handles empty directory gracefully', async () => {
|
|
46
|
+
const dir = createTempDir();
|
|
47
|
+
const summary = await scanPaths([dir]);
|
|
48
|
+
expect(summary.score).toBe(0);
|
|
49
|
+
expect(summary.totalFindings).toBe(0);
|
|
50
|
+
expect(summary.results).toHaveLength(0);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('unsupported languages', () => {
|
|
55
|
+
it('scanFile returns null for .txt files', () => {
|
|
56
|
+
const filePath = createTempFile('hello world', '.txt');
|
|
57
|
+
expect(scanFile(filePath)).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('scanFile returns null for .rs files', () => {
|
|
61
|
+
const filePath = createTempFile('fn main() {}', '.rs');
|
|
62
|
+
expect(scanFile(filePath)).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('scanFile returns null for .go files', () => {
|
|
66
|
+
const filePath = createTempFile('package main', '.go');
|
|
67
|
+
expect(scanFile(filePath)).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('scanPaths skips unsupported files in mixed directory', async () => {
|
|
71
|
+
const dir = createTempDir();
|
|
72
|
+
fs.writeFileSync(path.join(dir, 'a.js'), 'const x = 1;\n');
|
|
73
|
+
fs.writeFileSync(path.join(dir, 'b.txt'), 'not code');
|
|
74
|
+
fs.writeFileSync(path.join(dir, 'c.md'), '# readme');
|
|
75
|
+
const summary = await scanPaths([dir]);
|
|
76
|
+
expect(summary.results).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('nonexistent paths', () => {
|
|
81
|
+
it('scanPaths skips nonexistent paths without crashing', async () => {
|
|
82
|
+
const summary = await scanPaths(['/tmp/does-not-exist-distyll-test']);
|
|
83
|
+
expect(summary.score).toBe(0);
|
|
84
|
+
expect(summary.results).toHaveLength(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('scanFile returns null for nonexistent files', () => {
|
|
88
|
+
expect(scanFile('/tmp/does-not-exist-distyll-test.js')).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('safeReadFile', () => {
|
|
93
|
+
it('returns source for valid text files', () => {
|
|
94
|
+
const filePath = createTempFile('const x = 1;', '.js');
|
|
95
|
+
const result = safeReadFile(filePath);
|
|
96
|
+
expect(result).not.toBeNull();
|
|
97
|
+
expect(result!.source).toBe('const x = 1;');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns null for nonexistent files', () => {
|
|
101
|
+
expect(safeReadFile('/tmp/no-such-file-distyll.js')).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns null for empty files', () => {
|
|
105
|
+
const filePath = createTempFile('', '.js');
|
|
106
|
+
expect(safeReadFile(filePath)).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns null for binary files', () => {
|
|
110
|
+
const buf = Buffer.alloc(20);
|
|
111
|
+
buf[10] = 0;
|
|
112
|
+
const filePath = createTempFile(buf, '.js');
|
|
113
|
+
expect(safeReadFile(filePath)).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('error formatting', () => {
|
|
118
|
+
it('formats DistyllError with hint', () => {
|
|
119
|
+
const err = new DistyllError('Something failed', 'Try running with --verbose');
|
|
120
|
+
const msg = formatError(err);
|
|
121
|
+
expect(msg).toContain('Something failed');
|
|
122
|
+
expect(msg).toContain('Try running with --verbose');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('formats DistyllError without hint', () => {
|
|
126
|
+
const err = new DistyllError('Bad input');
|
|
127
|
+
const msg = formatError(err);
|
|
128
|
+
expect(msg).toBe('Error: Bad input');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('formats plain Error', () => {
|
|
132
|
+
const err = new Error('generic error');
|
|
133
|
+
const msg = formatError(err);
|
|
134
|
+
expect(msg).toBe('Error: generic error');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('formats non-Error values', () => {
|
|
138
|
+
expect(formatError('oops')).toBe('Error: oops');
|
|
139
|
+
expect(formatError(42)).toBe('Error: 42');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('special content handling', () => {
|
|
144
|
+
it('handles files with only whitespace', () => {
|
|
145
|
+
const filePath = createTempFile(' \n\n \n', '.js');
|
|
146
|
+
const result = scanFile(filePath);
|
|
147
|
+
// Should parse without crashing — whitespace-only file is technically valid
|
|
148
|
+
expect(result).not.toBeNull();
|
|
149
|
+
expect(result!.findings).toHaveLength(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('handles files with unicode content', () => {
|
|
153
|
+
const filePath = createTempFile(
|
|
154
|
+
'const greeting = "こんにちは世界";\nconsole.log(greeting);\n',
|
|
155
|
+
'.js'
|
|
156
|
+
);
|
|
157
|
+
const result = scanFile(filePath);
|
|
158
|
+
expect(result).not.toBeNull();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('handles very long single-line files', () => {
|
|
162
|
+
const longLine = 'const x = ' + '"a".repeat(10000)' + ';\n';
|
|
163
|
+
const filePath = createTempFile(longLine, '.js');
|
|
164
|
+
const result = scanFile(filePath);
|
|
165
|
+
expect(result).not.toBeNull();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { analyzeCodebase } from '../../src/fingerprint/analyzer';
|
|
6
|
+
|
|
7
|
+
describe('fingerprint analyzer', () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'distyll-fp-test-'));
|
|
12
|
+
|
|
13
|
+
// Create sample JS file with known patterns
|
|
14
|
+
fs.writeFileSync(
|
|
15
|
+
path.join(tmpDir, 'sample.js'),
|
|
16
|
+
`
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
|
|
20
|
+
// A simple function
|
|
21
|
+
function calculateTotal(items) {
|
|
22
|
+
let total = 0;
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
total += item.price;
|
|
25
|
+
}
|
|
26
|
+
return total;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatName(firstName, lastName) {
|
|
30
|
+
return firstName + ' ' + lastName;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const fetchData = async () => {
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch('/api/data');
|
|
36
|
+
return response.json();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(err);
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
class UserService {
|
|
44
|
+
getUser(id) {
|
|
45
|
+
return this.db.find(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`.trim(),
|
|
49
|
+
'utf-8'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Create sample Python file
|
|
53
|
+
fs.writeFileSync(
|
|
54
|
+
path.join(tmpDir, 'sample.py'),
|
|
55
|
+
`
|
|
56
|
+
import os
|
|
57
|
+
import json
|
|
58
|
+
|
|
59
|
+
def calculate_total(items):
|
|
60
|
+
total = 0
|
|
61
|
+
for item in items:
|
|
62
|
+
total += item["price"]
|
|
63
|
+
return total
|
|
64
|
+
|
|
65
|
+
def format_name(first_name, last_name):
|
|
66
|
+
return first_name + " " + last_name
|
|
67
|
+
`.trim(),
|
|
68
|
+
'utf-8'
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterAll(() => {
|
|
73
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('produces a valid style profile', async () => {
|
|
77
|
+
const profile = await analyzeCodebase([tmpDir]);
|
|
78
|
+
|
|
79
|
+
expect(profile.fileCount).toBe(2);
|
|
80
|
+
expect(profile.totalLoc).toBeGreaterThan(0);
|
|
81
|
+
expect(profile.generatedAt).toBeTruthy();
|
|
82
|
+
expect(profile.metrics).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('computes function length metrics', async () => {
|
|
86
|
+
const profile = await analyzeCodebase([tmpDir]);
|
|
87
|
+
|
|
88
|
+
expect(profile.metrics.medianFunctionLength).toBeGreaterThan(0);
|
|
89
|
+
expect(profile.metrics.averageFunctionLength).toBeGreaterThan(0);
|
|
90
|
+
expect(profile.metrics.maxFunctionLength).toBeGreaterThan(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('detects naming conventions', async () => {
|
|
94
|
+
const profile = await analyzeCodebase([tmpDir]);
|
|
95
|
+
const nc = profile.metrics.namingConventions;
|
|
96
|
+
|
|
97
|
+
// Should detect some camelCase (JS) and some snake_case (Python)
|
|
98
|
+
expect(nc.camelCase + nc.snake_case + nc.PascalCase + nc.other).toBeGreaterThan(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('computes comment-to-code ratio', async () => {
|
|
102
|
+
const profile = await analyzeCodebase([tmpDir]);
|
|
103
|
+
|
|
104
|
+
// We have a few comments so ratio should be > 0
|
|
105
|
+
expect(profile.metrics.commentToCodeRatio).toBeGreaterThanOrEqual(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('detects try-catch usage', async () => {
|
|
109
|
+
const profile = await analyzeCodebase([tmpDir]);
|
|
110
|
+
|
|
111
|
+
// We have one try-catch in the sample
|
|
112
|
+
expect(profile.metrics.tryCatchDensity).toBeGreaterThan(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('computes import metrics', async () => {
|
|
116
|
+
const profile = await analyzeCodebase([tmpDir]);
|
|
117
|
+
|
|
118
|
+
expect(profile.metrics.averageImportsPerFile).toBeGreaterThan(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('handles empty directory', async () => {
|
|
122
|
+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'distyll-empty-'));
|
|
123
|
+
const profile = await analyzeCodebase([emptyDir]);
|
|
124
|
+
|
|
125
|
+
expect(profile.fileCount).toBe(0);
|
|
126
|
+
expect(profile.totalLoc).toBe(0);
|
|
127
|
+
expect(profile.metrics.medianFunctionLength).toBe(0);
|
|
128
|
+
|
|
129
|
+
fs.rmSync(emptyDir, { recursive: true, force: true });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parse } from '../../src/parser';
|
|
3
|
+
import { fixUnnecessaryTryCatch } from '../../src/fixes/unnecessary-try-catch';
|
|
4
|
+
import { unnecessaryTryCatch } from '../../src/rules/unnecessary-try-catch';
|
|
5
|
+
import { attachFixes } from '../../src/fixes';
|
|
6
|
+
|
|
7
|
+
describe('fix: unnecessary-try-catch', () => {
|
|
8
|
+
it('generates a fix that unwraps the try body', () => {
|
|
9
|
+
const code = `try {
|
|
10
|
+
const x = 1;
|
|
11
|
+
const y = 2;
|
|
12
|
+
} catch (e) {
|
|
13
|
+
console.log(e);
|
|
14
|
+
}`;
|
|
15
|
+
const tree = parse(code, 'javascript');
|
|
16
|
+
|
|
17
|
+
const fix = fixUnnecessaryTryCatch(tree, code, 1);
|
|
18
|
+
expect(fix).not.toBeNull();
|
|
19
|
+
expect(fix!.description).toContain('Remove try-catch');
|
|
20
|
+
expect(fix!.replacement).toContain('const x = 1');
|
|
21
|
+
expect(fix!.replacement).toContain('const y = 2');
|
|
22
|
+
// Should not contain catch block
|
|
23
|
+
expect(fix!.replacement).not.toContain('catch');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns null for lines without try-catch', () => {
|
|
27
|
+
const code = `const x = 1;`;
|
|
28
|
+
const tree = parse(code, 'javascript');
|
|
29
|
+
|
|
30
|
+
const fix = fixUnnecessaryTryCatch(tree, code, 1);
|
|
31
|
+
expect(fix).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('attaches fix to findings via attachFixes', () => {
|
|
35
|
+
const code = `try {
|
|
36
|
+
const x = 1;
|
|
37
|
+
const y = 2;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.log(e);
|
|
40
|
+
}`;
|
|
41
|
+
const tree = parse(code, 'javascript');
|
|
42
|
+
const findings = unnecessaryTryCatch.check(tree, code, 'test.js');
|
|
43
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
44
|
+
|
|
45
|
+
const withFixes = attachFixes(findings, tree, code);
|
|
46
|
+
expect(withFixes[0].fix).toBeDefined();
|
|
47
|
+
expect(withFixes[0].fix!.description).toContain('Remove try-catch');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('generates fix for rethrow-only catch', () => {
|
|
51
|
+
const code = `try {
|
|
52
|
+
doSomething();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
throw e;
|
|
55
|
+
}`;
|
|
56
|
+
const tree = parse(code, 'javascript');
|
|
57
|
+
|
|
58
|
+
const fix = fixUnnecessaryTryCatch(tree, code, 1);
|
|
59
|
+
expect(fix).not.toBeNull();
|
|
60
|
+
expect(fix!.replacement).toContain('doSomething()');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseDiff } from '../src/git';
|
|
3
|
+
|
|
4
|
+
describe('parseDiff', () => {
|
|
5
|
+
it('should parse a simple diff with one file', () => {
|
|
6
|
+
const diff = `diff --git a/test.js b/test.js
|
|
7
|
+
index 1234567..abcdefg 100644
|
|
8
|
+
--- a/test.js
|
|
9
|
+
+++ b/test.js
|
|
10
|
+
@@ -1,3 +1,5 @@
|
|
11
|
+
const x = 1;
|
|
12
|
+
+const y = 2;
|
|
13
|
+
+const z = 3;
|
|
14
|
+
const a = 4;
|
|
15
|
+
`;
|
|
16
|
+
const hunks = parseDiff(diff, '/repo');
|
|
17
|
+
expect(hunks).toHaveLength(1);
|
|
18
|
+
expect(hunks[0].file).toBe('/repo/test.js');
|
|
19
|
+
expect(hunks[0].addedLines).toEqual([1, 2, 3, 4, 5]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should parse a diff with multiple files', () => {
|
|
23
|
+
const diff = `diff --git a/foo.js b/foo.js
|
|
24
|
+
index 1234567..abcdefg 100644
|
|
25
|
+
--- a/foo.js
|
|
26
|
+
+++ b/foo.js
|
|
27
|
+
@@ -0,0 +1,3 @@
|
|
28
|
+
+line 1
|
|
29
|
+
+line 2
|
|
30
|
+
+line 3
|
|
31
|
+
diff --git a/bar.js b/bar.js
|
|
32
|
+
index 1234567..abcdefg 100644
|
|
33
|
+
--- a/bar.js
|
|
34
|
+
+++ b/bar.js
|
|
35
|
+
@@ -5,0 +6,2 @@
|
|
36
|
+
+new line 1
|
|
37
|
+
+new line 2
|
|
38
|
+
`;
|
|
39
|
+
const hunks = parseDiff(diff, '/repo');
|
|
40
|
+
expect(hunks).toHaveLength(2);
|
|
41
|
+
expect(hunks[0].file).toBe('/repo/foo.js');
|
|
42
|
+
expect(hunks[0].addedLines).toEqual([1, 2, 3]);
|
|
43
|
+
expect(hunks[1].file).toBe('/repo/bar.js');
|
|
44
|
+
expect(hunks[1].addedLines).toEqual([6, 7]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle empty diff', () => {
|
|
48
|
+
const hunks = parseDiff('', '/repo');
|
|
49
|
+
expect(hunks).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should parse a diff with multiple hunks in one file', () => {
|
|
53
|
+
const diff = `diff --git a/test.js b/test.js
|
|
54
|
+
index 1234567..abcdefg 100644
|
|
55
|
+
--- a/test.js
|
|
56
|
+
+++ b/test.js
|
|
57
|
+
@@ -1,0 +2,1 @@
|
|
58
|
+
+new line at top
|
|
59
|
+
@@ -10,0 +12,2 @@
|
|
60
|
+
+new line in middle 1
|
|
61
|
+
+new line in middle 2
|
|
62
|
+
`;
|
|
63
|
+
const hunks = parseDiff(diff, '/repo');
|
|
64
|
+
expect(hunks).toHaveLength(1);
|
|
65
|
+
expect(hunks[0].addedLines).toEqual([2, 12, 13]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should handle single-line hunk with no count', () => {
|
|
69
|
+
const diff = `diff --git a/test.js b/test.js
|
|
70
|
+
--- a/test.js
|
|
71
|
+
+++ b/test.js
|
|
72
|
+
@@ -5 +5 @@
|
|
73
|
+
+replaced line
|
|
74
|
+
`;
|
|
75
|
+
const hunks = parseDiff(diff, '/repo');
|
|
76
|
+
expect(hunks).toHaveLength(1);
|
|
77
|
+
expect(hunks[0].addedLines).toEqual([5]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { hallucinatedImports } from '../../src/rules/hallucinated-imports';
|
|
6
|
+
import { parse } from '../../src/parser';
|
|
7
|
+
|
|
8
|
+
function checkJS(code: string, filePath?: string) {
|
|
9
|
+
const tree = parse(code, 'javascript');
|
|
10
|
+
return hallucinatedImports.check(tree, code, filePath ?? '/tmp/test.js');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function checkPython(code: string, filePath?: string) {
|
|
14
|
+
const tree = parse(code, 'python');
|
|
15
|
+
return hallucinatedImports.check(tree, code, filePath ?? '/tmp/test.py');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('hallucinated-imports', () => {
|
|
19
|
+
it('flags a relative import that does not exist', () => {
|
|
20
|
+
const findings = checkJS(`import { foo } from './nonexistent';`);
|
|
21
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
22
|
+
expect(findings[0].message).toContain('nonexistent');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('does NOT flag Node.js stdlib imports', () => {
|
|
26
|
+
const findings = checkJS(`import * as fs from 'fs';`);
|
|
27
|
+
expect(findings).toHaveLength(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does NOT flag node: protocol imports', () => {
|
|
31
|
+
const findings = checkJS(`import { readFile } from 'node:fs';`);
|
|
32
|
+
expect(findings).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('does NOT flag Python stdlib imports', () => {
|
|
36
|
+
const findings = checkPython(`import os\nimport sys\nimport json`);
|
|
37
|
+
expect(findings).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('does NOT flag Python from-imports of stdlib', () => {
|
|
41
|
+
const findings = checkPython(`from pathlib import Path`);
|
|
42
|
+
expect(findings).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does NOT flag installed node_modules', () => {
|
|
46
|
+
// Use a known installed package in this project
|
|
47
|
+
const projFile = path.join(process.cwd(), 'src', 'test.js');
|
|
48
|
+
const findings = checkJS(`import chalk from 'chalk';`, projFile);
|
|
49
|
+
expect(findings).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('flags relative import to missing file', () => {
|
|
53
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'distyll-test-'));
|
|
54
|
+
const testFile = path.join(tmpDir, 'index.js');
|
|
55
|
+
fs.writeFileSync(testFile, '');
|
|
56
|
+
const findings = checkJS(`import { bar } from './missing-module';`, testFile);
|
|
57
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
58
|
+
});
|
|
59
|
+
});
|