From ad782d4b81cd206a800ffd376bc00d20711957aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 02:48:33 +0000 Subject: [PATCH 1/6] Initial plan From 0954788fdfe4246ce76972250d46691e1855c8e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 03:10:46 +0000 Subject: [PATCH 2/6] feat: add DecoratedExpression AST node type and parser/checker/formatter support Co-authored-by: johanste <15110018+johanste@users.noreply.github.com> --- packages/compiler/src/ast/index.ts | 1 + packages/compiler/src/core/checker.ts | 29 +++++++++++++++++++ packages/compiler/src/core/parser.ts | 19 ++++++++++-- packages/compiler/src/core/types.ts | 10 ++++++- .../compiler/src/formatter/print/printer.ts | 12 ++++++++ 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/ast/index.ts b/packages/compiler/src/ast/index.ts index 8c126a4291e..e090af73163 100644 --- a/packages/compiler/src/ast/index.ts +++ b/packages/compiler/src/ast/index.ts @@ -38,6 +38,7 @@ export type { CallExpressionNode, ConstStatementNode, DeclarationNode, + DecoratedExpressionNode, DecoratorDeclarationStatementNode, DecoratorExpressionNode, DirectiveExpressionNode, diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 412963497fb..1cd17c5b7b0 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -56,6 +56,7 @@ import { DecoratorContext, DecoratorDeclarationStatementNode, DecoratorExpressionNode, + DecoratedExpressionNode, DecoratorValidatorCallbacks, Diagnostic, DiagnosticTarget, @@ -1037,6 +1038,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return checkCallExpression(ctx, node); case SyntaxKind.TypeOfExpression: return checkTypeOfExpression(ctx, node); + case SyntaxKind.DecoratedExpression: + return checkDecoratedExpression(ctx, node); case SyntaxKind.AugmentDecoratorStatement: return checkAugmentDecorator(ctx, node); case SyntaxKind.UsingStatement: @@ -3927,6 +3930,32 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return type; } + function checkDecoratedExpression( + ctx: CheckContext, + node: DecoratedExpressionNode, + ): Type | Value | IndeterminateEntity | null { + const targetResult = checkNode(ctx, node.target); + if (targetResult === null) { + return null; + } + + // Apply decorators to the resolved type + if (typeof targetResult === "object" && "entityKind" in targetResult) { + if (targetResult.entityKind === "Type" && "decorators" in targetResult) { + const type = targetResult as Type & { decorators: DecoratorApplication[] }; + for (const decNode of node.decorators) { + const decorator = checkDecoratorApplication(ctx, type, decNode); + if (decorator) { + type.decorators.unshift(decorator); + } + } + applyDecoratorsToType(type); + } + } + + return targetResult; + } + /** Find the indexer that applies to this model. Either defined on itself or from a base model */ function findIndexer(model: Model): ModelIndexer | undefined { let current: Model | undefined = model; diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index e977aa02fe2..d888ebf6be0 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -28,6 +28,7 @@ import { Comment, ConstStatementNode, DeclarationNode, + DecoratedExpressionNode, DecoratorDeclarationStatementNode, DecoratorExpressionNode, Diagnostic, @@ -1684,9 +1685,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.OpenParen: return parseParenthesizedExpression(); case Token.At: - const decorators = parseDecoratorList(); - reportInvalidDecorators(decorators, "expression"); - continue; + return parseDecoratedExpression(); case Token.Hash: const directives = parseDirectiveList(); reportInvalidDirective(directives, "expression"); @@ -1707,6 +1706,18 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa } } + function parseDecoratedExpression(): DecoratedExpressionNode { + const pos = tokenPos(); + const decorators = parseDecoratorList(); + const target = parseExpression(); + return { + kind: SyntaxKind.DecoratedExpression, + decorators, + target, + ...finishNode(pos), + }; + } + function parseExternKeyword(): ExternKeywordNode { const pos = tokenPos(); parseExpected(Token.ExternKeyword); @@ -2951,6 +2962,8 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined return visitNode(cb, node.base) || visitNode(cb, node.id); case SyntaxKind.ModelExpression: return visitEach(cb, node.properties); + case SyntaxKind.DecoratedExpression: + return visitEach(cb, node.decorators) || visitNode(cb, node.target); case SyntaxKind.ModelProperty: return ( visitEach(cb, node.decorators) || diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 1a8cb3b9a4c..4ea28d42674 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -1066,6 +1066,7 @@ export enum SyntaxKind { ConstStatement, CallExpression, ScalarConstructor, + DecoratedExpression, } export const enum NodeFlags { @@ -1357,7 +1358,8 @@ export type Expression = | StringTemplateExpressionNode | VoidKeywordNode | NeverKeywordNode - | AnyKeywordNode; + | AnyKeywordNode + | DecoratedExpressionNode; export type ReferenceExpression = | TypeReferenceNode @@ -1512,6 +1514,12 @@ export interface ModelExpressionNode extends BaseNode { readonly bodyRange: TextRange; } +export interface DecoratedExpressionNode extends BaseNode { + readonly kind: SyntaxKind.DecoratedExpression; + readonly decorators: readonly DecoratorExpressionNode[]; + readonly target: Expression; +} + export interface ArrayExpressionNode extends BaseNode { readonly kind: SyntaxKind.ArrayExpression; readonly elementType: Expression; diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index fe9834ce1c4..de312e9b76e 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -15,6 +15,7 @@ import { CallExpressionNode, Comment, ConstStatementNode, + DecoratedExpressionNode, DecoratorDeclarationStatementNode, DecoratorExpressionNode, DirectiveExpressionNode, @@ -189,6 +190,8 @@ export function printNode( return printBooleanLiteral(path as AstPath, options); case SyntaxKind.ModelExpression: return printModelExpression(path as AstPath, options, print); + case SyntaxKind.DecoratedExpression: + return printDecoratedExpression(path as AstPath, options, print); case SyntaxKind.ModelProperty: return printModelProperty(path as AstPath, options, print); case SyntaxKind.DecoratorExpression: @@ -937,6 +940,15 @@ export function printModelExpression( } } +export function printDecoratedExpression( + path: AstPath, + options: TypeSpecPrettierOptions, + print: PrettierChildPrint, +) { + const decorators = path.map((x) => [print(x as any), " "], "decorators"); + return group([...decorators, path.call(print, "target")]); +} + export function printObjectLiteral( path: AstPath, options: TypeSpecPrettierOptions, From 73e6e6680ca4f59410d0e049ef93b30aebe5a6b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 03:13:28 +0000 Subject: [PATCH 3/6] feat: handle DecoratedExpression in checkModelIs and checkClassHeritage; add tests Co-authored-by: johanste <15110018+johanste@users.noreply.github.com> --- packages/compiler/src/core/checker.ts | 50 ++++++-- packages/compiler/test/checker/model.test.ts | 119 +++++++++++++++++++ packages/compiler/test/parser.test.ts | 11 ++ 3 files changed, 169 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 1cd17c5b7b0..e02a306545e 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4777,7 +4777,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker model: ModelStatementNode, heritageRef: Expression, ): Model | undefined { - if (heritageRef.kind === SyntaxKind.ModelExpression) { + // Unwrap decorated expression to check the target + const innerRef = + heritageRef.kind === SyntaxKind.DecoratedExpression ? heritageRef.target : heritageRef; + if (innerRef.kind === SyntaxKind.ModelExpression) { reportCheckerDiagnostic( createDiagnostic({ code: "extend-model", @@ -4788,8 +4791,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; } if ( - heritageRef.kind !== SyntaxKind.TypeReference && - heritageRef.kind !== SyntaxKind.ArrayExpression + innerRef.kind !== SyntaxKind.TypeReference && + innerRef.kind !== SyntaxKind.ArrayExpression ) { reportCheckerDiagnostic( createDiagnostic({ @@ -4802,7 +4805,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const modelSymId = getNodeSym(model); pendingResolutions.start(modelSymId, ResolutionKind.BaseType); - const target = resolver.getNodeLinks(heritageRef).resolvedSymbol; + const target = resolver.getNodeLinks(innerRef).resolvedSymbol; if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { if (ctx.mapper === undefined) { reportCheckerDiagnostic( @@ -4815,7 +4818,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return undefined; } - const heritageType = getTypeForNode(heritageRef, ctx); + const heritageType = getTypeForNode(innerRef, ctx); pendingResolutions.finish(modelSymId, ResolutionKind.BaseType); if (isErrorType(heritageType)) { compilerAssert(program.hasError(), "Should already have reported an error.", heritageRef); @@ -4837,6 +4840,17 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); } + // Apply decorators from the decorated expression to the resolved type + if (heritageRef.kind === SyntaxKind.DecoratedExpression) { + for (const decNode of heritageRef.decorators) { + const decorator = checkDecoratorApplication(ctx, heritageType, decNode); + if (decorator) { + heritageType.decorators.unshift(decorator); + } + } + applyDecoratorsToType(heritageType); + } + return heritageType; } @@ -4850,7 +4864,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const modelSymId = getNodeSym(model); pendingResolutions.start(modelSymId, ResolutionKind.BaseType); let isType; - if (isExpr.kind === SyntaxKind.ModelExpression) { + // Unwrap decorated expression to check the target + const innerExpr = + isExpr.kind === SyntaxKind.DecoratedExpression ? isExpr.target : isExpr; + if (innerExpr.kind === SyntaxKind.ModelExpression) { reportCheckerDiagnostic( createDiagnostic({ code: "is-model", @@ -4859,10 +4876,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker }), ); return undefined; - } else if (isExpr.kind === SyntaxKind.ArrayExpression) { - isType = checkArrayExpression(ctx, isExpr); - } else if (isExpr.kind === SyntaxKind.TypeReference) { - const target = resolver.getNodeLinks(isExpr).resolvedSymbol; + } else if (innerExpr.kind === SyntaxKind.ArrayExpression) { + isType = checkArrayExpression(ctx, innerExpr); + } else if (innerExpr.kind === SyntaxKind.TypeReference) { + const target = resolver.getNodeLinks(innerExpr).resolvedSymbol; if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { if (ctx.mapper === undefined) { reportCheckerDiagnostic( @@ -4875,7 +4892,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } return undefined; } - isType = getTypeForNode(isExpr, ctx); + isType = getTypeForNode(innerExpr, ctx); } else { reportCheckerDiagnostic(createDiagnostic({ code: "is-model", target: isExpr })); return undefined; @@ -4895,6 +4912,17 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; } + // Apply decorators from the decorated expression to the resolved type + if (isExpr.kind === SyntaxKind.DecoratedExpression) { + for (const decNode of isExpr.decorators) { + const decorator = checkDecoratorApplication(ctx, isType, decNode); + if (decorator) { + isType.decorators.unshift(decorator); + } + } + applyDecoratorsToType(isType); + } + return isType; } diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index 5149d7e810a..c138c5d7f59 100644 --- a/packages/compiler/test/checker/model.test.ts +++ b/packages/compiler/test/checker/model.test.ts @@ -1350,4 +1350,123 @@ describe("compiler: models", () => { }); }); }); + + describe("decorated expressions", () => { + it("applies @doc decorator to inline model expression", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + @test model A { + prop: @doc("inline doc") { name: string }; + } + `, + ); + const { A } = (await testHost.compile("main.tsp")) as { + A: Model; + }; + + const propType = A.properties.get("prop")!.type as Model; + strictEqual(propType.kind, "Model"); + strictEqual(getDoc(testHost.program, propType), "inline doc"); + }); + + it("applies @doc decorator to model expression in alias", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + alias MyModel = @doc("alias doc") { name: string }; + @test model A { + prop: MyModel; + } + `, + ); + const { A } = (await testHost.compile("main.tsp")) as { + A: Model; + }; + + const propType = A.properties.get("prop")!.type as Model; + strictEqual(propType.kind, "Model"); + strictEqual(getDoc(testHost.program, propType), "alias doc"); + }); + + it("applies decorator to type reference expression", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model Base { name: string } + @test model A { + prop: @doc("ref doc") Base; + } + `, + ); + const { A } = (await testHost.compile("main.tsp")) as { + A: Model; + }; + + const propType = A.properties.get("prop")!.type as Model; + strictEqual(propType.kind, "Model"); + strictEqual(getDoc(testHost.program, propType), "ref doc"); + }); + + it("applies decorator to is expression", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + model Base { name: string } + @test model A is @doc("is doc") Base { } + `, + ); + const { A } = (await testHost.compile("main.tsp")) as { + A: Model; + }; + + strictEqual(A.sourceModel?.kind, "Model"); + strictEqual(getDoc(testHost.program, A.sourceModel!), "is doc"); + }); + + it("applies custom decorator to inline model expression", async () => { + let decoratedType: Model | undefined; + + testHost.addJsFile("dec.js", { + $myDec(p: any, t: Model) { + decoratedType = t; + }, + }); + + testHost.addTypeSpecFile( + "main.tsp", + ` + import "./dec.js"; + @test model A { + prop: @myDec { x: int32 }; + } + `, + ); + const { A } = (await testHost.compile("main.tsp")) as { + A: Model; + }; + + ok(decoratedType); + strictEqual(decoratedType!.kind, "Model"); + ok(decoratedType!.properties.has("x")); + }); + + it("applies decorator to union expression", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + @test model A { + prop: @doc("union doc") (string | int32); + } + `, + ); + const { A } = (await testHost.compile("main.tsp")) as { + A: Model; + }; + + const propType = A.properties.get("prop")!.type; + strictEqual(propType.kind, "Union"); + strictEqual(getDoc(testHost.program, propType), "union doc"); + }); + }); }); diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index 1e8d360c19d..5ffbef6e2b8 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -234,6 +234,17 @@ describe("compiler: parser", () => { parseEach(['model Car { engine: { type: "v8" } }']); }); + describe("decorated expressions", () => { + parseEach([ + 'model A { prop: @doc("inline") { name: string } }', + 'alias B = @doc("alias") { x: int32 };', + "model C { prop: @myDec SomeModel }", + "model D is @myDec Base { }", + 'model E { prop: @doc("union") (string | int32) }', + "alias F = @myDec string[];", + ]); + }); + describe("tuple model expressions", () => { parseEach([ 'namespace A { op b(param: [number, string]): [1, "hi"]; }', From 423eff27f5d52e12677940409042611c7cfa8bc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 03:18:48 +0000 Subject: [PATCH 4/6] docs: update decorators.md, models.md, and documentation sample with decorated expressions Co-authored-by: johanste <15110018+johanste@users.noreply.github.com> --- packages/samples/specs/documentation/docs.tsp | 5 ++++ .../docs/docs/language-basics/decorators.md | 25 ++++++++++++++++++- .../docs/docs/language-basics/models.md | 12 +++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/samples/specs/documentation/docs.tsp b/packages/samples/specs/documentation/docs.tsp index 96986fab78d..f9158ab07a8 100644 --- a/packages/samples/specs/documentation/docs.tsp +++ b/packages/samples/specs/documentation/docs.tsp @@ -48,3 +48,8 @@ model NotFoundWithDocsResp { @statusCode _: 404; details: string; } + +// Example of decorating inline model expressions +model Contact { + address: @doc("Mailing address") { street: string; city: string; zip: string }; +} diff --git a/website/src/content/docs/docs/language-basics/decorators.md b/website/src/content/docs/docs/language-basics/decorators.md index 92c820853a0..ab9d8501755 100644 --- a/website/src/content/docs/docs/language-basics/decorators.md +++ b/website/src/content/docs/docs/language-basics/decorators.md @@ -7,7 +7,7 @@ llmstxt: true Decorators in TypeSpec allow developers to attach metadata to types within a TypeSpec program. They can also be used to compute types based on their inputs. Decorators form the core of TypeSpec's extensibility, providing the flexibility to describe a wide variety of APIs and associated metadata such as documentation, constraints, samples, and more. -The vast majority of TypeSpec declarations may be decorated, including [namespaces](./namespaces.md), [interfaces](./interfaces.md), [operations](./operations.md) and their parameters, [scalars](./scalars.md), and [models](./models.md) and their members. In general, any declaration that creates a Type can be decorated. Notably, [aliases](./alias.md) cannot be decorated, as they do not create new Types, nor can any type expressions such as unions that use the `|` syntax or anonymous models, as they are not declarations. +The vast majority of TypeSpec declarations may be decorated, including [namespaces](./namespaces.md), [interfaces](./interfaces.md), [operations](./operations.md) and their parameters, [scalars](./scalars.md), and [models](./models.md) and their members. In general, any declaration that creates a Type can be decorated. [Aliases](./alias.md) cannot be decorated directly, as they do not create new Types, but the expressions they reference can be. Type expressions such as anonymous models, union expressions, and type references can also be decorated inline. Decorators are defined using JavaScript functions that are exported from a standard ECMAScript module. When a JavaScript file is imported, TypeSpec will look for any exported functions prefixed with `$`, and make them available as decorators within the TypeSpec syntax. When a decorated declaration is evaluated by TypeSpec, the decorator function is invoked, passing along a reference to the current compilation, an object representing the type it is attached to, and any arguments the user provided to the decorator. @@ -32,6 +32,29 @@ If no arguments are provided, the parentheses can be omitted. model Dog {} ``` +## Decorating expressions + +Decorators can be applied directly to type expressions such as inline models, type references, and union expressions. The decorator is placed before the expression it decorates. + +```typespec +model Pet { + owner: @doc("The pet owner's info") { name: string; phone: string }; +} +``` + +This also works with aliases: + +```typespec +alias Address = @doc("A mailing address") { street: string; city: string }; +``` + +Decorators on expressions work with `is` and `extends` as well: + +```typespec +model Base { id: string } +model Extended is @tag("extended") Base { extra: string } +``` + ## Augmenting decorators Decorators can also be applied from a different location by referring to the type being decorated. For this, you can declare an augment decorator using the `@@` prefix. The first argument of an augment decorator is the type reference that should be decorated. As the augment decorator is a statement, it must end with a semicolon (`;`). diff --git a/website/src/content/docs/docs/language-basics/models.md b/website/src/content/docs/docs/language-basics/models.md index 23c4eb0b9a6..14941975a95 100644 --- a/website/src/content/docs/docs/language-basics/models.md +++ b/website/src/content/docs/docs/language-basics/models.md @@ -235,3 +235,15 @@ Some model property meta types can be referenced using `::`. | Name | Example | Description | | ---- | ---------------- | ---------------------------------------- | | type | `Pet.name::type` | Reference the type of the model property | + +## Decorating inline models + +Decorators can be applied to inline model expressions and other type expressions used in property types, aliases, and `is`/`extends` clauses. Place the decorator before the expression: + +```typespec +model Pet { + owner: @doc("Owner contact info") { name: string; phone: string }; +} + +alias Address = @doc("A mailing address") { street: string; city: string }; +``` From eb2d23c8f7a7b5ffc4f2fdc60d7d90c80b238920 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 03:25:37 +0000 Subject: [PATCH 5/6] fix: apply only new decorators individually to avoid re-applying existing ones Co-authored-by: johanste <15110018+johanste@users.noreply.github.com> --- packages/compiler/src/core/checker.ts | 6 +++--- .../@typespec/openapi3/openapi.yaml | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index e02a306545e..181c3303e8e 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3947,9 +3947,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const decorator = checkDecoratorApplication(ctx, type, decNode); if (decorator) { type.decorators.unshift(decorator); + applyDecoratorToType(program, decorator, type); } } - applyDecoratorsToType(type); } } @@ -4846,9 +4846,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const decorator = checkDecoratorApplication(ctx, heritageType, decNode); if (decorator) { heritageType.decorators.unshift(decorator); + applyDecoratorToType(program, decorator, heritageType); } } - applyDecoratorsToType(heritageType); } return heritageType; @@ -4918,9 +4918,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const decorator = checkDecoratorApplication(ctx, isType, decNode); if (decorator) { isType.decorators.unshift(decorator); + applyDecoratorToType(program, decorator, isType); } } - applyDecoratorsToType(isType); } return isType; diff --git a/packages/samples/test/output/documentation/@typespec/openapi3/openapi.yaml b/packages/samples/test/output/documentation/@typespec/openapi3/openapi.yaml index 755b42d3dcb..e73f6b2dc72 100644 --- a/packages/samples/test/output/documentation/@typespec/openapi3/openapi.yaml +++ b/packages/samples/test/output/documentation/@typespec/openapi3/openapi.yaml @@ -86,6 +86,24 @@ paths: $ref: '#/components/schemas/Error' components: schemas: + Contact: + type: object + required: + - address + properties: + address: + type: object + properties: + street: + type: string + city: + type: string + zip: + type: string + required: + - street + - city + - zip Error: type: object required: From 0c8359aea49aabc08fbf6e337c14bd9983b85cfe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:12:49 +0000 Subject: [PATCH 6/6] fix: address CI failures - lint warning, formatting, changeset, and sample snapshot Co-authored-by: johanste <15110018+johanste@users.noreply.github.com> --- .../decorated-expressions-2026-2-19-21-3-38.md | 7 +++++++ packages/compiler/src/core/checker.ts | 5 ++--- packages/compiler/test/checker/model.test.ts | 4 +--- packages/samples/specs/documentation/docs.tsp | 5 ----- .../@typespec/openapi3/openapi.yaml | 18 ------------------ .../docs/docs/language-basics/decorators.md | 18 ++++++++++++++---- .../docs/docs/language-basics/models.md | 10 ++++++++-- 7 files changed, 32 insertions(+), 35 deletions(-) create mode 100644 .chronus/changes/decorated-expressions-2026-2-19-21-3-38.md diff --git a/.chronus/changes/decorated-expressions-2026-2-19-21-3-38.md b/.chronus/changes/decorated-expressions-2026-2-19-21-3-38.md new file mode 100644 index 00000000000..1cf3400269b --- /dev/null +++ b/.chronus/changes/decorated-expressions-2026-2-19-21-3-38.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Add support for decorating type expressions. Decorators can now be applied to inline models, type references, union expressions, and `is`/`extends` targets. diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 181c3303e8e..5cccbbbce40 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -50,13 +50,13 @@ import { CallExpressionNode, CodeFix, ConstStatementNode, + DecoratedExpressionNode, Decorator, DecoratorApplication, DecoratorArgument, DecoratorContext, DecoratorDeclarationStatementNode, DecoratorExpressionNode, - DecoratedExpressionNode, DecoratorValidatorCallbacks, Diagnostic, DiagnosticTarget, @@ -4865,8 +4865,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker pendingResolutions.start(modelSymId, ResolutionKind.BaseType); let isType; // Unwrap decorated expression to check the target - const innerExpr = - isExpr.kind === SyntaxKind.DecoratedExpression ? isExpr.target : isExpr; + const innerExpr = isExpr.kind === SyntaxKind.DecoratedExpression ? isExpr.target : isExpr; if (innerExpr.kind === SyntaxKind.ModelExpression) { reportCheckerDiagnostic( createDiagnostic({ diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts index c138c5d7f59..72e241b9785 100644 --- a/packages/compiler/test/checker/model.test.ts +++ b/packages/compiler/test/checker/model.test.ts @@ -1442,9 +1442,7 @@ describe("compiler: models", () => { } `, ); - const { A } = (await testHost.compile("main.tsp")) as { - A: Model; - }; + await testHost.compile("main.tsp"); ok(decoratedType); strictEqual(decoratedType!.kind, "Model"); diff --git a/packages/samples/specs/documentation/docs.tsp b/packages/samples/specs/documentation/docs.tsp index f9158ab07a8..96986fab78d 100644 --- a/packages/samples/specs/documentation/docs.tsp +++ b/packages/samples/specs/documentation/docs.tsp @@ -48,8 +48,3 @@ model NotFoundWithDocsResp { @statusCode _: 404; details: string; } - -// Example of decorating inline model expressions -model Contact { - address: @doc("Mailing address") { street: string; city: string; zip: string }; -} diff --git a/packages/samples/test/output/documentation/@typespec/openapi3/openapi.yaml b/packages/samples/test/output/documentation/@typespec/openapi3/openapi.yaml index e73f6b2dc72..755b42d3dcb 100644 --- a/packages/samples/test/output/documentation/@typespec/openapi3/openapi.yaml +++ b/packages/samples/test/output/documentation/@typespec/openapi3/openapi.yaml @@ -86,24 +86,6 @@ paths: $ref: '#/components/schemas/Error' components: schemas: - Contact: - type: object - required: - - address - properties: - address: - type: object - properties: - street: - type: string - city: - type: string - zip: - type: string - required: - - street - - city - - zip Error: type: object required: diff --git a/website/src/content/docs/docs/language-basics/decorators.md b/website/src/content/docs/docs/language-basics/decorators.md index ab9d8501755..cfb1cb37cb3 100644 --- a/website/src/content/docs/docs/language-basics/decorators.md +++ b/website/src/content/docs/docs/language-basics/decorators.md @@ -38,21 +38,31 @@ Decorators can be applied directly to type expressions such as inline models, ty ```typespec model Pet { - owner: @doc("The pet owner's info") { name: string; phone: string }; + owner: @doc("The pet owner's info") { + name: string; + phone: string; + }; } ``` This also works with aliases: ```typespec -alias Address = @doc("A mailing address") { street: string; city: string }; +alias Address = @doc("A mailing address") { + street: string; + city: string; +}; ``` Decorators on expressions work with `is` and `extends` as well: ```typespec -model Base { id: string } -model Extended is @tag("extended") Base { extra: string } +model Base { + id: string; +} +model Extended is @tag("extended") Base { + extra: string; +} ``` ## Augmenting decorators diff --git a/website/src/content/docs/docs/language-basics/models.md b/website/src/content/docs/docs/language-basics/models.md index 14941975a95..2320c48a8d0 100644 --- a/website/src/content/docs/docs/language-basics/models.md +++ b/website/src/content/docs/docs/language-basics/models.md @@ -242,8 +242,14 @@ Decorators can be applied to inline model expressions and other type expressions ```typespec model Pet { - owner: @doc("Owner contact info") { name: string; phone: string }; + owner: @doc("Owner contact info") { + name: string; + phone: string; + }; } -alias Address = @doc("A mailing address") { street: string; city: string }; +alias Address = @doc("A mailing address") { + street: string; + city: string; +}; ```