Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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