@systemfsoftware/oxlint-plugin-test-discipline 4.1.0 → 6.1.3
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/CHANGELOG.md +2 -52
- package/README.md +84 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +616 -435
- package/dist/no-inline-suppression.d.ts +1 -0
- package/dist/no-inline-suppression.mjs +144 -0
- package/dist/oxlint-plugin-test-discipline-untrimmed.d.ts +1 -0
- package/dist/oxlint-plugin-test-discipline.d.ts +1 -0
- package/package.json +20 -24
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { parseSync } from "oxc-parser";
|
|
6
|
+
const LINTER_FORMS = [
|
|
7
|
+
"oxlint-disable-next-line",
|
|
8
|
+
"oxlint-disable-line",
|
|
9
|
+
"oxlint-disable",
|
|
10
|
+
"eslint-disable-next-line",
|
|
11
|
+
"eslint-disable-line",
|
|
12
|
+
"eslint-disable",
|
|
13
|
+
"@ts-expect-error",
|
|
14
|
+
"@ts-ignore",
|
|
15
|
+
"@ts-nocheck"
|
|
16
|
+
].filter((form) => !form.startsWith("@"));
|
|
17
|
+
const LEADING_WHITESPACE = /^\s*/u;
|
|
18
|
+
/**
|
|
19
|
+
* TypeScript's own recognition, rewritten over `value` (the text after `//` or `/*`). In TypeScript 6.0.3:
|
|
20
|
+
* - `@ts-expect-error` / `@ts-ignore`: src/compiler/scanner.ts `commentDirectiveRegExSingleLine =
|
|
21
|
+
* /^\/\/\/?\s*@(ts-expect-error|ts-ignore)/` and `commentDirectiveRegExMultiLine =
|
|
22
|
+
* /^(?:\/|\*)*\s*@(ts-expect-error|ts-ignore)/` (lib/_tsc.js:8202-8203), matched case-sensitively and
|
|
23
|
+
* with no end anchor.
|
|
24
|
+
* - `@ts-nocheck`: src/compiler/parser.ts `singleLinePragmaRegEx =
|
|
25
|
+
* /^\/\/\/?\s*@([^\s:]+)((?:[^\S\r\n]|:).*)?$/m` (lib/_tsc.js:36318); the pragma is registered
|
|
26
|
+
* `kind: SingleLine` only (lib/_tsc.js:3907-3908), so a block comment never carries it, and its name is
|
|
27
|
+
* lower-cased before lookup (lib/_tsc.js:36368), so any casing counts. TypeScript reads it only from the
|
|
28
|
+
* comments before the first token; it is refused wherever it appears, failing closed.
|
|
29
|
+
*/
|
|
30
|
+
const TYPESCRIPT_DIRECTIVES = [
|
|
31
|
+
["@ts-expect-error", {
|
|
32
|
+
Line: /^\/?\s*@ts-expect-error/u,
|
|
33
|
+
Block: /^[/*]*\s*@ts-expect-error/u
|
|
34
|
+
}],
|
|
35
|
+
["@ts-ignore", {
|
|
36
|
+
Line: /^\/?\s*@ts-ignore/u,
|
|
37
|
+
Block: /^[/*]*\s*@ts-ignore/u
|
|
38
|
+
}],
|
|
39
|
+
["@ts-nocheck", {
|
|
40
|
+
Line: /^\/?\s*@ts-nocheck(?:[^\S\r\n]|:|$)/iu,
|
|
41
|
+
Block: /(?!)/u
|
|
42
|
+
}]
|
|
43
|
+
];
|
|
44
|
+
const typeScriptForm = (comment) => TYPESCRIPT_DIRECTIVES.find(([, recognisers]) => recognisers[comment.type].test(comment.value))?.[0];
|
|
45
|
+
const refusedForm = (comment) => {
|
|
46
|
+
const body = comment.value.replace(LEADING_WHITESPACE, "");
|
|
47
|
+
return LINTER_FORMS.find((form) => body.startsWith(form)) ?? typeScriptForm(comment);
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/rules/suppression-scan.ts
|
|
51
|
+
const positionOf = (sourceText, offset) => {
|
|
52
|
+
const before = sourceText.slice(0, offset);
|
|
53
|
+
return {
|
|
54
|
+
line: before.split("\n").length,
|
|
55
|
+
column: offset - before.lastIndexOf("\n")
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Every refused comment in `sourceText`. The parser tokenises the source, so a form inside a string,
|
|
60
|
+
* template or regular-expression literal is code, not a comment, and is never read. `filename` selects
|
|
61
|
+
* the dialect by extension (`.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.mjs`, `.cjs`, `.jsx`).
|
|
62
|
+
*/
|
|
63
|
+
const scanSuppressions = (filename, sourceText) => {
|
|
64
|
+
const parsed = parseSync(filename, sourceText);
|
|
65
|
+
return {
|
|
66
|
+
suppressions: parsed.comments.flatMap((comment) => {
|
|
67
|
+
const form = refusedForm(comment);
|
|
68
|
+
return form === void 0 ? [] : [{
|
|
69
|
+
...positionOf(sourceText, comment.start),
|
|
70
|
+
form
|
|
71
|
+
}];
|
|
72
|
+
}),
|
|
73
|
+
parseErrors: [...new Set(parsed.errors.map((error) => error.message))]
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/no-inline-suppression.ts
|
|
78
|
+
/**
|
|
79
|
+
* `no-inline-suppression [file...]` — fails when any scanned file holds a comment opening with an
|
|
80
|
+
* oxlint/eslint disable directive, `@ts-expect-error`, `@ts-ignore` or `@ts-nocheck`. With no arguments it scans the git-tracked
|
|
81
|
+
* sources under the working directory. It reads no configuration, and no comment can exempt a file:
|
|
82
|
+
* oxlint obeys a disable directive even for a JS plugin rule that names it, so this check runs outside
|
|
83
|
+
* oxlint.
|
|
84
|
+
*
|
|
85
|
+
* Exit status: 0 clean; 1 a refused comment or an unparseable file; 2 the files could not be listed or read.
|
|
86
|
+
*/
|
|
87
|
+
const SCANNED_PATHSPECS = [
|
|
88
|
+
"*.ts",
|
|
89
|
+
"*.tsx",
|
|
90
|
+
"*.mts",
|
|
91
|
+
"*.cts",
|
|
92
|
+
"*.js",
|
|
93
|
+
"*.jsx",
|
|
94
|
+
"*.mjs",
|
|
95
|
+
"*.cjs"
|
|
96
|
+
];
|
|
97
|
+
const run = promisify(execFile);
|
|
98
|
+
/** A file git still tracks but the working tree has deleted: nothing in it can suppress anything. */
|
|
99
|
+
const isMissing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
100
|
+
const trackedSources = async () => {
|
|
101
|
+
const { stdout } = await run("git", [
|
|
102
|
+
"ls-files",
|
|
103
|
+
"-z",
|
|
104
|
+
"--",
|
|
105
|
+
...SCANNED_PATHSPECS
|
|
106
|
+
], { maxBuffer: 1073741824 });
|
|
107
|
+
return stdout.split("\0").filter((path) => path.length > 0);
|
|
108
|
+
};
|
|
109
|
+
const readTracked = async (file) => {
|
|
110
|
+
try {
|
|
111
|
+
return await readFile(file, "utf8");
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (isMissing(error)) return void 0;
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const describeSuppression = (file, suppression) => `${file}:${String(suppression.line)}:${String(suppression.column)}: ${suppression.form} is forbidden. Expected: code that passes lint and type-check with no inline suppression. Actual: a comment opening with ${suppression.form}. Fix: delete the comment and fix what it silenced; no comment or setting inside the file exempts it.`;
|
|
118
|
+
const describeParseError = (file, message) => `${file}: cannot be parsed, so its comments cannot all be graded: ${message}. Fix: make the file parse, then rerun no-inline-suppression.`;
|
|
119
|
+
const gradeFile = (file, sourceText) => {
|
|
120
|
+
const scan = scanSuppressions(file, sourceText);
|
|
121
|
+
return [...scan.suppressions.map((suppression) => describeSuppression(file, suppression)), ...scan.parseErrors.map((message) => describeParseError(file, message))];
|
|
122
|
+
};
|
|
123
|
+
const main = async (args) => {
|
|
124
|
+
const explicit = args.length > 0;
|
|
125
|
+
const files = explicit ? args : await trackedSources();
|
|
126
|
+
const findings = [];
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const sourceText = explicit ? await readFile(file, "utf8") : await readTracked(file);
|
|
129
|
+
const lines = sourceText === void 0 ? [] : gradeFile(file, sourceText);
|
|
130
|
+
findings.push(...lines);
|
|
131
|
+
if (lines.length > 0) process.stdout.write(`${lines.join("\n")}\n`);
|
|
132
|
+
}
|
|
133
|
+
if (findings.length === 0) return 0;
|
|
134
|
+
process.stderr.write(`no-inline-suppression: ${String(findings.length)} finding(s) in ${String(files.length)} scanned file(s).\n`);
|
|
135
|
+
return 1;
|
|
136
|
+
};
|
|
137
|
+
try {
|
|
138
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
139
|
+
} catch (error) {
|
|
140
|
+
process.stderr.write(`no-inline-suppression: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
141
|
+
process.exitCode = 2;
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
export {};
|
|
@@ -21,6 +21,7 @@ declare const _default: {
|
|
|
21
21
|
'src-property-test-cell': Rule;
|
|
22
22
|
'test-file-outside-tests-dir': Rule;
|
|
23
23
|
'test-suffix-outside-src': Rule;
|
|
24
|
+
'e2e-tier-placement': Rule;
|
|
24
25
|
'behaviour-test-requires-gherkin': Rule;
|
|
25
26
|
'behaviour-exercises-use-case': Rule;
|
|
26
27
|
'behaviour-one-feature-per-file': Rule;
|
|
@@ -21,6 +21,7 @@ declare const _default: {
|
|
|
21
21
|
'src-property-test-cell': Rule;
|
|
22
22
|
'test-file-outside-tests-dir': Rule;
|
|
23
23
|
'test-suffix-outside-src': Rule;
|
|
24
|
+
'e2e-tier-placement': Rule;
|
|
24
25
|
'behaviour-test-requires-gherkin': Rule;
|
|
25
26
|
'behaviour-exercises-use-case': Rule;
|
|
26
27
|
'behaviour-one-feature-per-file': Rule;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@systemfsoftware/oxlint-plugin-test-discipline",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "6.1.3",
|
|
5
5
|
"author": "Ryan Lee <drdgvhbh@gmail.com>",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"typescript"
|
|
28
28
|
],
|
|
29
29
|
"type": "module",
|
|
30
|
+
"bin": {
|
|
31
|
+
"no-inline-suppression": "./dist/no-inline-suppression.mjs"
|
|
32
|
+
},
|
|
30
33
|
"files": [
|
|
31
34
|
"dist"
|
|
32
35
|
],
|
|
@@ -38,34 +41,29 @@
|
|
|
38
41
|
"./package.json": "./package.json"
|
|
39
42
|
},
|
|
40
43
|
"dependencies": {
|
|
41
|
-
"@oxc-project/types": "^0.
|
|
42
|
-
"@oxlint/plugins": "~1.
|
|
44
|
+
"@oxc-project/types": "^0.155.0",
|
|
45
|
+
"@oxlint/plugins": "~1.89.0",
|
|
46
|
+
"oxc-parser": "^0.155.0"
|
|
43
47
|
},
|
|
44
48
|
"devDependencies": {
|
|
45
|
-
"@microsoft/api-extractor": "^7.59.
|
|
49
|
+
"@microsoft/api-extractor": "^7.59.4",
|
|
46
50
|
"@systemfsoftware/arethetypeswrong-cli": "^4.2.0",
|
|
47
|
-
"@systemfsoftware/
|
|
48
|
-
"@systemfsoftware/
|
|
49
|
-
"@systemfsoftware/
|
|
50
|
-
"@systemfsoftware/
|
|
51
|
-
"@
|
|
52
|
-
"@
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"@systemfsoftware/vitest-config": "^0.1.0",
|
|
57
|
-
"@types/node": "^26",
|
|
58
|
-
"@vitest/coverage-v8": "^5",
|
|
59
|
-
"effect": "4.0.0-rc.117",
|
|
60
|
-
"fast-check": "^4",
|
|
61
|
-
"oxlint": "~1.82.0",
|
|
51
|
+
"@systemfsoftware/oxlint-config-rule-authoring": "^3.0.1",
|
|
52
|
+
"@systemfsoftware/tsconfig": "^2.1.0",
|
|
53
|
+
"@systemfsoftware/tsdown-config": "^0.1.2",
|
|
54
|
+
"@systemfsoftware/vitest-config": "^0.1.3",
|
|
55
|
+
"@types/node": "^26.6.5",
|
|
56
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
57
|
+
"effect": "^4.1.2",
|
|
58
|
+
"fast-check": "^4.10.2",
|
|
59
|
+
"oxlint": "~1.89.0",
|
|
62
60
|
"rimraf": "^6.1.3",
|
|
63
61
|
"tsdown": "^0.23.0",
|
|
64
|
-
"typescript": "^7",
|
|
65
|
-
"vitest": "^5"
|
|
62
|
+
"typescript": "^7.0.2",
|
|
63
|
+
"vitest": "^5.1.0"
|
|
66
64
|
},
|
|
67
65
|
"peerDependencies": {
|
|
68
|
-
"effect": "4.
|
|
66
|
+
"effect": "^4.1.2",
|
|
69
67
|
"typescript": ">=5.0.0"
|
|
70
68
|
},
|
|
71
69
|
"publishConfig": {
|
|
@@ -77,8 +75,6 @@
|
|
|
77
75
|
"clean": "rimraf dist",
|
|
78
76
|
"typecheck": "tsc -b",
|
|
79
77
|
"test": "vitest run",
|
|
80
|
-
"mutation": "stryker run",
|
|
81
|
-
"mutation:full": "stryker run --force",
|
|
82
78
|
"lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}",
|
|
83
79
|
"attw": "attw --pack .",
|
|
84
80
|
"api:check": "api-extractor-quiet run",
|