Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue diagnostic for a templated model with a template parameter that is not used. #5494

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
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
7 changes: 7 additions & 0 deletions .chronus/changes/fix181-2025-0-6-16-32-31.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Support diagnostics for unused template parameter in model template
7 changes: 7 additions & 0 deletions .chronus/changes/fix181-2025-0-7-14-51-31.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/protobuf"
---

Suppress unused-template-parameter diagnostic for Map model
55 changes: 53 additions & 2 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MultiKeyMap, Mutable, createRekeyableMap, isArray, mutate } from "../ut
import { createSymbol, getSymNode } from "./binder.js";
import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-identifier.codefix.js";
import { createModelToObjectValueCodeFix } from "./compiler-code-fixes/model-to-object-literal.codefix.js";
import { removeUnusedTemplateParameterCodeFix } from "./compiler-code-fixes/remove-unused-template-parameter.codefix.js";
import { createTupleToArrayValueCodeFix } from "./compiler-code-fixes/tuple-to-array-value.codefix.js";
import { getDeprecationDetails, markDeprecated } from "./deprecation.js";
import { ProjectionError, compilerAssert, ignoreDiagnostics } from "./diagnostics.js";
Expand Down Expand Up @@ -1108,7 +1109,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
);
}
}

return mapper ? mapper.getMappedType(type) : type;
}

Expand Down Expand Up @@ -1860,6 +1860,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
if (type === neverType) {
continue;
}
if (type.kind === "TemplateParameter") {
resolver.setTemplateParameterDeclarationNode(type.node);
}
if (type.kind === "Union" && type.expression) {
for (const [name, variant] of type.variants) {
unionType.variants.set(name, variant);
Expand Down Expand Up @@ -3551,6 +3554,24 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
if (mapper === undefined) {
for (const templateParameter of node.templateParameters) {
checkTemplateParameterDeclaration(templateParameter, undefined);
/* current we only check unused template parameter for model template.
* TODO: We should add more check for other template types in the future.
*/
if (node.kind === SyntaxKind.ModelStatement) {
if (!resolver.isUsedTemplateParameterDeclarationNode(templateParameter)) {
reportCheckerDiagnostic(
createDiagnostic({
code: "unused-template-parameter",
format: {
parameterName: templateParameter.id.sv,
type: node.symbol.name,
},
target: templateParameter,
codefixes: [removeUnusedTemplateParameterCodeFix(templateParameter)],
}),
);
}
}
}
}
}
Expand All @@ -3573,7 +3594,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
// we're not instantiating this model and we've already checked it
return links.declaredType as any;
}
checkTemplateDeclaration(node, mapper);

const decorators: DecoratorApplication[] = [];
const type: Model = createType({
Expand Down Expand Up @@ -3664,6 +3684,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
}
lateBindMemberContainer(type);
lateBindMembers(type);

checkTemplateDeclaration(node, mapper);
return type;
}

Expand Down Expand Up @@ -4335,6 +4357,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker

if (isType(entity)) {
if (entity.kind === "TemplateParameter") {
resolver.setTemplateParameterDeclarationNode(entity.node);
if (entity.constraint === undefined || entity.constraint.type !== undefined) {
// means this template constraint will accept values
reportCheckerDiagnostic(
Expand Down Expand Up @@ -4582,6 +4605,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
return undefined;
}

if (heritageType.kind === "Model") {
for (const arg of heritageType.templateMapper?.args ?? []) {
if ("kind" in arg && arg.kind === "TemplateParameter") {
resolver.setTemplateParameterDeclarationNode(arg.node);
}
}
}
if (heritageType.kind !== "Model") {
reportCheckerDiagnostic(createDiagnostic({ code: "extend-model", target: heritageRef }));
return undefined;
Expand Down Expand Up @@ -4648,6 +4678,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
return;
}

for (const arg of isType.templateMapper?.args ?? []) {
if ("kind" in arg && arg.kind === "TemplateParameter") {
resolver.setTemplateParameterDeclarationNode(arg.node);
}
}

if (isType.name === "") {
reportCheckerDiagnostic(
createDiagnostic({ code: "is-model", messageId: "modelExpression", target: isExpr }),
Expand All @@ -4666,6 +4702,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
): [ModelProperty[], ModelIndexer | undefined] {
const targetType = getTypeForNode(targetNode, mapper);

if (targetType.kind === "TemplateParameter") {
resolver.setTemplateParameterDeclarationNode(targetType.node);
}
if (targetType.kind === "TemplateParameter" || isErrorType(targetType)) {
return [[], undefined];
}
Expand Down Expand Up @@ -4783,6 +4822,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
}
}

if (type.type.kind === "TemplateParameter") {
resolver.setTemplateParameterDeclarationNode(type.type.node);
}
type.decorators = checkDecorators(type, prop, mapper);
const parentTemplate = getParentTemplateNode(prop);
linkMapper(type, mapper);
Expand Down Expand Up @@ -5220,6 +5262,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
for (const decNode of decoratorNodes) {
const decorator = checkDecoratorApplication(targetType, decNode, mapper);
if (decorator) {
/* check template parameters */
for (const arg of decorator.args) {
if ("kind" in arg.value && arg.value.kind === "TemplateParameter") {
resolver.setTemplateParameterDeclarationNode(arg.value.node);
}
}
decorators.unshift(decorator);
}
}
Expand Down Expand Up @@ -5716,6 +5764,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker

const name = variantNode.id ? variantNode.id.sv : Symbol("name");
const type = getTypeForNode(variantNode.value, mapper);
if (type.kind === "TemplateParameter") {
resolver.setTemplateParameterDeclarationNode(type.node);
}
const variantType: UnionVariant = createType({
kind: "UnionVariant",
name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { defineCodeFix, getSourceLocation } from "../diagnostics.js";
import { TemplateParameterDeclarationNode } from "../types.js";

/**
* Quick fix that remove unused template parameter.
*/
export function removeUnusedTemplateParameterCodeFix(node: TemplateParameterDeclarationNode) {
return defineCodeFix({
id: "remove-unused-template-parameter",
label: `Remove unused template parameter ${node.id.sv}`,
fix: (context) => {
let location = getSourceLocation(node);
const parent = node.parent;
if (parent) {
const length = parent.templateParameters.length;
if (length === 1) {
location = {
file: location.file,
pos: parent.templateParametersRange.pos,
end: parent.templateParametersRange.end,
};
} else {
const index = parent.templateParameters.findIndex((param) => param === node);
if (index !== -1) {
if (index !== parent.templateParameters.length - 1) {
location = {
file: location.file,
pos: location.pos,
end: parent.templateParameters[index + 1].pos,
};
} else {
location = {
file: location.file,
pos: parent.templateParameters[index - 1].end,
end: location.end,
};
}
}
}
}
return context.replaceText(location, "");
},
});
}
4 changes: 2 additions & 2 deletions packages/compiler/src/core/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export type DiagnosticHandler = (diagnostic: Diagnostic) => void;
export function logDiagnostics(diagnostics: readonly Diagnostic[], logger: LogSink) {
for (const diagnostic of diagnostics) {
logger.log({
level: diagnostic.severity,
level: diagnostic.severity === "hint" ? "trace" : diagnostic.severity,
message: diagnostic.message,
code: diagnostic.code,
url: diagnostic.url,
Expand All @@ -52,7 +52,7 @@ export function formatDiagnostic(diagnostic: Diagnostic, options: FormatDiagnost
return formatLog(
{
code: diagnostic.code,
level: diagnostic.severity,
level: diagnostic.severity === "hint" ? "trace" : diagnostic.severity,
message: diagnostic.message,
url: diagnostic.url,
sourceLocation: getSourceLocation(diagnostic.target, { locateId: true }),
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,12 @@ const diagnostics = {
default: paramMessage`Object value may only specify known properties, and '${"propertyName"}' does not exist in type '${"type"}'.`,
},
},
"unused-template-parameter": {
severity: "hint",
messages: {
default: paramMessage`Templates should use all specified parameters, and parameter '${"parameterName"}' does not exist in type '${"type"}'. Consider removing this parameter.`,
},
},
"extends-interface": {
severity: "error",
messages: {
Expand Down
22 changes: 22 additions & 0 deletions packages/compiler/src/core/name-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ export interface NameResolver {
/** Return augment decorator nodes that are bound to this symbol */
getAugmentDecoratorsForSym(symbol: Sym): AugmentDecoratorStatementNode[];

/** Check if a template parameter delare node is used or not */
isUsedTemplateParameterDeclarationNode(node: TemplateParameterDeclarationNode): boolean;

/** Set used template parameter */
setTemplateParameterDeclarationNode(node: TemplateParameterDeclarationNode): void;

/**
* Resolve the member expression using the given symbol as base.
* This can be used to follow the name resolution for template instance which are not statically linked.
Expand Down Expand Up @@ -180,6 +186,11 @@ export function createResolver(program: Program): NameResolver {
const nullSym = createSymbol(undefined, "null", SymbolFlags.None);
const augmentDecoratorsForSym = new Map<Sym, AugmentDecoratorStatementNode[]>();

/**
* Tracking the template parameters that are used.
*/
const usedTemplateParameterDeclarationNodes = new Set<TemplateParameterDeclarationNode>();

return {
symbols: { global: globalNamespaceSym, null: nullSym },
resolveProgram() {
Expand Down Expand Up @@ -222,6 +233,8 @@ export function createResolver(program: Program): NameResolver {
resolveTypeReference,

getAugmentDecoratorsForSym,
isUsedTemplateParameterDeclarationNode,
setTemplateParameterDeclarationNode,
};

function getAugmentDecoratorsForSym(sym: Sym) {
Expand All @@ -233,6 +246,15 @@ export function createResolver(program: Program): NameResolver {
return mergedSymbols.get(sym) || sym;
}

function isUsedTemplateParameterDeclarationNode(node: TemplateParameterDeclarationNode): boolean {
if (!node) return false;
return usedTemplateParameterDeclarationNodes.has(node);
}

function setTemplateParameterDeclarationNode(node: TemplateParameterDeclarationNode): void {
if (!node) return;
usedTemplateParameterDeclarationNodes.add(node);
}
/**
* @internal
*/
Expand Down
35 changes: 34 additions & 1 deletion packages/compiler/src/core/type-relation-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export interface TypeRelation {
isReflectionType(type: Type): type is Model & { name: ReflectionTypeName };

areScalarsRelated(source: Scalar, target: Scalar): boolean;
isUnusedTemplateParameter(
source: Value,
target: Type,
diagnosticTarget: Entity | Node,
): [boolean, readonly Diagnostic[]];
}

enum Related {
Expand All @@ -74,7 +79,8 @@ interface TypeRelationError {
| "missing-index"
| "property-required"
| "missing-property"
| "unexpected-property";
| "unexpected-property"
| "unused-template-parameter";
message: string;
children: readonly TypeRelationError[];
target: Entity | Node;
Expand Down Expand Up @@ -110,6 +116,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T
isValueOfType,
isReflectionType,
areScalarsRelated,
isUnusedTemplateParameter,
};

/**
Expand Down Expand Up @@ -147,6 +154,17 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T
return false;
}

function isUnusedTemplateParameter(
source: Value,
target: Type,
diagnosticTarget: Entity | Node,
): [boolean, readonly Diagnostic[]] {
const errors: TypeRelationError[] = [
createUnusedTemplateParameterDiagnostic(source, target, diagnosticTarget),
];
return [true, convertErrorsToDiagnostics(errors, diagnosticTarget)];
}

function convertErrorsToDiagnostics(
errors: readonly TypeRelationError[],
diagnosticBase: Entity | Node,
Expand Down Expand Up @@ -1024,4 +1042,19 @@ function createUnassignableDiagnostic(
details,
});
}

function createUnusedTemplateParameterDiagnostic(
source: Value,
target: Type,
diagnosticTarget: Entity | Node,
): TypeRelationError {
return createTypeRelationError({
code: "unused-template-parameter",
format: {
parameterName: getEntityName(source),
type: getEntityName(target),
},
diagnosticTarget,
});
}
// #endregion
5 changes: 3 additions & 2 deletions packages/compiler/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2300,7 +2300,7 @@ export interface TemplateInstanceTarget {

export type DiagnosticTarget = TypeSpecDiagnosticTarget | SourceLocation;

export type DiagnosticSeverity = "error" | "warning";
export type DiagnosticSeverity = "error" | "warning" | "hint";

export interface Diagnostic {
code: string;
Expand Down Expand Up @@ -2529,8 +2529,9 @@ export interface DiagnosticDefinition<M extends DiagnosticMessages> {
* Diagnostic severity.
* - `warning` - Suppressable, should be used to represent potential issues but not blocking.
* - `error` - Non-suppressable, should be used to represent failure to move forward.
* - `hint` - Suppressable, should be used to represent suggestions.
*/
readonly severity: "warning" | "error";
readonly severity: "warning" | "error" | "hint";
/** Messages that can be reported with the diagnostic. */
readonly messages: M;
/** Short description of the diagnostic */
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler/src/server/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,13 @@ function getVSLocationWithTypeInfo(
};
}

function convertSeverity(severity: "warning" | "error"): DiagnosticSeverity {
function convertSeverity(severity: "warning" | "error" | "hint"): DiagnosticSeverity {
switch (severity) {
case "warning":
return DiagnosticSeverity.Warning;
case "error":
return DiagnosticSeverity.Error;
case "hint":
return DiagnosticSeverity.Hint;
}
}
Loading
Loading