Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
40 changes: 35 additions & 5 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
DecoratorContext,
DecoratorDeclarationStatementNode,
DecoratorExpressionNode,
DecoratorPostValidator,
Diagnostic,
DiagnosticTarget,
DocContent,
Expand Down Expand Up @@ -368,6 +369,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
* Key is the SymId of a node. It can be retrieved with getNodeSymId(node)
*/
const pendingResolutions = new PendingResolutions();
const postCheckValidators: DecoratorPostValidator[] = [];

const typespecNamespaceBinding = resolver.symbols.global.exports!.get("TypeSpec");
if (typespecNamespaceBinding) {
Expand Down Expand Up @@ -3485,6 +3487,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker

internalDecoratorValidation();
assertNoPendingResolutions();
runPostValidators(postCheckValidators);
}

function assertNoPendingResolutions() {
Expand Down Expand Up @@ -5956,9 +5959,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker

if (!options.skipDecorators) {
if ("decorators" in typeDef) {
for (const decApp of typeDef.decorators) {
applyDecoratorToType(program, decApp, typeDef);
}
applyDecoratorsToType(typeDef);
}
typeDef.isFinished = true;
Object.setPrototypeOf(typeDef, typePrototype);
Expand All @@ -5968,6 +5969,31 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
return typeDef;
}

function applyDecoratorsToType(typeDef: Type & { decorators: DecoratorApplication[] }) {
const postSelfValidators: DecoratorPostValidator[] = [];
for (const decApp of typeDef.decorators) {
const validator = applyDecoratorToType(program, decApp, typeDef);
if (validator) {
switch (validator.kind) {
case "postSelf":
postSelfValidators.push(validator);
break;
case "post":
postCheckValidators.push(validator);
break;
}
}
}
runPostValidators(postSelfValidators);
}

/** Run a list of post validator */
function runPostValidators(validators: DecoratorPostValidator[]) {
for (const validator of validators) {
program.reportDiagnostics(validator.validator());
}
}

function markAsChecked<T extends Type>(type: T) {
if (!type.creating) return;
delete type.creating;
Expand Down Expand Up @@ -6669,7 +6695,11 @@ function reportDeprecation(
}
}

function applyDecoratorToType(program: Program, decApp: DecoratorApplication, target: Type) {
function applyDecoratorToType(
program: Program,
decApp: DecoratorApplication,
target: Type,
): DecoratorPostValidator | void {
compilerAssert("decorators" in target, "Cannot apply decorator to non-decoratable type", target);

for (const arg of decApp.args) {
Expand Down Expand Up @@ -6697,7 +6727,7 @@ function applyDecoratorToType(program: Program, decApp: DecoratorApplication, ta
const args = decApp.args.map((x) => x.jsValue);
const fn = decApp.decorator;
const context = createDecoratorContext(program, decApp);
fn(context, target, ...args);
return fn(context, target, ...args);
} catch (error: any) {
// do not fail the language server for exceptions in decorators
if (program.compilerOptions.designTimeBuild) {
Expand Down
18 changes: 17 additions & 1 deletion packages/compiler/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,27 @@ export interface DecoratorApplication {
node?: DecoratorExpressionNode | AugmentDecoratorStatementNode;
}

/**
* Signature for a decorator JS implementation function.
* Use `@typespec/tspd` to generate an accurate signature from the `extern dec`
*/
export interface DecoratorFunction {
(program: DecoratorContext, target: any, ...customArgs: any[]): void;
(program: DecoratorContext, target: any, ...customArgs: any[]): DecoratorPostValidator | void;
namespace?: string;
}

export interface DecoratorPostValidator {
/**
* When should this validator run.
* "postSelf": After all decorators are run on the same type. Useful if trying to validate this decorator is compatible with other decorators without relying on the order they are applied.
* "post": After everything is checked in the program. Useful when trying to get an overall view of the program.
*/
readonly kind: "postSelf" | "post";

/** Validator implementation. This function will be run according to the kind defined above. */
readonly validator: () => readonly Diagnostic[];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right now have this expecting an array of diagnostic, this feels nicer that always expecting it to be reported to the program but also less consistent with the rest. It also doesn't pervent you from directly reporting to program if you prefer. What do people think?

}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open to suggestion on the property names, the union values as well as a good way to have a oneliner function to build this object

// 1
const myDec: MyDecDecorator = (contex, target) => {
  return context.validate.postSelf(() => {
    // ....
  })
}

// 2
const myDec: MyDecDecorator = (contex, target) => {
  return DecoratorPostValidator.postSelf(() => {
    // ....
  })
}

// 3
const myDec: MyDecDecorator = (contex, target) => {
  return postSelf(() => {
    // ....
  })
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current name suggestions "onFinish" | "onGraphFinish"


export interface BaseType {
readonly entityKind: "Type";
kind: string;
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ export type {
DecoratorContext,
DecoratorFunction,
DecoratorImplementations,
DecoratorPostValidator,
DeprecatedDirective,
Diagnostic,
DiagnosticCreator,
Expand Down
89 changes: 89 additions & 0 deletions packages/compiler/test/checker/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { beforeEach, describe, it } from "vitest";
import { numericRanges } from "../../src/core/numeric-ranges.js";
import { Numeric } from "../../src/core/numeric.js";
import {
DecoratorContext,
DecoratorFunction,
Model,
Namespace,
PackageFlags,
setTypeSpecNamespace,
Expand All @@ -14,7 +16,9 @@ import {
createTestHost,
createTestWrapper,
expectDiagnostics,
mockFile,
} from "../../src/testing/index.js";
import { Tester } from "../tester.js";

describe("compiler: checker: decorators", () => {
let testHost: TestHost;
Expand Down Expand Up @@ -546,3 +550,88 @@ describe("compiler: checker: decorators", () => {
ok(result, "expected Foo to be blue in isBlue decorator");
});
});

describe("validators", () => {
async function testerForDecorator(fn: DecoratorFunction) {
return await Tester.files({
"dec.tsp": `
import "./dec.js";
namespace MyLibrary;
extern dec myDecorator(target: unknown);
`,
"dec.js": mockFile.js({
$decorators: {
MyLibrary: {
myDecorator: fn,
},
},
}),
})
.import("./dec.tsp")
.using("MyLibrary");
}

it("postSelf apply validator after checking the type", async () => {
const order: string[] = [];
const tester = await testerForDecorator((_: DecoratorContext, target: Model) => {
order.push(`apply(${target.name})`);
return {
kind: "postSelf",
validator: () => {
order.push(`validate(${target.name})`);
return [];
},
};
});
await tester.compile(`
@myDecorator
@myDecorator
model A {}
@myDecorator
@myDecorator
model B {}
`);
deepStrictEqual(order, [
`apply(A)`,
`apply(A)`,
`validate(A)`,
`validate(A)`,
`apply(B)`,
`apply(B)`,
`validate(B)`,
`validate(B)`,
]);
});

it("post apply validator after checking every type", async () => {
const order: string[] = [];
const tester = await testerForDecorator((_: DecoratorContext, target: Model) => {
order.push(`apply(${target.name})`);
return {
kind: "post",
validator: () => {
order.push(`validate(${target.name})`);
return [];
},
};
});
await tester.compile(`
@myDecorator
@myDecorator
model A {}
@myDecorator
@myDecorator
model B {}
`);
deepStrictEqual(order, [
`apply(A)`,
`apply(A)`,
`apply(B)`,
`apply(B)`,
`validate(A)`,
`validate(A)`,
`validate(B)`,
`validate(B)`,
]);
});
});
20 changes: 16 additions & 4 deletions packages/events/generated-defs/TypeSpec.Events.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { DecoratorContext, ModelProperty, Union, UnionVariant } from "@typespec/compiler";
import type {
DecoratorContext,
DecoratorPostValidator,
ModelProperty,
Union,
UnionVariant,
} from "@typespec/compiler";

/**
* Specify that this union describes a set of events.
Expand All @@ -13,7 +19,10 @@ import type { DecoratorContext, ModelProperty, Union, UnionVariant } from "@type
* }
* ```
*/
export type EventsDecorator = (context: DecoratorContext, target: Union) => void;
export type EventsDecorator = (
context: DecoratorContext,
target: Union,
) => DecoratorPostValidator | void;

/**
* Specifies the content type of the event envelope, event body, or event payload.
Expand Down Expand Up @@ -41,7 +50,7 @@ export type ContentTypeDecorator = (
context: DecoratorContext,
target: UnionVariant | ModelProperty,
contentType: string,
) => void;
) => DecoratorPostValidator | void;

/**
* Identifies the payload of an event.
Expand All @@ -54,7 +63,10 @@ export type ContentTypeDecorator = (
* }
* ```
*/
export type DataDecorator = (context: DecoratorContext, target: ModelProperty) => void;
export type DataDecorator = (
context: DecoratorContext,
target: ModelProperty,
) => DecoratorPostValidator | void;

export type TypeSpecEventsDecorators = {
events: EventsDecorator;
Expand Down
4 changes: 2 additions & 2 deletions packages/http-client/generated-defs/TypeSpec.HttpClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DecoratorContext, Type } from "@typespec/compiler";
import type { DecoratorContext, DecoratorPostValidator, Type } from "@typespec/compiler";

export interface FeatureLifecycleOptions {
readonly emitterScope?: string;
Expand All @@ -8,7 +8,7 @@ export type ExperimentalDecorator = (
context: DecoratorContext,
target: Type,
options?: FeatureLifecycleOptions,
) => void;
) => DecoratorPostValidator | void;

export type TypeSpecHttpClientDecorators = {
experimental: ExperimentalDecorator;
Expand Down
28 changes: 20 additions & 8 deletions packages/http/generated-defs/TypeSpec.Http.Private.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { DecoratorContext, Model, ModelProperty, Type } from "@typespec/compiler";
import type {
DecoratorContext,
DecoratorPostValidator,
Model,
ModelProperty,
Type,
} from "@typespec/compiler";

export interface HttpPartOptions {
readonly name?: string;
Expand All @@ -8,16 +14,22 @@ export interface ApplyMergePatchOptions {
readonly visibilityMode: unknown;
}

export type PlainDataDecorator = (context: DecoratorContext, target: Model) => void;
export type PlainDataDecorator = (
context: DecoratorContext,
target: Model,
) => DecoratorPostValidator | void;

export type HttpFileDecorator = (context: DecoratorContext, target: Model) => void;
export type HttpFileDecorator = (
context: DecoratorContext,
target: Model,
) => DecoratorPostValidator | void;

export type HttpPartDecorator = (
context: DecoratorContext,
target: Model,
type: Type,
options: HttpPartOptions,
) => void;
) => DecoratorPostValidator | void;

/**
* Performs the canonical merge-patch transformation on the given model and injects its
Expand All @@ -29,7 +41,7 @@ export type ApplyMergePatchDecorator = (
source: Model,
nameTemplate: string,
options: ApplyMergePatchOptions,
) => void;
) => DecoratorPostValidator | void;

/**
* Specify if inapplicable metadata should be included in the payload for the given entity.
Expand All @@ -40,7 +52,7 @@ export type IncludeInapplicableMetadataInPayloadDecorator = (
context: DecoratorContext,
target: Type,
value: boolean,
) => void;
) => DecoratorPostValidator | void;

/**
* Marks a model that was generated by applying the MergePatch
Expand All @@ -50,7 +62,7 @@ export type MergePatchModelDecorator = (
context: DecoratorContext,
target: Model,
source: Model,
) => void;
) => DecoratorPostValidator | void;

/**
* Links a modelProperty mutated as part of a mergePatch transform to
Expand All @@ -60,7 +72,7 @@ export type MergePatchPropertyDecorator = (
context: DecoratorContext,
target: ModelProperty,
source: ModelProperty,
) => void;
) => DecoratorPostValidator | void;

export type TypeSpecHttpPrivateDecorators = {
plainData: PlainDataDecorator;
Expand Down
Loading
Loading