Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/compiler"
---

Warn on duplicate `#suppress` directives on the same node.
6 changes: 6 additions & 0 deletions packages/compiler/src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler/src/core/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
SuppressionTracker,
createSuppressionTracker,
findDirectiveSuppressingOnNode,
findDuplicateSuppressions,
} from "./suppression-tracking.js";
import {
CompilerHost,
Expand Down Expand Up @@ -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());
Expand Down
38 changes: 38 additions & 0 deletions packages/compiler/src/core/suppression-tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface UnusedSuppression {
directive: SuppressDirective;
}

export interface DuplicateSuppression {
directive: SuppressDirective;
}

export interface SuppressionTracker {
markUsed(directiveNode: DirectiveExpressionNode): void;
getUnusedSuppressions(): UnusedSuppression[];
Expand Down Expand Up @@ -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<string>();
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;
Expand Down
48 changes: 48 additions & 0 deletions packages/compiler/test/suppression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
`
Expand Down
Loading