diff --git a/.chronus/changes/copilot-add-warning-on-double-suppression-2026-5-30-14-49-17.md b/.chronus/changes/copilot-add-warning-on-double-suppression-2026-5-30-14-49-17.md new file mode 100644 index 00000000000..22132cb887a --- /dev/null +++ b/.chronus/changes/copilot-add-warning-on-double-suppression-2026-5-30-14-49-17.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Warn on duplicate `#suppress` directives on the same node. diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index f4ba1c4fbbd..bb6ec406040 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -746,6 +746,12 @@ const diagnostics = { default: paramMessage`Duplicate name: "${"name"}"`, }, }, + "duplicate-suppression": { + severity: "warning", + messages: { + default: paramMessage`Diagnostic "${"code"}" is already suppressed on this node.`, + }, + }, "decorator-decl-target": { severity: "error", messages: { diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index cf03ca50562..513170c5a61 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -45,6 +45,7 @@ import { SuppressionTracker, createSuppressionTracker, findDirectiveSuppressingOnNode, + findDuplicateSuppressions, } from "./suppression-tracking.js"; import { CompilerHost, @@ -305,6 +306,15 @@ async function createProgram( oldProgram = undefined; suppressionTracker = createSuppressionTracker(sourceResolution); + reportDiagnostics( + findDuplicateSuppressions(sourceResolution).map(({ directive }) => + createDiagnostic({ + code: "duplicate-suppression", + format: { code: directive.code }, + target: directive.node, + }), + ), + ); const resolver = (program.resolver = createResolver(program)); runtimeStats.resolver = perf.time(() => resolver.resolveProgram()); diff --git a/packages/compiler/src/core/suppression-tracking.ts b/packages/compiler/src/core/suppression-tracking.ts index 39970305945..947b62ac258 100644 --- a/packages/compiler/src/core/suppression-tracking.ts +++ b/packages/compiler/src/core/suppression-tracking.ts @@ -16,6 +16,10 @@ export interface UnusedSuppression { directive: SuppressDirective; } +export interface DuplicateSuppression { + directive: SuppressDirective; +} + export interface SuppressionTracker { markUsed(directiveNode: DirectiveExpressionNode): void; getUnusedSuppressions(): UnusedSuppression[]; @@ -53,6 +57,40 @@ export function createSuppressionTracker(sourceResolution: SourceResolution): Su }; } +export function findDuplicateSuppressions( + sourceResolution: SourceResolution, +): DuplicateSuppression[] { + const duplicates: DuplicateSuppression[] = []; + for (const script of sourceResolution.sourceFiles.values()) { + if (sourceResolution.locationContexts.get(script.file)?.type !== "project") { + continue; + } + + visit(script); + } + + return duplicates; + + function visit(node: Node) { + const seenSuppressions = new Set(); + for (const directiveNode of node.directives ?? []) { + const directive = parseDirective(directiveNode); + if (directive?.name !== "suppress") { + continue; + } + + if (seenSuppressions.has(directive.code)) { + duplicates.push({ directive }); + continue; + } + + seenSuppressions.add(directive.code); + } + + visitChildren(node, visit); + } +} + interface SuppressionRecord { directive: SuppressDirective; used: boolean; diff --git a/packages/compiler/test/suppression.test.ts b/packages/compiler/test/suppression.test.ts index d6b19b9f29b..270d7c5317c 100644 --- a/packages/compiler/test/suppression.test.ts +++ b/packages/compiler/test/suppression.test.ts @@ -86,6 +86,54 @@ it("error diagnostics cannot be suppressed and emit another error", async () => expectDiagnostics(diagnostics, [{ code: "suppress-error" }, { code: "no-id-property" }]); }); +it("warns on duplicate suppressions with a message", async () => { + const [, diagnostics] = await Tester.compileAndDiagnose( + ` + #deprecated "Old is deprecated" + model Old {} + + model Foo { + #suppress "deprecated" "intentional" + #suppress "deprecated" "duplicate" + prop: Old; + } + `, + { + compilerOptions: { nostdlib: true }, + }, + ); + + expectDiagnostics(diagnostics, { + code: "duplicate-suppression", + severity: "warning", + message: 'Diagnostic "deprecated" is already suppressed on this node.', + }); +}); + +it("warns on duplicate suppressions without a message", async () => { + const [, diagnostics] = await Tester.compileAndDiagnose( + ` + #deprecated "Old is deprecated" + model Old {} + + model Foo { + #suppress "deprecated" + #suppress "deprecated" + prop: Old; + } + `, + { + compilerOptions: { nostdlib: true }, + }, + ); + + expectDiagnostics(diagnostics, { + code: "duplicate-suppression", + severity: "warning", + message: 'Diagnostic "deprecated" is already suppressed on this node.', + }); +}); + it("reports unused suppression via tracker", async () => { const { program } = await Tester.compile( `