Skip to content

Commit

Permalink
Better decorator param validation (#582)
Browse files Browse the repository at this point in the history
  • Loading branch information
timotheeguerin authored Jun 7, 2022
1 parent fddb735 commit 3fd2fbe
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/compiler",
"comment": "Added new decorator signatgure validation helper",
"type": "minor"
}
],
"packageName": "@cadl-lang/compiler"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/rest",
"comment": "Uptake changes to compiler with decorator validator helpers",
"type": "minor"
}
],
"packageName": "@cadl-lang/rest"
}
146 changes: 124 additions & 22 deletions packages/compiler/core/decorator-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,35 +114,14 @@ function getTypeKind(target: CadlValue): Type["kind"] | undefined {
}
}

/**
* Emit diagnostic if the number of arguments passed to decorator is more or less than the expected count.
*/
export function validateDecoratorParamCount(
program: Program,
target: Type,
args: unknown[],
expected: number
) {
if (args.length !== expected) {
reportDiagnostic(program, {
code: "invalid-argument-count",
format: {
actual: args.length.toString(),
expected: expected.toString(),
},
target,
});
return;
}
}

/**
* Validate a decorator parameter has the correct type.
* @param program Program
* @param target Decorator target
* @param value Value of the parameter.
* @param expectedType Expected type or list of expected type
* @returns true if the value is of one of the type in the list of expected types. If not emit a diagnostic.
* @deprecated use @see createDecoratorDefinition#validate instead.
*/
export function validateDecoratorParamType<K extends Type["kind"]>(
program: Program,
Expand All @@ -165,6 +144,129 @@ export function validateDecoratorParamType<K extends Type["kind"]>(
return true;
}

export interface DecoratorDefinition<
T extends Type["kind"],
P extends readonly DecoratorParamDefinition<any>[]
> {
readonly name: string;
readonly target: T | T[];
readonly args: P;
}

export interface DecoratorParamDefinition<K extends Type["kind"]> {
readonly kind: K | K[];
readonly optional?: boolean;
}

type InferParameters<P extends readonly DecoratorParamDefinition<any>[]> = {
[K in keyof P]: InferParameter<P[K]>;
};
type InferParameter<P extends DecoratorParamDefinition<any>> = P["optional"] extends true
? InferredCadlValue<P["kind"]> | undefined
: InferredCadlValue<P["kind"]>;

export interface DecoratorValidator<
T extends Type["kind"],
P extends readonly DecoratorParamDefinition<any>[]
> {
validate(
context: DecoratorContext,
target: InferredCadlValue<T>,
parameters: InferParameters<P>
): boolean;
}

export function createDecoratorDefinition<
T extends Type["kind"],
P extends readonly DecoratorParamDefinition<any>[]
>(definition: DecoratorDefinition<T, P>): DecoratorValidator<T, P> {
const minParams = definition.args.filter((x) => !x.optional).length;
const maxParams = definition.args.length;

function validate(context: DecoratorContext, target: Type, args: CadlValue[]) {
if (
!validateDecoratorTarget(context, target, definition.name, definition.target) ||
!validateDecoratorParamCount(context, minParams, maxParams, args)
) {
return false;
}

for (const [index, arg] of args.entries()) {
const paramDefinition = definition.args[index];
if (arg === undefined) {
if (!paramDefinition.optional) {
reportDiagnostic(context.program, {
code: "invalid-argument",
format: {
value: "undefined",
actual: "undefined",
expected: expectedTypeList(paramDefinition.kind),
},
target: context.getArgumentTarget(index)!,
});
return false;
}
} else if (!isCadlValueTypeOf(arg, paramDefinition.kind)) {
reportDiagnostic(context.program, {
code: "invalid-argument",
format: {
value: prettyValue(context.program, arg),
actual: getTypeKind(arg)!,
expected: expectedTypeList(paramDefinition.kind),
},
target: context.getArgumentTarget(index)!,
});
return false;
}
}

return true;
}

return {
validate(context: DecoratorContext, target, parameters) {
return validate(context, target as any, parameters as any);
},
};
}

function expectedTypeList(expectedType: Type["kind"] | Type["kind"][]) {
return typeof expectedType === "string" ? expectedType : expectedType.join(", ");
}

export function validateDecoratorParamCount(
context: DecoratorContext,
min: number,
max: number,
parameters: unknown[]
): boolean {
if (parameters.length < min || parameters.length > max) {
if (min === max) {
reportDiagnostic(context.program, {
code: "invalid-argument-count",
format: {
actual: parameters.length.toString(),
expected: min.toString(),
},
target: context.decoratorTarget,
});
} else {
reportDiagnostic(context.program, {
code: "invalid-argument-count",
messageId: "between",
format: {
actual: parameters.length.toString(),
min: min.toString(),
max: max.toString(),
},
target: context.decoratorTarget,
});
}
return false;
}
return true;
}

function prettyValue(program: Program, value: any) {
if (typeof value === "object" && value !== null && "kind" in value) {
return program.checker.getTypeName(value);
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ const diagnostics = {
severity: "error",
messages: {
default: paramMessage`Expected ${"expected"} arguments, but got ${"actual"}.`,
between: paramMessage`Expected between ${"min"} and ${"max"} arguments, but got ${"actual"}.`,
},
},
"known-values-invalid-enum": {
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface DecoratorApplication {
}

export interface DecoratorFunction {
(program: DecoratorContext, target: Type, ...customArgs: any[]): void;
(program: DecoratorContext, target: any, ...customArgs: any[]): void;
namespace?: string;
}

Expand Down
40 changes: 24 additions & 16 deletions packages/rest/src/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
createDecoratorDefinition,
DecoratorContext,
ModelType,
ModelTypeProperty,
Expand All @@ -11,13 +12,14 @@ import {
} from "@cadl-lang/compiler";
import { reportDiagnostic } from "./diagnostics.js";

const headerDecorator = createDecoratorDefinition({
name: "@header",
target: "ModelProperty",
args: [{ kind: "String", optional: true }],
} as const);
const headerFieldsKey = Symbol("header");
export function $header(context: DecoratorContext, entity: Type, headerName?: string) {
if (!validateDecoratorTarget(context, entity, "@header", "ModelProperty")) {
return;
}

if (headerName && !validateDecoratorParamType(context.program, entity, headerName, "String")) {
export function $header(context: DecoratorContext, entity: ModelTypeProperty, headerName?: string) {
if (!headerDecorator.validate(context, entity, [headerName])) {
return;
}

Expand Down Expand Up @@ -122,13 +124,19 @@ export function $statusCode(context: DecoratorContext, entity: Type) {
codes.push(String(option.value));
}
} else {
reportDiagnostic(context.program, { code: "status-code-invalid", target: entity });
reportDiagnostic(context.program, {
code: "status-code-invalid",
target: entity,
});
}
}
} else if (entity.type.kind === "TemplateParameter") {
// Ignore template parameters
} else {
reportDiagnostic(context.program, { code: "status-code-invalid", target: entity });
reportDiagnostic(context.program, {
code: "status-code-invalid",
target: entity,
});
}
setStatusCode(context.program, entity, codes);
}
Expand Down Expand Up @@ -241,38 +249,38 @@ export function getOperationVerb(program: Program, entity: Type): HttpVerb | und
}

export function $get(context: DecoratorContext, entity: Type, ...args: unknown[]) {
validateVerbNoArgs(context.program, entity, args);
validateVerbNoArgs(context, args);
setOperationVerb(context.program, entity, "get");
}

export function $put(context: DecoratorContext, entity: Type, ...args: unknown[]) {
validateVerbNoArgs(context.program, entity, args);
validateVerbNoArgs(context, args);
setOperationVerb(context.program, entity, "put");
}

export function $post(context: DecoratorContext, entity: Type, ...args: unknown[]) {
validateVerbNoArgs(context.program, entity, args);
validateVerbNoArgs(context, args);
setOperationVerb(context.program, entity, "post");
}

export function $patch(context: DecoratorContext, entity: Type, ...args: unknown[]) {
validateVerbNoArgs(context.program, entity, args);
validateVerbNoArgs(context, args);
setOperationVerb(context.program, entity, "patch");
}

export function $delete(context: DecoratorContext, entity: Type, ...args: unknown[]) {
validateVerbNoArgs(context.program, entity, args);
validateVerbNoArgs(context, args);
setOperationVerb(context.program, entity, "delete");
}

export function $head(context: DecoratorContext, entity: Type, ...args: unknown[]) {
validateVerbNoArgs(context.program, entity, args);
validateVerbNoArgs(context, args);
setOperationVerb(context.program, entity, "head");
}

// TODO: replace with built-in decorator validation https://github.com/Azure/cadl-azure/issues/1022
function validateVerbNoArgs(program: Program, target: Type, args: unknown[]) {
validateDecoratorParamCount(program, target, args, 0);
function validateVerbNoArgs(context: DecoratorContext, args: unknown[]) {
validateDecoratorParamCount(context, 0, 0, args);
}

setDecoratorNamespace(
Expand Down

0 comments on commit 3fd2fbe

Please sign in to comment.