tackbox 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/LICENSE +21 -0
- package/README.md +552 -0
- package/bin/tackbox-eslint.js +147 -0
- package/bin/tackbox-mdlint.js +45 -0
- package/eslint.config.preset.js +21 -0
- package/js/README.md +209 -0
- package/js/eslint-plugin.js +48 -0
- package/js/markdownlint-rules/no-non-ascii.js +132 -0
- package/js/report.js +187 -0
- package/js/rules/_shared.js +809 -0
- package/js/rules/no-broad-notify.js +47 -0
- package/js/rules/no-console-error.js +23 -0
- package/js/rules/no-focused-test.js +22 -0
- package/js/rules/no-parse-fallback.js +150 -0
- package/js/rules/no-skipped-test.js +95 -0
- package/js/rules/no-swallow-allsettled.js +52 -0
- package/js/rules/no-swallow-catch.js +31 -0
- package/js/rules/no-swallow-promise-catch.js +38 -0
- package/js/rules/no-throw-and-report.js +30 -0
- package/js/rules/ts-exit-in-catch.js +26 -0
- package/js/rules/ts-rethrow-without-cause.js +62 -0
- package/js/rules/ts-useless-catch.js +25 -0
- package/js/rules/valid-dedup-key.js +50 -0
- package/js/rules/valid-error-report.js +99 -0
- package/js/tests/eslint-wrapper.test.js +53 -0
- package/js/tests/mdlint-wrapper.test.js +85 -0
- package/js/tests/no-non-ascii.test.js +124 -0
- package/js/tests/report.test.js +182 -0
- package/js/tests/reporters.test.js +223 -0
- package/js/tests/rules.test.js +650 -0
- package/js/tests/svelte.test.js +61 -0
- package/package.json +43 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const { test } = require('node:test')
|
|
2
|
+
const assert = require('node:assert')
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const os = require('node:os')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
const { RuleTester } = require('eslint')
|
|
7
|
+
|
|
8
|
+
const ruleTester = new RuleTester({
|
|
9
|
+
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const noSwallow = require('../rules/no-swallow-catch')
|
|
13
|
+
const noSwallowPromise = require('../rules/no-swallow-promise-catch')
|
|
14
|
+
const noConsoleError = require('../rules/no-console-error')
|
|
15
|
+
const validReport = require('../rules/valid-error-report')
|
|
16
|
+
|
|
17
|
+
const decl = (code, invalid) => ({
|
|
18
|
+
code,
|
|
19
|
+
filename: 'app.js',
|
|
20
|
+
settings: { tackbox: { reporters: ['app.js#myReport'] } },
|
|
21
|
+
...(invalid ? { errors: [{ messageId: 'swallow' }] } : {}),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// tier-1: name-trust is dead - a local function that merely shares a reporter
|
|
25
|
+
// name does not count as a reporter.
|
|
26
|
+
test('name-trust death: local reportError does not satisfy no-swallow-catch', () => {
|
|
27
|
+
ruleTester.run('no-swallow-catch', noSwallow, {
|
|
28
|
+
valid: [],
|
|
29
|
+
invalid: [
|
|
30
|
+
{
|
|
31
|
+
code: "function reportError(m, e) {}\ntry { f() } catch (e) { reportError('handled it', e) }",
|
|
32
|
+
errors: [{ messageId: 'swallow' }],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// tier-1: named / renamed / default-member / namespace-member / CJS destructure
|
|
39
|
+
// all resolve to a tackbox/report origin.
|
|
40
|
+
test('tier-1 import forms are recognized', () => {
|
|
41
|
+
const body = "try { f() } catch (e) { %CALL% }"
|
|
42
|
+
ruleTester.run('no-swallow-catch', noSwallow, {
|
|
43
|
+
valid: [
|
|
44
|
+
"import { reportError } from 'tackbox/report'\n" + body.replace('%CALL%', "reportError('connection lost mid-stream', e)"),
|
|
45
|
+
"import { reportError as re } from 'tackbox/report'\n" + body.replace('%CALL%', "re('connection lost mid-stream', e)"),
|
|
46
|
+
"import report from 'tackbox/report'\n" + body.replace('%CALL%', "report.reportError('connection lost mid-stream', e)"),
|
|
47
|
+
"import * as report from 'tackbox/report'\n" + body.replace('%CALL%', "report.reportError('connection lost mid-stream', e)"),
|
|
48
|
+
"const { reportError } = require('tackbox/report')\n" + body.replace('%CALL%', "reportError('connection lost mid-stream', e)"),
|
|
49
|
+
],
|
|
50
|
+
invalid: [],
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// tier-2: a declared local function counts only when the caught error flows in.
|
|
55
|
+
test('tier-2 declaration with argument-flow', () => {
|
|
56
|
+
ruleTester.run('no-swallow-catch', noSwallow, {
|
|
57
|
+
valid: [
|
|
58
|
+
decl("function myReport(m, e) {}\ntry { f() } catch (e) { myReport('handled', e) }"),
|
|
59
|
+
],
|
|
60
|
+
invalid: [
|
|
61
|
+
decl("function myReport(m, e) {}\ntry { f() } catch (e) { myReport('handled') }", true),
|
|
62
|
+
],
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// tier-2: the promise-catch handler parameter is an argument-flow source too.
|
|
67
|
+
test('tier-2 promise-catch argument-flow', () => {
|
|
68
|
+
ruleTester.run('no-swallow-promise-catch', noSwallowPromise, {
|
|
69
|
+
valid: [
|
|
70
|
+
decl("function myReport(m, e) {}\np.catch(e => myReport('handled', e))"),
|
|
71
|
+
],
|
|
72
|
+
invalid: [
|
|
73
|
+
decl("function myReport(m) {}\np.catch(() => myReport('handled'))", true),
|
|
74
|
+
],
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// no-console-error is silent inside a declared reporter's body, loud outside.
|
|
79
|
+
test('no-console-error exempts declared reporter bodies', () => {
|
|
80
|
+
ruleTester.run('no-console-error', noConsoleError, {
|
|
81
|
+
valid: [
|
|
82
|
+
{
|
|
83
|
+
code: 'function myReport(m, e) { console.error(m, e) }',
|
|
84
|
+
filename: 'app.js',
|
|
85
|
+
settings: { tackbox: { reporters: ['app.js#myReport'] } },
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
invalid: [
|
|
89
|
+
{
|
|
90
|
+
code: "function other() { console.error('boom') }",
|
|
91
|
+
filename: 'app.js',
|
|
92
|
+
settings: { tackbox: { reporters: ['app.js#myReport'] } },
|
|
93
|
+
errors: [{ messageId: 'use' }],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// tier-2 multi-extension: a `.svelte.ts` rune module declared as a reporter must
|
|
100
|
+
// match specifiers that omit the compound extension. A Svelte rune module keeps
|
|
101
|
+
// the double extension; `./errors`, `./errors.svelte`, and `./errors.svelte.ts`
|
|
102
|
+
// all name errors.svelte.ts and must all resolve to the declaration.
|
|
103
|
+
test('tier-2 multi-extension: shortened .svelte(.ts) specifiers match a .svelte.ts declaration', () => {
|
|
104
|
+
const body = "\ntry { f() } catch (e) { myReport('connection dropped mid-stream', e) }"
|
|
105
|
+
const forms = ['./errors.svelte.ts', './errors.svelte', './errors']
|
|
106
|
+
ruleTester.run('no-swallow-catch', noSwallow, {
|
|
107
|
+
valid: forms.map(spec => ({
|
|
108
|
+
code: `import { myReport } from '${spec}'` + body,
|
|
109
|
+
filename: 'app.js',
|
|
110
|
+
settings: { tackbox: { reporters: ['errors.svelte.ts#myReport'] } },
|
|
111
|
+
})),
|
|
112
|
+
invalid: [],
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// tier-2 declared reporters carry only the argument-flow contract. The strict
|
|
117
|
+
// tier-1 signature checks (here valid-error-report's 4-arg dedupKey demand) must
|
|
118
|
+
// stay off them even after alias / multi-extension resolution recognizes them -
|
|
119
|
+
// a 3-arg reportError declared via a relative `.svelte` import earns no finding.
|
|
120
|
+
test('valid-error-report leaves tier-2 declared reporters alone (no dedupKey demand)', () => {
|
|
121
|
+
ruleTester.run('valid-error-report', validReport, {
|
|
122
|
+
valid: [
|
|
123
|
+
{
|
|
124
|
+
code: "import { reportError } from './errors.svelte'\ntry { f() } catch (e) { reportError('debug probe failed', e, { c: 'x' }) }",
|
|
125
|
+
filename: 'app.js',
|
|
126
|
+
settings: { tackbox: { reporters: ['errors.svelte.ts#reportError'] } },
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
invalid: [],
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// tier-2 $lib alias: a SvelteKit `$lib/...` import of a declared `.svelte.ts`
|
|
134
|
+
// reporter is recognized. Resolution is convention-based (nearest ancestor
|
|
135
|
+
// svelte.config.* -> its src/lib), so it holds on a fresh clone / CI where the
|
|
136
|
+
// generated .svelte-kit/tsconfig.json is absent. A real on-disk tree exercises
|
|
137
|
+
// the fs walk; an in-repo fixture tree is banned (self-lint would scan it).
|
|
138
|
+
async function lintTree(files, reporters) {
|
|
139
|
+
const { ESLint } = require('eslint')
|
|
140
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tbx-lib-'))
|
|
141
|
+
try {
|
|
142
|
+
for (const [rel, code] of Object.entries(files)) {
|
|
143
|
+
const abs = path.join(root, rel)
|
|
144
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true })
|
|
145
|
+
fs.writeFileSync(abs, code)
|
|
146
|
+
}
|
|
147
|
+
const eslint = new ESLint({
|
|
148
|
+
cwd: root,
|
|
149
|
+
overrideConfigFile: path.join(__dirname, '..', '..', 'eslint.config.preset.js'),
|
|
150
|
+
overrideConfig: [{ settings: { tackbox: { reporters } } }],
|
|
151
|
+
})
|
|
152
|
+
const targets = Object.keys(files)
|
|
153
|
+
.filter(rel => /\.(svelte|ts|js)$/.test(rel) && !rel.endsWith('svelte.config.js'))
|
|
154
|
+
.map(rel => path.join(root, rel))
|
|
155
|
+
const results = await eslint.lintFiles(targets)
|
|
156
|
+
return results.map(r => ({
|
|
157
|
+
file: path.relative(root, r.filePath),
|
|
158
|
+
errors: r.messages.filter(m => m.severity === 2).map(m => `${m.ruleId}:${m.line}`),
|
|
159
|
+
}))
|
|
160
|
+
} finally {
|
|
161
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
test('tier-2 $lib alias: aliased .svelte.ts reporter is recognized; bare/undeclared swallows still flagged', async () => {
|
|
166
|
+
const reporterSrc =
|
|
167
|
+
'export function reportError(msg, cause, tags) { void msg; void cause; void tags }\n'
|
|
168
|
+
const consumer =
|
|
169
|
+
"<script>\n" +
|
|
170
|
+
" import { reportError } from '$lib/stores/errors.svelte';\n" +
|
|
171
|
+
" function go() {\n" +
|
|
172
|
+
" doThing().catch((e) => reportError('inline edit commit failed', e, { c: 'x' }));\n" +
|
|
173
|
+
" try { risky() } catch (e) { reportError('inline edit focus failed', e, { c: 'y' }) }\n" +
|
|
174
|
+
" }\n" +
|
|
175
|
+
"</script>\n"
|
|
176
|
+
// A dropped error and a call to an undeclared local reporter must still be
|
|
177
|
+
// caught - recognition widens for the declared alias only, nothing else.
|
|
178
|
+
const bad =
|
|
179
|
+
"<script>\n" +
|
|
180
|
+
" function reportError(m, e) { void m; void e }\n" +
|
|
181
|
+
" function go() {\n" +
|
|
182
|
+
" try { risky() } catch (e) { const _ = 1; void _ }\n" +
|
|
183
|
+
" try { risky() } catch (e) { reportError('local shadow not declared', e) }\n" +
|
|
184
|
+
" }\n" +
|
|
185
|
+
"</script>\n"
|
|
186
|
+
|
|
187
|
+
const out = await lintTree(
|
|
188
|
+
{
|
|
189
|
+
'frontend/svelte.config.js': 'export default {}\n',
|
|
190
|
+
'frontend/src/lib/stores/errors.svelte.ts': reporterSrc,
|
|
191
|
+
'frontend/src/lib/components/InlineEdit.svelte': consumer,
|
|
192
|
+
'frontend/src/lib/components/Bad.svelte': bad,
|
|
193
|
+
},
|
|
194
|
+
['frontend/src/lib/stores/errors.svelte.ts#reportError'],
|
|
195
|
+
)
|
|
196
|
+
const byFile = Object.fromEntries(out.map(r => [r.file, r.errors]))
|
|
197
|
+
assert.deepEqual(
|
|
198
|
+
byFile['frontend/src/lib/components/InlineEdit.svelte'],
|
|
199
|
+
[],
|
|
200
|
+
'aliased declared reporter should be recognized',
|
|
201
|
+
)
|
|
202
|
+
const badErrs = byFile['frontend/src/lib/components/Bad.svelte'] || []
|
|
203
|
+
assert.equal(badErrs.length, 2, 'bare swallow + undeclared reporter must both flag: ' + badErrs.join(', '))
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// report.js self-lints clean when the repo-root .tackbox-reporters declares
|
|
207
|
+
// reportPanic as the fatal-lane console sink (matching the real self-lint): the
|
|
208
|
+
// declaration exempts that one console.error, the catches carry no-report
|
|
209
|
+
// markers, and the reportError calls sit in event callbacks, not catches. This
|
|
210
|
+
// is an acceptance assert, not a resolution branch.
|
|
211
|
+
test('report.js self-lint is clean with reportPanic declared', async () => {
|
|
212
|
+
const { ESLint } = require('eslint')
|
|
213
|
+
const root = path.join(__dirname, '..', '..')
|
|
214
|
+
const eslint = new ESLint({
|
|
215
|
+
cwd: root,
|
|
216
|
+
overrideConfigFile: path.join(root, 'eslint.config.preset.js'),
|
|
217
|
+
overrideConfig: [{ settings: { tackbox: { reporters: ['js/report.js#reportPanic'] } } }],
|
|
218
|
+
})
|
|
219
|
+
const results = await eslint.lintFiles([path.join(__dirname, '..', 'report.js')])
|
|
220
|
+
const errs = results.reduce((a, r) => a + r.errorCount + r.fatalErrorCount, 0)
|
|
221
|
+
const msgs = results.flatMap(r => r.messages.map(m => `${m.ruleId}:${m.line} ${m.message}`))
|
|
222
|
+
assert.equal(errs, 0, 'report.js not clean with reportPanic declared:\n' + msgs.join('\n'))
|
|
223
|
+
})
|