From 2b6e54f0d657d9ce6a9b51c0ffd38f222890b114 Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Sun, 13 Sep 2026 22:22:07 +0000 Subject: [PATCH] feat: say which kinds of typescript diagnostic to report `ignoreDiagnostics` drops what a program answered; `diagnosticOptions` asks it for less, so a check that only wants syntax errors does not pay for the type checking it then throws away. Every kind is reported unless one is turned off, which is the default `fork-ts-checker-webpack-plugin` spells the same way with `semantic` alone. What the config file itself is wrong about is reported whatever is turned off: with the config file misread, nothing below it would be answering the right question. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GzZci4NQeiqwdrVfd7dGXy --- .changeset/typescript-diagnostic-options.md | 5 ++ README.md | 79 +++++++++++++------ src/checks/typescript.js | 18 ++++- src/checks/typescript.json | 19 +++++ test/typescript/fixtures/kinds/index.js | 3 + test/typescript/fixtures/kinds/syntax.ts | 1 + test/typescript/fixtures/kinds/tsconfig.json | 9 +++ .../kinds/tsconfig.unknown-option.json | 4 + test/typescript/fixtures/kinds/types.ts | 1 + test/typescript/kinds.test.js | 69 ++++++++++++++++ 10 files changed, 182 insertions(+), 26 deletions(-) create mode 100644 .changeset/typescript-diagnostic-options.md create mode 100644 test/typescript/fixtures/kinds/index.js create mode 100644 test/typescript/fixtures/kinds/syntax.ts create mode 100644 test/typescript/fixtures/kinds/tsconfig.json create mode 100644 test/typescript/fixtures/kinds/tsconfig.unknown-option.json create mode 100644 test/typescript/fixtures/kinds/types.ts create mode 100644 test/typescript/kinds.test.js diff --git a/.changeset/typescript-diagnostic-options.md b/.changeset/typescript-diagnostic-options.md new file mode 100644 index 00000000..9b13cf91 --- /dev/null +++ b/.changeset/typescript-diagnostic-options.md @@ -0,0 +1,5 @@ +--- +"diagnostics-webpack-plugin": minor +--- + +Add `diagnosticOptions` to the `typescript` check, which says whether to report each kind of diagnostic — syntactic, semantic, declaration and global. Turning one off asks TypeScript for less rather than dropping what it answered. diff --git a/README.md b/README.md index d4dc0ed8..52429559 100644 --- a/README.md +++ b/README.md @@ -88,27 +88,27 @@ Every check to run is an entry in `checks`, named by its `use`. The list may nam ### Every option -| Option | Layer | What it decides | -| :--------------------------------------------------------------------------------------------- | :----- | :--------------------------------------------------------------------------- | -| [`checks`](#checks) | Plugin | Which checks run, and the options only each of them understands. | -| [`context`](#context) | Plugin | The folder every relative `files` and `exclude` pattern is resolved against. | -| [`lintOnStart`](#lintonstart) | Plugin | Whether the first compilation checks everything it covers. | -| [`cache`](#cache) | Shared | Whether the tool keeps a cache of its own between runs. | -| [`cacheLocation`](#cachelocation) | Shared | Where that cache is written. | -| [`exclude`](#exclude) | Shared | What is left out. | -| [`extensions`](#extensions) | Shared | Which extensions a named folder is walked for. | -| [`files`](#files) | Shared | What to check: naming it checks every file it matches, built or not. | -| [`fix`](#fix) | Shared | Whether the tool writes back what it can fix. | -| [`formatter`](#formatter) | Shared | How results are turned into the message that is reported. | -| [`outputReport`](#outputreport) | Shared | A file the same results are written to. | -| [`reportAs`](#reportas) | Shared | What the build carries: an error, a warning, a log line, or nothing. | -| [`resourceQueryExclude`](#resourcequeryexclude) | Shared | Which module queries are left out. | -| [`threads`](#threads) | Shared | How wide the work is spread. | -| [`configType`, `eslintPath`](#eslint) | Check | ESLint's own. | -| [`stylelintPath`](#stylelint) | Check | Stylelint's own. | -| [`oxlintPath`, `configFile`, `args`](#oxlint) | Check | oxlint's own. | -| [`biomePath`, `command`, `configFile`, `args`](#biome) | Check | Biome's own. | -| [`typescriptPath`, `configFile`, `compilerOptions`, `build`, `ignoreDiagnostics`](#typescript) | Check | TypeScript's own. | +| Option | Layer | What it decides | +| :------------------------------------------------------------------------------------------------------------------ | :----- | :--------------------------------------------------------------------------- | +| [`checks`](#checks) | Plugin | Which checks run, and the options only each of them understands. | +| [`context`](#context) | Plugin | The folder every relative `files` and `exclude` pattern is resolved against. | +| [`lintOnStart`](#lintonstart) | Plugin | Whether the first compilation checks everything it covers. | +| [`cache`](#cache) | Shared | Whether the tool keeps a cache of its own between runs. | +| [`cacheLocation`](#cachelocation) | Shared | Where that cache is written. | +| [`exclude`](#exclude) | Shared | What is left out. | +| [`extensions`](#extensions) | Shared | Which extensions a named folder is walked for. | +| [`files`](#files) | Shared | What to check: naming it checks every file it matches, built or not. | +| [`fix`](#fix) | Shared | Whether the tool writes back what it can fix. | +| [`formatter`](#formatter) | Shared | How results are turned into the message that is reported. | +| [`outputReport`](#outputreport) | Shared | A file the same results are written to. | +| [`reportAs`](#reportas) | Shared | What the build carries: an error, a warning, a log line, or nothing. | +| [`resourceQueryExclude`](#resourcequeryexclude) | Shared | Which module queries are left out. | +| [`threads`](#threads) | Shared | How wide the work is spread. | +| [`configType`, `eslintPath`](#eslint) | Check | ESLint's own. | +| [`stylelintPath`](#stylelint) | Check | Stylelint's own. | +| [`oxlintPath`, `configFile`, `args`](#oxlint) | Check | oxlint's own. | +| [`biomePath`, `command`, `configFile`, `args`](#biome) | Check | Biome's own. | +| [`typescriptPath`, `configFile`, `compilerOptions`, `build`, `diagnosticOptions`, `ignoreDiagnostics`](#typescript) | Check | TypeScript's own. | Anything else written in a `checks` entry is handed to the tool itself, so its own Node.js API options go next to these. @@ -808,6 +808,41 @@ that fails is never up to date. Pair it with [`reportAs: "log"`](#reportas) if you would rather a watch rebuild did not wait for that. +### `diagnosticOptions` + +- Type: + +```ts +interface diagnosticOptions { + syntactic?: boolean | undefined; + semantic?: boolean | undefined; + declaration?: boolean | undefined; + global?: boolean | undefined; +} +``` + +- Default: every kind reported + +Which kinds of diagnostic to report. Turning one off asks TypeScript for less +rather than dropping what it answered, so it is also the one filter that saves +the work — `semantic: false` is what makes a check that only wants syntax errors +cheap. + +```js +new DiagnosticsPlugin({ + // webpack's own parser reports a syntax error in a file it builds + checks: [{ use: "typescript", diagnosticOptions: { syntactic: false } }], +}); +``` + +What the `tsconfig.json` itself is wrong about is reported whatever is turned +off here: with the config file misread, nothing below it would be answering the +right question. [`build`](#build) reports the whole of what it finds either way, +since a solution is built rather than asked kind by kind. + +`fork-ts-checker-webpack-plugin` spells this option the same way, with +`semantic` alone on by default. + ### `ignoreDiagnostics` - Type: @@ -1035,7 +1070,7 @@ rather than the project again. What that costs and saves is under | `typescript.typescriptPath` | [`typescriptPath`](#typescriptpath). | | `typescript.mode` | Not an option: nothing is written without `build`, and `build` writes the declarations a referenced project publishes and nothing else. | | `typescript.memoryLimit`, `profile` | Nothing to set — the check is not a forked process. | -| `typescript.diagnosticOptions` | No switch per kind; [`ignoreDiagnostics`](#ignorediagnostics) leaves out the codes you name. | +| `typescript.diagnosticOptions` | [`diagnosticOptions`](#diagnosticoptions), spelled the same way. Every kind is reported here unless you turn one off; there, only `semantic` is on to begin with. | | `issue.include`, `issue.exclude` | [`files`](#files) and [`exclude`](#exclude) choose the files, [`ignoreDiagnostics`](#ignorediagnostics) the codes. A predicate of your own has no equivalent. | | `formatter` | [`formatter`](#formatter). Unset, TypeScript's own formatter is used, with color and the source line. | | `logger` | Webpack's logger is what the plugin writes to; `reportAs: "log"` is what sends results there rather than onto the compilation. | diff --git a/src/checks/typescript.js b/src/checks/typescript.js index ab033cf4..02ec3204 100644 --- a/src/checks/typescript.js +++ b/src/checks/typescript.js @@ -335,15 +335,25 @@ function check(ts, options, held) { : []; const ignored = new Set(options.ignoreDiagnostics || []); + // What the config file itself is wrong about is reported whatever else is + // turned off: nothing below it would be answering the right question. + const kinds = { + syntactic: true, + semantic: true, + declaration: true, + global: true, + ...options.diagnosticOptions, + }; const diagnostics = [ ...unrecoverable, ...ts.sortAndDeduplicateDiagnostics([ ...program.getConfigFileParsingDiagnostics(), ...program.getOptionsDiagnostics(), - ...held.program.getSyntacticDiagnostics(), - ...program.getGlobalDiagnostics(), - ...held.program.getSemanticDiagnostics(), - ...(parsed.options.declaration || parsed.options.composite + ...(kinds.syntactic ? held.program.getSyntacticDiagnostics() : []), + ...(kinds.global ? program.getGlobalDiagnostics() : []), + ...(kinds.semantic ? held.program.getSemanticDiagnostics() : []), + ...(kinds.declaration && + (parsed.options.declaration || parsed.options.composite) ? program.getDeclarationDiagnostics() : []), ]), diff --git a/src/checks/typescript.json b/src/checks/typescript.json index d6ba238c..5f2969f9 100644 --- a/src/checks/typescript.json +++ b/src/checks/typescript.json @@ -24,6 +24,25 @@ "build": { "description": "Build the projects the config file references first, the way `tsc -b` does, and report what the whole solution finds.", "type": "boolean" + }, + "diagnosticOptions": { + "description": "Which kinds of diagnostic to report. Every kind is reported unless it is turned off here; `build` reports the whole of what it finds either way.", + "type": "object", + "additionalProperties": false, + "properties": { + "syntactic": { + "type": "boolean" + }, + "semantic": { + "type": "boolean" + }, + "declaration": { + "type": "boolean" + }, + "global": { + "type": "boolean" + } + } } } } diff --git a/test/typescript/fixtures/kinds/index.js b/test/typescript/fixtures/kinds/index.js new file mode 100644 index 00000000..acbbf325 --- /dev/null +++ b/test/typescript/fixtures/kinds/index.js @@ -0,0 +1,3 @@ +const entry = 1; + +module.exports = entry; diff --git a/test/typescript/fixtures/kinds/syntax.ts b/test/typescript/fixtures/kinds/syntax.ts new file mode 100644 index 00000000..46d06eb4 --- /dev/null +++ b/test/typescript/fixtures/kinds/syntax.ts @@ -0,0 +1 @@ +export const broken: number = ; diff --git a/test/typescript/fixtures/kinds/tsconfig.json b/test/typescript/fixtures/kinds/tsconfig.json new file mode 100644 index 00000000..c5577b7f --- /dev/null +++ b/test/typescript/fixtures/kinds/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "strict": true, + "target": "es2022", + "module": "preserve", + "skipLibCheck": true + }, + "include": ["*.ts"] +} diff --git a/test/typescript/fixtures/kinds/tsconfig.unknown-option.json b/test/typescript/fixtures/kinds/tsconfig.unknown-option.json new file mode 100644 index 00000000..aba54332 --- /dev/null +++ b/test/typescript/fixtures/kinds/tsconfig.unknown-option.json @@ -0,0 +1,4 @@ +{ + "compilerOptions": { "strict": true, "nonsense": true }, + "include": ["*.ts"] +} diff --git a/test/typescript/fixtures/kinds/types.ts b/test/typescript/fixtures/kinds/types.ts new file mode 100644 index 00000000..2d6a4ec3 --- /dev/null +++ b/test/typescript/fixtures/kinds/types.ts @@ -0,0 +1 @@ +export const wrong: string = 42; diff --git a/test/typescript/kinds.test.js b/test/typescript/kinds.test.js new file mode 100644 index 00000000..675021a6 --- /dev/null +++ b/test/typescript/kinds.test.js @@ -0,0 +1,69 @@ +import assert from "node:assert/strict"; +import { join } from "node:path"; +import { describe, it } from "node:test"; + +import pack from "./utils/pack.js"; + +const reported = async (/** @type {EXPECTED_ANY} */ diagnosticOptions) => { + const stats = await pack("kinds", { diagnosticOptions }).runAsync(); + + return stats.compilation.errors.map(({ message }) => message).join("\n"); +}; + +describe("diagnostic options", () => { + it("should report every kind when none is turned off", async () => { + const messages = await reported(undefined); + + assert.match(messages, /TS1109/u); + assert.match(messages, /TS2322/u); + }); + + it("should leave out the syntactic ones", async () => { + const messages = await reported({ syntactic: false }); + + assert.doesNotMatch(messages, /TS1109/u); + assert.match(messages, /TS2322/u); + }); + + it("should leave out the semantic ones", async () => { + const messages = await reported({ semantic: false }); + + assert.match(messages, /TS1109/u); + assert.doesNotMatch(messages, /TS2322/u); + }); + + it("should report nothing of a program with every kind turned off", async () => { + const stats = await pack("kinds", { + diagnosticOptions: { + declaration: false, + global: false, + semantic: false, + syntactic: false, + }, + }).runAsync(); + + assert.strictEqual(stats.hasErrors(), false); + assert.strictEqual(stats.hasWarnings(), false); + }); + + it("should report what the config file itself is wrong about either way", async () => { + const stats = await pack("kinds", { + // The config file names an option TypeScript does not have. + configFile: join( + import.meta.dirname, + "fixtures", + "kinds", + "tsconfig.unknown-option.json", + ), + diagnosticOptions: { + declaration: false, + global: false, + semantic: false, + syntactic: false, + }, + }).runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.match(stats.compilation.errors[0].message, /TS5023/u); + }); +});