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,650 @@
|
|
|
1
|
+
const { test } = require('node:test')
|
|
2
|
+
const { RuleTester } = require('eslint')
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
// The Result-boundary exit (F2) keys off the enclosing function's return-type
|
|
9
|
+
// annotation, so those fixtures need the TS parser (espree has no returnType).
|
|
10
|
+
const tsRuleTester = new RuleTester({
|
|
11
|
+
languageOptions: { parser: require('@typescript-eslint/parser'), ecmaVersion: 2022, sourceType: 'module' },
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// Reporter calls are recognized only through a tackbox/report import (tier-1)
|
|
15
|
+
// or a .tackbox-reporters declaration (tier-2); a bare name is not trusted.
|
|
16
|
+
// These rule tests import the canonical reporters so the calls resolve.
|
|
17
|
+
const R =
|
|
18
|
+
"import { reportError, reportSynth, reportSynthError, reportApiError, reportWarn, reportLayerError } from 'tackbox/report'\n"
|
|
19
|
+
|
|
20
|
+
// Namespace import: qualified `report.reportError(...)` calls must resolve to the
|
|
21
|
+
// same tier-1 origin as the bare named form. Proves origin resolution on
|
|
22
|
+
// member-expression callees, not only bare identifiers.
|
|
23
|
+
const NS = "import * as report from 'tackbox/report'\n"
|
|
24
|
+
|
|
25
|
+
// notify is origin-gated like a reporter but is NOT in REPORTER_NAMES; the
|
|
26
|
+
// D006/D007/D008 rules resolve it through the same tackbox/report import.
|
|
27
|
+
const N = "import { notify, reportError, reportWarn } from 'tackbox/report'\n"
|
|
28
|
+
|
|
29
|
+
test('no-swallow-catch', () => {
|
|
30
|
+
ruleTester.run('no-swallow-catch', require('../rules/no-swallow-catch'), {
|
|
31
|
+
valid: [
|
|
32
|
+
'try { f() } catch (e) { throw e }',
|
|
33
|
+
R + 'try { f() } catch (e) { reportError("connection lost mid-stream", e) }',
|
|
34
|
+
// reporter anywhere in the block (after other statements) keeps the catch
|
|
35
|
+
// clean: block-scan, pinned so a future path-sensitive port cannot regress it (F2).
|
|
36
|
+
R + 'try { f() } catch (e) { cleanupState(); logLocally(e); reportError("connection lost mid-stream", e) }',
|
|
37
|
+
'// no-report: bootstrap-only, no Sentry stack yet\ntry { f() } catch (e) {}',
|
|
38
|
+
'// no-report: bootstrap-only, no Sentry stack yet, a reason long\n// enough that splitting it across lines is the point\ntry { f() } catch (e) {}',
|
|
39
|
+
],
|
|
40
|
+
invalid: [
|
|
41
|
+
{ code: 'try { f() } catch (e) {}', errors: [{ messageId: 'swallow' }] },
|
|
42
|
+
{ code: 'try { f() } catch (e) { console.log(e) }', errors: [{ messageId: 'swallow' }] },
|
|
43
|
+
{ code: '// no-report: reason\n\ntry { f() } catch (e) {}', errors: [{ messageId: 'swallow' }] },
|
|
44
|
+
],
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// F2: typed Result-boundary is the third legal catch exit (throw / reporter /
|
|
49
|
+
// boundary). Legal only when the enclosing fn is annotated Result / Attempt /
|
|
50
|
+
// Promise<Result|Attempt> AND the caught err flows into `{ ok:false, cause:err }`.
|
|
51
|
+
test('no-swallow-catch result-boundary (F2)', () => {
|
|
52
|
+
tsRuleTester.run('no-swallow-catch', require('../rules/no-swallow-catch'), {
|
|
53
|
+
valid: [
|
|
54
|
+
'function f(): Result<T> { try { g() } catch (e) { return { ok: false, cause: e } } }',
|
|
55
|
+
'function f(): Attempt { try { g() } catch (e) { return { ok: false, message: e } } }',
|
|
56
|
+
'async function f(): Promise<Result<T>> { try { await g() } catch (e) { return { ok: false, cause: e } } }',
|
|
57
|
+
],
|
|
58
|
+
invalid: [
|
|
59
|
+
// bare { ok:false } drops the caught error -> swallow.
|
|
60
|
+
{ code: 'function f(): Result<T> { try { g() } catch (e) { return { ok: false } } }', errors: [{ messageId: 'swallow' }] },
|
|
61
|
+
// boundary carries some other identifier, not the caught err -> swallow.
|
|
62
|
+
{ code: 'function f(): Result<T> { try { g() } catch (e) { return { ok: false, cause: other } } }', errors: [{ messageId: 'swallow' }] },
|
|
63
|
+
// no Result annotation on the enclosing fn -> no boundary credit (annotation-based).
|
|
64
|
+
{ code: 'function f() { try { g() } catch (e) { return { ok: false, cause: e } } }', errors: [{ messageId: 'swallow' }] },
|
|
65
|
+
// non-Result return annotation -> no credit.
|
|
66
|
+
{ code: 'function f(): void { try { g() } catch (e) { return { ok: false, cause: e } } }', errors: [{ messageId: 'swallow' }] },
|
|
67
|
+
],
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// F2b: one coherent path-sensitive analysis over all three exits. Every path
|
|
72
|
+
// must terminate (throw / boundary) or pass a sticky reporter before the end;
|
|
73
|
+
// a path reaching the end without an event is a finding. Reporters need not be
|
|
74
|
+
// terminal (sticky). Opaque constructs (switch/loop) do not surface events.
|
|
75
|
+
test('no-swallow-catch path-sensitive (F2b)', () => {
|
|
76
|
+
ruleTester.run('no-swallow-catch', require('../rules/no-swallow-catch'), {
|
|
77
|
+
valid: [
|
|
78
|
+
// both branches handled (reporter on one, throw on the other).
|
|
79
|
+
R + 'try { f() } catch (e) { if (x) { reportError("connection lost mid-stream", e) } else { throw e } }',
|
|
80
|
+
// reporter is sticky: statements after it on the same path are fine.
|
|
81
|
+
R + 'try { f() } catch (e) { reportError("connection lost mid-stream", e); cleanup() }',
|
|
82
|
+
],
|
|
83
|
+
invalid: [
|
|
84
|
+
// throw on only one branch: the else path falls through (flip of block-scan).
|
|
85
|
+
{ code: 'try { f() } catch (e) { if (x) { throw e } }', errors: [{ messageId: 'swallow' }] },
|
|
86
|
+
// reporter on only one branch: the else path swallows (flip of block-scan).
|
|
87
|
+
{ code: R + 'try { f() } catch (e) { if (x) { reportError("connection lost mid-stream", e) } }', errors: [{ messageId: 'swallow' }] },
|
|
88
|
+
// switch is opaque: a reporter inside it does not count as a path event.
|
|
89
|
+
{ code: R + 'try { f() } catch (e) { switch (x) { case 1: reportError("connection lost mid-stream", e) } }', errors: [{ messageId: 'swallow' }] },
|
|
90
|
+
],
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('no-swallow-catch path-sensitive boundary (F2b)', () => {
|
|
95
|
+
tsRuleTester.run('no-swallow-catch', require('../rules/no-swallow-catch'), {
|
|
96
|
+
valid: [
|
|
97
|
+
// both branches terminate: boundary on one, throw on the other.
|
|
98
|
+
'function f(): Result<T> { try { g() } catch (e) { if (x) { return { ok: false, cause: e } } else { throw e } } }',
|
|
99
|
+
],
|
|
100
|
+
invalid: [
|
|
101
|
+
// boundary on one branch, the else path just logs and falls through.
|
|
102
|
+
{ code: 'function f(): Result<T> { try { g() } catch (e) { if (x) { return { ok: false, cause: e } } else { log(e) } } }', errors: [{ messageId: 'swallow' }] },
|
|
103
|
+
],
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('no-swallow-promise-catch', () => {
|
|
108
|
+
ruleTester.run('no-swallow-promise-catch', require('../rules/no-swallow-promise-catch'), {
|
|
109
|
+
valid: [
|
|
110
|
+
'p.catch(e => { throw e })',
|
|
111
|
+
R + 'p.catch(e => { reportError("api call failed mid-flight", e) })',
|
|
112
|
+
],
|
|
113
|
+
invalid: [
|
|
114
|
+
{ code: 'p.catch(e => {})', errors: [{ messageId: 'swallow' }] },
|
|
115
|
+
{ code: 'p.catch(e => { console.log(e) })', errors: [{ messageId: 'swallow' }] },
|
|
116
|
+
{ code: 'p.catch(function (e) {})', errors: [{ messageId: 'swallow' }] },
|
|
117
|
+
],
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Result-boundary conversion is NOT accepted in a promise .catch handler (gmux
|
|
122
|
+
// allowBoundary:false): the enclosing fn's Result type does not govern the
|
|
123
|
+
// callback's return, so it stays a swallow even under Promise<Result<T>>.
|
|
124
|
+
test('no-swallow-promise-catch result-boundary refusal (F2)', () => {
|
|
125
|
+
tsRuleTester.run('no-swallow-promise-catch', require('../rules/no-swallow-promise-catch'), {
|
|
126
|
+
valid: [],
|
|
127
|
+
invalid: [
|
|
128
|
+
{ code: 'function f(): Promise<Result<T>> { return p.catch(e => { return { ok: false, cause: e } }) }', errors: [{ messageId: 'swallow' }] },
|
|
129
|
+
],
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// F2b: the same path-sensitive analysis governs promise .catch handlers.
|
|
134
|
+
test('no-swallow-promise-catch path-sensitive (F2b)', () => {
|
|
135
|
+
ruleTester.run('no-swallow-promise-catch', require('../rules/no-swallow-promise-catch'), {
|
|
136
|
+
valid: [
|
|
137
|
+
R + 'p.catch(e => { if (x) { reportError("api call failed mid-flight", e) } else { throw e } })',
|
|
138
|
+
],
|
|
139
|
+
invalid: [
|
|
140
|
+
// reporter on only one branch: the else path swallows (flip of block-scan).
|
|
141
|
+
{ code: R + 'p.catch(e => { if (x) { reportError("api call failed mid-flight", e) } })', errors: [{ messageId: 'swallow' }] },
|
|
142
|
+
],
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// F7a: the rejection handler of a two-arg .then(onOk, onErr) is a full
|
|
147
|
+
// rejection handler - run through the SAME path-sensitive analysis as .catch
|
|
148
|
+
// (allowBoundary:false). .then(ok) alone propagates the rejection (not a
|
|
149
|
+
// finding); .then(null, onErr) is .catch-equivalent; a non-function second arg
|
|
150
|
+
// (e.g. null) is .then(ok)-equivalent.
|
|
151
|
+
test('no-swallow-promise-catch two-arg then (F7a)', () => {
|
|
152
|
+
ruleTester.run('no-swallow-promise-catch', require('../rules/no-swallow-promise-catch'), {
|
|
153
|
+
valid: [
|
|
154
|
+
// single-arg then: the rejection propagates naturally, no handler to check.
|
|
155
|
+
'p.then(v => use(v))',
|
|
156
|
+
// onErr rethrows on every path.
|
|
157
|
+
'p.then(v => use(v), e => { throw e })',
|
|
158
|
+
// onErr reports (reporter resolved through the tackbox import).
|
|
159
|
+
R + 'p.then(v => use(v), e => { reportError("api call failed mid-flight", e) })',
|
|
160
|
+
// .then(null, onErr) is .catch-equivalent; a handled onErr is clean.
|
|
161
|
+
'p.then(null, e => { throw e })',
|
|
162
|
+
// second arg is not a function literal (null) -> equivalent to .then(ok).
|
|
163
|
+
'p.then(v => use(v), null)',
|
|
164
|
+
],
|
|
165
|
+
invalid: [
|
|
166
|
+
// onErr ignores the rejection entirely: swallow.
|
|
167
|
+
{ code: 'p.then(v => use(v), e => {})', errors: [{ messageId: 'swallow' }] },
|
|
168
|
+
{ code: 'p.then(v => use(v), e => { cleanup() })', errors: [{ messageId: 'swallow' }] },
|
|
169
|
+
// .then(null, onErr) that only logs: swallow.
|
|
170
|
+
{ code: 'p.then(null, e => { console.log(e) })', errors: [{ messageId: 'swallow' }] },
|
|
171
|
+
// fail closed: a reportError NOT imported from tackbox does not resolve to
|
|
172
|
+
// a reporter (name-only match is dead), so this onErr swallows.
|
|
173
|
+
{ code: 'p.then(v => use(v), e => { reportError("api call failed mid-flight", e) })', errors: [{ messageId: 'swallow' }] },
|
|
174
|
+
],
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// F7cal: two consumer-calibration exits. (1) calling the enclosing `new
|
|
179
|
+
// Promise(...)` executor's reject parameter is the promise's own rethrow -
|
|
180
|
+
// resolution is structural (the scope binding must be that parameter), a
|
|
181
|
+
// free-standing function named `reject` earns nothing. (2) an identity onErr
|
|
182
|
+
// (`e => e`) settles the chain with the caught error object itself - the
|
|
183
|
+
// recognized rejection-to-value idiom; any wrapper object stays a swallow
|
|
184
|
+
// (the F2 boundary refusal is untouched).
|
|
185
|
+
test('no-swallow-promise-catch executor-reject and identity (F7cal)', () => {
|
|
186
|
+
ruleTester.run('no-swallow-promise-catch', require('../rules/no-swallow-promise-catch'), {
|
|
187
|
+
valid: [
|
|
188
|
+
// reject(e): the executor's second parameter, err object flows in.
|
|
189
|
+
'new Promise((resolve, reject) => { doThing().then(v => resolve(v), e => reject(e)) })',
|
|
190
|
+
// reject with a wrapped error still carries the object (cause).
|
|
191
|
+
'new Promise((resolve, reject) => { doThing().then(v => resolve(v), e => { cleanup(); reject(new Error("op failed", { cause: e })) }) })',
|
|
192
|
+
// identity onErr: the settled value IS the caught error object.
|
|
193
|
+
'const failure = op.then(() => null, err => err)',
|
|
194
|
+
'p.catch(e => e)',
|
|
195
|
+
// identity on one path, rethrow on the other: both terminate.
|
|
196
|
+
'p.then(v => use(v), e => { if (transient(e)) return e; throw e })',
|
|
197
|
+
],
|
|
198
|
+
invalid: [
|
|
199
|
+
// a free-standing function named reject is not the executor parameter.
|
|
200
|
+
{ code: 'function reject(e) { count += 1 }\np.then(v => use(v), e => reject(e))', errors: [{ messageId: 'swallow' }] },
|
|
201
|
+
// reject fed the stringified error: the object dies on the way out.
|
|
202
|
+
{ code: 'new Promise((resolve, reject) => { p.then(v => resolve(v), e => reject(e.message)) })', errors: [{ messageId: 'swallow' }] },
|
|
203
|
+
// stringified identity is not identity.
|
|
204
|
+
{ code: 'const failure = op.then(() => null, err => err.message)', errors: [{ messageId: 'swallow' }] },
|
|
205
|
+
// a fall-through path drops the rejection.
|
|
206
|
+
{ code: 'op.then(() => null, err => { if (x) return err; })', errors: [{ messageId: 'swallow' }] },
|
|
207
|
+
// a plain-object carrier is not the error itself (F2 refusal holds).
|
|
208
|
+
{ code: 'op.then(() => null, err => ({ wrapped: err }))', errors: [{ messageId: 'swallow' }] },
|
|
209
|
+
],
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// F7cal: reject(e) is equally the terminal exit of a sync catch inside the
|
|
214
|
+
// executor.
|
|
215
|
+
test('no-swallow-catch executor-reject (F7cal)', () => {
|
|
216
|
+
ruleTester.run('no-swallow-catch', require('../rules/no-swallow-catch'), {
|
|
217
|
+
valid: [
|
|
218
|
+
'new Promise((resolve, reject) => { try { resolve(f()) } catch (e) { reject(e) } })',
|
|
219
|
+
],
|
|
220
|
+
invalid: [
|
|
221
|
+
{ code: 'function reject(e) { count += 1 }\nnew Promise((resolve, rej) => { try { resolve(f()) } catch (e) { reject(e) } })', errors: [{ messageId: 'swallow' }] },
|
|
222
|
+
],
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// F7b: a bound/used Promise.allSettled result launders rejections into values;
|
|
227
|
+
// it is a finding unless the enclosing function contains at least one syntactic
|
|
228
|
+
// `.reason` access (fail closed, per F2b - allSettled is rare enough that the
|
|
229
|
+
// coarse gate is acceptable). Passing the result whole to a helper is opaque
|
|
230
|
+
// (no visible `.reason`) and is a finding. Escape: `// no-report: <reason>`.
|
|
231
|
+
test('no-swallow-allsettled (F7b)', () => {
|
|
232
|
+
ruleTester.run('no-swallow-allsettled', require('../rules/no-swallow-allsettled'), {
|
|
233
|
+
valid: [
|
|
234
|
+
// rejected reasons are inspected in the same scope.
|
|
235
|
+
"const rs = await Promise.allSettled(ps); for (const r of rs) { if (r.status === 'rejected') report(r.reason) }",
|
|
236
|
+
// `.reason` reached inside a nested callback (the scan descends into it).
|
|
237
|
+
"async function f() { const rs = await Promise.allSettled(ps); rs.filter(r => r.status === 'rejected').forEach(r => log(r.reason)) }",
|
|
238
|
+
// computed `.reason` access also counts.
|
|
239
|
+
"const rs = await Promise.allSettled(ps); rs.forEach(r => handle(r['reason']))",
|
|
240
|
+
// marker escape (would be a finding without it - see the invalid twin).
|
|
241
|
+
'// no-report: partial batch, failures surfaced by the caller\nconst rs = await Promise.allSettled(ps); use(rs)',
|
|
242
|
+
// marker-escaped fire-and-forget.
|
|
243
|
+
'// no-report: best-effort broadcast, outcomes intentionally dropped\nawait Promise.allSettled(ps)',
|
|
244
|
+
],
|
|
245
|
+
invalid: [
|
|
246
|
+
// fire-and-forget discards every outcome: allSettled never rejects, so
|
|
247
|
+
// this is the quietest swallow of all.
|
|
248
|
+
{ code: 'await Promise.allSettled(ps)', errors: [{ messageId: 'swallow' }] },
|
|
249
|
+
// only fulfilled values are read; rejected reasons are dropped.
|
|
250
|
+
{ code: "const rs = await Promise.allSettled(ps); const ok = rs.filter(r => r.status === 'fulfilled').map(r => r.value)", errors: [{ messageId: 'swallow' }] },
|
|
251
|
+
// the result is handed whole to a helper: opaque, no visible `.reason`.
|
|
252
|
+
{ code: 'async function f() { const rs = await Promise.allSettled(ps); return processAll(rs) }', errors: [{ messageId: 'swallow' }] },
|
|
253
|
+
// bound through a .then continuation with no `.reason` touch.
|
|
254
|
+
{ code: 'Promise.allSettled(ps).then(rs => { doStuff(rs) })', errors: [{ messageId: 'swallow' }] },
|
|
255
|
+
// only the count is used.
|
|
256
|
+
{ code: 'const rs = await Promise.allSettled(ps); log(rs.length)', errors: [{ messageId: 'swallow' }] },
|
|
257
|
+
],
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// F7c: a try containing JSON.parse must propagate the parse error - every catch
|
|
262
|
+
// path throws the caught object or converts to a Result boundary carrying it
|
|
263
|
+
// (object-flow principle F5; stringification breaks the chain). A fallback
|
|
264
|
+
// value, a report-and-continue, or a stringified rethrow swallows it
|
|
265
|
+
// (report+default = finding). Escape: `// parse-skip: <reason>` above the try.
|
|
266
|
+
test('no-parse-fallback (F7c)', () => {
|
|
267
|
+
ruleTester.run('no-parse-fallback', require('../rules/no-parse-fallback'), {
|
|
268
|
+
valid: [
|
|
269
|
+
// bare rethrow of the caught error object.
|
|
270
|
+
'try { const x = JSON.parse(s) } catch (e) { throw e }',
|
|
271
|
+
// rewrap preserving the object via `cause`.
|
|
272
|
+
"try { JSON.parse(s) } catch (e) { throw new Error('bad config payload', { cause: e }) }",
|
|
273
|
+
// report then rethrow: reported AND propagated.
|
|
274
|
+
R + 'try { JSON.parse(s) } catch (e) { reportError("config parse failed mid-load", e); throw e }',
|
|
275
|
+
// two-step wrap: the object flows through a local carrier (F5 credits it).
|
|
276
|
+
"try { JSON.parse(s) } catch (e) { const wrapped = new Error('bad config', { cause: e }); throw wrapped }",
|
|
277
|
+
// both branches throw the object.
|
|
278
|
+
"try { JSON.parse(s) } catch (e) { if (x) { throw e } else { throw new Error('parse failed', { cause: e }) } }",
|
|
279
|
+
// marker escape (the twin below without the marker is a finding).
|
|
280
|
+
'// parse-skip: optional config, absence is expected\ntry { JSON.parse(s) } catch (e) { useDefault() }',
|
|
281
|
+
// no catch: the parse error propagates through finally, nothing swallows it.
|
|
282
|
+
'try { JSON.parse(s) } finally { cleanup() }',
|
|
283
|
+
// no surrounding try: the error propagates naturally, out of scope.
|
|
284
|
+
'const x = JSON.parse(s)',
|
|
285
|
+
],
|
|
286
|
+
invalid: [
|
|
287
|
+
// fallback value instead of propagating.
|
|
288
|
+
{ code: 'function f() { try { JSON.parse(s) } catch (e) { return {} } }', errors: [{ messageId: 'fallback' }] },
|
|
289
|
+
// stringified rethrow drops the error object (chain break).
|
|
290
|
+
{ code: "try { JSON.parse(s) } catch (e) { throw new Error(e.message) }", errors: [{ messageId: 'fallback' }] },
|
|
291
|
+
// throw a fresh error that does not carry the caught one.
|
|
292
|
+
{ code: "try { JSON.parse(s) } catch (e) { throw new Error('parse failed') }", errors: [{ messageId: 'fallback' }] },
|
|
293
|
+
// report + default: reporting does not license the fallback.
|
|
294
|
+
{ code: R + 'function f() { try { JSON.parse(s) } catch (e) { reportError("config parse failed mid-load", e); return {} } }', errors: [{ messageId: 'fallback' }] },
|
|
295
|
+
// report only, then fall through the end of the catch.
|
|
296
|
+
{ code: R + 'try { JSON.parse(s) } catch (e) { reportError("config parse failed mid-load", e) }', errors: [{ messageId: 'fallback' }] },
|
|
297
|
+
// throw on one branch only; the other path falls through.
|
|
298
|
+
{ code: 'try { JSON.parse(s) } catch (e) { if (x) { throw e } }', errors: [{ messageId: 'fallback' }] },
|
|
299
|
+
],
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// F7c boundary: a Result boundary is a legal parse-error exit only when the
|
|
304
|
+
// enclosing fn is annotated Result/Attempt AND the caught error flows in as an
|
|
305
|
+
// object (a stringified `message: e.message` breaks the chain, like F2).
|
|
306
|
+
test('no-parse-fallback result-boundary (F7c)', () => {
|
|
307
|
+
tsRuleTester.run('no-parse-fallback', require('../rules/no-parse-fallback'), {
|
|
308
|
+
valid: [
|
|
309
|
+
'function f(): Result<T> { try { JSON.parse(s) } catch (e) { return { ok: false, cause: e } } }',
|
|
310
|
+
'async function f(): Promise<Result<T>> { try { JSON.parse(await read()) } catch (e) { return { ok: false, cause: e } } }',
|
|
311
|
+
],
|
|
312
|
+
invalid: [
|
|
313
|
+
// stringified boundary: message carries only the text, not the object.
|
|
314
|
+
{ code: 'function f(): Result<T> { try { JSON.parse(s) } catch (e) { return { ok: false, message: e.message } } }', errors: [{ messageId: 'fallback' }] },
|
|
315
|
+
// no Result annotation on the enclosing fn: no boundary credit.
|
|
316
|
+
{ code: 'function f() { try { JSON.parse(s) } catch (e) { return { ok: false, cause: e } } }', errors: [{ messageId: 'fallback' }] },
|
|
317
|
+
// bare { ok: false } drops the caught error.
|
|
318
|
+
{ code: 'function f(): Result<T> { try { JSON.parse(s) } catch (e) { return { ok: false } } }', errors: [{ messageId: 'fallback' }] },
|
|
319
|
+
],
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('no-console-error', () => {
|
|
324
|
+
ruleTester.run('no-console-error', require('../rules/no-console-error'), {
|
|
325
|
+
valid: [
|
|
326
|
+
'console.log("hi")',
|
|
327
|
+
'foo("connection lost mid-stream", err)',
|
|
328
|
+
],
|
|
329
|
+
invalid: [
|
|
330
|
+
{ code: 'console.error("boom")', errors: [{ messageId: 'use' }] },
|
|
331
|
+
],
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('valid-error-report', () => {
|
|
336
|
+
ruleTester.run('valid-error-report', require('../rules/valid-error-report'), {
|
|
337
|
+
valid: [
|
|
338
|
+
R + 'reportError("connection lost mid-stream", err, null, "api.lost")',
|
|
339
|
+
R + 'reportError("connection lost mid-stream", err, { area: "api" }, "api.lost")',
|
|
340
|
+
R + 'reportSynthError("retry budget exhausted at boot stage", null, "boot.retry")',
|
|
341
|
+
// namespace call: qualified callee resolves to the tier-1 origin.
|
|
342
|
+
NS + 'report.reportError("connection lost mid-stream", err, null, "api.lost")',
|
|
343
|
+
// CJS named destructure resolves to the tier-1 origin.
|
|
344
|
+
"const { reportError } = require('tackbox/report')\nreportError('connection lost mid-stream', err, null, 'api.lost')",
|
|
345
|
+
// a same-named LOCAL function is not a reporter: origin gating means an
|
|
346
|
+
// otherwise-invalid call (no args) earns nothing.
|
|
347
|
+
'function reportError() {}\nreportError()',
|
|
348
|
+
],
|
|
349
|
+
invalid: [
|
|
350
|
+
{ code: R + 'reportError(`oops ${x}`, err, null, "api.lost")', errors: [{ messageId: 'msgNotStatic' }] },
|
|
351
|
+
{ code: R + 'reportError("short", err, null, "api.lost")', errors: [{ messageId: 'msgTooShort' }] },
|
|
352
|
+
{ code: R + 'reportError("' + 'x'.repeat(201) + '", err, null, "api.lost")', errors: [{ messageId: 'msgTooLong' }] },
|
|
353
|
+
{ code: R + 'reportError("connection lost mid-stream", null, null, "api.lost")', errors: [{ messageId: 'causeMissing' }] },
|
|
354
|
+
{ code: R + 'reportError("connection lost mid-stream", err, {}, "api.lost")', errors: [{ messageId: 'tagsEmpty' }] },
|
|
355
|
+
{ code: R + 'reportError("connection lost mid-stream", err)', errors: [{ messageId: 'dedupMissing' }] },
|
|
356
|
+
{ code: R + 'reportError()', errors: [{ messageId: 'noArgs' }] },
|
|
357
|
+
// synth arity: (msg, tags, dedupKey); a 2-arg call is missing the dedupKey.
|
|
358
|
+
{ code: R + 'reportSynthError("retry budget exhausted at boot stage", null)', errors: [{ messageId: 'dedupMissing' }] },
|
|
359
|
+
// namespace form: origin resolution on a qualified callee still enforces arity.
|
|
360
|
+
{ code: NS + 'report.reportError("connection lost mid-stream", err)', errors: [{ messageId: 'dedupMissing' }] },
|
|
361
|
+
],
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('valid-dedup-key', () => {
|
|
366
|
+
ruleTester.run('valid-dedup-key', require('../rules/valid-dedup-key'), {
|
|
367
|
+
valid: [
|
|
368
|
+
R + 'reportError("connection lost mid-stream", err, null, "api.lost")',
|
|
369
|
+
R + 'reportError("connection lost mid-stream", err, null, "api.lost:user_42")',
|
|
370
|
+
// full-reporter dedupKey lives at slot 3; synth at slot 2.
|
|
371
|
+
R + 'reportSynthError("retry budget exhausted at boot stage", null, "boot.retry")',
|
|
372
|
+
// no-expression template literal is a static string -> accepted.
|
|
373
|
+
R + 'reportError("connection lost mid-stream", err, null, `api.lost`)',
|
|
374
|
+
// namespace call: dedupKey slot is validated on the qualified callee too.
|
|
375
|
+
NS + 'report.reportError("connection lost mid-stream", err, null, "api.lost")',
|
|
376
|
+
// CJS namespace resolves to the tier-1 origin.
|
|
377
|
+
"const report = require('tackbox/report')\nreport.reportError('connection lost mid-stream', err, null, 'api.lost')",
|
|
378
|
+
// a same-named LOCAL function is not a reporter: a non-literal key earns nothing.
|
|
379
|
+
'function reportError(m, e, t, k) {}\nreportError("m", err, null, dynKey)',
|
|
380
|
+
// D-4: the reporter-arg rules skip test files - a dynamic key is clean there.
|
|
381
|
+
{ code: R + 'reportError("connection lost mid-stream", err, null, key)', filename: 'widget.test.js' },
|
|
382
|
+
],
|
|
383
|
+
invalid: [
|
|
384
|
+
{ code: R + 'reportError("connection lost mid-stream", err, null, key)', errors: [{ messageId: 'notLiteral' }] },
|
|
385
|
+
{ code: R + 'reportError("connection lost mid-stream", err, null, "BadFormat")', errors: [{ messageId: 'badFormat' }] },
|
|
386
|
+
{ code: R + 'reportError("connection lost mid-stream", err, null, "no_dot")', errors: [{ messageId: 'badFormat' }] },
|
|
387
|
+
// synth slot (2): a non-dotted key is bad format at the right positional slot.
|
|
388
|
+
{ code: R + 'reportSynthError("retry budget exhausted at boot stage", null, "nodot")', errors: [{ messageId: 'badFormat' }] },
|
|
389
|
+
// synth slot (2): a non-literal key is caught at the right positional slot.
|
|
390
|
+
{ code: R + 'reportSynthError("retry budget exhausted at boot stage", null, dyn)', errors: [{ messageId: 'notLiteral' }] },
|
|
391
|
+
// namespace form: origin resolution on a qualified callee catches a non-literal key.
|
|
392
|
+
{ code: NS + 'report.reportError("connection lost mid-stream", err, null, key)', errors: [{ messageId: 'notLiteral' }] },
|
|
393
|
+
],
|
|
394
|
+
})
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
test('no-throw-and-report', () => {
|
|
398
|
+
ruleTester.run('no-throw-and-report', require('../rules/no-throw-and-report'), {
|
|
399
|
+
valid: [
|
|
400
|
+
'try { f() } catch (e) { throw e }',
|
|
401
|
+
R + 'try { f() } catch (e) { reportError("api call failed mid-flight", e, null, "api.fail") }',
|
|
402
|
+
],
|
|
403
|
+
invalid: [
|
|
404
|
+
{
|
|
405
|
+
code: R + 'try { f() } catch (e) { reportError("api call failed mid-flight", e, null, "api.fail"); throw e }',
|
|
406
|
+
errors: [{ messageId: 'both' }],
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
// D006 double-lane: a capture and a notify on one path both reach the user.
|
|
413
|
+
test('no-throw-and-report double-lane (D006)', () => {
|
|
414
|
+
ruleTester.run('no-throw-and-report', require('../rules/no-throw-and-report'), {
|
|
415
|
+
valid: [
|
|
416
|
+
// notify in one leg, capture in the exclusive leg: different paths.
|
|
417
|
+
N + 'try { f() } catch (e) { if (isOffline(e)) { notify("connection lost", e, null, "net.offline") } else { reportError("server unreachable now", e, null, "net.fail") } }',
|
|
418
|
+
// notify in one switch case, capture in the exclusive default case.
|
|
419
|
+
N + 'try { f() } catch (e) { switch (code) { case 503: notify("connection lost", e, null, "net.offline"); break; default: reportError("server unreachable now", e, null, "net.fail") } }',
|
|
420
|
+
// notify only, no capture: no-broad-notify governs narrowing, not this rule.
|
|
421
|
+
N + 'try { f() } catch (e) { notify("connection lost", e, null, "net.offline") }',
|
|
422
|
+
// D-4: the double-lane arm skips test files (the `both` arm still runs).
|
|
423
|
+
{ code: N + 'try { f() } catch (e) { reportError("server unreachable now", e, null, "net.fail"); notify("connection lost", e, null, "net.offline") }', filename: 'net.spec.js' },
|
|
424
|
+
],
|
|
425
|
+
invalid: [
|
|
426
|
+
{
|
|
427
|
+
code: N + 'try { f() } catch (e) { reportError("server unreachable now", e, null, "net.fail"); notify("connection lost", e, null, "net.offline") }',
|
|
428
|
+
errors: [{ messageId: 'doubleLane' }],
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
// capture and notify in the SAME switch case run on one path.
|
|
432
|
+
code: N + 'try { f() } catch (e) { switch (code) { case 503: reportError("server unreachable now", e, null, "net.fail"); notify("connection lost", e, null, "net.offline"); break } }',
|
|
433
|
+
errors: [{ messageId: 'doubleLane' }],
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
// notify inside a loop, capture after it: the loop body may run alongside.
|
|
437
|
+
code: N + 'try { f() } catch (e) { for (const x of xs) { notify("connection lost", e, null, "net.offline") } reportError("server unreachable now", e, null, "net.fail") }',
|
|
438
|
+
errors: [{ messageId: 'doubleLane' }],
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
// D006 notify gate: an unconditional notify handling the whole catch is a
|
|
445
|
+
// finding; a notify under an additional condition is narrowed.
|
|
446
|
+
test('no-broad-notify', () => {
|
|
447
|
+
ruleTester.run('no-broad-notify', require('../rules/no-broad-notify'), {
|
|
448
|
+
valid: [
|
|
449
|
+
// conditional notify (offline) with the complement reported: narrowed.
|
|
450
|
+
N + 'try { f() } catch (e) { if (isOffline(e)) { notify("connection lost", e, null, "net.offline") } else { reportError("server unreachable now", e, null, "net.fail") } }',
|
|
451
|
+
// notify under a switch case is narrowed too.
|
|
452
|
+
N + 'try { f() } catch (e) { switch (code) { case 503: notify("connection lost", e, null, "net.offline"); break; default: reportError("server error now", e, null, "net.fail") } }',
|
|
453
|
+
// the caught error does not flow into notify: not terminating this path.
|
|
454
|
+
N + 'try { f() } catch (e) { notify("connection lost", other, null, "net.offline") }',
|
|
455
|
+
// a no-report marker directly above the try suppresses.
|
|
456
|
+
N + '// no-report: bootstrap notice, telemetry wired later in the boot sequence\ntry { f() } catch (e) { notify("connection lost", e, null, "net.offline") }',
|
|
457
|
+
// D-4: the notify gate skips test files - an unconditional notify is clean there.
|
|
458
|
+
{ code: N + 'try { f() } catch (e) { notify("connection lost", e, null, "net.offline") }', filename: '__tests__/widget.js' },
|
|
459
|
+
],
|
|
460
|
+
invalid: [
|
|
461
|
+
{
|
|
462
|
+
code: N + 'try { f() } catch (e) { notify("connection lost", e, null, "net.offline") }',
|
|
463
|
+
errors: [{ messageId: 'broad' }],
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
})
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// D006: notify credits its path in the swallow rule; the complement stays checked.
|
|
470
|
+
test('no-swallow-catch notify credit (D006)', () => {
|
|
471
|
+
ruleTester.run('no-swallow-catch', require('../rules/no-swallow-catch'), {
|
|
472
|
+
valid: [
|
|
473
|
+
// notify carrying the caught error routes it to the user lane: handled.
|
|
474
|
+
N + 'try { f() } catch (e) { notify("connection lost", e, null, "net.offline") }',
|
|
475
|
+
// conditional notify + reported complement: both paths handled.
|
|
476
|
+
N + 'try { f() } catch (e) { if (isOffline(e)) { notify("connection lost", e, null, "net.offline") } else { reportError("server error now", e, null, "net.fail") } }',
|
|
477
|
+
],
|
|
478
|
+
invalid: [
|
|
479
|
+
// notify the caught error does not reach is not credited: still swallows.
|
|
480
|
+
{ code: N + 'try { f() } catch (e) { notify("connection lost", other, null, "net.offline") }', errors: [{ messageId: 'swallow' }] },
|
|
481
|
+
// conditional notify with an unhandled complement: the else path swallows.
|
|
482
|
+
{ code: N + 'try { f() } catch (e) { if (isOffline(e)) { notify("connection lost", e, null, "net.offline") } }', errors: [{ messageId: 'swallow' }] },
|
|
483
|
+
],
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
// D007: notify's user-lane msg must be a static literal (valid-error-report).
|
|
488
|
+
test('valid-error-report notify (D007)', () => {
|
|
489
|
+
ruleTester.run('valid-error-report', require('../rules/valid-error-report'), {
|
|
490
|
+
valid: [
|
|
491
|
+
N + 'notify("connection lost, retrying", err, null, "net.offline")',
|
|
492
|
+
],
|
|
493
|
+
invalid: [
|
|
494
|
+
{ code: N + 'notify(`offline ${x} now`, err, null, "net.offline")', errors: [{ messageId: 'msgNotStatic' }] },
|
|
495
|
+
{ code: N + 'notify("short", err, null, "net.offline")', errors: [{ messageId: 'msgTooShort' }] },
|
|
496
|
+
{ code: N + 'notify("connection lost, retrying", null, null, "net.offline")', errors: [{ messageId: 'causeMissing' }] },
|
|
497
|
+
{ code: N + 'notify("connection lost, retrying", err)', errors: [{ messageId: 'dedupMissing' }] },
|
|
498
|
+
],
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
// D008: notify's dedupKey is validated like a reporter's (valid-dedup-key).
|
|
503
|
+
test('valid-dedup-key notify (D008)', () => {
|
|
504
|
+
ruleTester.run('valid-dedup-key', require('../rules/valid-dedup-key'), {
|
|
505
|
+
valid: [
|
|
506
|
+
N + 'notify("connection lost, retrying", err, null, "net.offline")',
|
|
507
|
+
N + 'notify("connection lost, retrying", err, null, "net.offline:user_7")',
|
|
508
|
+
],
|
|
509
|
+
invalid: [
|
|
510
|
+
{ code: N + 'notify("connection lost, retrying", err, null, key)', errors: [{ messageId: 'notLiteral' }] },
|
|
511
|
+
{ code: N + 'notify("connection lost, retrying", err, null, "BadKey")', errors: [{ messageId: 'badFormat' }] },
|
|
512
|
+
],
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
// D009: a suppression marker reason under 10 chars is too cheap to suppress.
|
|
517
|
+
test('no-swallow-catch reason length (D009)', () => {
|
|
518
|
+
ruleTester.run('no-swallow-catch', require('../rules/no-swallow-catch'), {
|
|
519
|
+
valid: [
|
|
520
|
+
// 10-char reason suppresses.
|
|
521
|
+
'// no-report: shared-css\ntry { f() } catch (e) {}',
|
|
522
|
+
],
|
|
523
|
+
invalid: [
|
|
524
|
+
// 9-char reason does not suppress: the swallow still fires.
|
|
525
|
+
{ code: '// no-report: too short\ntry { f() } catch (e) {}', errors: [{ messageId: 'swallow' }] },
|
|
526
|
+
],
|
|
527
|
+
})
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
test('ts-rethrow-without-cause', () => {
|
|
531
|
+
ruleTester.run('ts-rethrow-without-cause', require('../rules/ts-rethrow-without-cause'), {
|
|
532
|
+
valid: [
|
|
533
|
+
'try { f() } catch (e) { throw new Error("wrap failed", { cause: e }) }',
|
|
534
|
+
'try { f() } catch (e) { throw new WrapError("bad gateway", { status: 502, cause: e }) }',
|
|
535
|
+
'try { f() } catch (e) { throw e }',
|
|
536
|
+
'try { f() } catch { throw new Error("no binding to chain") }',
|
|
537
|
+
'throw new Error("not inside a catch")',
|
|
538
|
+
'try { f() } catch (e) { throw new AggregateError([e], "all downstream calls failed") }',
|
|
539
|
+
'try { f() } catch (e) { throw new AggregateError([first, e], "batch had failures") }',
|
|
540
|
+
],
|
|
541
|
+
invalid: [
|
|
542
|
+
{ code: 'try { f() } catch (e) { throw new Error("connection dropped") }', errors: [{ messageId: 'noCause' }] },
|
|
543
|
+
{ code: 'try { f() } catch (e) { throw new HttpError("bad gateway", { status: 502 }) }', errors: [{ messageId: 'noCause' }] },
|
|
544
|
+
{ code: 'try { f() } catch (e) { throw new Error("wrong cause", { cause: other }) }', errors: [{ messageId: 'noCause' }] },
|
|
545
|
+
{ code: 'try { f() } catch (e) { throw new AggregateError([], "nothing captured") }', errors: [{ messageId: 'noCause' }] },
|
|
546
|
+
{ code: 'try { f() } catch (e) { throw new AggregateError([other], "wrong error kept") }', errors: [{ messageId: 'noCause' }] },
|
|
547
|
+
],
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
test('ts-useless-catch', () => {
|
|
552
|
+
ruleTester.run('ts-useless-catch', require('../rules/ts-useless-catch'), {
|
|
553
|
+
valid: [
|
|
554
|
+
'try { f() } catch (e) { throw new Error("wrap failed", { cause: e }) }',
|
|
555
|
+
'try { f() } catch (e) { log(e); throw e }',
|
|
556
|
+
'try { f() } catch (e) { throw other }',
|
|
557
|
+
'try { f() } catch {}',
|
|
558
|
+
],
|
|
559
|
+
invalid: [
|
|
560
|
+
{ code: 'try { f() } catch (e) { throw e }', errors: [{ messageId: 'useless' }] },
|
|
561
|
+
],
|
|
562
|
+
})
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
test('ts-exit-in-catch', () => {
|
|
566
|
+
ruleTester.run('ts-exit-in-catch', require('../rules/ts-exit-in-catch'), {
|
|
567
|
+
valid: [
|
|
568
|
+
'try { f() } catch (e) { throw e }',
|
|
569
|
+
'try { f() } catch (e) { cleanup() }',
|
|
570
|
+
'process.exit(1)',
|
|
571
|
+
],
|
|
572
|
+
invalid: [
|
|
573
|
+
{ code: 'try { f() } catch (e) { process.exit(1) }', errors: [{ messageId: 'exit' }] },
|
|
574
|
+
{ code: 'try { f() } catch { process.exit(2) }', errors: [{ messageId: 'exit' }] },
|
|
575
|
+
],
|
|
576
|
+
})
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
// A skip/todo/skipIf/fixme in a chain rooted at bare it/test/describe, or a
|
|
580
|
+
// bare xit/xdescribe/xtest, drops the test unless it carries a non-empty
|
|
581
|
+
// in-call reason (node:test options skip/todo, playwright (cond, 'reason'))
|
|
582
|
+
// or a // test-skip: <reason> marker above the statement. Chained forms
|
|
583
|
+
// (skipIf(...)(...), skip.each(...)(...)) report once, on the inner call
|
|
584
|
+
// carrying the skip property. A deeper root (queue.skip, foo.test.skip) is
|
|
585
|
+
// out of scope.
|
|
586
|
+
test('no-skipped-test', () => {
|
|
587
|
+
ruleTester.run('no-skipped-test', require('../rules/no-skipped-test'), {
|
|
588
|
+
valid: [
|
|
589
|
+
'it("runs", () => {})',
|
|
590
|
+
'// test-skip: flaky upstream, issue 12\nit.skip("later", () => {})',
|
|
591
|
+
'queue.skip()',
|
|
592
|
+
'foo.test.skip("x")',
|
|
593
|
+
'// test-skip: pending backend, issue 34\nit.skipIf(cond)("n", f)',
|
|
594
|
+
// node:test options with a reason; non-literal reasons are trusted.
|
|
595
|
+
'it("n", { skip: "flaky upstream, issue 12" }, () => {})',
|
|
596
|
+
'test("n", { todo: "needs api endpoint" }, () => {})',
|
|
597
|
+
'test({ skip: "no name form, reason present" }, () => {})',
|
|
598
|
+
'it("n", { skip: why }, () => {})',
|
|
599
|
+
'it("n", { skip: `${why}` }, () => {})',
|
|
600
|
+
// falsy skip is not a skip; unrelated options are not inspectable.
|
|
601
|
+
'it("n", { skip: false }, () => {})',
|
|
602
|
+
'it("n", { concurrency: 2 }, () => {})',
|
|
603
|
+
'// test-skip: quarantined, issue 7\nit("n", { skip: true }, () => {})',
|
|
604
|
+
// playwright conditional with reason; non-literal reason trusted.
|
|
605
|
+
'test.skip(isMobile, "touch-only flow")',
|
|
606
|
+
'test.fixme(isWebkit, "portal rendering, issue 9")',
|
|
607
|
+
'test.skip(isMobile, reasonFor(env))',
|
|
608
|
+
],
|
|
609
|
+
invalid: [
|
|
610
|
+
{ code: 'it.skip("later", () => {})', errors: [{ messageId: 'skipped' }] },
|
|
611
|
+
{ code: 'test.todo("write this")', errors: [{ messageId: 'skipped' }] },
|
|
612
|
+
{ code: 'xit("x", () => {})', errors: [{ messageId: 'skipped' }] },
|
|
613
|
+
{ code: 'xdescribe("grp", () => {})', errors: [{ messageId: 'skipped' }] },
|
|
614
|
+
{ code: 'it.skipIf(isCi)("n", f)', errors: [{ messageId: 'skipped' }] },
|
|
615
|
+
{ code: '// test-skip:\nit.skip("x", () => {})', errors: [{ messageId: 'skipped' }] },
|
|
616
|
+
{ code: '// test-skip: reason\n\nit.skip("x", () => {})', errors: [{ messageId: 'skipped' }] },
|
|
617
|
+
{ code: 'it.skip.each([1])("n", f)', errors: [{ messageId: 'skipped' }] },
|
|
618
|
+
// node:test reasonless options.
|
|
619
|
+
{ code: 'it("n", { skip: true }, () => {})', errors: [{ messageId: 'skipped' }] },
|
|
620
|
+
{ code: 'test("n", { skip: "" }, () => {})', errors: [{ messageId: 'skipped' }] },
|
|
621
|
+
{ code: 'it("n", { skip: " " }, () => {})', errors: [{ messageId: 'skipped' }] },
|
|
622
|
+
{ code: 'test("n", { todo: true }, () => {})', errors: [{ messageId: 'skipped' }] },
|
|
623
|
+
// playwright cond-only, bare in-body, and declaration forms.
|
|
624
|
+
{ code: 'test.skip(isMobile)', errors: [{ messageId: 'skipped' }] },
|
|
625
|
+
{ code: 'test.skip()', errors: [{ messageId: 'skipped' }] },
|
|
626
|
+
{ code: 'test.skip("title", () => {})', errors: [{ messageId: 'skipped' }] },
|
|
627
|
+
{ code: 'test.fixme()', errors: [{ messageId: 'skipped' }] },
|
|
628
|
+
],
|
|
629
|
+
})
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
// A .only in a chain rooted at bare it/test/describe, or a bare fit/fdescribe/
|
|
633
|
+
// ftest, focuses the suite and disables the rest. No escape hatch. Chained
|
|
634
|
+
// only.each(...)(...) reports once, on the inner call. A deeper root
|
|
635
|
+
// (myobj.only) is out of scope.
|
|
636
|
+
test('no-focused-test', () => {
|
|
637
|
+
ruleTester.run('no-focused-test', require('../rules/no-focused-test'), {
|
|
638
|
+
valid: [
|
|
639
|
+
'it("x", f)',
|
|
640
|
+
'myobj.only("x")',
|
|
641
|
+
],
|
|
642
|
+
invalid: [
|
|
643
|
+
{ code: 'it.only("x", f)', errors: [{ messageId: 'focused' }] },
|
|
644
|
+
{ code: 'describe.only("g", f)', errors: [{ messageId: 'focused' }] },
|
|
645
|
+
{ code: 'fit("x", f)', errors: [{ messageId: 'focused' }] },
|
|
646
|
+
{ code: 'fdescribe("g", f)', errors: [{ messageId: 'focused' }] },
|
|
647
|
+
{ code: 'test.only.each([1])("n", f)', errors: [{ messageId: 'focused' }] },
|
|
648
|
+
],
|
|
649
|
+
})
|
|
650
|
+
})
|