@systemfsoftware/oxlint-plugin-test-discipline 5.0.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.
@@ -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": "5.0.0",
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.150.0",
42
- "@oxlint/plugins": "~1.82.0"
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.1",
49
+ "@microsoft/api-extractor": "^7.59.4",
46
50
  "@systemfsoftware/arethetypeswrong-cli": "^4.2.0",
47
- "@systemfsoftware/stryker-config": "^0.1.0",
48
- "@systemfsoftware/stryker-ignorer-effect-schema-declarations": "latest",
49
- "@systemfsoftware/stryker-ignorer-in-source-vitest-block": "latest",
50
- "@systemfsoftware/stryker-js": "latest",
51
- "@systemfsoftware/stryker-js-typescript-checker": "latest",
52
- "@systemfsoftware/stryker-js-vitest-runner": "latest",
53
- "@systemfsoftware/stryker-test-contribution": "latest",
54
- "@systemfsoftware/tsconfig": "^2.0.1",
55
- "@systemfsoftware/tsdown-config": "^0.1.0",
56
- "@systemfsoftware/vitest-config": "^0.1.0",
57
- "@types/node": "^26",
58
- "@vitest/coverage-v8": "^5",
59
- "effect": "^4",
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",