diff --git a/.chronus/changes/decorator-post-validator-2025-10-26-18-27-31.md b/.chronus/changes/decorator-post-validator-2025-10-26-18-27-31.md new file mode 100644 index 00000000000..90099e0719c --- /dev/null +++ b/.chronus/changes/decorator-post-validator-2025-10-26-18-27-31.md @@ -0,0 +1,21 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@typespec/events" + - "@typespec/http-client" + - "@typespec/http" + - "@typespec/json-schema" + - "@typespec/openapi" + - "@typespec/openapi3" + - "@typespec/protobuf" + - "@typespec/rest" + - "@typespec/spector" + - "@typespec/sse" + - "@typespec/streams" + - "@typespec/tspd" + - "@typespec/versioning" + - "@typespec/xml" +--- + +Regenerate signature diff --git a/.chronus/changes/decorator-post-validator-2025-11-1-16-26-16.md b/.chronus/changes/decorator-post-validator-2025-11-1-16-26-16.md new file mode 100644 index 00000000000..4873d64e76e --- /dev/null +++ b/.chronus/changes/decorator-post-validator-2025-11-1-16-26-16.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/compiler" +--- + +[API] Introduction of decorator validator callbacks. A decorator can define some callbacks to achieve some deferred validation (After the type is finished or the whole graph is) diff --git a/packages/compiler/generated-defs/TypeSpec.Prototypes.ts b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts index 0819a6017d2..50d03cc251d 100644 --- a/packages/compiler/generated-defs/TypeSpec.Prototypes.ts +++ b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts @@ -1,6 +1,9 @@ -import type { DecoratorContext, Type } from "../src/index.js"; +import type { DecoratorContext, DecoratorValidatorCallbacks, Type } from "../src/index.js"; -export type GetterDecorator = (context: DecoratorContext, target: Type) => void; +export type GetterDecorator = ( + context: DecoratorContext, + target: Type, +) => DecoratorValidatorCallbacks | void; export type TypeSpecPrototypesDecorators = { getter: GetterDecorator; diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index 3325db0772a..177b9afc681 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -1,5 +1,6 @@ import type { DecoratorContext, + DecoratorValidatorCallbacks, Enum, EnumValue, Interface, @@ -70,7 +71,7 @@ export type MediaTypeHintDecorator = ( context: DecoratorContext, target: Model | Scalar | Enum | Union, mediaType: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify how to encode the target type. @@ -102,7 +103,7 @@ export type EncodeDecorator = ( target: Scalar | ModelProperty, encodingOrEncodeAs: Scalar | string | EnumValue, encodedAs?: Scalar, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Attach a documentation string. Content support CommonMark markdown formatting. @@ -120,17 +121,23 @@ export type DocDecorator = ( target: Type, doc: string, formatArgs?: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Returns the model with required properties removed. */ -export type WithOptionalPropertiesDecorator = (context: DecoratorContext, target: Model) => void; +export type WithOptionalPropertiesDecorator = ( + context: DecoratorContext, + target: Model, +) => DecoratorValidatorCallbacks | void; /** * Returns the model with non-updateable properties removed. */ -export type WithUpdateablePropertiesDecorator = (context: DecoratorContext, target: Model) => void; +export type WithUpdateablePropertiesDecorator = ( + context: DecoratorContext, + target: Model, +) => DecoratorValidatorCallbacks | void; /** * Returns the model with the given properties omitted. @@ -141,7 +148,7 @@ export type WithoutOmittedPropertiesDecorator = ( context: DecoratorContext, target: Model, omit: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Returns the model with only the given properties included. @@ -152,12 +159,15 @@ export type WithPickedPropertiesDecorator = ( context: DecoratorContext, target: Model, pick: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Returns the model with any default values removed. */ -export type WithoutDefaultValuesDecorator = (context: DecoratorContext, target: Model) => void; +export type WithoutDefaultValuesDecorator = ( + context: DecoratorContext, + target: Model, +) => DecoratorValidatorCallbacks | void; /** * Set the visibility of key properties in a model if not already set. @@ -178,7 +188,7 @@ export type WithDefaultKeyVisibilityDecorator = ( context: DecoratorContext, target: Model, visibility: EnumValue, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Typically a short, single-line description. @@ -190,7 +200,11 @@ export type WithDefaultKeyVisibilityDecorator = ( * model Pet {} * ``` */ -export type SummaryDecorator = (context: DecoratorContext, target: Type, summary: string) => void; +export type SummaryDecorator = ( + context: DecoratorContext, + target: Type, + summary: string, +) => DecoratorValidatorCallbacks | void; /** * Attach a documentation string to describe the successful return types of an operation. @@ -207,7 +221,7 @@ export type ReturnsDocDecorator = ( context: DecoratorContext, target: Operation, doc: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Attach a documentation string to describe the error return types of an operation. @@ -224,7 +238,7 @@ export type ErrorsDocDecorator = ( context: DecoratorContext, target: Operation, doc: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Mark this namespace as describing a service and configure service properties. @@ -245,7 +259,7 @@ export type ServiceDecorator = ( context: DecoratorContext, target: Namespace, options?: ServiceOptions, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that this model is an error type. Operations return error types when the operation has failed. @@ -259,7 +273,10 @@ export type ServiceDecorator = ( * } * ``` */ -export type ErrorDecorator = (context: DecoratorContext, target: Model) => void; +export type ErrorDecorator = ( + context: DecoratorContext, + target: Model, +) => DecoratorValidatorCallbacks | void; /** * Specify a known data format hint for this string type. For example `uuid`, `uri`, etc. @@ -277,7 +294,7 @@ export type FormatDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, format: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the the pattern this string should respect using simple regular expression syntax. @@ -302,7 +319,7 @@ export type PatternDecorator = ( target: Scalar | ModelProperty, pattern: string, validationMessage?: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the minimum length this string type should be. @@ -318,7 +335,7 @@ export type MinLengthDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, value: Numeric, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the maximum length this string type should be. @@ -334,7 +351,7 @@ export type MaxLengthDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, value: Numeric, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the minimum number of items this array should have. @@ -350,7 +367,7 @@ export type MinItemsDecorator = ( context: DecoratorContext, target: Type | ModelProperty, value: Numeric, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the maximum number of items this array should have. @@ -366,7 +383,7 @@ export type MaxItemsDecorator = ( context: DecoratorContext, target: Type | ModelProperty, value: Numeric, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the minimum value this numeric type should be. @@ -382,7 +399,7 @@ export type MinValueDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, value: Numeric | ScalarValue | ScalarValue | ScalarValue | ScalarValue | ScalarValue, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the maximum value this numeric type should be. @@ -398,7 +415,7 @@ export type MaxValueDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, value: Numeric | ScalarValue | ScalarValue | ScalarValue | ScalarValue | ScalarValue, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the minimum value this numeric type should be, exclusive of the given @@ -415,7 +432,7 @@ export type MinValueExclusiveDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, value: Numeric | ScalarValue | ScalarValue | ScalarValue | ScalarValue | ScalarValue, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the maximum value this numeric type should be, exclusive of the given @@ -432,7 +449,7 @@ export type MaxValueExclusiveDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, value: Numeric | ScalarValue | ScalarValue | ScalarValue | ScalarValue | ScalarValue, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Mark this value as a secret value that should be treated carefully to avoid exposure @@ -446,7 +463,7 @@ export type MaxValueExclusiveDecorator = ( export type SecretDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty | Model | Union | Enum, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Attaches a tag to an operation, interface, or namespace. Multiple `@tag` decorators can be specified to attach multiple tags to a TypeSpec element. @@ -457,7 +474,7 @@ export type TagDecorator = ( context: DecoratorContext, target: Namespace | Interface | Operation, tag: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specifies how a templated type should name their instances. @@ -478,7 +495,7 @@ export type FriendlyNameDecorator = ( target: Type, name: string, formatArgs?: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Mark a model property as the key to identify instances of that type @@ -495,7 +512,7 @@ export type KeyDecorator = ( context: DecoratorContext, target: ModelProperty, altName?: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify this operation is an overload of the given operation. @@ -514,7 +531,7 @@ export type OverloadDecorator = ( context: DecoratorContext, target: Operation, overloadbase: Operation, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Provide an alternative name for this type when serialized to the given mime type. @@ -541,7 +558,7 @@ export type EncodedNameDecorator = ( target: Type, mimeType: string, name: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that this union is discriminated. @@ -603,7 +620,7 @@ export type DiscriminatedDecorator = ( context: DecoratorContext, target: Union, options?: DiscriminatedOptions, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the property to be used to discriminate this type. @@ -622,7 +639,7 @@ export type DiscriminatorDecorator = ( context: DecoratorContext, target: Model, propertyName: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Provide an example value for a data type. @@ -643,7 +660,7 @@ export type ExampleDecorator = ( target: Model | Enum | Scalar | Union | ModelProperty | UnionVariant, example: unknown, options?: ExampleOptions, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Provide example values for an operation's parameters and corresponding return type. @@ -661,12 +678,15 @@ export type OpExampleDecorator = ( target: Operation, example: OperationExample, options?: ExampleOptions, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Mark this operation as a `list` operation that returns a paginated list of items. */ -export type ListDecorator = (context: DecoratorContext, target: Operation) => void; +export type ListDecorator = ( + context: DecoratorContext, + target: Operation, +) => DecoratorValidatorCallbacks | void; /** * Pagination property defining the number of items to skip. @@ -679,7 +699,10 @@ export type ListDecorator = (context: DecoratorContext, target: Operation) => vo * @list op listPets(@offset skip: int32, @pageSize pageSize: int8): Page; * ``` */ -export type OffsetDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type OffsetDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Pagination property defining the page index. @@ -692,7 +715,10 @@ export type OffsetDecorator = (context: DecoratorContext, target: ModelProperty) * @list op listPets(@pageIndex page: int32, @pageSize pageSize: int8): Page; * ``` */ -export type PageIndexDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type PageIndexDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Specify the pagination parameter that controls the maximum number of items to include in a page. @@ -705,7 +731,10 @@ export type PageIndexDecorator = (context: DecoratorContext, target: ModelProper * @list op listPets(@pageIndex page: int32, @pageSize pageSize: int8): Page; * ``` */ -export type PageSizeDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type PageSizeDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Specify the the property that contains the array of page items. @@ -718,7 +747,10 @@ export type PageSizeDecorator = (context: DecoratorContext, target: ModelPropert * @list op listPets(@pageIndex page: int32, @pageSize pageSize: int8): Page; * ``` */ -export type PageItemsDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type PageItemsDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Pagination property defining the token to get to the next page. @@ -733,7 +765,10 @@ export type PageItemsDecorator = (context: DecoratorContext, target: ModelProper * @list op listPets(@continuationToken continuationToken: string): Page; * ``` */ -export type ContinuationTokenDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type ContinuationTokenDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Pagination property defining a link to the next page. @@ -752,7 +787,10 @@ export type ContinuationTokenDecorator = (context: DecoratorContext, target: Mod * @list op listPets(): Page; * ``` */ -export type NextLinkDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type NextLinkDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Pagination property defining a link to the previous page. @@ -771,7 +809,10 @@ export type NextLinkDecorator = (context: DecoratorContext, target: ModelPropert * @list op listPets(): Page; * ``` */ -export type PrevLinkDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type PrevLinkDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Pagination property defining a link to the first page. @@ -790,7 +831,10 @@ export type PrevLinkDecorator = (context: DecoratorContext, target: ModelPropert * @list op listPets(): Page; * ``` */ -export type FirstLinkDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type FirstLinkDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Pagination property defining a link to the last page. @@ -809,14 +853,21 @@ export type FirstLinkDecorator = (context: DecoratorContext, target: ModelProper * @list op listPets(): Page; * ``` */ -export type LastLinkDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type LastLinkDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * A debugging decorator used to inspect a type. * * @param text Custom text to log */ -export type InspectTypeDecorator = (context: DecoratorContext, target: Type, text: string) => void; +export type InspectTypeDecorator = ( + context: DecoratorContext, + target: Type, + text: string, +) => DecoratorValidatorCallbacks | void; /** * A debugging decorator used to inspect a type name. @@ -827,7 +878,7 @@ export type InspectTypeNameDecorator = ( context: DecoratorContext, target: Type, text: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Sets the visibility modifiers that are active on a property, indicating that it is only considered to be present @@ -879,7 +930,7 @@ export type VisibilityDecorator = ( context: DecoratorContext, target: ModelProperty, ...visibilities: EnumValue[] -) => void; +) => DecoratorValidatorCallbacks | void; /** * Indicates that a property is not visible in the given visibility class. @@ -901,7 +952,7 @@ export type InvisibleDecorator = ( context: DecoratorContext, target: ModelProperty, visibilityClass: Enum, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Removes visibility modifiers from a property. @@ -926,7 +977,7 @@ export type RemoveVisibilityDecorator = ( context: DecoratorContext, target: ModelProperty, ...visibilities: EnumValue[] -) => void; +) => DecoratorValidatorCallbacks | void; /** * Removes properties that do not have at least one of the given visibility modifiers @@ -975,7 +1026,7 @@ export type WithVisibilityDecorator = ( context: DecoratorContext, target: Model, ...visibilities: EnumValue[] -) => void; +) => DecoratorValidatorCallbacks | void; /** * Declares the visibility constraint of the parameters of a given operation. @@ -991,7 +1042,7 @@ export type ParameterVisibilityDecorator = ( context: DecoratorContext, target: Operation, ...visibilities: EnumValue[] -) => void; +) => DecoratorValidatorCallbacks | void; /** * Declares the visibility constraint of the return type of a given operation. @@ -1007,7 +1058,7 @@ export type ReturnTypeVisibilityDecorator = ( context: DecoratorContext, target: Operation, ...visibilities: EnumValue[] -) => void; +) => DecoratorValidatorCallbacks | void; /** * Declares the default visibility modifiers for a visibility class. @@ -1023,7 +1074,7 @@ export type DefaultVisibilityDecorator = ( context: DecoratorContext, target: Enum, ...visibilities: EnumValue[] -) => void; +) => DecoratorValidatorCallbacks | void; /** * Applies the given visibility filter to the properties of the target model. @@ -1058,7 +1109,7 @@ export type WithVisibilityFilterDecorator = ( target: Model, filter: VisibilityFilter, nameTemplate?: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Transforms the `target` model to include only properties that are visible during the @@ -1096,7 +1147,7 @@ export type WithLifecycleUpdateDecorator = ( context: DecoratorContext, target: Model, nameTemplate?: string, -) => void; +) => DecoratorValidatorCallbacks | void; export type TypeSpecDecorators = { mediaTypeHint: MediaTypeHintDecorator; diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index b39e55cd85f..27fc653172f 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -56,6 +56,7 @@ import { DecoratorContext, DecoratorDeclarationStatementNode, DecoratorExpressionNode, + DecoratorValidatorCallbacks, Diagnostic, DiagnosticTarget, DocContent, @@ -156,6 +157,7 @@ import { UnionVariantNode, UnknownType, UsingStatementNode, + ValidatorFn, Value, ValueWithTemplate, VoidType, @@ -368,6 +370,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: ValidatorFn[] = []; const typespecNamespaceBinding = resolver.symbols.global.exports!.get("TypeSpec"); if (typespecNamespaceBinding) { @@ -3485,6 +3488,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker internalDecoratorValidation(); assertNoPendingResolutions(); + runPostValidators(postCheckValidators); } function assertNoPendingResolutions() { @@ -5955,19 +5959,42 @@ export function createChecker(program: Program, resolver: NameResolver): Checker stats.finishedTypes++; if (!options.skipDecorators) { + let postSelfValidators: ValidatorFn[] = []; if ("decorators" in typeDef) { - for (const decApp of typeDef.decorators) { - applyDecoratorToType(program, decApp, typeDef); - } + postSelfValidators = applyDecoratorsToType(typeDef); } typeDef.isFinished = true; Object.setPrototypeOf(typeDef, typePrototype); + runPostValidators(postSelfValidators); } markAsChecked(typeDef); return typeDef; } + function applyDecoratorsToType( + typeDef: Type & { decorators: DecoratorApplication[] }, + ): ValidatorFn[] { + const postSelfValidators: ValidatorFn[] = []; + for (const decApp of typeDef.decorators) { + const validators = applyDecoratorToType(program, decApp, typeDef); + if (validators?.onTargetFinish) { + postSelfValidators.push(validators.onTargetFinish); + } + if (validators?.onGraphFinish) { + postCheckValidators.push(validators.onGraphFinish); + } + } + return postSelfValidators; + } + + /** Run a list of post validator */ + function runPostValidators(validators: ValidatorFn[]) { + for (const validator of validators) { + program.reportDiagnostics(validator()); + } + } + function markAsChecked(type: T) { if (!type.creating) return; delete type.creating; @@ -6669,7 +6696,11 @@ function reportDeprecation( } } -function applyDecoratorToType(program: Program, decApp: DecoratorApplication, target: Type) { +function applyDecoratorToType( + program: Program, + decApp: DecoratorApplication, + target: Type, +): DecoratorValidatorCallbacks | void { compilerAssert("decorators" in target, "Cannot apply decorator to non-decoratable type", target); for (const arg of decApp.args) { @@ -6697,7 +6728,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) { diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index c0eeefb81fd..d5038ab0343 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -147,7 +147,9 @@ interface EmitterRef { interface Validator { metadata: LibraryMetadata; - callback: (program: Program) => void | Promise; + callback: ( + program: Program, + ) => void | readonly Diagnostic[] | Promise | Promise; } interface TypeSpecLibraryReference { @@ -638,7 +640,10 @@ async function createProgram( runtimeStats.validation.validators.compiler = start.end(); for (const validator of validateCbs) { const start = startTimer(); - await runValidator(validator); + const diagnostics = await runValidator(validator); + if (diagnostics && Array.isArray(diagnostics)) { + program.reportDiagnostics(diagnostics); + } runtimeStats.validation.validators[validator.metadata.name ?? ""] = start.end(); } runtimeStats.validation.total = start.end(); @@ -646,7 +651,7 @@ async function createProgram( async function runValidator(validator: Validator) { try { - await validator.callback(program); + return await validator.callback(program); } catch (error: any) { if (options.designTimeBuild) { program.reportDiagnostic( diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 7dd56a806a2..7a5c95746f3 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -48,11 +48,35 @@ 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[] + ): DecoratorValidatorCallbacks | void; namespace?: string; } +export type ValidatorFn = () => readonly Diagnostic[]; + +export interface DecoratorValidatorCallbacks { + /** + * Run validation 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. + * @note This is meant for validation which means the type graph should be treated as readonly in this function. + */ + readonly onTargetFinish?: ValidatorFn; + + /** + * Run validation after everything is checked in the type graph. Useful when trying to get an overall view of the program. + * @note This is meant for validation which means the type graph should be treated as readonly in this function. + */ + readonly onGraphFinish?: ValidatorFn; +} + export interface BaseType { readonly entityKind: "Type"; kind: string; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 55ae15c153d..1bee9b67ed8 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -322,6 +322,7 @@ export type { DecoratorContext, DecoratorFunction, DecoratorImplementations, + DecoratorValidatorCallbacks, DeprecatedDirective, Diagnostic, DiagnosticCreator, diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 0841d5ec379..bc5c2dd0e1a 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -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, @@ -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; @@ -546,3 +550,86 @@ 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 { + onTargetFinish: () => { + 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 { + onGraphFinish: () => { + 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)`, + ]); + }); +}); diff --git a/packages/events/generated-defs/TypeSpec.Events.ts b/packages/events/generated-defs/TypeSpec.Events.ts index 00f7e0c222c..7a4e9bf094d 100644 --- a/packages/events/generated-defs/TypeSpec.Events.ts +++ b/packages/events/generated-defs/TypeSpec.Events.ts @@ -1,4 +1,10 @@ -import type { DecoratorContext, ModelProperty, Union, UnionVariant } from "@typespec/compiler"; +import type { + DecoratorContext, + DecoratorValidatorCallbacks, + ModelProperty, + Union, + UnionVariant, +} from "@typespec/compiler"; /** * Specify that this union describes a set of events. @@ -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, +) => DecoratorValidatorCallbacks | void; /** * Specifies the content type of the event envelope, event body, or event payload. @@ -41,7 +50,7 @@ export type ContentTypeDecorator = ( context: DecoratorContext, target: UnionVariant | ModelProperty, contentType: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Identifies the payload of an event. @@ -54,7 +63,10 @@ export type ContentTypeDecorator = ( * } * ``` */ -export type DataDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type DataDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; export type TypeSpecEventsDecorators = { events: EventsDecorator; diff --git a/packages/http-client/generated-defs/TypeSpec.HttpClient.ts b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts index d6001ffcbe3..15824d985f4 100644 --- a/packages/http-client/generated-defs/TypeSpec.HttpClient.ts +++ b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts @@ -1,4 +1,4 @@ -import type { DecoratorContext, Type } from "@typespec/compiler"; +import type { DecoratorContext, DecoratorValidatorCallbacks, Type } from "@typespec/compiler"; export interface FeatureLifecycleOptions { readonly emitterScope?: string; @@ -8,7 +8,7 @@ export type ExperimentalDecorator = ( context: DecoratorContext, target: Type, options?: FeatureLifecycleOptions, -) => void; +) => DecoratorValidatorCallbacks | void; export type TypeSpecHttpClientDecorators = { experimental: ExperimentalDecorator; diff --git a/packages/http/generated-defs/TypeSpec.Http.Private.ts b/packages/http/generated-defs/TypeSpec.Http.Private.ts index ec331e57121..489b6a81a32 100644 --- a/packages/http/generated-defs/TypeSpec.Http.Private.ts +++ b/packages/http/generated-defs/TypeSpec.Http.Private.ts @@ -1,4 +1,10 @@ -import type { DecoratorContext, Model, ModelProperty, Type } from "@typespec/compiler"; +import type { + DecoratorContext, + DecoratorValidatorCallbacks, + Model, + ModelProperty, + Type, +} from "@typespec/compiler"; export interface HttpPartOptions { readonly name?: string; @@ -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, +) => DecoratorValidatorCallbacks | void; -export type HttpFileDecorator = (context: DecoratorContext, target: Model) => void; +export type HttpFileDecorator = ( + context: DecoratorContext, + target: Model, +) => DecoratorValidatorCallbacks | void; export type HttpPartDecorator = ( context: DecoratorContext, target: Model, type: Type, options: HttpPartOptions, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Performs the canonical merge-patch transformation on the given model and injects its @@ -29,7 +41,7 @@ export type ApplyMergePatchDecorator = ( source: Model, nameTemplate: string, options: ApplyMergePatchOptions, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify if inapplicable metadata should be included in the payload for the given entity. @@ -40,7 +52,7 @@ export type IncludeInapplicableMetadataInPayloadDecorator = ( context: DecoratorContext, target: Type, value: boolean, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Marks a model that was generated by applying the MergePatch @@ -50,7 +62,7 @@ export type MergePatchModelDecorator = ( context: DecoratorContext, target: Model, source: Model, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Links a modelProperty mutated as part of a mergePatch transform to @@ -60,7 +72,7 @@ export type MergePatchPropertyDecorator = ( context: DecoratorContext, target: ModelProperty, source: ModelProperty, -) => void; +) => DecoratorValidatorCallbacks | void; export type TypeSpecHttpPrivateDecorators = { plainData: PlainDataDecorator; diff --git a/packages/http/generated-defs/TypeSpec.Http.ts b/packages/http/generated-defs/TypeSpec.Http.ts index 7e0a318f61d..025e8a09e56 100644 --- a/packages/http/generated-defs/TypeSpec.Http.ts +++ b/packages/http/generated-defs/TypeSpec.Http.ts @@ -1,5 +1,6 @@ import type { DecoratorContext, + DecoratorValidatorCallbacks, Interface, ModelProperty, Namespace, @@ -46,7 +47,10 @@ export interface PatchOptions { * }; * ``` */ -export type StatusCodeDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type StatusCodeDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Explicitly specify that this property type will be exactly the HTTP body. @@ -60,7 +64,10 @@ export type StatusCodeDecorator = (context: DecoratorContext, target: ModelPrope * op download(): {@body image: bytes}; * ``` */ -export type BodyDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type BodyDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Specify this property is to be sent or received as an HTTP header. @@ -83,7 +90,7 @@ export type HeaderDecorator = ( context: DecoratorContext, target: ModelProperty, headerNameOrOptions?: string | HeaderOptions, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify this property is to be sent or received in the cookie. @@ -106,7 +113,7 @@ export type CookieDecorator = ( context: DecoratorContext, target: ModelProperty, cookieNameOrOptions?: string | CookieOptions, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify this property is to be sent as a query parameter. @@ -122,7 +129,7 @@ export type QueryDecorator = ( context: DecoratorContext, target: ModelProperty, queryNameOrOptions?: string | QueryOptions, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Explicitly specify that this property is to be interpolated as a path parameter. @@ -138,7 +145,7 @@ export type PathDecorator = ( context: DecoratorContext, target: ModelProperty, paramNameOrOptions?: string | PathOptions, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that the body resolution should be resolved from that property. @@ -151,7 +158,10 @@ export type PathDecorator = ( * op download(): {@bodyRoot user: {name: string, @header id: string}}; * ``` */ -export type BodyRootDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type BodyRootDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Specify that this property shouldn't be included in the HTTP body. @@ -162,7 +172,10 @@ export type BodyRootDecorator = (context: DecoratorContext, target: ModelPropert * op upload(name: string, @bodyIgnore headers: {@header id: string}): void; * ``` */ -export type BodyIgnoreDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type BodyIgnoreDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * @@ -179,7 +192,10 @@ export type BodyIgnoreDecorator = (context: DecoratorContext, target: ModelPrope * ): void; * ``` */ -export type MultipartBodyDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type MultipartBodyDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Specify the HTTP verb for the target operation to be `GET`. @@ -189,7 +205,10 @@ export type MultipartBodyDecorator = (context: DecoratorContext, target: ModelPr * @get op read(): string * ``` */ -export type GetDecorator = (context: DecoratorContext, target: Operation) => void; +export type GetDecorator = ( + context: DecoratorContext, + target: Operation, +) => DecoratorValidatorCallbacks | void; /** * Specify the HTTP verb for the target operation to be `PUT`. @@ -199,7 +218,10 @@ export type GetDecorator = (context: DecoratorContext, target: Operation) => voi * @put op set(pet: Pet): void * ``` */ -export type PutDecorator = (context: DecoratorContext, target: Operation) => void; +export type PutDecorator = ( + context: DecoratorContext, + target: Operation, +) => DecoratorValidatorCallbacks | void; /** * Specify the HTTP verb for the target operation to be `POST`. @@ -209,7 +231,10 @@ export type PutDecorator = (context: DecoratorContext, target: Operation) => voi * @post op create(pet: Pet): void * ``` */ -export type PostDecorator = (context: DecoratorContext, target: Operation) => void; +export type PostDecorator = ( + context: DecoratorContext, + target: Operation, +) => DecoratorValidatorCallbacks | void; /** * Specify the HTTP verb for the target operation to be `PATCH`. @@ -231,7 +256,7 @@ export type PatchDecorator = ( context: DecoratorContext, target: Operation, options?: PatchOptions, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the HTTP verb for the target operation to be `DELETE`. @@ -241,7 +266,10 @@ export type PatchDecorator = ( * @delete op set(petId: string): void * ``` */ -export type DeleteDecorator = (context: DecoratorContext, target: Operation) => void; +export type DeleteDecorator = ( + context: DecoratorContext, + target: Operation, +) => DecoratorValidatorCallbacks | void; /** * Specify the HTTP verb for the target operation to be `HEAD`. @@ -251,7 +279,10 @@ export type DeleteDecorator = (context: DecoratorContext, target: Operation) => * @head op ping(petId: string): void * ``` */ -export type HeadDecorator = (context: DecoratorContext, target: Operation) => void; +export type HeadDecorator = ( + context: DecoratorContext, + target: Operation, +) => DecoratorValidatorCallbacks | void; /** * Specify an endpoint for this service. Multiple `@server` decorators can be used to specify multiple endpoints. @@ -296,7 +327,7 @@ export type ServerDecorator = ( url: string, description?: string, parameters?: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify authentication for a whole service or specific methods. See the [documentation in the Http library](https://typespec.io/docs/libraries/http/authentication) for full details. @@ -313,7 +344,7 @@ export type UseAuthDecorator = ( context: DecoratorContext, target: Namespace | Interface | Operation, auth: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Defines the relative route URI template for the target operation as defined by [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.3) @@ -340,7 +371,7 @@ export type RouteDecorator = ( context: DecoratorContext, target: Namespace | Interface | Operation, path: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * `@sharedRoute` marks the operation as sharing a route path with other operations. @@ -356,7 +387,10 @@ export type RouteDecorator = ( * op getWidget(@path id: string): Widget; * ``` */ -export type SharedRouteDecorator = (context: DecoratorContext, target: Operation) => void; +export type SharedRouteDecorator = ( + context: DecoratorContext, + target: Operation, +) => DecoratorValidatorCallbacks | void; export type TypeSpecHttpDecorators = { statusCode: StatusCodeDecorator; diff --git a/packages/http/src/private.decorators.ts b/packages/http/src/private.decorators.ts index aa0bb3aa9ed..c045cd3235e 100644 --- a/packages/http/src/private.decorators.ts +++ b/packages/http/src/private.decorators.ts @@ -20,6 +20,16 @@ import { HttpPartOptions, PlainDataDecorator, } from "../generated-defs/TypeSpec.Http.Private.js"; +import { + getCookieParamOptions, + getHeaderFieldOptions, + getPathOptions, + getQueryOptions, + isBody, + isBodyRoot, + isMultipartBodyProperty, + isStatusCode, +} from "./decorators.js"; import { HttpStateKeys, reportDiagnostic } from "./lib.js"; import { HttpOperationFileBody } from "./types.js"; @@ -151,8 +161,72 @@ export const $httpFile: HttpFileDecorator = (context: DecoratorContext, target: return undefined; } + + return { + onGraphFinish: () => { + validateHttpFileModel(context.program, target); + return []; + }, + }; }; +function validateHttpFileModel(program: Program, model: Model) { + for (const prop of model.properties.values()) { + switch (prop.name) { + case "contentType": + case "contents": { + // Check if these properties have any HTTP metadata and if so, report an error + const annotations = { + header: getHeaderFieldOptions(program, prop), + cookie: getCookieParamOptions(program, prop), + query: getQueryOptions(program, prop), + path: getPathOptions(program, prop), + body: isBody(program, prop), + bodyRoot: isBodyRoot(program, prop), + multipartBody: isMultipartBodyProperty(program, prop), + statusCode: isStatusCode(program, prop), + }; + + reportDisallowed(prop, annotations); + break; + } + case "filename": { + const annotations = { + body: isBody(program, prop), + bodyRoot: isBodyRoot(program, prop), + multipartBody: isMultipartBodyProperty(program, prop), + statusCode: isStatusCode(program, prop), + cookie: getCookieParamOptions(program, prop), + }; + + reportDisallowed(prop, annotations); + break; + } + default: + reportDiagnostic(program, { + code: "http-file-extra-property", + format: { propName: prop.name }, + target: prop, + }); + } + } + for (const child of model.derivedModels) { + validateHttpFileModel(program, child); + } + + function reportDisallowed(target: ModelProperty, annotations: Record) { + const metadataEntries = Object.entries(annotations).filter((e) => !!e[1]); + + for (const [metadataType] of metadataEntries) { + reportDiagnostic(program, { + code: "http-file-disallowed-metadata", + format: { propName: target.name, metadataType }, + target: target, + }); + } + } +} + /** * Check if the given type is an `HttpFile` */ diff --git a/packages/http/src/validate.ts b/packages/http/src/validate.ts index 340356a2594..ea9d2c76500 100644 --- a/packages/http/src/validate.ts +++ b/packages/http/src/validate.ts @@ -1,16 +1,6 @@ -import type { Model, ModelProperty, Program } from "@typespec/compiler"; -import { - getCookieParamOptions, - getHeaderFieldOptions, - getPathOptions, - getQueryOptions, - isBody, - isBodyRoot, - isMultipartBodyProperty, - isStatusCode, -} from "./decorators.js"; +import type { Program } from "@typespec/compiler"; import { isSharedRoute } from "./decorators/shared-route.js"; -import { HttpStateKeys, reportDiagnostic } from "./lib.js"; +import { reportDiagnostic } from "./lib.js"; import { getAllHttpServices } from "./operations.js"; import { HttpOperation, HttpService } from "./types.js"; @@ -21,74 +11,6 @@ export function $onValidate(program: Program) { program.reportDiagnostics(diagnostics); } validateSharedRouteConsistency(program, services); - validateHttpFiles(program); -} - -function validateHttpFiles(program: Program) { - const httpFiles = [...program.stateSet(HttpStateKeys.file)]; - - for (const model of httpFiles) { - if (model.kind === "Model") { - validateHttpFileModel(program, model); - } - } -} - -function validateHttpFileModel(program: Program, model: Model) { - for (const prop of model.properties.values()) { - switch (prop.name) { - case "contentType": - case "contents": { - // Check if these properties have any HTTP metadata and if so, report an error - const annotations = { - header: getHeaderFieldOptions(program, prop), - cookie: getCookieParamOptions(program, prop), - query: getQueryOptions(program, prop), - path: getPathOptions(program, prop), - body: isBody(program, prop), - bodyRoot: isBodyRoot(program, prop), - multipartBody: isMultipartBodyProperty(program, prop), - statusCode: isStatusCode(program, prop), - }; - - reportDisallowed(prop, annotations); - break; - } - case "filename": { - const annotations = { - body: isBody(program, prop), - bodyRoot: isBodyRoot(program, prop), - multipartBody: isMultipartBodyProperty(program, prop), - statusCode: isStatusCode(program, prop), - cookie: getCookieParamOptions(program, prop), - }; - - reportDisallowed(prop, annotations); - break; - } - default: - reportDiagnostic(program, { - code: "http-file-extra-property", - format: { propName: prop.name }, - target: prop, - }); - } - } - for (const child of model.derivedModels) { - validateHttpFileModel(program, child); - } - - function reportDisallowed(target: ModelProperty, annotations: Record) { - const metadataEntries = Object.entries(annotations).filter((e) => !!e[1]); - - for (const [metadataType] of metadataEntries) { - reportDiagnostic(program, { - code: "http-file-disallowed-metadata", - format: { propName: target.name, metadataType }, - target: target, - }); - } - } } function groupHttpOperations( diff --git a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.Private.ts b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.Private.ts index 2c4f69bbc62..29591df51c8 100644 --- a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.Private.ts +++ b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.Private.ts @@ -1,10 +1,15 @@ -import type { DecoratorContext, Model, Type } from "@typespec/compiler"; +import type { + DecoratorContext, + DecoratorValidatorCallbacks, + Model, + Type, +} from "@typespec/compiler"; export type ValidatesRawJsonDecorator = ( context: DecoratorContext, target: Model, value: Type, -) => void; +) => DecoratorValidatorCallbacks | void; export type TypeSpecJsonSchemaPrivateDecorators = { validatesRawJson: ValidatesRawJsonDecorator; diff --git a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts index ee71c6f53a6..cbf82755d92 100644 --- a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts +++ b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts @@ -1,5 +1,6 @@ import type { DecoratorContext, + DecoratorValidatorCallbacks, ModelProperty, Namespace, Numeric, @@ -21,7 +22,7 @@ export type JsonSchemaDecorator = ( context: DecoratorContext, target: Type, baseUri?: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Set the base URI for any schemas emitted from types within this namespace. @@ -32,7 +33,7 @@ export type BaseUriDecorator = ( context: DecoratorContext, target: Namespace, baseUri: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the JSON Schema id. If this model or a parent namespace has a base URI, @@ -42,12 +43,19 @@ export type BaseUriDecorator = ( * * @param id The id of the JSON schema for this declaration. */ -export type IdDecorator = (context: DecoratorContext, target: Type, id: string) => void; +export type IdDecorator = ( + context: DecoratorContext, + target: Type, + id: string, +) => DecoratorValidatorCallbacks | void; /** * Specify that `oneOf` should be used instead of `anyOf` for that union. */ -export type OneOfDecorator = (context: DecoratorContext, target: Union | ModelProperty) => void; +export type OneOfDecorator = ( + context: DecoratorContext, + target: Union | ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Specify that the numeric type must be a multiple of some numeric value. @@ -58,7 +66,7 @@ export type MultipleOfDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, value: Numeric, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that the array must contain at least one instance of the provided type. @@ -70,7 +78,7 @@ export type ContainsDecorator = ( context: DecoratorContext, target: Type | ModelProperty, value: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Used in conjunction with the `@contains` decorator, @@ -82,7 +90,7 @@ export type MinContainsDecorator = ( context: DecoratorContext, target: Type | ModelProperty, value: number, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Used in conjunction with the `@contains` decorator, @@ -94,7 +102,7 @@ export type MaxContainsDecorator = ( context: DecoratorContext, target: Type | ModelProperty, value: number, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that every item in the array must be unique. @@ -102,7 +110,7 @@ export type MaxContainsDecorator = ( export type UniqueItemsDecorator = ( context: DecoratorContext, target: Type | ModelProperty, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the minimum number of properties this object can have. @@ -113,7 +121,7 @@ export type MinPropertiesDecorator = ( context: DecoratorContext, target: Type | ModelProperty, value: number, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the maximum number of properties this object can have. @@ -124,7 +132,7 @@ export type MaxPropertiesDecorator = ( context: DecoratorContext, target: Type | ModelProperty, value: number, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the encoding used for the contents of a string. @@ -137,7 +145,7 @@ export type ContentEncodingDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, value: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that the target array must begin with the provided types. @@ -148,7 +156,7 @@ export type PrefixItemsDecorator = ( context: DecoratorContext, target: Type | ModelProperty, value: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the content type of content stored in a string. @@ -159,7 +167,7 @@ export type ContentMediaTypeDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, value: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify the schema for the contents of a string when interpreted according to the content's @@ -171,7 +179,7 @@ export type ContentSchemaDecorator = ( context: DecoratorContext, target: Scalar | ModelProperty, value: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify a custom property to add to the emitted schema. This is useful for adding custom keywords @@ -197,7 +205,7 @@ export type ExtensionDecorator = ( target: Type, key: string, value: Type | unknown, -) => void; +) => DecoratorValidatorCallbacks | void; export type TypeSpecJsonSchemaDecorators = { jsonSchema: JsonSchemaDecorator; diff --git a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts index b032c2f6889..b04fb4dec2b 100644 --- a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts +++ b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts @@ -1,4 +1,11 @@ -import type { DecoratorContext, Model, Namespace, Operation, Type } from "@typespec/compiler"; +import type { + DecoratorContext, + DecoratorValidatorCallbacks, + Model, + Namespace, + Operation, + Type, +} from "@typespec/compiler"; export interface AdditionalInfo { readonly [key: string]: unknown; @@ -49,7 +56,7 @@ export type OperationIdDecorator = ( context: DecoratorContext, target: Operation, operationId: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Attach some custom data to the OpenAPI element generated from this type. @@ -68,7 +75,7 @@ export type ExtensionDecorator = ( target: Type, key: string, value: unknown, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that this model is to be treated as the OpenAPI `default` response. @@ -82,7 +89,10 @@ export type ExtensionDecorator = ( * op listPets(): Pet[] | PetStoreResponse; * ``` */ -export type DefaultResponseDecorator = (context: DecoratorContext, target: Model) => void; +export type DefaultResponseDecorator = ( + context: DecoratorContext, + target: Model, +) => DecoratorValidatorCallbacks | void; /** * Specify the OpenAPI `externalDocs` property for this type. @@ -100,7 +110,7 @@ export type ExternalDocsDecorator = ( target: Type, url: string, description?: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify OpenAPI additional information. @@ -112,7 +122,7 @@ export type InfoDecorator = ( context: DecoratorContext, target: Namespace, additionalInfo: AdditionalInfo, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify OpenAPI additional information. @@ -131,7 +141,7 @@ export type TagMetadataDecorator = ( target: Namespace, name: string, tagMetadata: TagMetadata, -) => void; +) => DecoratorValidatorCallbacks | void; export type TypeSpecOpenAPIDecorators = { operationId: OperationIdDecorator; diff --git a/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts b/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts index 1a9d7dc6755..6d1f7bdfb43 100644 --- a/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts +++ b/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts @@ -1,9 +1,18 @@ -import type { DecoratorContext, Model, ModelProperty, Union } from "@typespec/compiler"; +import type { + DecoratorContext, + DecoratorValidatorCallbacks, + Model, + ModelProperty, + Union, +} from "@typespec/compiler"; /** * Specify that `oneOf` should be used instead of `anyOf` for that union. */ -export type OneOfDecorator = (context: DecoratorContext, target: Union | ModelProperty) => void; +export type OneOfDecorator = ( + context: DecoratorContext, + target: Union | ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Specify an external reference that should be used inside of emitting this type. @@ -14,7 +23,7 @@ export type UseRefDecorator = ( context: DecoratorContext, target: Model | ModelProperty, ref: string, -) => void; +) => DecoratorValidatorCallbacks | void; export type TypeSpecOpenAPIDecorators = { oneOf: OneOfDecorator; diff --git a/packages/protobuf/generated-defs/TypeSpec.Protobuf.Private.ts b/packages/protobuf/generated-defs/TypeSpec.Protobuf.Private.ts index 9e0312b7668..402d545b5ee 100644 --- a/packages/protobuf/generated-defs/TypeSpec.Protobuf.Private.ts +++ b/packages/protobuf/generated-defs/TypeSpec.Protobuf.Private.ts @@ -1,13 +1,21 @@ -import type { DecoratorContext, Model, Type } from "@typespec/compiler"; +import type { + DecoratorContext, + DecoratorValidatorCallbacks, + Model, + Type, +} from "@typespec/compiler"; export type ExternRefDecorator = ( context: DecoratorContext, target: Model, path: Type, name: Type, -) => void; +) => DecoratorValidatorCallbacks | void; -export type _mapDecorator = (context: DecoratorContext, target: Model) => void; +export type _mapDecorator = ( + context: DecoratorContext, + target: Model, +) => DecoratorValidatorCallbacks | void; export type TypeSpecProtobufPrivateDecorators = { externRef: ExternRefDecorator; diff --git a/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts b/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts index 0e59e7977b2..a0a5160e184 100644 --- a/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts +++ b/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts @@ -1,5 +1,6 @@ import type { DecoratorContext, + DecoratorValidatorCallbacks, Interface, ModelProperty, Namespace, @@ -17,7 +18,10 @@ import type { * * This decorator will force the emitter to check and emit a model. */ -export type MessageDecorator = (context: DecoratorContext, target: Type) => void; +export type MessageDecorator = ( + context: DecoratorContext, + target: Type, +) => DecoratorValidatorCallbacks | void; /** * Defines the field index of a model property for conversion to a Protobuf @@ -52,7 +56,7 @@ export type FieldDecorator = ( context: DecoratorContext, target: ModelProperty, index: number, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Reserve a field index, range, or name. If a field definition collides with a reservation, the emitter will produce @@ -90,13 +94,16 @@ export type ReserveDecorator = ( context: DecoratorContext, target: Type, ...reservations: (string | unknown | number)[] -) => void; +) => DecoratorValidatorCallbacks | void; /** * Declares that a TypeSpec interface constitutes a Protobuf service. The contents of the interface will be converted to * a `service` declaration in the resulting Protobuf file. */ -export type ServiceDecorator = (context: DecoratorContext, target: Interface) => void; +export type ServiceDecorator = ( + context: DecoratorContext, + target: Interface, +) => DecoratorValidatorCallbacks | void; /** * Declares that a TypeSpec namespace constitutes a Protobuf package. The contents of the namespace will be emitted to a @@ -108,7 +115,7 @@ export type PackageDecorator = ( context: DecoratorContext, target: Namespace, details?: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Set the streaming mode of an operation. See [StreamMode](./data-types#TypeSpec.Protobuf.StreamMode) for more information. @@ -125,7 +132,11 @@ export type PackageDecorator = ( * op connectToMessageService(...Message): Message; * ``` */ -export type StreamDecorator = (context: DecoratorContext, target: Operation, mode: Type) => void; +export type StreamDecorator = ( + context: DecoratorContext, + target: Operation, + mode: Type, +) => DecoratorValidatorCallbacks | void; export type TypeSpecProtobufDecorators = { message: MessageDecorator; diff --git a/packages/rest/generated-defs/TypeSpec.Rest.Private.ts b/packages/rest/generated-defs/TypeSpec.Rest.Private.ts index 0c6a8327b19..7368924b62e 100644 --- a/packages/rest/generated-defs/TypeSpec.Rest.Private.ts +++ b/packages/rest/generated-defs/TypeSpec.Rest.Private.ts @@ -1,5 +1,6 @@ import type { DecoratorContext, + DecoratorValidatorCallbacks, Model, ModelProperty, Operation, @@ -11,31 +12,31 @@ export type ResourceLocationDecorator = ( context: DecoratorContext, target: Scalar, resourceType: Model, -) => void; +) => DecoratorValidatorCallbacks | void; export type ValidateHasKeyDecorator = ( context: DecoratorContext, target: Type, value: Type, -) => void; +) => DecoratorValidatorCallbacks | void; export type ValidateIsErrorDecorator = ( context: DecoratorContext, target: Type, value: Type, -) => void; +) => DecoratorValidatorCallbacks | void; export type ActionSegmentDecorator = ( context: DecoratorContext, target: Operation, value: string, -) => void; +) => DecoratorValidatorCallbacks | void; export type ResourceTypeForKeyParamDecorator = ( context: DecoratorContext, entity: ModelProperty, resourceType: Model, -) => void; +) => DecoratorValidatorCallbacks | void; export type TypeSpecRestPrivateDecorators = { resourceLocation: ResourceLocationDecorator; diff --git a/packages/rest/generated-defs/TypeSpec.Rest.ts b/packages/rest/generated-defs/TypeSpec.Rest.ts index 90f68ad386e..14a2b275266 100644 --- a/packages/rest/generated-defs/TypeSpec.Rest.ts +++ b/packages/rest/generated-defs/TypeSpec.Rest.ts @@ -1,5 +1,6 @@ import type { DecoratorContext, + DecoratorValidatorCallbacks, Interface, Model, ModelProperty, @@ -17,7 +18,10 @@ import type { * } * ``` */ -export type AutoRouteDecorator = (context: DecoratorContext, target: Interface | Operation) => void; +export type AutoRouteDecorator = ( + context: DecoratorContext, + target: Interface | Operation, +) => DecoratorValidatorCallbacks | void; /** * Defines the preceding path segment for a @@ -38,7 +42,7 @@ export type SegmentDecorator = ( context: DecoratorContext, target: Model | ModelProperty | Operation, name: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Returns the URL segment of a given model if it has `@segment` and `@key` decorator. @@ -49,7 +53,7 @@ export type SegmentOfDecorator = ( context: DecoratorContext, target: Operation, type: Model, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Defines the separator string that is inserted before the action name in auto-generated routes for actions. @@ -60,7 +64,7 @@ export type ActionSeparatorDecorator = ( context: DecoratorContext, target: Model | ModelProperty | Operation, seperator: "/" | ":" | "/:", -) => void; +) => DecoratorValidatorCallbacks | void; /** * Mark this model as a resource type with a name. @@ -71,7 +75,7 @@ export type ResourceDecorator = ( context: DecoratorContext, target: Model, collectionName: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Mark model as a child of the given parent resource. @@ -82,7 +86,7 @@ export type ParentResourceDecorator = ( context: DecoratorContext, target: Model, parent: Model, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that this is a Read operation for a given resource. @@ -96,7 +100,7 @@ export type ReadsResourceDecorator = ( context: DecoratorContext, target: Operation, resourceType: Model, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that this is a Create operation for a given resource. @@ -110,7 +114,7 @@ export type CreatesResourceDecorator = ( context: DecoratorContext, target: Operation, resourceType: Model, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that this is a CreateOrReplace operation for a given resource. @@ -124,7 +128,7 @@ export type CreatesOrReplacesResourceDecorator = ( context: DecoratorContext, target: Operation, resourceType: Model, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that this is a CreatesOrUpdate operation for a given resource. @@ -138,7 +142,7 @@ export type CreatesOrUpdatesResourceDecorator = ( context: DecoratorContext, target: Operation, resourceType: Model, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that this is a Update operation for a given resource. @@ -152,7 +156,7 @@ export type UpdatesResourceDecorator = ( context: DecoratorContext, target: Operation, resourceType: Model, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that this is a Delete operation for a given resource. @@ -166,7 +170,7 @@ export type DeletesResourceDecorator = ( context: DecoratorContext, target: Operation, resourceType: Model, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify that this is a List operation for a given resource. @@ -180,14 +184,18 @@ export type ListsResourceDecorator = ( context: DecoratorContext, target: Operation, resourceType: Model, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify this operation is an action. (Scoped to a resource item /pets/{petId}/my-action) * * @param name Name of the action. If not specified, the name of the operation will be used. */ -export type ActionDecorator = (context: DecoratorContext, target: Operation, name?: string) => void; +export type ActionDecorator = ( + context: DecoratorContext, + target: Operation, + name?: string, +) => DecoratorValidatorCallbacks | void; /** * Specify this operation is a collection action. (Scopped to a resource, /pets/my-action) @@ -203,7 +211,7 @@ export type CollectionActionDecorator = ( target: Operation, resourceType: Model, name?: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Copy the resource key parameters on the model @@ -214,7 +222,7 @@ export type CopyResourceKeyParametersDecorator = ( context: DecoratorContext, target: Model, filter?: string, -) => void; +) => DecoratorValidatorCallbacks | void; export type TypeSpecRestDecorators = { autoRoute: AutoRouteDecorator; diff --git a/packages/spector/generated-defs/TypeSpec.Spector.ts b/packages/spector/generated-defs/TypeSpec.Spector.ts index 6003f5b7864..62c13be7be7 100644 --- a/packages/spector/generated-defs/TypeSpec.Spector.ts +++ b/packages/spector/generated-defs/TypeSpec.Spector.ts @@ -1,5 +1,6 @@ import type { DecoratorContext, + DecoratorValidatorCallbacks, Interface, Model, Namespace, @@ -15,7 +16,7 @@ export type ScenarioServiceDecorator = ( target: Namespace, route: string, options?: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Mark an operation, interface or namespace as a scenario. All containing operations will be part of the same scenario. @@ -24,7 +25,7 @@ export type ScenarioDecorator = ( context: DecoratorContext, target: Namespace | Interface | Operation, name?: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Specify documentation on how to implement this scenario. @@ -37,7 +38,7 @@ export type ScenarioDocDecorator = ( target: Namespace | Interface | Operation, doc: string, formatArgs?: Model, -) => void; +) => DecoratorValidatorCallbacks | void; export type TypeSpecSpectorDecorators = { scenarioService: ScenarioServiceDecorator; diff --git a/packages/sse/generated-defs/TypeSpec.SSE.ts b/packages/sse/generated-defs/TypeSpec.SSE.ts index 387c44b7bda..7829e893d4a 100644 --- a/packages/sse/generated-defs/TypeSpec.SSE.ts +++ b/packages/sse/generated-defs/TypeSpec.SSE.ts @@ -1,10 +1,17 @@ -import type { DecoratorContext, UnionVariant } from "@typespec/compiler"; +import type { + DecoratorContext, + DecoratorValidatorCallbacks, + UnionVariant, +} from "@typespec/compiler"; /** * Indicates that the presence of this event is a terminal event, * and the client should disconnect from the server. */ -export type TerminalEventDecorator = (context: DecoratorContext, target: UnionVariant) => void; +export type TerminalEventDecorator = ( + context: DecoratorContext, + target: UnionVariant, +) => DecoratorValidatorCallbacks | void; export type TypeSpecSSEDecorators = { terminalEvent: TerminalEventDecorator; diff --git a/packages/streams/generated-defs/TypeSpec.Streams.ts b/packages/streams/generated-defs/TypeSpec.Streams.ts index 92e5538c695..12eafde676e 100644 --- a/packages/streams/generated-defs/TypeSpec.Streams.ts +++ b/packages/streams/generated-defs/TypeSpec.Streams.ts @@ -1,4 +1,9 @@ -import type { DecoratorContext, Model, Type } from "@typespec/compiler"; +import type { + DecoratorContext, + DecoratorValidatorCallbacks, + Model, + Type, +} from "@typespec/compiler"; /** * Specify that a model represents a stream protocol type whose data is described @@ -18,7 +23,11 @@ import type { DecoratorContext, Model, Type } from "@typespec/compiler"; * } * ``` */ -export type StreamOfDecorator = (context: DecoratorContext, target: Model, type: Type) => void; +export type StreamOfDecorator = ( + context: DecoratorContext, + target: Model, + type: Type, +) => DecoratorValidatorCallbacks | void; export type TypeSpecStreamsDecorators = { streamOf: StreamOfDecorator; diff --git a/packages/tspd/src/gen-extern-signatures/components/decorator-signature-type.tsx b/packages/tspd/src/gen-extern-signatures/components/decorator-signature-type.tsx index 9a9211dbf80..6be3b79168e 100644 --- a/packages/tspd/src/gen-extern-signatures/components/decorator-signature-type.tsx +++ b/packages/tspd/src/gen-extern-signatures/components/decorator-signature-type.tsx @@ -1,4 +1,4 @@ -import { For, join, List, Refkey, refkey } from "@alloy-js/core"; +import { code, For, join, List, Refkey, refkey } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import { getSourceLocation, @@ -58,7 +58,10 @@ export function DecoratorSignatureType(props: Readonly) name={props.signature.typeName} doc={getDocComment(props.signature.decorator)} > - + ); } diff --git a/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts b/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts index b734fb02f17..7d609020ebc 100644 --- a/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts +++ b/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts @@ -21,6 +21,7 @@ export const typespecCompiler = createPackage({ "EnumValue", "Numeric", "ScalarValue", + "DecoratorValidatorCallbacks", ], }, }, diff --git a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts index 4f0be902852..2b7a405940e 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -44,7 +44,7 @@ it("generate simple decorator with no parameters", async () => { expected: ` ${importLine(["Type"])} -export type SimpleDecorator = (context: DecoratorContext, target: Type) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Type) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -72,7 +72,7 @@ describe("generate target type", () => { expected: ` ${importLine([expected])} -export type SimpleDecorator = (context: DecoratorContext, target: ${expected}) => void; +export type SimpleDecorator = (context: DecoratorContext, target: ${expected}) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -92,7 +92,7 @@ export type Decorators = { expected: ` ${importLine([...expected])} -export type SimpleDecorator = (context: DecoratorContext, target: ${expected.join(" | ")}) => void; +export type SimpleDecorator = (context: DecoratorContext, target: ${expected.join(" | ")}) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -113,7 +113,7 @@ export type Decorators = { expected: ` ${importLine([expected])} -export type SimpleDecorator = (context: DecoratorContext, target: ${expected}) => void; +export type SimpleDecorator = (context: DecoratorContext, target: ${expected}) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -129,7 +129,7 @@ export type Decorators = { expected: ` ${importLine(["Model", "Scalar"])} -export type SimpleDecorator = (context: DecoratorContext, target: Scalar | Model) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Scalar | Model) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -158,7 +158,7 @@ describe("generate parameter type", () => { expected: ` ${importLine(["Type", expected])} -export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: ${expected}) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: ${expected}) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -178,7 +178,7 @@ export type Decorators = { expected: ` ${importLine(["Type", ...expected])} -export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: ${expected.join(" | ")}) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: ${expected.join(" | ")}) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -210,7 +210,7 @@ export type Decorators = { expected: ` ${importLine(["Type", ...(expected === "Numeric" ? ["Numeric"] : [])])} -export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: ${expected}) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: ${expected}) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -232,7 +232,7 @@ export type SimpleDecorator = ( readonly [key: string]: number; readonly other: string; }, -) => void; +) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -254,7 +254,7 @@ export type SimpleDecorator = ( readonly name: string; readonly age?: number; }, -) => void; +) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -269,7 +269,7 @@ export type Decorators = { expected: ` ${importLine(["ScalarValue", "Type"])} -export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: number | string | ScalarValue) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: number | string | ScalarValue) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -291,7 +291,7 @@ export interface Info { readonly age?: number; } -export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: Info) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: Info) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -311,7 +311,7 @@ export type Decorators = { expected: ` ${importLine(["Type", expected])} -export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: ${expected}) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: ${expected}) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -334,7 +334,7 @@ ${importLine(["Type", "Model"])} /** * Some doc comment */ -export type SimpleDecorator = (context: DecoratorContext, target: Type, ...args: Model[]) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Type, ...args: Model[]) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -362,7 +362,7 @@ export type Decorators = { expected: ` ${importLine(["Type", ...(expected === "Numeric[]" ? ["Numeric"] : [])])} -export type SimpleDecorator = (context: DecoratorContext, target: Type, ...args: ${expected}) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Type, ...args: ${expected}) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -385,7 +385,7 @@ ${importLine(["Type"])} /** * Some doc comment */ -export type SimpleDecorator = (context: DecoratorContext, target: Type) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Type) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -413,7 +413,7 @@ ${importLine(["Type"])} * @param arg1 This is the first argument * @param arg2 This is the second argument */ -export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: Type, arg2: Type) => void; +export type SimpleDecorator = (context: DecoratorContext, target: Type, arg1: Type, arg2: Type) => DecoratorValidatorCallbacks | void; export type Decorators = { simple: SimpleDecorator; @@ -424,6 +424,6 @@ export type Decorators = { }); function importLine(imports: string[]) { - const all = new Set(["DecoratorContext", ...imports]); + const all = new Set(["DecoratorContext", "DecoratorValidatorCallbacks", ...imports]); return `import type { ${[...all].sort().join(", ")} } from "@typespec/compiler";`; } diff --git a/packages/versioning/generated-defs/TypeSpec.Versioning.ts b/packages/versioning/generated-defs/TypeSpec.Versioning.ts index 03e4b22cf1f..8e8668c1c45 100644 --- a/packages/versioning/generated-defs/TypeSpec.Versioning.ts +++ b/packages/versioning/generated-defs/TypeSpec.Versioning.ts @@ -1,5 +1,6 @@ import type { DecoratorContext, + DecoratorValidatorCallbacks, Enum, EnumMember, Interface, @@ -32,7 +33,7 @@ export type VersionedDecorator = ( context: DecoratorContext, target: Namespace, versions: Enum, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Identifies that a namespace or a given versioning enum member relies upon a versioned package. @@ -63,7 +64,7 @@ export type UseDependencyDecorator = ( context: DecoratorContext, target: EnumMember | Namespace, ...versionRecords: EnumMember[] -) => void; +) => DecoratorValidatorCallbacks | void; /** * Identifies when the target was added. @@ -98,7 +99,7 @@ export type AddedDecorator = ( | Scalar | Interface, version: EnumMember, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Identifies when the target was removed. @@ -133,7 +134,7 @@ export type RemovedDecorator = ( | Scalar | Interface, version: EnumMember, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Identifies when the target has been renamed. @@ -160,7 +161,7 @@ export type RenamedFromDecorator = ( | Interface, version: EnumMember, oldName: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Identifies when a target was made optional. @@ -179,7 +180,7 @@ export type MadeOptionalDecorator = ( context: DecoratorContext, target: ModelProperty, version: EnumMember, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Identifies when a target was made required. @@ -198,7 +199,7 @@ export type MadeRequiredDecorator = ( context: DecoratorContext, target: ModelProperty, version: EnumMember, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Identifies when the target type changed. @@ -211,7 +212,7 @@ export type TypeChangedFromDecorator = ( target: ModelProperty, version: EnumMember, oldType: Type, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Identifies when the target type changed. @@ -224,7 +225,7 @@ export type ReturnTypeChangedFromDecorator = ( target: Operation, version: EnumMember, oldType: Type, -) => void; +) => DecoratorValidatorCallbacks | void; export type TypeSpecVersioningDecorators = { versioned: VersionedDecorator; diff --git a/packages/xml/generated-defs/TypeSpec.Xml.ts b/packages/xml/generated-defs/TypeSpec.Xml.ts index 99ebfb9c9e5..0c603f645c6 100644 --- a/packages/xml/generated-defs/TypeSpec.Xml.ts +++ b/packages/xml/generated-defs/TypeSpec.Xml.ts @@ -1,4 +1,10 @@ -import type { DecoratorContext, Enum, ModelProperty, Type } from "@typespec/compiler"; +import type { + DecoratorContext, + DecoratorValidatorCallbacks, + Enum, + ModelProperty, + Type, +} from "@typespec/compiler"; /** * Provide the name of the XML element or attribute. This means the same thing as @@ -23,7 +29,11 @@ import type { DecoratorContext, Enum, ModelProperty, Type } from "@typespec/comp * * ``` */ -export type NameDecorator = (context: DecoratorContext, target: Type, name: string) => void; +export type NameDecorator = ( + context: DecoratorContext, + target: Type, + name: string, +) => DecoratorValidatorCallbacks | void; /** * Specify that the target property should be encoded as an XML attribute instead of node. @@ -54,7 +64,10 @@ export type NameDecorator = (context: DecoratorContext, target: Type, name: stri * * ``` */ -export type AttributeDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type AttributeDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Specify that the target property shouldn't create a wrapper node. This can be used to flatten list nodes into the model node or to include raw text in the model node. @@ -121,7 +134,10 @@ export type AttributeDecorator = (context: DecoratorContext, target: ModelProper * * ``` */ -export type UnwrappedDecorator = (context: DecoratorContext, target: ModelProperty) => void; +export type UnwrappedDecorator = ( + context: DecoratorContext, + target: ModelProperty, +) => DecoratorValidatorCallbacks | void; /** * Specify the XML namespace for this element. It can be used in 2 different ways: @@ -164,12 +180,15 @@ export type NsDecorator = ( target: Type, ns: Type, prefix?: string, -) => void; +) => DecoratorValidatorCallbacks | void; /** * Mark an enum as declaring XML namespaces. See `@ns` */ -export type NsDeclarationsDecorator = (context: DecoratorContext, target: Enum) => void; +export type NsDeclarationsDecorator = ( + context: DecoratorContext, + target: Enum, +) => DecoratorValidatorCallbacks | void; export type TypeSpecXmlDecorators = { name: NameDecorator; diff --git a/website/src/content/docs/docs/extending-typespec/create-decorators.md b/website/src/content/docs/docs/extending-typespec/create-decorators.md index 380b7e8e2ff..32bd9ecbf77 100644 --- a/website/src/content/docs/docs/extending-typespec/create-decorators.md +++ b/website/src/content/docs/docs/extending-typespec/create-decorators.md @@ -120,6 +120,92 @@ model Dog { } ``` +### Decorator validation + +Decorators often need to validate that they are being used correctly. TypeSpec provides different validation strategies depending on your requirements: + +:::warning +The purpose of validation callbacks is specifically to report diagnostics, not to modify the type graph. Modifying the type graph during validation may lead to unexpected behavior. +::: + +#### Immediate validation + +For simple validation cases (such as checking parameter values), you can use the `reportDiagnostic` method directly in the decorator implementation. This is suitable when validation only depends on the decorator's parameters. + +```ts +export function $maxLength(context: DecoratorContext, target: Type, max: number) { + if (max < 0) { + reportDiagnostic(context.program, { + code: "invalid-max-length", + target: context.getArgumentTarget(0), + format: { max }, + }); + } + // ... rest of decorator implementation +} +``` + +#### Post-validation callbacks + +When you need to validate interactions between decorators or verify constraints across the type graph, use validation callbacks. These callbacks allow you to defer validation until specific points in the compilation process. + +There are two different events you can use: + +##### `onTargetFinish` + +Called when the target type is fully processed. At this point, all decorators have been applied to the type, making it suitable for validating decorator conflicts or combinations on a single type. + +```ts +export function $track(context: DecoratorContext, target: Type) { + return { + onTargetFinish() { + // Validate that @track and @deprecated are not used together + if (isDeprecated(context.program, target)) { + return [ + createDiagnostic(context.program, { + code: "track-deprecated-conflict", + target: context.decoratorTarget, + }), + ]; + } + return []; + }, + }; +} +``` + +##### `onGraphFinish` + +Called after the entire type graph has been resolved during the checker phase. Use this when you need to validate relationships across multiple types or perform analysis that requires the complete type graph. + +```ts +export function $foreignKey(context: DecoratorContext, target: ModelProperty, ref: Model) { + return { + onGraphFinish() { + // Validate that the referenced model exists in the type graph + // and has appropriate key properties + const refKeys = getKeyProperties(context.program, ref); + if (refKeys.length === 0) { + return [ + createDiagnostic(context.program, { + code: "foreign-key-no-primary", + target: context.decoratorTarget, + format: { modelName: ref.name }, + }), + ]; + } + return []; + }, + }; +} +``` + +#### Choosing the right validation approach + +- Use **immediate validation** for parameter validation and simple checks +- Use **`onTargetFinish`** when you need to check decorator combinations on a single type +- Use **`onGraphFinish`** when you need to validate relationships across multiple types or require the complete type graph + ### Decorator parameter marshalling When decorators are passed types, the type is passed as-is. When a decorator is passed a TypeSpec value, the decorator receives a JavaScript value with a type that is appropriate for representing that value.