From 5ffae9bcef8bddd6d08cea375342bc452e1df0ae Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 30 Mar 2026 09:25:27 -0400 Subject: [PATCH 01/13] Pass 1 --- packages/compiler/src/core/checker.ts | 43 +++++++++++++++++-- packages/compiler/src/core/parser.ts | 15 +++++++ packages/compiler/src/core/scanner.ts | 3 ++ packages/compiler/src/core/types.ts | 12 +++++- .../compiler/src/formatter/print/printer.ts | 2 + .../compiler/test/formatter/formatter.test.ts | 22 ++++++++++ packages/compiler/test/parser.test.ts | 4 ++ packages/compiler/test/scanner.test.ts | 1 + 8 files changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 66a1658d6c0..4de42c203b4 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1,4 +1,5 @@ import { Realm } from "../experimental/realm.js"; +import { getDataDecoratorStateKey } from "../lib/data-decorator.js"; import { docFromCommentDecorator, getIndexer } from "../lib/intrinsic/decorators.js"; import { $ } from "../typekit/index.js"; import { DuplicateTracker } from "../utils/duplicate-tracker.js"; @@ -107,6 +108,7 @@ import { ModelProperty, ModelPropertyNode, ModelStatementNode, + ModifierFlags, Namespace, NamespaceStatementNode, NeverType, @@ -2099,8 +2101,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); const name = node.id.sv; - const implementation = symbol.value; - if (implementation === undefined) { + const isData = (node.modifierFlags & ModifierFlags.Data) !== 0; + let implementation = symbol.value; + if (isData) { + implementation = createDataDecoratorImplementation(symbol, node); + } else if (implementation === undefined) { reportCheckerDiagnostic(createDiagnostic({ code: "missing-implementation", target: node })); } const decoratorType: Decorator = createType({ @@ -2111,6 +2116,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker target: checkFunctionParameter(ctx, node.target, true), parameters: node.parameters.map((param) => checkFunctionParameter(ctx, param, true)), implementation: implementation ?? (() => {}), + ...(isData ? { isData: true } : {}), }); namespace.decoratorDeclarations.set(name, decoratorType); @@ -2120,6 +2126,36 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return decoratorType; } + function createDataDecoratorImplementation( + symbol: Sym, + node: DecoratorDeclarationStatementNode, + ): (ctx: DecoratorContext, target: Type, ...args: unknown[]) => void { + const fqn = getFullyQualifiedSymbolName(symbol); + const stateKey = getDataDecoratorStateKey(fqn); + const paramNames = node.parameters.map((p) => p.id.sv); + + if (paramNames.length === 0) { + // No args beyond target — boolean flag via stateSet + return (context: DecoratorContext, target: Type) => { + context.program.stateSet(stateKey).add(target); + }; + } else if (paramNames.length === 1) { + // Single arg — store value directly + return (context: DecoratorContext, target: Type, value: unknown) => { + context.program.stateMap(stateKey).set(target, value); + }; + } else { + // Multiple args — store as named record + return (context: DecoratorContext, target: Type, ...args: unknown[]) => { + const data: Record = {}; + for (let i = 0; i < paramNames.length; i++) { + data[paramNames[i]] = args[i]; + } + context.program.stateMap(stateKey).set(target, data); + }; + } + } + function checkFunctionDeclaration( ctx: CheckContext, node: FunctionDeclarationStatementNode, @@ -5746,9 +5782,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; } + const impl = sym.value ?? symbolLinks.declaredType?.implementation; return { definition: symbolLinks.declaredType, - decorator: sym.value ?? ((...args: any[]) => {}), + decorator: impl ?? ((...args: any[]) => {}), node: decNode, args, }; diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 63ed0076f13..613988fd02a 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -29,6 +29,7 @@ import { CallExpressionNode, Comment, ConstStatementNode, + DataKeywordNode, Declaration, DeclarationNode, DecoratorDeclarationStatementNode, @@ -461,6 +462,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.ConstKeyword: case Token.ExternKeyword: case Token.InternalKeyword: + case Token.DataKeyword: case Token.FnKeyword: case Token.DecKeyword: item = parseDeclaration(pos, decorators, docs, directives); @@ -532,6 +534,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.ConstKeyword: case Token.ExternKeyword: case Token.InternalKeyword: + case Token.DataKeyword: case Token.FnKeyword: case Token.DecKeyword: item = parseDeclaration(pos, decorators, docs, directives); @@ -1770,6 +1773,15 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } + function parseDataKeyword(): DataKeywordNode { + const pos = tokenPos(); + parseExpected(Token.DataKeyword); + return { + kind: SyntaxKind.DataKeyword, + ...finishNode(pos), + }; + } + function parseVoidKeyword(): VoidKeywordNode { const pos = tokenPos(); parseExpected(Token.VoidKeyword); @@ -2090,6 +2102,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa return parseExternKeyword(); case Token.InternalKeyword: return parseInternalKeyword(); + case Token.DataKeyword: + return parseDataKeyword(); default: return undefined; } @@ -3167,6 +3181,7 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined case SyntaxKind.NeverKeyword: case SyntaxKind.ExternKeyword: case SyntaxKind.InternalKeyword: + case SyntaxKind.DataKeyword: case SyntaxKind.UnknownKeyword: case SyntaxKind.JsSourceFile: case SyntaxKind.JsNamespaceDeclaration: diff --git a/packages/compiler/src/core/scanner.ts b/packages/compiler/src/core/scanner.ts index 67fd2454300..fda053c5589 100644 --- a/packages/compiler/src/core/scanner.ts +++ b/packages/compiler/src/core/scanner.ts @@ -139,6 +139,7 @@ export enum Token { ExternKeyword = __StartModifierKeyword, InternalKeyword, + DataKeyword, /** @internal */ __EndModifierKeyword, /////////////////////////////////////////////////////////////// @@ -310,6 +311,7 @@ export const TokenDisplay = getTokenDisplayTable([ [Token.NeverKeyword, "'never'"], [Token.UnknownKeyword, "'unknown'"], [Token.ExternKeyword, "'extern'"], + [Token.DataKeyword, "'data'"], // Reserved keywords [Token.StatemachineKeyword, "'statemachine'"], @@ -383,6 +385,7 @@ export const Keywords: ReadonlyMap = new Map([ ["never", Token.NeverKeyword], ["unknown", Token.UnknownKeyword], ["extern", Token.ExternKeyword], + ["data", Token.DataKeyword], ["internal", Token.InternalKeyword], // Reserved keywords diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 29560bd3609..9ff66cdb8b1 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -721,6 +721,8 @@ export interface Decorator extends BaseType { target: MixedFunctionParameter; parameters: MixedFunctionParameter[]; implementation: (ctx: DecoratorContext, target: Type, ...args: unknown[]) => void; + /** Whether this is a data decorator (declared with `data dec`). */ + isData?: boolean; } /** @@ -1175,6 +1177,7 @@ export enum SyntaxKind { CallExpression, ScalarConstructor, InternalKeyword, + DataKeyword, FunctionTypeExpression, } @@ -1741,6 +1744,10 @@ export interface InternalKeywordNode extends BaseNode { readonly kind: SyntaxKind.InternalKeyword; } +export interface DataKeywordNode extends BaseNode { + readonly kind: SyntaxKind.DataKeyword; +} + export interface VoidKeywordNode extends BaseNode { readonly kind: SyntaxKind.VoidKeyword; } @@ -1797,11 +1804,12 @@ export const enum ModifierFlags { None, Extern = 1 << 1, Internal = 1 << 2, + Data = 1 << 3, - All = Extern | Internal, + All = Extern | Internal | Data, } -export type Modifier = ExternKeywordNode | InternalKeywordNode; +export type Modifier = ExternKeywordNode | InternalKeywordNode | DataKeywordNode; /** * Represent a decorator declaration diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 5ceb7b70abc..73765ad59f1 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -259,6 +259,8 @@ export function printNode( return "extern"; case SyntaxKind.InternalKeyword: return "internal"; + case SyntaxKind.DataKeyword: + return "data"; case SyntaxKind.VoidKeyword: return "void"; case SyntaxKind.NeverKeyword: diff --git a/packages/compiler/test/formatter/formatter.test.ts b/packages/compiler/test/formatter/formatter.test.ts index 1360a82e543..916052a66bb 100644 --- a/packages/compiler/test/formatter/formatter.test.ts +++ b/packages/compiler/test/formatter/formatter.test.ts @@ -2975,6 +2975,28 @@ internal extern dec foo(target: Type, arg1: StringLiteral); }); }); + it("format data dec", async () => { + await assertFormat({ + code: ` +data dec foo(target: Type, arg1: StringLiteral); +`, + expected: ` +data dec foo(target: Type, arg1: StringLiteral); +`, + }); + }); + + it("format internal data dec", async () => { + await assertFormat({ + code: ` +internal data dec foo(target: Type, arg1: StringLiteral); +`, + expected: ` +internal data dec foo(target: Type, arg1: StringLiteral); +`, + }); + }); + it("format internal extern fn", async () => { await assertFormat({ code: ` diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index e3d78e94607..ab89deaa460 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -866,6 +866,10 @@ describe("compiler: parser", () => { parseEach([ "dec myDec(target: Type);", "extern dec myDec(target: Type);", + "data dec myDec(target: Type);", + "data dec myDec(target: Type, arg1: StringLiteral);", + "internal data dec myDec(target: Type);", + "namespace Lib { data dec myDec(target: Type);}", "namespace Lib { extern dec myDec(target: Type);}", "extern dec myDec(target: Type, arg1: StringLiteral);", "extern dec myDec(target: Type, optional?: StringLiteral);", diff --git a/packages/compiler/test/scanner.test.ts b/packages/compiler/test/scanner.test.ts index 9dfa8f645fa..15754bdd6a4 100644 --- a/packages/compiler/test/scanner.test.ts +++ b/packages/compiler/test/scanner.test.ts @@ -397,6 +397,7 @@ describe("compiler: scanner", () => { Token.UnknownKeyword, Token.ExternKeyword, Token.InternalKeyword, + Token.DataKeyword, Token.ValueOfKeyword, Token.TypeOfKeyword, // `fn` can be either a statement or the start of an expr depending on context. From b25c44f9d706077cc4ae71cc51eebcdf903962df Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 30 Mar 2026 09:42:19 -0400 Subject: [PATCH 02/13] pass 2 --- packages/compiler/src/core/checker.ts | 6 +- packages/compiler/src/core/messages.ts | 2 + packages/compiler/src/core/modifiers.ts | 56 ++++++++-- packages/compiler/src/core/parser.ts | 2 +- packages/compiler/src/core/types.ts | 4 +- packages/compiler/src/index.ts | 5 + packages/compiler/src/lib/data-decorator.ts | 57 ++++++++++ .../compiler/test/checker/decorators.test.ts | 92 +++++++++++++++- packages/events/lib/decorators.tsp | 2 +- .../components/data-decorator-accessors.tsx | 102 ++++++++++++++++++ .../components/decorator-signature-type.tsx | 2 +- .../components/entity-signature-tests.tsx | 8 +- .../components/entity-signatures.tsx | 66 +++++++----- .../external-packages/compiler.ts | 2 + .../gen-extern-signatures.ts | 1 + .../tspd/src/gen-extern-signatures/types.ts | 3 + .../decorators-signatures.test.ts | 73 +++++++++++++ 17 files changed, 434 insertions(+), 49 deletions(-) create mode 100644 packages/compiler/src/lib/data-decorator.ts create mode 100644 packages/tspd/src/gen-extern-signatures/components/data-decorator-accessors.tsx diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 4de42c203b4..1440f06675a 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2116,7 +2116,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker target: checkFunctionParameter(ctx, node.target, true), parameters: node.parameters.map((param) => checkFunctionParameter(ctx, param, true)), implementation: implementation ?? (() => {}), - ...(isData ? { isData: true } : {}), + declarationKind: isData ? "data" : "extern", }); namespace.decoratorDeclarations.set(name, decoratorType); @@ -2135,9 +2135,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const paramNames = node.parameters.map((p) => p.id.sv); if (paramNames.length === 0) { - // No args beyond target — boolean flag via stateSet + // No args beyond target — store `true` as a boolean flag in stateMap return (context: DecoratorContext, target: Type) => { - context.program.stateSet(stateKey).add(target); + context.program.stateMap(stateKey).set(target, true); }; } else if (paramNames.length === 1) { // Single arg — store value directly diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 71e73369579..a593513f3f2 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -552,6 +552,8 @@ const diagnostics = { messages: { default: paramMessage`Modifier '${"modifier"}' is invalid.`, "missing-required": paramMessage`Declaration of type '${"nodeKind"}' is missing required modifier '${"modifier"}'.`, + "missing-required-one-of": paramMessage`Declaration of type '${"nodeKind"}' is missing one of the required modifiers: ${"modifiers"}.`, + "mutually-exclusive": paramMessage`Modifiers '${"modifierA"}' and '${"modifierB"}' cannot be used together.`, "not-allowed": paramMessage`Modifier '${"modifier"}' cannot be used on declarations of type '${"nodeKind"}'.`, }, }, diff --git a/packages/compiler/src/core/modifiers.ts b/packages/compiler/src/core/modifiers.ts index 6e4b49f4941..e9b95957469 100644 --- a/packages/compiler/src/core/modifiers.ts +++ b/packages/compiler/src/core/modifiers.ts @@ -12,8 +12,10 @@ import { Declaration, Modifier, ModifierFlags, SyntaxKind } from "./types.js"; interface ModifierCompatibility { /** A set of modifier flags that are allowed on the node type. */ readonly allowed: ModifierFlags; - /** A set of modifier flags that are _required_ on the node type. */ + /** At least one of these modifier flags must be present. */ readonly required: ModifierFlags; + /** Pairs of modifier flags that cannot be used together. */ + readonly mutuallyExclusive?: readonly [ModifierFlags, ModifierFlags][]; } /** @@ -44,7 +46,8 @@ const SYNTAX_MODIFIERS: Readonly `'${n}'`).join(" or "), + nodeKind: getDeclarationKindText(node.kind), + }, target: node, }), ); } } + if (compatibility.mutuallyExclusive) { + for (const [a, b] of compatibility.mutuallyExclusive) { + if (node.modifierFlags & a && node.modifierFlags & b) { + isValid = false; + + const nameA = getNamesOfModifierFlags(a)[0]; + const nameB = getNamesOfModifierFlags(b)[0]; + program.reportDiagnostic( + createDiagnostic({ + code: "invalid-modifier", + messageId: "mutually-exclusive", + format: { modifierA: nameA, modifierB: nameB }, + target: node, + }), + ); + } + } + } + return isValid; } @@ -148,6 +181,8 @@ function modifierToFlag(modifier: Modifier): ModifierFlags { return ModifierFlags.Extern; case SyntaxKind.InternalKeyword: return ModifierFlags.Internal; + case SyntaxKind.DataKeyword: + return ModifierFlags.Data; default: compilerAssert(false, `Unknown modifier kind: ${(modifier as Modifier).kind}`); } @@ -159,6 +194,8 @@ function getTextForModifier(modifier: Modifier): string { return "extern"; case SyntaxKind.InternalKeyword: return "internal"; + case SyntaxKind.DataKeyword: + return "data"; default: compilerAssert(false, `Unknown modifier kind: ${(modifier as Modifier).kind}`); } @@ -172,6 +209,9 @@ function getNamesOfModifierFlags(flags: ModifierFlags): string[] { if (flags & ModifierFlags.Internal) { names.push("internal"); } + if (flags & ModifierFlags.Data) { + names.push("data"); + } return names; } diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 613988fd02a..0f15c0e3409 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -2115,7 +2115,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): DecoratorDeclarationStatementNode { const modifierFlags = modifiersToFlags(modifiers); parseExpected(Token.DecKeyword); - const id = parseIdentifier(); + const id = parseIdentifier({ allowReservedIdentifier: true }); const allParamListDetail = parseFunctionParameters(); let [target, ...parameters] = allParamListDetail.items; if (target === undefined) { diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 9ff66cdb8b1..4f4315a07d7 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -721,8 +721,8 @@ export interface Decorator extends BaseType { target: MixedFunctionParameter; parameters: MixedFunctionParameter[]; implementation: (ctx: DecoratorContext, target: Type, ...args: unknown[]) => void; - /** Whether this is a data decorator (declared with `data dec`). */ - isData?: boolean; + /** How this decorator was declared. */ + declarationKind: "extern" | "data"; } /** diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 99122b7b8d0..9b19304c33c 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -105,6 +105,11 @@ export { NodeHost } from "./core/node-host.js"; export { isNumeric, Numeric } from "./core/numeric.js"; export type { CompilerOptions } from "./core/options.js"; export { getPositionBeforeTrivia } from "./core/parser-utils.js"; +export { + getDataDecoratorTargets, + getDataDecoratorValue, + hasDataDecorator, +} from "./lib/data-decorator.js"; export { $defaultVisibility, $discriminator, diff --git a/packages/compiler/src/lib/data-decorator.ts b/packages/compiler/src/lib/data-decorator.ts new file mode 100644 index 00000000000..dc6b575b3ff --- /dev/null +++ b/packages/compiler/src/lib/data-decorator.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT License. + +import type { Program } from "../core/program.js"; +import type { Type } from "../core/types.js"; + +/** + * Get the state key for a data decorator given its fully-qualified name. + * @internal + */ +export function getDataDecoratorStateKey(decoratorFqn: string): symbol { + return Symbol.for(`data-dec:${decoratorFqn}`); +} + +/** + * Check if a data decorator has been applied to a target. + * @param program - The current program. + * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). + * @param target - The type to check. + */ +export function hasDataDecorator(program: Program, decoratorFqn: string, target: Type): boolean { + const key = getDataDecoratorStateKey(decoratorFqn); + return program.stateMap(key).has(target); +} + +/** + * Get the stored value for a data decorator applied to a target. + * For no-arg data decorators, returns `true` if applied, `undefined` otherwise. + * For single-arg data decorators, returns the value directly. + * For multi-arg data decorators, returns a record of `{ paramName: value }`. + * @param program - The current program. + * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). + * @param target - The type to get the value for. + * @returns The stored value, or `undefined` if the decorator was not applied. + */ +export function getDataDecoratorValue( + program: Program, + decoratorFqn: string, + target: Type, +): unknown | undefined { + const key = getDataDecoratorStateKey(decoratorFqn); + return program.stateMap(key).get(target); +} + +/** + * Get all targets that have a specific data decorator applied, along with their stored values. + * @param program - The current program. + * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). + * @returns A map of target types to their stored values. + */ +export function getDataDecoratorTargets( + program: Program, + decoratorFqn: string, +): Map { + const key = getDataDecoratorStateKey(decoratorFqn); + return program.stateMap(key); +} diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index cd46988fc77..6dcf71aba12 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -111,13 +111,24 @@ describe("compiler: checker: decorators", () => { }); }); - it("errors if decorator is missing extern modifier", async () => { + it("errors if decorator is missing extern or data modifier", async () => { const diagnostics = await DecTester.diagnose(` dec testDec(target: unknown); `); expectDiagnostics(diagnostics, { code: "invalid-modifier", - message: "Declaration of type 'dec' is missing required modifier 'extern'.", + message: + "Declaration of type 'dec' is missing one of the required modifiers: 'extern' or 'data'.", + }); + }); + + it("errors if both extern and data modifiers are used", async () => { + const diagnostics = await DecTester.diagnose(` + data extern dec testDec(target: unknown); + `); + expectDiagnostics(diagnostics, { + code: "invalid-modifier", + message: "Modifiers 'extern' and 'data' cannot be used together.", }); }); @@ -142,6 +153,83 @@ describe("compiler: checker: decorators", () => { }); }); + describe("data decorators", () => { + it("data decorator does not require an implementation", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile(` + data dec myFlag(target: Model); + `); + + const dec = program.getGlobalNamespaceType().decoratorDeclarations.get("myFlag"); + ok(dec); + strictEqual(dec.declarationKind, "data"); + ok(dec.implementation, "should have auto-generated implementation"); + }); + + it("data decorator with no args stores in stateMap", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile(` + data dec myFlag(target: Model); + + @myFlag + model Foo {} + `); + + const Foo = program.getGlobalNamespaceType().models.get("Foo")!; + ok(Foo, "Foo should exist"); + const { getDataDecoratorValue } = await import("../../src/lib/data-decorator.js"); + strictEqual(getDataDecoratorValue(program, "myFlag", Foo), true); + }); + + it("data decorator with single arg stores value in stateMap", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile(` + data dec myLabel(target: Model, label: valueof string); + + @myLabel("hello") + model Foo {} + `); + + const Foo = program.getGlobalNamespaceType().models.get("Foo")!; + const { getDataDecoratorValue } = await import("../../src/lib/data-decorator.js"); + strictEqual(getDataDecoratorValue(program, "myLabel", Foo), "hello"); + }); + + it("data decorator with multiple args stores named record in stateMap", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile(` + data dec myMeta(target: Model, name: valueof string, version: valueof int32); + + @myMeta("test", 42) + model Foo {} + `); + + const Foo = program.getGlobalNamespaceType().models.get("Foo")!; + const { getDataDecoratorValue } = await import("../../src/lib/data-decorator.js"); + const value = getDataDecoratorValue(program, "myMeta", Foo) as any; + deepStrictEqual(value, { name: "test", version: 42 }); + }); + + it("data decorator in namespace uses FQN for state key", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile(` + namespace MyLib { + data dec myLabel(target: Model, label: valueof string); + } + + @MyLib.myLabel("world") + model Foo {} + `); + + const Foo = program.getGlobalNamespaceType().models.get("Foo")!; + const { getDataDecoratorValue } = await import("../../src/lib/data-decorator.js"); + strictEqual(getDataDecoratorValue(program, "MyLib.myLabel", Foo), "world"); + }); + + it("internal data dec is valid", async () => { + const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose(` + #suppress "experimental-feature" + internal data dec myDec(target: unknown); + `); + strictEqual(diagnostics.length, 0); + }); + }); + describe("usage", () => { let calledArgs: any[] | undefined; const UsageTester = Tester.files({ diff --git a/packages/events/lib/decorators.tsp b/packages/events/lib/decorators.tsp index a216bd869b1..75ce10c3c26 100644 --- a/packages/events/lib/decorators.tsp +++ b/packages/events/lib/decorators.tsp @@ -56,4 +56,4 @@ extern dec contentType(target: UnionVariant | ModelProperty, contentType: valueo * } * ``` */ -extern dec data(target: ModelProperty); +extern dec `data`(target: ModelProperty); diff --git a/packages/tspd/src/gen-extern-signatures/components/data-decorator-accessors.tsx b/packages/tspd/src/gen-extern-signatures/components/data-decorator-accessors.tsx new file mode 100644 index 00000000000..c9fb13d5989 --- /dev/null +++ b/packages/tspd/src/gen-extern-signatures/components/data-decorator-accessors.tsx @@ -0,0 +1,102 @@ +import { code, For } from "@alloy-js/core"; +import * as ts from "@alloy-js/typescript"; +import { typespecCompiler } from "../external-packages/compiler.js"; +import { DecoratorSignature } from "../types.js"; +import { ParameterTsType, TargetParameterTsType } from "./decorator-signature-type.js"; + +export interface DataDecoratorAccessorsProps { + decorators: DecoratorSignature[]; + namespaceName: string; +} + +/** + * Generate typed accessor functions for data decorators. + * These are thin wrappers around the compiler's generic data decorator API. + */ +export function DataDecoratorAccessors(props: Readonly) { + const dataDecorators = props.decorators.filter((d) => d.isData); + if (dataDecorators.length === 0) { + return undefined; + } + + return ( + + {(signature) => ( + + )} + + ); +} + +interface DataDecoratorAccessorProps { + signature: DecoratorSignature; + namespaceName: string; +} + +function DataDecoratorAccessor(props: Readonly) { + const decorator = props.signature.decorator; + const name = decorator.name.slice(1); // remove @ + const capitalizedName = name[0].toUpperCase() + name.slice(1); + const fqn = props.namespaceName ? `${props.namespaceName}.${name}` : name; + const params = decorator.parameters; + const targetType = ; + + if (params.length === 0) { + // No-arg data decorator — generate `is*` function + return ( + + {code`return ${typespecCompiler.hasDataDecorator}(program, "${fqn}", ${decorator.target.name});`} + + ); + } + + // Decorators with args — generate `get*` function + let returnType; + if (params.length === 1) { + const param = params[0]; + returnType = ( + <> + + {" | undefined"} + + ); + } else { + // Multi-arg — return type is an interface with named properties + returnType = ( + <> + {"{"} + + {(param) => ( + <> + {" "} + {param.name}: + + )} + + {" } | undefined"} + + ); + } + + return ( + + {code`return ${typespecCompiler.getDataDecoratorValue}(program, "${fqn}", ${decorator.target.name}) as any;`} + + ); +} 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 58d98921bb5..48f851e2077 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 @@ -117,7 +117,7 @@ export function ParameterTsType({ constraint }: ParameterTsTypeProps) { return typespecCompiler.Type; } -function TargetParameterTsType(props: { type: Type | undefined }) { +export function TargetParameterTsType(props: { type: Type | undefined }) { const type = props.type; if (type === undefined) { return typespecCompiler.Type; diff --git a/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx b/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx index 939b289db50..2bc681259a2 100644 --- a/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx +++ b/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx @@ -1,6 +1,6 @@ import { Refkey, Show } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; -import { EntitySignature } from "../types.js"; +import { DecoratorSignature, EntitySignature } from "../types.js"; export interface EntitySignatureTests { namespaceName: string; @@ -19,12 +19,14 @@ export function EntitySignatureTests({ dollarFunctionsRefKey, dollarFunctionsTypeRefKey, }: Readonly) { - const hasDecorators = entities.some((e) => e.kind === "Decorator"); + const hasExternDecorators = entities.some( + (e): e is DecoratorSignature => e.kind === "Decorator" && !e.isData, + ); const hasFunctions = entities.some((e) => e.kind === "Function"); return ( <> - + e.kind === "Decorator"); + const externDecorators = decorators.filter((d) => !d.isData); + const dataDecorators = decorators.filter((d) => d.isData); const functions = entities.filter((e): e is FunctionSignature => e.kind === "Function"); return ( - - - 0}> - - - - {(signature) => } - - - - - - 0}> - - - - {(signature) => } - + <> + + + 0}> + + + + {(signature) => } + + + + + + 0}> + + + + {(signature) => } + + + + + + + 0}> - + - + ); } 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 80a0013da31..a65e5f37a46 100644 --- a/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts +++ b/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts @@ -24,6 +24,8 @@ export const typespecCompiler = createPackage({ "Numeric", "ScalarValue", "DecoratorValidatorCallbacks", + "getDataDecoratorValue", + "hasDataDecorator", ], }, }, diff --git a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts index 0fd36a7f075..995ae0d3c98 100644 --- a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts @@ -194,6 +194,7 @@ function resolveDecoratorSignature(decorator: Decorator): DecoratorSignature { name: decorator.name, jsName: "$" + decorator.name.slice(1), typeName: decorator.name[1].toUpperCase() + decorator.name.slice(2) + "Decorator", + isData: decorator.declarationKind === "data", }; } diff --git a/packages/tspd/src/gen-extern-signatures/types.ts b/packages/tspd/src/gen-extern-signatures/types.ts index cb01eb3d7ca..d0d39f265fb 100644 --- a/packages/tspd/src/gen-extern-signatures/types.ts +++ b/packages/tspd/src/gen-extern-signatures/types.ts @@ -15,6 +15,9 @@ export interface DecoratorSignature { typeName: string; decorator: Decorator; + + /** Whether this is a data decorator (declared with `data dec`). */ + isData: boolean; } export interface FunctionSignature { 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 27ff5c8eb74..3ae72d5af8a 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -423,3 +423,76 @@ function importLine(imports: string[]) { const all = new Set(["DecoratorContext", "DecoratorValidatorCallbacks", ...imports]); return `import type { ${[...all].sort().join(", ")} } from "@typespec/compiler";`; } + +function dataImportLine(typeImports: string[], valueImports: string[]) { + const parts: string[] = []; + const all = [...valueImports, ...typeImports.map((t) => `type ${t}`)]; + all.sort((a, b) => { + const nameA = a.replace("type ", ""); + const nameB = b.replace("type ", ""); + return nameA.localeCompare(nameB); + }); + return `import { ${all.join(", ")} } from "@typespec/compiler";`; +} + +describe("data decorator accessors", () => { + it("generate accessor for no-arg data decorator (boolean flag)", async () => { + await expectSignatures({ + code: `data dec myFlag(target: Model);`, + expected: ` +${dataImportLine(["Model", "Program"], ["hasDataDecorator"])} + +export function isMyFlag(program: Program, target: Model): boolean { + return hasDataDecorator(program, "myFlag", target); +} + `, + }); + }); + + it("generate accessor for single-arg data decorator", async () => { + await expectSignatures({ + code: `data dec myLabel(target: Model, label: valueof string);`, + expected: ` +${dataImportLine(["Model", "Program"], ["getDataDecoratorValue"])} + +export function getMyLabel(program: Program, target: Model): string | undefined { + return getDataDecoratorValue(program, "myLabel", target) as any; +} + `, + }); + }); + + it("generate accessor for multi-arg data decorator", async () => { + await expectSignatures({ + code: `data dec myMeta(target: Model, name: valueof string, version: valueof int32);`, + expected: ` +${dataImportLine(["Model", "Program"], ["getDataDecoratorValue"])} + +export function getMyMeta(program: Program, target: Model): { name: string; version: number } | undefined { + return getDataDecoratorValue(program, "myMeta", target) as any; +} + `, + }); + }); + + it("does not generate $decorators type for data decorators", async () => { + const result = await generateDecoratorSignatures(`data dec myFlag(target: Model);`); + expect(result).not.toContain("Decorators"); + expect(result).not.toContain("$myFlag"); + }); + + it("generates both extern and data decorator outputs when mixed", async () => { + const result = await generateDecoratorSignatures(` + extern dec externDec(target: Model); + data dec dataFlag(target: Model); + `); + // Verify extern decorator parts + expect(result).toContain("ExternDecDecorator"); + expect(result).toContain("externDec: ExternDecDecorator"); + // Verify data decorator parts + expect(result).toContain("isDataFlag"); + expect(result).toContain("hasDataDecorator"); + // Verify no $decorators type for data decorators + expect(result).not.toContain("dataFlag: "); + }); +}); From f9a34b4ec14aa840eab2602484618a47fc5f671b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 30 Mar 2026 10:09:35 -0400 Subject: [PATCH 03/13] chg --- .chronus/changes/data-decorators-2026-3-30.md | 16 +++++ .../changes/data-decorators-tspd-2026-3-30.md | 7 ++ .../extending-typespec/create-decorators.md | 71 +++++++++++++++++++ .../docs/docs/language-basics/decorators.md | 6 ++ 4 files changed, 100 insertions(+) create mode 100644 .chronus/changes/data-decorators-2026-3-30.md create mode 100644 .chronus/changes/data-decorators-tspd-2026-3-30.md diff --git a/.chronus/changes/data-decorators-2026-3-30.md b/.chronus/changes/data-decorators-2026-3-30.md new file mode 100644 index 00000000000..4a7996d8c6b --- /dev/null +++ b/.chronus/changes/data-decorators-2026-3-30.md @@ -0,0 +1,16 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Added `data` decorator modifier for declaring decorators that auto-store their arguments as metadata without requiring a JavaScript implementation. + +```typespec +data dec label(target: Model, value: valueof string); + +@label("my-model") +model Foo {} +``` + +Added compiler API `hasDataDecorator`, `getDataDecoratorValue`, and `getDataDecoratorTargets` for reading data decorator values by FQN. diff --git a/.chronus/changes/data-decorators-tspd-2026-3-30.md b/.chronus/changes/data-decorators-tspd-2026-3-30.md new file mode 100644 index 00000000000..74ecfce1547 --- /dev/null +++ b/.chronus/changes/data-decorators-tspd-2026-3-30.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/tspd" +--- + +`tspd gen-extern-signature` now generates typed accessor functions for `data` decorators (e.g., `isMyFlag`, `getMyLabel`). 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 71c3ae95d09..d58dcfcef90 100644 --- a/website/src/content/docs/docs/extending-typespec/create-decorators.md +++ b/website/src/content/docs/docs/extending-typespec/create-decorators.md @@ -8,6 +8,77 @@ TypeSpec decorators are implemented as JavaScript functions. The process of crea 1. [Declare the decorator signature in TypeSpec](#declare-the-decorator-signature) (optional but recommended) 2. [Implement the decorator in JavaScript](#javascript-decorator-implementation) +Alternatively, for decorators that simply store metadata, you can use [data decorators](#data-decorators) which require no JavaScript implementation at all. + +## Data decorators + +Data decorators are a simplified way to declare decorators that only store metadata. They are declared with the `data` modifier and require no JavaScript implementation — the compiler auto-generates the storage logic. + +```typespec +// A boolean flag (no parameters beyond the target) +data dec deprecated(target: unknown); + +// A single value +data dec label(target: Model, value: valueof string); + +// Multiple values (stored as a named record) +data dec metadata(target: Model, name: valueof string, version: valueof int32); +``` + +### How data is stored + +Data decorator arguments are stored automatically in the program's state map, keyed by the decorator's fully-qualified name: + +- **No parameters** (flag): stores `true` +- **Single parameter**: stores the value directly +- **Multiple parameters**: stores a record with parameter names as keys, e.g. `{ name: "hello", version: 1 }` + +### Reading data decorator values + +The compiler provides a generic API to read data decorator values without any generated code: + +```ts +import { hasDataDecorator, getDataDecoratorValue } from "@typespec/compiler"; + +// Check if a flag decorator was applied +if (hasDataDecorator(program, "MyLib.deprecated", type)) { + // ... +} + +// Get the stored value +const label = getDataDecoratorValue(program, "MyLib.label", type) as string; + +// Get a multi-arg record +const meta = getDataDecoratorValue(program, "MyLib.metadata", type) as { + name: string; + version: number; +}; +``` + +### Generated typed accessors + +When using `tspd gen-extern-signature`, typed accessor functions are generated for data decorators: + +```ts +// Generated for: data dec deprecated(target: Model); +export function isDeprecated(program: Program, target: Model): boolean; + +// Generated for: data dec label(target: Model, value: valueof string); +export function getLabel(program: Program, target: Model): string | undefined; +``` + +### Combining with `internal` + +Data decorators can be combined with the `internal` modifier: + +```typespec +internal data dec myInternalFlag(target: Model); +``` + +:::note +`data` and `extern` are mutually exclusive — a decorator is either auto-implemented (data) or externally implemented (extern). +::: + ## Declare the decorator signature While this step is optional, it offers significant benefits: diff --git a/website/src/content/docs/docs/language-basics/decorators.md b/website/src/content/docs/docs/language-basics/decorators.md index 92c820853a0..f017a890504 100644 --- a/website/src/content/docs/docs/language-basics/decorators.md +++ b/website/src/content/docs/docs/language-basics/decorators.md @@ -62,3 +62,9 @@ model Dog { ## Creating decorators For more information on creating decorators, see [Creating Decorators](../extending-typespec/create-decorators.md). + +For decorators that simply attach metadata without custom logic, TypeSpec provides [data decorators](../extending-typespec/create-decorators.md#data-decorators) which require no JavaScript implementation: + +```typespec +data dec label(target: Model, value: valueof string); +``` From 80a5c9421e28516e73d6d4dccc5d5ff65bb056e3 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 30 Mar 2026 13:29:42 -0400 Subject: [PATCH 04/13] fix --- packages/events/lib/decorators.tsp | 2 +- .../html-program-viewer/src/react/type-config.ts | 1 + .../decorators-signatures.test.ts | 1 - .../docs/extending-typespec/create-decorators.md | 12 ++++++------ 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/events/lib/decorators.tsp b/packages/events/lib/decorators.tsp index 75ce10c3c26..a216bd869b1 100644 --- a/packages/events/lib/decorators.tsp +++ b/packages/events/lib/decorators.tsp @@ -56,4 +56,4 @@ extern dec contentType(target: UnionVariant | ModelProperty, contentType: valueo * } * ``` */ -extern dec `data`(target: ModelProperty); +extern dec data(target: ModelProperty); diff --git a/packages/html-program-viewer/src/react/type-config.ts b/packages/html-program-viewer/src/react/type-config.ts index 8bda40b820a..68c2caa8366 100644 --- a/packages/html-program-viewer/src/react/type-config.ts +++ b/packages/html-program-viewer/src/react/type-config.ts @@ -118,6 +118,7 @@ export const TypeConfig: TypeGraphConfig = buildConfig({ parameters: "nested-items", implementation: "skip", target: "ref", + declarationKind: "value", }, ScalarConstructor: { scalar: "parent", 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 3ae72d5af8a..7a5678e78e6 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -425,7 +425,6 @@ function importLine(imports: string[]) { } function dataImportLine(typeImports: string[], valueImports: string[]) { - const parts: string[] = []; const all = [...valueImports, ...typeImports.map((t) => `type ${t}`)]; all.sort((a, b) => { const nameA = a.replace("type ", ""); 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 d58dcfcef90..bb8fb372b43 100644 --- a/website/src/content/docs/docs/extending-typespec/create-decorators.md +++ b/website/src/content/docs/docs/extending-typespec/create-decorators.md @@ -16,13 +16,13 @@ Data decorators are a simplified way to declare decorators that only store metad ```typespec // A boolean flag (no parameters beyond the target) -data dec deprecated(target: unknown); +data dec tracked(target: unknown); // A single value data dec label(target: Model, value: valueof string); // Multiple values (stored as a named record) -data dec metadata(target: Model, name: valueof string, version: valueof int32); +data dec serviceInfo(target: Model, name: valueof string, version: valueof int32); ``` ### How data is stored @@ -41,7 +41,7 @@ The compiler provides a generic API to read data decorator values without any ge import { hasDataDecorator, getDataDecoratorValue } from "@typespec/compiler"; // Check if a flag decorator was applied -if (hasDataDecorator(program, "MyLib.deprecated", type)) { +if (hasDataDecorator(program, "MyLib.tracked", type)) { // ... } @@ -49,7 +49,7 @@ if (hasDataDecorator(program, "MyLib.deprecated", type)) { const label = getDataDecoratorValue(program, "MyLib.label", type) as string; // Get a multi-arg record -const meta = getDataDecoratorValue(program, "MyLib.metadata", type) as { +const info = getDataDecoratorValue(program, "MyLib.serviceInfo", type) as { name: string; version: number; }; @@ -60,8 +60,8 @@ const meta = getDataDecoratorValue(program, "MyLib.metadata", type) as { When using `tspd gen-extern-signature`, typed accessor functions are generated for data decorators: ```ts -// Generated for: data dec deprecated(target: Model); -export function isDeprecated(program: Program, target: Model): boolean; +// Generated for: data dec tracked(target: Model); +export function isTracked(program: Program, target: Model): boolean; // Generated for: data dec label(target: Model, value: valueof string); export function getLabel(program: Program, target: Model): string | undefined; From c6e831cf2ef12d90e39f3abada7f79764c6b0ea2 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 30 Mar 2026 11:31:34 -0700 Subject: [PATCH 05/13] Change changeKind from 'fix' to 'internal' --- .chronus/changes/data-decorators-2026-2-30-17-38-53.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/data-decorators-2026-2-30-17-38-53.md diff --git a/.chronus/changes/data-decorators-2026-2-30-17-38-53.md b/.chronus/changes/data-decorators-2026-2-30-17-38-53.md new file mode 100644 index 00000000000..aff2f00c104 --- /dev/null +++ b/.chronus/changes/data-decorators-2026-2-30-17-38-53.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@typespec/html-program-viewer" +--- + +Data decorators From a26092761bda1d074eda2b89f6a93c4efa74989a Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 4 Jun 2026 09:25:52 -0400 Subject: [PATCH 06/13] Apply discussion --- packages/compiler/src/core/checker.ts | 47 ++++++------- packages/compiler/src/core/features.ts | 4 ++ packages/compiler/src/core/messages.ts | 2 + packages/compiler/src/core/modifiers.ts | 16 ++--- packages/compiler/src/core/parser.ts | 18 ++--- packages/compiler/src/core/scanner.ts | 10 +-- packages/compiler/src/core/types.ts | 14 ++-- .../compiler/src/formatter/print/printer.ts | 4 +- packages/compiler/src/lib/data-decorator.ts | 12 ++-- .../compiler/test/checker/decorators.test.ts | 70 ++++++++++++------- .../test/core/cli/actions/info.test.ts | 1 + .../compiler/test/formatter/formatter.test.ts | 12 ++-- packages/compiler/test/parser.test.ts | 8 +-- packages/compiler/test/scanner.test.ts | 2 +- .../test/server/completion.tspconfig.test.ts | 13 ++-- .../gen-extern-signatures.ts | 2 +- .../decorators-signatures.test.ts | 32 +++++---- .../extending-typespec/create-decorators.md | 39 +++++------ .../docs/docs/language-basics/decorators.md | 4 +- 19 files changed, 167 insertions(+), 143 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 2e6545d6ec5..eefe25d11fc 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2120,10 +2120,19 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); const name = node.id.sv; - const isData = (node.modifierFlags & ModifierFlags.Data) !== 0; + const isAuto = (node.modifierFlags & ModifierFlags.Auto) !== 0; + if (isAuto && !isCompilerFeatureEnabled(program, "auto-decorators", node)) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "experimental-feature", + messageId: "autoDecorators", + target: node, + }), + ); + } let implementation = symbol.value; - if (isData) { - implementation = createDataDecoratorImplementation(symbol, node); + if (isAuto) { + implementation = createAutoDecoratorImplementation(symbol, node); } else if (implementation === undefined) { reportCheckerDiagnostic(createDiagnostic({ code: "missing-implementation", target: node })); } @@ -2135,7 +2144,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker target: checkFunctionParameter(ctx, node.target, true), parameters: node.parameters.map((param) => checkFunctionParameter(ctx, param, true)), implementation: implementation ?? (() => {}), - declarationKind: isData ? "data" : "extern", + declarationKind: isAuto ? "auto" : "extern", }); namespace.decoratorDeclarations.set(name, decoratorType); @@ -2145,7 +2154,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return decoratorType; } - function createDataDecoratorImplementation( + function createAutoDecoratorImplementation( symbol: Sym, node: DecoratorDeclarationStatementNode, ): (ctx: DecoratorContext, target: Type, ...args: unknown[]) => void { @@ -2153,26 +2162,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const stateKey = getDataDecoratorStateKey(fqn); const paramNames = node.parameters.map((p) => p.id.sv); - if (paramNames.length === 0) { - // No args beyond target — store `true` as a boolean flag in stateMap - return (context: DecoratorContext, target: Type) => { - context.program.stateMap(stateKey).set(target, true); - }; - } else if (paramNames.length === 1) { - // Single arg — store value directly - return (context: DecoratorContext, target: Type, value: unknown) => { - context.program.stateMap(stateKey).set(target, value); - }; - } else { - // Multiple args — store as named record - return (context: DecoratorContext, target: Type, ...args: unknown[]) => { - const data: Record = {}; - for (let i = 0; i < paramNames.length; i++) { - data[paramNames[i]] = args[i]; - } - context.program.stateMap(stateKey).set(target, data); - }; - } + // Always store as key-value record { paramName: value } + return (context: DecoratorContext, target: Type, ...args: unknown[]) => { + const data: Record = {}; + for (let i = 0; i < paramNames.length; i++) { + data[paramNames[i]] = args[i]; + } + context.program.stateMap(stateKey).set(target, data); + }; } function checkFunctionDeclaration( diff --git a/packages/compiler/src/core/features.ts b/packages/compiler/src/core/features.ts index f1b2449fa3e..01547806fe0 100644 --- a/packages/compiler/src/core/features.ts +++ b/packages/compiler/src/core/features.ts @@ -11,6 +11,10 @@ export const compilerFeatures = { description: "Allows use of function declarations without experimental warnings in project code.", }, + "auto-decorators": { + description: + "Allows use of auto decorator declarations without experimental warnings in project code.", + }, } as const satisfies Record; export type CompilerFeatureName = keyof typeof compilerFeatures; diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index db7c168ac0a..df7950b8791 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -267,6 +267,8 @@ const diagnostics = { default: paramMessage`${"feature"} is an experimental feature. It may change in the future or be removed. Use with caution and consider providing feedback on this feature.`, functionDeclarations: "Function declarations are an experimental feature that may change in the future. Use with caution and consider providing feedback to the TypeSpec team.", + autoDecorators: + "Auto decorator declarations are an experimental feature that may change in the future. Use with caution and consider providing feedback to the TypeSpec team.", }, }, "using-invalid-ref": { diff --git a/packages/compiler/src/core/modifiers.ts b/packages/compiler/src/core/modifiers.ts index fa3f5388e37..85c3706e222 100644 --- a/packages/compiler/src/core/modifiers.ts +++ b/packages/compiler/src/core/modifiers.ts @@ -46,8 +46,8 @@ const SYNTAX_MODIFIERS: Readonly(node: Node, cb: NodeCallback): T | undefined case SyntaxKind.NeverKeyword: case SyntaxKind.ExternKeyword: case SyntaxKind.InternalKeyword: - case SyntaxKind.DataKeyword: + case SyntaxKind.AutoKeyword: case SyntaxKind.UnknownKeyword: case SyntaxKind.JsSourceFile: case SyntaxKind.JsNamespaceDeclaration: diff --git a/packages/compiler/src/core/scanner.ts b/packages/compiler/src/core/scanner.ts index fda053c5589..d940a6b4dd5 100644 --- a/packages/compiler/src/core/scanner.ts +++ b/packages/compiler/src/core/scanner.ts @@ -139,7 +139,7 @@ export enum Token { ExternKeyword = __StartModifierKeyword, InternalKeyword, - DataKeyword, + AutoKeyword, /** @internal */ __EndModifierKeyword, /////////////////////////////////////////////////////////////// @@ -195,7 +195,6 @@ export enum Token { ImplKeyword, SatisfiesKeyword, FlagKeyword, - AutoKeyword, PartialKeyword, PrivateKeyword, PublicKeyword, @@ -311,7 +310,7 @@ export const TokenDisplay = getTokenDisplayTable([ [Token.NeverKeyword, "'never'"], [Token.UnknownKeyword, "'unknown'"], [Token.ExternKeyword, "'extern'"], - [Token.DataKeyword, "'data'"], + [Token.AutoKeyword, "'auto'"], // Reserved keywords [Token.StatemachineKeyword, "'statemachine'"], @@ -344,7 +343,6 @@ export const TokenDisplay = getTokenDisplayTable([ [Token.ImplKeyword, "'impl'"], [Token.SatisfiesKeyword, "'satisfies'"], [Token.FlagKeyword, "'flag'"], - [Token.AutoKeyword, "'auto'"], [Token.PartialKeyword, "'partial'"], [Token.PrivateKeyword, "'private'"], [Token.PublicKeyword, "'public'"], @@ -385,7 +383,7 @@ export const Keywords: ReadonlyMap = new Map([ ["never", Token.NeverKeyword], ["unknown", Token.UnknownKeyword], ["extern", Token.ExternKeyword], - ["data", Token.DataKeyword], + ["auto", Token.AutoKeyword], ["internal", Token.InternalKeyword], // Reserved keywords @@ -419,7 +417,6 @@ export const Keywords: ReadonlyMap = new Map([ ["impl", Token.ImplKeyword], ["satisfies", Token.SatisfiesKeyword], ["flag", Token.FlagKeyword], - ["auto", Token.AutoKeyword], ["partial", Token.PartialKeyword], ["private", Token.PrivateKeyword], ["public", Token.PublicKeyword], @@ -458,7 +455,6 @@ export const ReservedKeywords: ReadonlyMap = new Map([ ["impl", Token.ImplKeyword], ["satisfies", Token.SatisfiesKeyword], ["flag", Token.FlagKeyword], - ["auto", Token.AutoKeyword], ["partial", Token.PartialKeyword], ["private", Token.PrivateKeyword], ["public", Token.PublicKeyword], diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 157fc4a3794..50c36efbf4b 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -759,7 +759,7 @@ export interface Decorator extends BaseType { parameters: MixedFunctionParameter[]; implementation: (ctx: DecoratorContext, target: Type, ...args: unknown[]) => void; /** How this decorator was declared. */ - declarationKind: "extern" | "data"; + declarationKind: "extern" | "auto"; } /** @@ -1214,7 +1214,7 @@ export enum SyntaxKind { CallExpression, ScalarConstructor, InternalKeyword, - DataKeyword, + AutoKeyword, FunctionTypeExpression, } @@ -1781,8 +1781,8 @@ export interface InternalKeywordNode extends BaseNode { readonly kind: SyntaxKind.InternalKeyword; } -export interface DataKeywordNode extends BaseNode { - readonly kind: SyntaxKind.DataKeyword; +export interface AutoKeywordNode extends BaseNode { + readonly kind: SyntaxKind.AutoKeyword; } export interface VoidKeywordNode extends BaseNode { @@ -1841,12 +1841,12 @@ export const enum ModifierFlags { None, Extern = 1 << 1, Internal = 1 << 2, - Data = 1 << 3, + Auto = 1 << 3, - All = Extern | Internal | Data, + All = Extern | Internal | Auto, } -export type Modifier = ExternKeywordNode | InternalKeywordNode | DataKeywordNode; +export type Modifier = ExternKeywordNode | InternalKeywordNode | AutoKeywordNode; /** * Represent a decorator declaration diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 404dfca4bbe..aa4fafd56ad 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -259,8 +259,8 @@ export function printNode( return "extern"; case SyntaxKind.InternalKeyword: return "internal"; - case SyntaxKind.DataKeyword: - return "data"; + case SyntaxKind.AutoKeyword: + return "auto"; case SyntaxKind.VoidKeyword: return "void"; case SyntaxKind.NeverKeyword: diff --git a/packages/compiler/src/lib/data-decorator.ts b/packages/compiler/src/lib/data-decorator.ts index dc6b575b3ff..4e34c97a1ac 100644 --- a/packages/compiler/src/lib/data-decorator.ts +++ b/packages/compiler/src/lib/data-decorator.ts @@ -24,22 +24,20 @@ export function hasDataDecorator(program: Program, decoratorFqn: string, target: } /** - * Get the stored value for a data decorator applied to a target. - * For no-arg data decorators, returns `true` if applied, `undefined` otherwise. - * For single-arg data decorators, returns the value directly. - * For multi-arg data decorators, returns a record of `{ paramName: value }`. + * Get the stored value for an auto decorator applied to a target. + * Always returns a record of `{ paramName: value }` (empty record `{}` for no-arg decorators). * @param program - The current program. * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). * @param target - The type to get the value for. - * @returns The stored value, or `undefined` if the decorator was not applied. + * @returns The stored record, or `undefined` if the decorator was not applied. */ export function getDataDecoratorValue( program: Program, decoratorFqn: string, target: Type, -): unknown | undefined { +): Record | undefined { const key = getDataDecoratorStateKey(decoratorFqn); - return program.stateMap(key).get(target); + return program.stateMap(key).get(target) as Record | undefined; } /** diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 6dcf71aba12..304cdc46b51 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -111,25 +111,30 @@ describe("compiler: checker: decorators", () => { }); }); - it("errors if decorator is missing extern or data modifier", async () => { + it("errors if decorator is missing extern or auto modifier", async () => { const diagnostics = await DecTester.diagnose(` dec testDec(target: unknown); `); expectDiagnostics(diagnostics, { code: "invalid-modifier", message: - "Declaration of type 'dec' is missing one of the required modifiers: 'extern' or 'data'.", + "Declaration of type 'dec' is missing one of the required modifiers: 'extern' or 'auto'.", }); }); - it("errors if both extern and data modifiers are used", async () => { + it("errors if both extern and auto modifiers are used", async () => { const diagnostics = await DecTester.diagnose(` - data extern dec testDec(target: unknown); + auto extern dec testDec(target: unknown); `); - expectDiagnostics(diagnostics, { - code: "invalid-modifier", - message: "Modifiers 'extern' and 'data' cannot be used together.", - }); + expectDiagnostics(diagnostics, [ + { + code: "invalid-modifier", + message: "Modifiers 'extern' and 'auto' cannot be used together.", + }, + { + code: "experimental-feature", + }, + ]); }); it("errors if rest parameter type is not an array expression", async () => { @@ -153,21 +158,23 @@ describe("compiler: checker: decorators", () => { }); }); - describe("data decorators", () => { - it("data decorator does not require an implementation", async () => { + describe("auto decorators", () => { + it("auto decorator does not require an implementation", async () => { const { program } = await Tester.using("TypeSpec.Reflection").compile(` - data dec myFlag(target: Model); + #suppress "experimental-feature" + auto dec myFlag(target: Model); `); const dec = program.getGlobalNamespaceType().decoratorDeclarations.get("myFlag"); ok(dec); - strictEqual(dec.declarationKind, "data"); + strictEqual(dec.declarationKind, "auto"); ok(dec.implementation, "should have auto-generated implementation"); }); - it("data decorator with no args stores in stateMap", async () => { + it("auto decorator with no args stores empty record in stateMap", async () => { const { program } = await Tester.using("TypeSpec.Reflection").compile(` - data dec myFlag(target: Model); + #suppress "experimental-feature" + auto dec myFlag(target: Model); @myFlag model Foo {} @@ -176,12 +183,13 @@ describe("compiler: checker: decorators", () => { const Foo = program.getGlobalNamespaceType().models.get("Foo")!; ok(Foo, "Foo should exist"); const { getDataDecoratorValue } = await import("../../src/lib/data-decorator.js"); - strictEqual(getDataDecoratorValue(program, "myFlag", Foo), true); + deepStrictEqual(getDataDecoratorValue(program, "myFlag", Foo), {}); }); - it("data decorator with single arg stores value in stateMap", async () => { + it("auto decorator with single arg stores as key-value record in stateMap", async () => { const { program } = await Tester.using("TypeSpec.Reflection").compile(` - data dec myLabel(target: Model, label: valueof string); + #suppress "experimental-feature" + auto dec myLabel(target: Model, label: valueof string); @myLabel("hello") model Foo {} @@ -189,12 +197,13 @@ describe("compiler: checker: decorators", () => { const Foo = program.getGlobalNamespaceType().models.get("Foo")!; const { getDataDecoratorValue } = await import("../../src/lib/data-decorator.js"); - strictEqual(getDataDecoratorValue(program, "myLabel", Foo), "hello"); + deepStrictEqual(getDataDecoratorValue(program, "myLabel", Foo), { label: "hello" }); }); - it("data decorator with multiple args stores named record in stateMap", async () => { + it("auto decorator with multiple args stores named record in stateMap", async () => { const { program } = await Tester.using("TypeSpec.Reflection").compile(` - data dec myMeta(target: Model, name: valueof string, version: valueof int32); + #suppress "experimental-feature" + auto dec myMeta(target: Model, name: valueof string, version: valueof int32); @myMeta("test", 42) model Foo {} @@ -206,10 +215,11 @@ describe("compiler: checker: decorators", () => { deepStrictEqual(value, { name: "test", version: 42 }); }); - it("data decorator in namespace uses FQN for state key", async () => { + it("auto decorator in namespace uses FQN for state key", async () => { const { program } = await Tester.using("TypeSpec.Reflection").compile(` namespace MyLib { - data dec myLabel(target: Model, label: valueof string); + #suppress "experimental-feature" + auto dec myLabel(target: Model, label: valueof string); } @MyLib.myLabel("world") @@ -218,16 +228,26 @@ describe("compiler: checker: decorators", () => { const Foo = program.getGlobalNamespaceType().models.get("Foo")!; const { getDataDecoratorValue } = await import("../../src/lib/data-decorator.js"); - strictEqual(getDataDecoratorValue(program, "MyLib.myLabel", Foo), "world"); + deepStrictEqual(getDataDecoratorValue(program, "MyLib.myLabel", Foo), { label: "world" }); }); - it("internal data dec is valid", async () => { + it("internal auto dec is valid", async () => { const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose(` #suppress "experimental-feature" - internal data dec myDec(target: unknown); + internal auto dec myDec(target: unknown); `); strictEqual(diagnostics.length, 0); }); + + it("emits experimental warning without feature flag", async () => { + const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose(` + auto dec myFlag(target: Model); + `); + expectDiagnostics(diagnostics, { + code: "experimental-feature", + message: /Auto decorator declarations are an experimental feature/, + }); + }); }); describe("usage", () => { diff --git a/packages/compiler/test/core/cli/actions/info.test.ts b/packages/compiler/test/core/cli/actions/info.test.ts index 244a1b86fc3..ad2fb784148 100644 --- a/packages/compiler/test/core/cli/actions/info.test.ts +++ b/packages/compiler/test/core/cli/actions/info.test.ts @@ -22,6 +22,7 @@ describe("formatCompilerFeatures", () => { "Compiler Features", "", " enabled function-declarations Allows use of function declarations without experimental warnings in project code.", + " disabled auto-decorators Allows use of auto decorator declarations without experimental warnings in project code.", ]); }); }); diff --git a/packages/compiler/test/formatter/formatter.test.ts b/packages/compiler/test/formatter/formatter.test.ts index c5309edb29f..1f18484ac0f 100644 --- a/packages/compiler/test/formatter/formatter.test.ts +++ b/packages/compiler/test/formatter/formatter.test.ts @@ -3030,24 +3030,24 @@ internal extern dec foo(target: Type, arg1: StringLiteral); }); }); - it("format data dec", async () => { + it("format auto dec", async () => { await assertFormat({ code: ` -data dec foo(target: Type, arg1: StringLiteral); +auto dec foo(target: Type, arg1: StringLiteral); `, expected: ` -data dec foo(target: Type, arg1: StringLiteral); +auto dec foo(target: Type, arg1: StringLiteral); `, }); }); - it("format internal data dec", async () => { + it("format internal auto dec", async () => { await assertFormat({ code: ` -internal data dec foo(target: Type, arg1: StringLiteral); +internal auto dec foo(target: Type, arg1: StringLiteral); `, expected: ` -internal data dec foo(target: Type, arg1: StringLiteral); +internal auto dec foo(target: Type, arg1: StringLiteral); `, }); }); diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index ab89deaa460..4e20f08e7d9 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -866,10 +866,10 @@ describe("compiler: parser", () => { parseEach([ "dec myDec(target: Type);", "extern dec myDec(target: Type);", - "data dec myDec(target: Type);", - "data dec myDec(target: Type, arg1: StringLiteral);", - "internal data dec myDec(target: Type);", - "namespace Lib { data dec myDec(target: Type);}", + "auto dec myDec(target: Type);", + "auto dec myDec(target: Type, arg1: StringLiteral);", + "internal auto dec myDec(target: Type);", + "namespace Lib { auto dec myDec(target: Type);}", "namespace Lib { extern dec myDec(target: Type);}", "extern dec myDec(target: Type, arg1: StringLiteral);", "extern dec myDec(target: Type, optional?: StringLiteral);", diff --git a/packages/compiler/test/scanner.test.ts b/packages/compiler/test/scanner.test.ts index 15754bdd6a4..dde07fe87b5 100644 --- a/packages/compiler/test/scanner.test.ts +++ b/packages/compiler/test/scanner.test.ts @@ -397,7 +397,7 @@ describe("compiler: scanner", () => { Token.UnknownKeyword, Token.ExternKeyword, Token.InternalKeyword, - Token.DataKeyword, + Token.AutoKeyword, Token.ValueOfKeyword, Token.TypeOfKeyword, // `fn` can be either a statement or the start of an expr depending on context. diff --git a/packages/compiler/test/server/completion.tspconfig.test.ts b/packages/compiler/test/server/completion.tspconfig.test.ts index e7b58e43bab..9e27ae368f9 100644 --- a/packages/compiler/test/server/completion.tspconfig.test.ts +++ b/packages/compiler/test/server/completion.tspconfig.test.ts @@ -134,19 +134,19 @@ describe("Test completion items for features", () => { it.each([ { config: `features:\n - ┆`, - expected: ['"function-declarations"'], + expected: ['"auto-decorators"', '"function-declarations"'], }, { config: `features:\n - "┆"`, - expected: ["function-declarations"], + expected: ["auto-decorators", "function-declarations"], }, { config: `features:\n - "function┆"`, - expected: ["function-declarations"], + expected: ["auto-decorators", "function-declarations"], }, { config: `features:\n - function-declarations\n - ┆`, - expected: [], + expected: ['"auto-decorators"'], }, ])("#%# Test features: $config", async ({ config, expected }) => { await checkCompletionItems(config, true, expected); @@ -156,7 +156,10 @@ describe("Test completion items for features", () => { await checkCompletionItems( `features:\n - ┆`, true, - ["Allows use of function declarations without experimental warnings in project code."], + [ + "Allows use of auto decorator declarations without experimental warnings in project code.", + "Allows use of function declarations without experimental warnings in project code.", + ], true, ); }); diff --git a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts index 995ae0d3c98..0127a0011dc 100644 --- a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts @@ -194,7 +194,7 @@ function resolveDecoratorSignature(decorator: Decorator): DecoratorSignature { name: decorator.name, jsName: "$" + decorator.name.slice(1), typeName: decorator.name[1].toUpperCase() + decorator.name.slice(2) + "Decorator", - isData: decorator.declarationKind === "data", + isData: decorator.declarationKind === "auto", }; } 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 7a5678e78e6..4399889a4e9 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -17,7 +17,11 @@ async function generateDecoratorSignatures(code: string) { compilerOptions: { parseOptions: { comments: true, docs: true } }, }); - expectDiagnosticEmpty(program.diagnostics.filter((x) => x.code !== "missing-implementation")); + expectDiagnosticEmpty( + program.diagnostics.filter( + (x) => x.code !== "missing-implementation" && x.code !== "experimental-feature", + ), + ); const result = await generateExternDecorators(program, "test-lib", { prettierConfig: { @@ -434,10 +438,10 @@ function dataImportLine(typeImports: string[], valueImports: string[]) { return `import { ${all.join(", ")} } from "@typespec/compiler";`; } -describe("data decorator accessors", () => { - it("generate accessor for no-arg data decorator (boolean flag)", async () => { +describe("auto decorator accessors", () => { + it("generate accessor for no-arg auto decorator (boolean flag)", async () => { await expectSignatures({ - code: `data dec myFlag(target: Model);`, + code: `auto dec myFlag(target: Model);`, expected: ` ${dataImportLine(["Model", "Program"], ["hasDataDecorator"])} @@ -448,9 +452,9 @@ export function isMyFlag(program: Program, target: Model): boolean { }); }); - it("generate accessor for single-arg data decorator", async () => { + it("generate accessor for single-arg auto decorator", async () => { await expectSignatures({ - code: `data dec myLabel(target: Model, label: valueof string);`, + code: `auto dec myLabel(target: Model, label: valueof string);`, expected: ` ${dataImportLine(["Model", "Program"], ["getDataDecoratorValue"])} @@ -461,9 +465,9 @@ export function getMyLabel(program: Program, target: Model): string | undefined }); }); - it("generate accessor for multi-arg data decorator", async () => { + it("generate accessor for multi-arg auto decorator", async () => { await expectSignatures({ - code: `data dec myMeta(target: Model, name: valueof string, version: valueof int32);`, + code: `auto dec myMeta(target: Model, name: valueof string, version: valueof int32);`, expected: ` ${dataImportLine(["Model", "Program"], ["getDataDecoratorValue"])} @@ -474,24 +478,24 @@ export function getMyMeta(program: Program, target: Model): { name: string; vers }); }); - it("does not generate $decorators type for data decorators", async () => { - const result = await generateDecoratorSignatures(`data dec myFlag(target: Model);`); + it("does not generate $decorators type for auto decorators", async () => { + const result = await generateDecoratorSignatures(`auto dec myFlag(target: Model);`); expect(result).not.toContain("Decorators"); expect(result).not.toContain("$myFlag"); }); - it("generates both extern and data decorator outputs when mixed", async () => { + it("generates both extern and auto decorator outputs when mixed", async () => { const result = await generateDecoratorSignatures(` extern dec externDec(target: Model); - data dec dataFlag(target: Model); + auto dec dataFlag(target: Model); `); // Verify extern decorator parts expect(result).toContain("ExternDecDecorator"); expect(result).toContain("externDec: ExternDecDecorator"); - // Verify data decorator parts + // Verify auto decorator parts expect(result).toContain("isDataFlag"); expect(result).toContain("hasDataDecorator"); - // Verify no $decorators type for data decorators + // Verify no $decorators type for auto decorators expect(result).not.toContain("dataFlag: "); }); }); 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 bb8fb372b43..45196bf6dd0 100644 --- a/website/src/content/docs/docs/extending-typespec/create-decorators.md +++ b/website/src/content/docs/docs/extending-typespec/create-decorators.md @@ -8,34 +8,33 @@ TypeSpec decorators are implemented as JavaScript functions. The process of crea 1. [Declare the decorator signature in TypeSpec](#declare-the-decorator-signature) (optional but recommended) 2. [Implement the decorator in JavaScript](#javascript-decorator-implementation) -Alternatively, for decorators that simply store metadata, you can use [data decorators](#data-decorators) which require no JavaScript implementation at all. +Alternatively, for decorators that simply store metadata, you can use [auto decorators](#auto-decorators) which require no JavaScript implementation at all. -## Data decorators +## Auto decorators -Data decorators are a simplified way to declare decorators that only store metadata. They are declared with the `data` modifier and require no JavaScript implementation — the compiler auto-generates the storage logic. +Auto decorators are a simplified way to declare decorators that only store metadata. They are declared with the `auto` modifier and require no JavaScript implementation — the compiler auto-generates the storage logic. ```typespec // A boolean flag (no parameters beyond the target) -data dec tracked(target: unknown); +auto dec tracked(target: unknown); // A single value -data dec label(target: Model, value: valueof string); +auto dec label(target: Model, value: valueof string); // Multiple values (stored as a named record) -data dec serviceInfo(target: Model, name: valueof string, version: valueof int32); +auto dec serviceInfo(target: Model, name: valueof string, version: valueof int32); ``` ### How data is stored -Data decorator arguments are stored automatically in the program's state map, keyed by the decorator's fully-qualified name: +Auto decorator arguments are always stored as a record with parameter names as keys in the program's state map, keyed by the decorator's fully-qualified name: -- **No parameters** (flag): stores `true` -- **Single parameter**: stores the value directly -- **Multiple parameters**: stores a record with parameter names as keys, e.g. `{ name: "hello", version: 1 }` +- **No parameters** (flag): stores `{}` (empty record) +- **One or more parameters**: stores `{ paramName: value, ... }`, e.g. `{ name: "hello", version: 1 }` -### Reading data decorator values +### Reading auto decorator values -The compiler provides a generic API to read data decorator values without any generated code: +The compiler provides a generic API to read auto decorator values without any generated code: ```ts import { hasDataDecorator, getDataDecoratorValue } from "@typespec/compiler"; @@ -45,8 +44,8 @@ if (hasDataDecorator(program, "MyLib.tracked", type)) { // ... } -// Get the stored value -const label = getDataDecoratorValue(program, "MyLib.label", type) as string; +// Get the stored record +const label = getDataDecoratorValue(program, "MyLib.label", type) as { value: string }; // Get a multi-arg record const info = getDataDecoratorValue(program, "MyLib.serviceInfo", type) as { @@ -57,26 +56,26 @@ const info = getDataDecoratorValue(program, "MyLib.serviceInfo", type) as { ### Generated typed accessors -When using `tspd gen-extern-signature`, typed accessor functions are generated for data decorators: +When using `tspd gen-extern-signature`, typed accessor functions are generated for auto decorators: ```ts -// Generated for: data dec tracked(target: Model); +// Generated for: auto dec tracked(target: Model); export function isTracked(program: Program, target: Model): boolean; -// Generated for: data dec label(target: Model, value: valueof string); +// Generated for: auto dec label(target: Model, value: valueof string); export function getLabel(program: Program, target: Model): string | undefined; ``` ### Combining with `internal` -Data decorators can be combined with the `internal` modifier: +Auto decorators can be combined with the `internal` modifier: ```typespec -internal data dec myInternalFlag(target: Model); +internal auto dec myInternalFlag(target: Model); ``` :::note -`data` and `extern` are mutually exclusive — a decorator is either auto-implemented (data) or externally implemented (extern). +`auto` and `extern` are mutually exclusive — a decorator is either auto-implemented (auto) or externally implemented (extern). ::: ## Declare the decorator signature diff --git a/website/src/content/docs/docs/language-basics/decorators.md b/website/src/content/docs/docs/language-basics/decorators.md index f017a890504..b779393173b 100644 --- a/website/src/content/docs/docs/language-basics/decorators.md +++ b/website/src/content/docs/docs/language-basics/decorators.md @@ -63,8 +63,8 @@ model Dog { For more information on creating decorators, see [Creating Decorators](../extending-typespec/create-decorators.md). -For decorators that simply attach metadata without custom logic, TypeSpec provides [data decorators](../extending-typespec/create-decorators.md#data-decorators) which require no JavaScript implementation: +For decorators that simply attach metadata without custom logic, TypeSpec provides [auto decorators](../extending-typespec/create-decorators.md#auto-decorators) which require no JavaScript implementation: ```typespec -data dec label(target: Model, value: valueof string); +auto dec label(target: Model, value: valueof string); ``` From ff566b69916db5810e8b725f46051a4a6c647534 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 8 Jun 2026 13:32:02 -0400 Subject: [PATCH 07/13] Format --- packages/compiler/src/core/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 02b1402042e..d1effe15602 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -24,12 +24,12 @@ import { AnyKeywordNode, ArrayLiteralNode, AugmentDecoratorStatementNode, + AutoKeywordNode, BlockComment, BooleanLiteralNode, CallExpressionNode, Comment, ConstStatementNode, - AutoKeywordNode, Declaration, DeclarationNode, DecoratorDeclarationStatementNode, From 913b47b7498e93b3930e714716fbb74b43b5560e Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 8 Jun 2026 14:05:27 -0400 Subject: [PATCH 08/13] Fixes --- .chronus/changes/data-decorators-2026-3-30.md | 6 +- .../changes/data-decorators-tspd-2026-3-30.md | 2 +- packages/compiler/src/core/checker.ts | 41 ++++++- packages/compiler/src/core/parser.ts | 2 +- packages/compiler/src/index.ts | 8 +- packages/compiler/src/lib/data-decorator.ts | 55 --------- .../compiler/test/checker/decorators.test.ts | 115 ++++++++++++++++-- .../components/data-decorator-accessors.tsx | 102 ---------------- .../components/entity-signature-tests.tsx | 2 +- .../components/entity-signatures.tsx | 10 +- .../external-packages/compiler.ts | 4 +- .../gen-extern-signatures.ts | 2 +- .../tspd/src/gen-extern-signatures/types.ts | 4 +- .../decorators-signatures.test.ts | 16 +-- .../extending-typespec/create-decorators.md | 8 +- 15 files changed, 175 insertions(+), 202 deletions(-) delete mode 100644 packages/compiler/src/lib/data-decorator.ts delete mode 100644 packages/tspd/src/gen-extern-signatures/components/data-decorator-accessors.tsx diff --git a/.chronus/changes/data-decorators-2026-3-30.md b/.chronus/changes/data-decorators-2026-3-30.md index 4a7996d8c6b..778672b2020 100644 --- a/.chronus/changes/data-decorators-2026-3-30.md +++ b/.chronus/changes/data-decorators-2026-3-30.md @@ -4,13 +4,13 @@ packages: - "@typespec/compiler" --- -Added `data` decorator modifier for declaring decorators that auto-store their arguments as metadata without requiring a JavaScript implementation. +Added `auto` decorator modifier for declaring decorators that auto-store their arguments as metadata without requiring a JavaScript implementation. ```typespec -data dec label(target: Model, value: valueof string); +auto dec label(target: Model, value: valueof string); @label("my-model") model Foo {} ``` -Added compiler API `hasDataDecorator`, `getDataDecoratorValue`, and `getDataDecoratorTargets` for reading data decorator values by FQN. +Added compiler API `hasAutoDecorator`, `getAutoDecoratorValue`, and `getAutoDecoratorTargets` for reading auto decorator values by FQN. diff --git a/.chronus/changes/data-decorators-tspd-2026-3-30.md b/.chronus/changes/data-decorators-tspd-2026-3-30.md index 74ecfce1547..113435de2cc 100644 --- a/.chronus/changes/data-decorators-tspd-2026-3-30.md +++ b/.chronus/changes/data-decorators-tspd-2026-3-30.md @@ -4,4 +4,4 @@ packages: - "@typespec/tspd" --- -`tspd gen-extern-signature` now generates typed accessor functions for `data` decorators (e.g., `isMyFlag`, `getMyLabel`). +`tspd gen-extern-signature` now generates typed accessor functions for `auto` decorators (e.g., `isMyFlag`, `getMyLabel`). diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index eefe25d11fc..11bd47c404d 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1,5 +1,5 @@ import { Realm } from "../experimental/realm.js"; -import { getDataDecoratorStateKey } from "../lib/data-decorator.js"; +import { getAutoDecoratorStateKey } from "../lib/auto-decorator.js"; import { docFromCommentDecorator, getIndexer } from "../lib/intrinsic/decorators.js"; import { $ } from "../typekit/index.js"; import { DuplicateTracker } from "../utils/duplicate-tracker.js"; @@ -2159,14 +2159,45 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: DecoratorDeclarationStatementNode, ): (ctx: DecoratorContext, target: Type, ...args: unknown[]) => void { const fqn = getFullyQualifiedSymbolName(symbol); - const stateKey = getDataDecoratorStateKey(fqn); + const stateKey = getAutoDecoratorStateKey(fqn); const paramNames = node.parameters.map((p) => p.id.sv); + const lastParamIsRest = + node.parameters.length > 0 && node.parameters[node.parameters.length - 1].rest; - // Always store as key-value record { paramName: value } return (context: DecoratorContext, target: Type, ...args: unknown[]) => { + // Check for duplicate application on the same declaration node + if ("decorators" in target) { + const sameDecorators = (target as any).decorators.filter( + (x: any) => + x.definition?.name === `@${node.id.sv}` && + x.node?.kind === SyntaxKind.DecoratorExpression && + x.node?.parent === (target as any).node, + ); + if (sameDecorators.length > 1) { + context.program.reportDiagnostic( + createDiagnostic({ + code: "duplicate-decorator", + format: { decoratorName: `@${node.id.sv}` }, + target: context.decoratorTarget, + }), + ); + return; + } + } + + // Store as key-value record { paramName: value } const data: Record = {}; - for (let i = 0; i < paramNames.length; i++) { - data[paramNames[i]] = args[i]; + if (lastParamIsRest) { + // Non-rest params first + for (let i = 0; i < paramNames.length - 1; i++) { + data[paramNames[i]] = args[i]; + } + // Rest param collects remaining args as an array + data[paramNames[paramNames.length - 1]] = args.slice(paramNames.length - 1); + } else { + for (let i = 0; i < paramNames.length; i++) { + data[paramNames[i]] = args[i]; + } } context.program.stateMap(stateKey).set(target, data); }; diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index d1effe15602..340c3e4ceea 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -2115,7 +2115,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): DecoratorDeclarationStatementNode { const modifierFlags = modifiersToFlags(modifiers); parseExpected(Token.DecKeyword); - const id = parseIdentifier({ allowReservedIdentifier: true }); + const id = parseIdentifier(); const allParamListDetail = parseFunctionParameters(); let [target, ...parameters] = allParamListDetail.items; if (target === undefined) { diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 3edba182841..eada5814a79 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -106,10 +106,10 @@ export { isNumeric, Numeric } from "./core/numeric.js"; export type { CompilerOptions } from "./core/options.js"; export { getPositionBeforeTrivia } from "./core/parser-utils.js"; export { - getDataDecoratorTargets, - getDataDecoratorValue, - hasDataDecorator, -} from "./lib/data-decorator.js"; + getAutoDecoratorTargets, + getAutoDecoratorValue, + hasAutoDecorator, +} from "./lib/auto-decorator.js"; export { $defaultVisibility, $discriminator, diff --git a/packages/compiler/src/lib/data-decorator.ts b/packages/compiler/src/lib/data-decorator.ts deleted file mode 100644 index 4e34c97a1ac..00000000000 --- a/packages/compiler/src/lib/data-decorator.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation -// Licensed under the MIT License. - -import type { Program } from "../core/program.js"; -import type { Type } from "../core/types.js"; - -/** - * Get the state key for a data decorator given its fully-qualified name. - * @internal - */ -export function getDataDecoratorStateKey(decoratorFqn: string): symbol { - return Symbol.for(`data-dec:${decoratorFqn}`); -} - -/** - * Check if a data decorator has been applied to a target. - * @param program - The current program. - * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). - * @param target - The type to check. - */ -export function hasDataDecorator(program: Program, decoratorFqn: string, target: Type): boolean { - const key = getDataDecoratorStateKey(decoratorFqn); - return program.stateMap(key).has(target); -} - -/** - * Get the stored value for an auto decorator applied to a target. - * Always returns a record of `{ paramName: value }` (empty record `{}` for no-arg decorators). - * @param program - The current program. - * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). - * @param target - The type to get the value for. - * @returns The stored record, or `undefined` if the decorator was not applied. - */ -export function getDataDecoratorValue( - program: Program, - decoratorFqn: string, - target: Type, -): Record | undefined { - const key = getDataDecoratorStateKey(decoratorFqn); - return program.stateMap(key).get(target) as Record | undefined; -} - -/** - * Get all targets that have a specific data decorator applied, along with their stored values. - * @param program - The current program. - * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). - * @returns A map of target types to their stored values. - */ -export function getDataDecoratorTargets( - program: Program, - decoratorFqn: string, -): Map { - const key = getDataDecoratorStateKey(decoratorFqn); - return program.stateMap(key); -} diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 304cdc46b51..ce6956abebb 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -182,8 +182,8 @@ describe("compiler: checker: decorators", () => { const Foo = program.getGlobalNamespaceType().models.get("Foo")!; ok(Foo, "Foo should exist"); - const { getDataDecoratorValue } = await import("../../src/lib/data-decorator.js"); - deepStrictEqual(getDataDecoratorValue(program, "myFlag", Foo), {}); + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "myFlag", Foo), {}); }); it("auto decorator with single arg stores as key-value record in stateMap", async () => { @@ -196,8 +196,8 @@ describe("compiler: checker: decorators", () => { `); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; - const { getDataDecoratorValue } = await import("../../src/lib/data-decorator.js"); - deepStrictEqual(getDataDecoratorValue(program, "myLabel", Foo), { label: "hello" }); + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "myLabel", Foo), { label: "hello" }); }); it("auto decorator with multiple args stores named record in stateMap", async () => { @@ -210,8 +210,8 @@ describe("compiler: checker: decorators", () => { `); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; - const { getDataDecoratorValue } = await import("../../src/lib/data-decorator.js"); - const value = getDataDecoratorValue(program, "myMeta", Foo) as any; + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const value = getAutoDecoratorValue(program, "myMeta", Foo) as any; deepStrictEqual(value, { name: "test", version: 42 }); }); @@ -227,8 +227,8 @@ describe("compiler: checker: decorators", () => { `); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; - const { getDataDecoratorValue } = await import("../../src/lib/data-decorator.js"); - deepStrictEqual(getDataDecoratorValue(program, "MyLib.myLabel", Foo), { label: "world" }); + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "MyLib.myLabel", Foo), { label: "world" }); }); it("internal auto dec is valid", async () => { @@ -248,6 +248,105 @@ describe("compiler: checker: decorators", () => { message: /Auto decorator declarations are an experimental feature/, }); }); + + it("auto decorator with rest params stores as array in record", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile(` + #suppress "experimental-feature" + auto dec tags(target: Model, ...tags: valueof string[]); + + @tags("a", "b", "c") + model Foo {} + `); + + const Foo = program.getGlobalNamespaceType().models.get("Foo")!; + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "tags", Foo), { tags: ["a", "b", "c"] }); + }); + + it("auto decorator with mixed params and rest stores correctly", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile(` + #suppress "experimental-feature" + auto dec route(target: Model, path: valueof string, ...tags: valueof string[]); + + @route("/foo", "tag1", "tag2") + model Foo {} + `); + + const Foo = program.getGlobalNamespaceType().models.get("Foo")!; + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "route", Foo), { + path: "/foo", + tags: ["tag1", "tag2"], + }); + }); + + it("emits duplicate-decorator warning when applied twice on same node", async () => { + const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose(` + #suppress "experimental-feature" + auto dec myFlag(target: Model); + + @myFlag + @myFlag + model Foo {} + `); + expectDiagnostics(diagnostics, [ + { + code: "duplicate-decorator", + message: /Decorator @myFlag cannot be used twice on the same declaration/, + }, + { + code: "duplicate-decorator", + message: /Decorator @myFlag cannot be used twice on the same declaration/, + }, + ]); + }); + + it("augment decorator overwrites auto decorator value (last-write-wins)", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile(` + #suppress "experimental-feature" + auto dec myLabel(target: Model, label: valueof string); + + @myLabel("first") + model Foo {} + + @@myLabel(Foo, "second"); + `); + + const Foo = program.getGlobalNamespaceType().models.get("Foo")!; + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "myLabel", Foo), { label: "second" }); + }); + + it("auto decorator with optional param stores undefined for missing arg", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile(` + #suppress "experimental-feature" + auto dec myDec(target: Model, required: valueof string, optional?: valueof int32); + + @myDec("hello") + model Foo {} + `); + + const Foo = program.getGlobalNamespaceType().models.get("Foo")!; + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "myDec", Foo), { + required: "hello", + optional: undefined, + }); + }); + + it("getAutoDecoratorTargets returns all targets", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile(` + #suppress "experimental-feature" + auto dec tracked(target: Model); + + @tracked model Foo {} + @tracked model Bar {} + `); + + const { getAutoDecoratorTargets } = await import("../../src/lib/auto-decorator.js"); + const targets = getAutoDecoratorTargets(program, "tracked"); + strictEqual(targets.size, 2); + }); }); describe("usage", () => { diff --git a/packages/tspd/src/gen-extern-signatures/components/data-decorator-accessors.tsx b/packages/tspd/src/gen-extern-signatures/components/data-decorator-accessors.tsx deleted file mode 100644 index c9fb13d5989..00000000000 --- a/packages/tspd/src/gen-extern-signatures/components/data-decorator-accessors.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { code, For } from "@alloy-js/core"; -import * as ts from "@alloy-js/typescript"; -import { typespecCompiler } from "../external-packages/compiler.js"; -import { DecoratorSignature } from "../types.js"; -import { ParameterTsType, TargetParameterTsType } from "./decorator-signature-type.js"; - -export interface DataDecoratorAccessorsProps { - decorators: DecoratorSignature[]; - namespaceName: string; -} - -/** - * Generate typed accessor functions for data decorators. - * These are thin wrappers around the compiler's generic data decorator API. - */ -export function DataDecoratorAccessors(props: Readonly) { - const dataDecorators = props.decorators.filter((d) => d.isData); - if (dataDecorators.length === 0) { - return undefined; - } - - return ( - - {(signature) => ( - - )} - - ); -} - -interface DataDecoratorAccessorProps { - signature: DecoratorSignature; - namespaceName: string; -} - -function DataDecoratorAccessor(props: Readonly) { - const decorator = props.signature.decorator; - const name = decorator.name.slice(1); // remove @ - const capitalizedName = name[0].toUpperCase() + name.slice(1); - const fqn = props.namespaceName ? `${props.namespaceName}.${name}` : name; - const params = decorator.parameters; - const targetType = ; - - if (params.length === 0) { - // No-arg data decorator — generate `is*` function - return ( - - {code`return ${typespecCompiler.hasDataDecorator}(program, "${fqn}", ${decorator.target.name});`} - - ); - } - - // Decorators with args — generate `get*` function - let returnType; - if (params.length === 1) { - const param = params[0]; - returnType = ( - <> - - {" | undefined"} - - ); - } else { - // Multi-arg — return type is an interface with named properties - returnType = ( - <> - {"{"} - - {(param) => ( - <> - {" "} - {param.name}: - - )} - - {" } | undefined"} - - ); - } - - return ( - - {code`return ${typespecCompiler.getDataDecoratorValue}(program, "${fqn}", ${decorator.target.name}) as any;`} - - ); -} diff --git a/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx b/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx index 2bc681259a2..e87a7b99b0b 100644 --- a/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx +++ b/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx @@ -20,7 +20,7 @@ export function EntitySignatureTests({ dollarFunctionsTypeRefKey, }: Readonly) { const hasExternDecorators = entities.some( - (e): e is DecoratorSignature => e.kind === "Decorator" && !e.isData, + (e): e is DecoratorSignature => e.kind === "Decorator" && !e.isAuto, ); const hasFunctions = entities.some((e) => e.kind === "Function"); diff --git a/packages/tspd/src/gen-extern-signatures/components/entity-signatures.tsx b/packages/tspd/src/gen-extern-signatures/components/entity-signatures.tsx index a9340024e54..9f9c70b4dec 100644 --- a/packages/tspd/src/gen-extern-signatures/components/entity-signatures.tsx +++ b/packages/tspd/src/gen-extern-signatures/components/entity-signatures.tsx @@ -12,7 +12,7 @@ import * as ts from "@alloy-js/typescript"; import { Program } from "@typespec/compiler"; import { typespecCompiler } from "../external-packages/compiler.js"; import { DecoratorSignature, EntitySignature, FunctionSignature } from "../types.js"; -import { DataDecoratorAccessors } from "./data-decorator-accessors.js"; +import { AutoDecoratorAccessors } from "./auto-decorator-accessors.js"; import { DecoratorSignatureType, ValueOfModelTsInterfaceBody } from "./decorator-signature-type.js"; import { DollarDecoratorsType } from "./dollar-decorators-type.js"; import { DollarFunctionsType } from "./dollar-functions-type.jsx"; @@ -34,8 +34,8 @@ export function EntitySignatures({ dollarFunctionsRefKey: dollarFunctionsRefkey, }: EntitySignaturesProps) { const decorators = entities.filter((e): e is DecoratorSignature => e.kind === "Decorator"); - const externDecorators = decorators.filter((d) => !d.isData); - const dataDecorators = decorators.filter((d) => d.isData); + const externDecorators = decorators.filter((d) => !d.isAuto); + const autoDecorators = decorators.filter((d) => d.isAuto); const functions = entities.filter((e): e is FunctionSignature => e.kind === "Function"); @@ -72,10 +72,10 @@ export function EntitySignatures({ /> - 0}> + 0}> - + ); 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 a65e5f37a46..afff12fa90d 100644 --- a/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts +++ b/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts @@ -24,8 +24,8 @@ export const typespecCompiler = createPackage({ "Numeric", "ScalarValue", "DecoratorValidatorCallbacks", - "getDataDecoratorValue", - "hasDataDecorator", + "getAutoDecoratorValue", + "hasAutoDecorator", ], }, }, diff --git a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts index 0127a0011dc..e281cf7a72f 100644 --- a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts @@ -194,7 +194,7 @@ function resolveDecoratorSignature(decorator: Decorator): DecoratorSignature { name: decorator.name, jsName: "$" + decorator.name.slice(1), typeName: decorator.name[1].toUpperCase() + decorator.name.slice(2) + "Decorator", - isData: decorator.declarationKind === "auto", + isAuto: decorator.declarationKind === "auto", }; } diff --git a/packages/tspd/src/gen-extern-signatures/types.ts b/packages/tspd/src/gen-extern-signatures/types.ts index d0d39f265fb..1c94fec5d30 100644 --- a/packages/tspd/src/gen-extern-signatures/types.ts +++ b/packages/tspd/src/gen-extern-signatures/types.ts @@ -16,8 +16,8 @@ export interface DecoratorSignature { decorator: Decorator; - /** Whether this is a data decorator (declared with `data dec`). */ - isData: boolean; + /** Whether this is an auto decorator (declared with `auto dec`). */ + isAuto: boolean; } export interface FunctionSignature { 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 4399889a4e9..ac26014d974 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -428,7 +428,7 @@ function importLine(imports: string[]) { return `import type { ${[...all].sort().join(", ")} } from "@typespec/compiler";`; } -function dataImportLine(typeImports: string[], valueImports: string[]) { +function autoImportLine(typeImports: string[], valueImports: string[]) { const all = [...valueImports, ...typeImports.map((t) => `type ${t}`)]; all.sort((a, b) => { const nameA = a.replace("type ", ""); @@ -443,10 +443,10 @@ describe("auto decorator accessors", () => { await expectSignatures({ code: `auto dec myFlag(target: Model);`, expected: ` -${dataImportLine(["Model", "Program"], ["hasDataDecorator"])} +${autoImportLine(["Model", "Program"], ["hasAutoDecorator"])} export function isMyFlag(program: Program, target: Model): boolean { - return hasDataDecorator(program, "myFlag", target); + return hasAutoDecorator(program, "myFlag", target); } `, }); @@ -456,10 +456,10 @@ export function isMyFlag(program: Program, target: Model): boolean { await expectSignatures({ code: `auto dec myLabel(target: Model, label: valueof string);`, expected: ` -${dataImportLine(["Model", "Program"], ["getDataDecoratorValue"])} +${autoImportLine(["Model", "Program"], ["getAutoDecoratorValue"])} export function getMyLabel(program: Program, target: Model): string | undefined { - return getDataDecoratorValue(program, "myLabel", target) as any; + return getAutoDecoratorValue(program, "myLabel", target) as any; } `, }); @@ -469,10 +469,10 @@ export function getMyLabel(program: Program, target: Model): string | undefined await expectSignatures({ code: `auto dec myMeta(target: Model, name: valueof string, version: valueof int32);`, expected: ` -${dataImportLine(["Model", "Program"], ["getDataDecoratorValue"])} +${autoImportLine(["Model", "Program"], ["getAutoDecoratorValue"])} export function getMyMeta(program: Program, target: Model): { name: string; version: number } | undefined { - return getDataDecoratorValue(program, "myMeta", target) as any; + return getAutoDecoratorValue(program, "myMeta", target) as any; } `, }); @@ -494,7 +494,7 @@ export function getMyMeta(program: Program, target: Model): { name: string; vers expect(result).toContain("externDec: ExternDecDecorator"); // Verify auto decorator parts expect(result).toContain("isDataFlag"); - expect(result).toContain("hasDataDecorator"); + expect(result).toContain("hasAutoDecorator"); // Verify no $decorators type for auto decorators expect(result).not.toContain("dataFlag: "); }); 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 45196bf6dd0..02e41b1301d 100644 --- a/website/src/content/docs/docs/extending-typespec/create-decorators.md +++ b/website/src/content/docs/docs/extending-typespec/create-decorators.md @@ -37,18 +37,18 @@ Auto decorator arguments are always stored as a record with parameter names as k The compiler provides a generic API to read auto decorator values without any generated code: ```ts -import { hasDataDecorator, getDataDecoratorValue } from "@typespec/compiler"; +import { hasAutoDecorator, getAutoDecoratorValue } from "@typespec/compiler"; // Check if a flag decorator was applied -if (hasDataDecorator(program, "MyLib.tracked", type)) { +if (hasAutoDecorator(program, "MyLib.tracked", type)) { // ... } // Get the stored record -const label = getDataDecoratorValue(program, "MyLib.label", type) as { value: string }; +const label = getAutoDecoratorValue(program, "MyLib.label", type) as { value: string }; // Get a multi-arg record -const info = getDataDecoratorValue(program, "MyLib.serviceInfo", type) as { +const info = getAutoDecoratorValue(program, "MyLib.serviceInfo", type) as { name: string; version: number; }; From 1c5dc285bbe999a791d1cc4d855671cb13598caa Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 8 Jun 2026 14:43:49 -0400 Subject: [PATCH 09/13] Fixes --- packages/compiler/src/core/checker.ts | 3 +- packages/compiler/src/core/messages.ts | 9 +- packages/compiler/src/lib/auto-decorator.ts | 55 +++++++++ .../compiler/test/checker/decorators.test.ts | 116 ++++++++++++------ .../components/auto-decorator-accessors.tsx | 102 +++++++++++++++ .../decorators-signatures.test.ts | 15 ++- 6 files changed, 252 insertions(+), 48 deletions(-) create mode 100644 packages/compiler/src/lib/auto-decorator.ts create mode 100644 packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 11bd47c404d..6a19e70d309 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2124,8 +2124,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (isAuto && !isCompilerFeatureEnabled(program, "auto-decorators", node)) { reportCheckerDiagnostic( createDiagnostic({ - code: "experimental-feature", - messageId: "autoDecorators", + code: "auto-decorator-disabled", target: node, }), ); diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index df7950b8791..31a97d8cbc1 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -267,8 +267,13 @@ const diagnostics = { default: paramMessage`${"feature"} is an experimental feature. It may change in the future or be removed. Use with caution and consider providing feedback on this feature.`, functionDeclarations: "Function declarations are an experimental feature that may change in the future. Use with caution and consider providing feedback to the TypeSpec team.", - autoDecorators: - "Auto decorator declarations are an experimental feature that may change in the future. Use with caution and consider providing feedback to the TypeSpec team.", + }, + }, + "auto-decorator-disabled": { + severity: "error", + messages: { + default: + "Auto decorator declarations require the 'auto-decorators' feature to be enabled. Add 'auto-decorators' to the 'features' list in your tspconfig.yaml.", }, }, "using-invalid-ref": { diff --git a/packages/compiler/src/lib/auto-decorator.ts b/packages/compiler/src/lib/auto-decorator.ts new file mode 100644 index 00000000000..e5af5abf90a --- /dev/null +++ b/packages/compiler/src/lib/auto-decorator.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT License. + +import type { Program } from "../core/program.js"; +import type { Type } from "../core/types.js"; + +/** + * Get the state key for an auto decorator given its fully-qualified name. + * @internal + */ +export function getAutoDecoratorStateKey(decoratorFqn: string): symbol { + return Symbol.for(`auto-dec:${decoratorFqn}`); +} + +/** + * Check if an auto decorator has been applied to a target. + * @param program - The current program. + * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). + * @param target - The type to check. + */ +export function hasAutoDecorator(program: Program, decoratorFqn: string, target: Type): boolean { + const key = getAutoDecoratorStateKey(decoratorFqn); + return program.stateMap(key).has(target); +} + +/** + * Get the stored value for an auto decorator applied to a target. + * Always returns a record of `{ paramName: value }` (empty record `{}` for no-arg decorators). + * @param program - The current program. + * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). + * @param target - The type to get the value for. + * @returns The stored record, or `undefined` if the decorator was not applied. + */ +export function getAutoDecoratorValue( + program: Program, + decoratorFqn: string, + target: Type, +): Record | undefined { + const key = getAutoDecoratorStateKey(decoratorFqn); + return program.stateMap(key).get(target) as Record | undefined; +} + +/** + * Get all targets that have a specific auto decorator applied, along with their stored values. + * @param program - The current program. + * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). + * @returns A map of target types to their stored values. + */ +export function getAutoDecoratorTargets( + program: Program, + decoratorFqn: string, +): Map { + const key = getAutoDecoratorStateKey(decoratorFqn); + return program.stateMap(key); +} diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index ce6956abebb..4eafe169c65 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -132,7 +132,7 @@ describe("compiler: checker: decorators", () => { message: "Modifiers 'extern' and 'auto' cannot be used together.", }, { - code: "experimental-feature", + code: "auto-decorator-disabled", }, ]); }); @@ -159,11 +159,25 @@ describe("compiler: checker: decorators", () => { }); describe("auto decorators", () => { + const autoDecOptions = { + compilerOptions: { + configFile: { + projectRoot: ".", + kind: "project" as const, + features: ["auto-decorators"], + diagnostics: [] as any[], + outputDir: "tsp-output", + }, + }, + }; + it("auto decorator does not require an implementation", async () => { - const { program } = await Tester.using("TypeSpec.Reflection").compile(` - #suppress "experimental-feature" + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` auto dec myFlag(target: Model); - `); + `, + autoDecOptions, + ); const dec = program.getGlobalNamespaceType().decoratorDeclarations.get("myFlag"); ok(dec); @@ -172,13 +186,15 @@ describe("compiler: checker: decorators", () => { }); it("auto decorator with no args stores empty record in stateMap", async () => { - const { program } = await Tester.using("TypeSpec.Reflection").compile(` - #suppress "experimental-feature" + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` auto dec myFlag(target: Model); @myFlag model Foo {} - `); + `, + autoDecOptions, + ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; ok(Foo, "Foo should exist"); @@ -187,13 +203,15 @@ describe("compiler: checker: decorators", () => { }); it("auto decorator with single arg stores as key-value record in stateMap", async () => { - const { program } = await Tester.using("TypeSpec.Reflection").compile(` - #suppress "experimental-feature" + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` auto dec myLabel(target: Model, label: valueof string); @myLabel("hello") model Foo {} - `); + `, + autoDecOptions, + ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); @@ -201,13 +219,15 @@ describe("compiler: checker: decorators", () => { }); it("auto decorator with multiple args stores named record in stateMap", async () => { - const { program } = await Tester.using("TypeSpec.Reflection").compile(` - #suppress "experimental-feature" + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` auto dec myMeta(target: Model, name: valueof string, version: valueof int32); @myMeta("test", 42) model Foo {} - `); + `, + autoDecOptions, + ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); @@ -216,15 +236,17 @@ describe("compiler: checker: decorators", () => { }); it("auto decorator in namespace uses FQN for state key", async () => { - const { program } = await Tester.using("TypeSpec.Reflection").compile(` + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` namespace MyLib { - #suppress "experimental-feature" auto dec myLabel(target: Model, label: valueof string); } @MyLib.myLabel("world") model Foo {} - `); + `, + autoDecOptions, + ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); @@ -232,31 +254,35 @@ describe("compiler: checker: decorators", () => { }); it("internal auto dec is valid", async () => { - const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose(` - #suppress "experimental-feature" + const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose( + ` internal auto dec myDec(target: unknown); - `); + `, + autoDecOptions, + ); strictEqual(diagnostics.length, 0); }); - it("emits experimental warning without feature flag", async () => { + it("emits error without feature flag", async () => { const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose(` auto dec myFlag(target: Model); `); expectDiagnostics(diagnostics, { - code: "experimental-feature", - message: /Auto decorator declarations are an experimental feature/, + code: "auto-decorator-disabled", + message: /Auto decorator declarations require the 'auto-decorators' feature to be enabled/, }); }); it("auto decorator with rest params stores as array in record", async () => { - const { program } = await Tester.using("TypeSpec.Reflection").compile(` - #suppress "experimental-feature" + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` auto dec tags(target: Model, ...tags: valueof string[]); @tags("a", "b", "c") model Foo {} - `); + `, + autoDecOptions, + ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); @@ -264,13 +290,15 @@ describe("compiler: checker: decorators", () => { }); it("auto decorator with mixed params and rest stores correctly", async () => { - const { program } = await Tester.using("TypeSpec.Reflection").compile(` - #suppress "experimental-feature" + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` auto dec route(target: Model, path: valueof string, ...tags: valueof string[]); @route("/foo", "tag1", "tag2") model Foo {} - `); + `, + autoDecOptions, + ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); @@ -281,14 +309,16 @@ describe("compiler: checker: decorators", () => { }); it("emits duplicate-decorator warning when applied twice on same node", async () => { - const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose(` - #suppress "experimental-feature" + const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose( + ` auto dec myFlag(target: Model); @myFlag @myFlag model Foo {} - `); + `, + autoDecOptions, + ); expectDiagnostics(diagnostics, [ { code: "duplicate-decorator", @@ -302,15 +332,17 @@ describe("compiler: checker: decorators", () => { }); it("augment decorator overwrites auto decorator value (last-write-wins)", async () => { - const { program } = await Tester.using("TypeSpec.Reflection").compile(` - #suppress "experimental-feature" + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` auto dec myLabel(target: Model, label: valueof string); @myLabel("first") model Foo {} @@myLabel(Foo, "second"); - `); + `, + autoDecOptions, + ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); @@ -318,13 +350,15 @@ describe("compiler: checker: decorators", () => { }); it("auto decorator with optional param stores undefined for missing arg", async () => { - const { program } = await Tester.using("TypeSpec.Reflection").compile(` - #suppress "experimental-feature" + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` auto dec myDec(target: Model, required: valueof string, optional?: valueof int32); @myDec("hello") model Foo {} - `); + `, + autoDecOptions, + ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); @@ -335,13 +369,15 @@ describe("compiler: checker: decorators", () => { }); it("getAutoDecoratorTargets returns all targets", async () => { - const { program } = await Tester.using("TypeSpec.Reflection").compile(` - #suppress "experimental-feature" + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` auto dec tracked(target: Model); @tracked model Foo {} @tracked model Bar {} - `); + `, + autoDecOptions, + ); const { getAutoDecoratorTargets } = await import("../../src/lib/auto-decorator.js"); const targets = getAutoDecoratorTargets(program, "tracked"); diff --git a/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx b/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx new file mode 100644 index 00000000000..e74663dd875 --- /dev/null +++ b/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx @@ -0,0 +1,102 @@ +import { code, For } from "@alloy-js/core"; +import * as ts from "@alloy-js/typescript"; +import { typespecCompiler } from "../external-packages/compiler.js"; +import { DecoratorSignature } from "../types.js"; +import { ParameterTsType, TargetParameterTsType } from "./decorator-signature-type.js"; + +export interface AutoDecoratorAccessorsProps { + decorators: DecoratorSignature[]; + namespaceName: string; +} + +/** + * Generate typed accessor functions for auto decorators. + * These are thin wrappers around the compiler's generic auto decorator API. + */ +export function AutoDecoratorAccessors(props: Readonly) { + const autoDecorators = props.decorators.filter((d) => d.isAuto); + if (autoDecorators.length === 0) { + return undefined; + } + + return ( + + {(signature) => ( + + )} + + ); +} + +interface AutoDecoratorAccessorProps { + signature: DecoratorSignature; + namespaceName: string; +} + +function AutoDecoratorAccessor(props: Readonly) { + const decorator = props.signature.decorator; + const name = decorator.name.slice(1); // remove @ + const capitalizedName = name[0].toUpperCase() + name.slice(1); + const fqn = props.namespaceName ? `${props.namespaceName}.${name}` : name; + const params = decorator.parameters; + const targetType = ; + + if (params.length === 0) { + // No-arg auto decorator — generate `is*` function + return ( + + {code`return ${typespecCompiler.hasAutoDecorator}(program, "${fqn}", ${decorator.target.name});`} + + ); + } + + // Decorators with args — generate `get*` function + let returnType; + if (params.length === 1) { + const param = params[0]; + returnType = ( + <> + + {" | undefined"} + + ); + } else { + // Multi-arg — return type is an interface with named properties + returnType = ( + <> + {"{"} + + {(param) => ( + <> + {" "} + {param.name}: + + )} + + {" } | undefined"} + + ); + } + + return ( + + {code`return ${typespecCompiler.getAutoDecoratorValue}(program, "${fqn}", ${decorator.target.name}) as any;`} + + ); +} 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 ac26014d974..9ab841d26f9 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -14,13 +14,20 @@ const Tester = createTester(resolvePath(import.meta.dirname, "../.."), { async function generateDecoratorSignatures(code: string) { const [{ program }] = await Tester.compileAndDiagnose(code, { - compilerOptions: { parseOptions: { comments: true, docs: true } }, + compilerOptions: { + parseOptions: { comments: true, docs: true }, + configFile: { + projectRoot: ".", + kind: "project", + features: ["auto-decorators"], + diagnostics: [], + outputDir: "tsp-output", + }, + } as any, }); expectDiagnosticEmpty( - program.diagnostics.filter( - (x) => x.code !== "missing-implementation" && x.code !== "experimental-feature", - ), + program.diagnostics.filter((x) => x.code !== "missing-implementation"), ); const result = await generateExternDecorators(program, "test-lib", { From 46aa6598111d61556597c240d85ea72e405c3cd0 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 9 Jun 2026 08:16:44 -0400 Subject: [PATCH 10/13] Update auto decorator state key prefix to enable seamless migration --- packages/compiler/src/lib/auto-decorator.ts | 4 +++- .../test/gen-extern-signature/decorators-signatures.test.ts | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/lib/auto-decorator.ts b/packages/compiler/src/lib/auto-decorator.ts index e5af5abf90a..9ffb34d99a4 100644 --- a/packages/compiler/src/lib/auto-decorator.ts +++ b/packages/compiler/src/lib/auto-decorator.ts @@ -6,10 +6,12 @@ import type { Type } from "../core/types.js"; /** * Get the state key for an auto decorator given its fully-qualified name. + * Uses `dec:` prefix so the state key is based on decorator identity, + * not declaration style — allows seamless migration from auto to extern. * @internal */ export function getAutoDecoratorStateKey(decoratorFqn: string): symbol { - return Symbol.for(`auto-dec:${decoratorFqn}`); + return Symbol.for(`TypeSpec.dec:${decoratorFqn}`); } /** 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 9ab841d26f9..42f17129251 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -26,9 +26,7 @@ async function generateDecoratorSignatures(code: string) { } as any, }); - expectDiagnosticEmpty( - program.diagnostics.filter((x) => x.code !== "missing-implementation"), - ); + expectDiagnosticEmpty(program.diagnostics.filter((x) => x.code !== "missing-implementation")); const result = await generateExternDecorators(program, "test-lib", { prettierConfig: { From aa9386dd26cef9a19159f1719c12c46292737d3d Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 15 Jun 2026 16:09:02 -0400 Subject: [PATCH 11/13] fix(compiler): unwrap single-arg auto decorator accessor and unify duplicate handling - Single-arg tspd typed accessor now unwraps the stored record to the bare value, preserving parity with hand-written extern getters - Auto decorator duplicate detection reuses validateDecoratorUniqueOnNode (identity match, warning) and no longer early-returns, so duplicates are last-write-wins like extern decorators --- packages/compiler/src/core/checker.ts | 27 +++++++------------ .../compiler/test/checker/decorators.test.ts | 20 ++++++++++++++ .../components/auto-decorator-accessors.tsx | 7 ++++- .../decorators-signatures.test.ts | 15 ++++++++++- 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 6a19e70d309..8d39c606c18 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -10,6 +10,7 @@ import { createModelToObjectValueCodeFix, createTupleToArrayValueCodeFix, } from "./compiler-code-fixes/convert-to-value.codefix.js"; +import { validateDecoratorUniqueOnNode } from "./decorator-utils.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; import { compilerAssert, @@ -2163,25 +2164,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const lastParamIsRest = node.parameters.length > 0 && node.parameters[node.parameters.length - 1].rest; - return (context: DecoratorContext, target: Type, ...args: unknown[]) => { - // Check for duplicate application on the same declaration node + const impl = (context: DecoratorContext, target: Type, ...args: unknown[]) => { + // Warn (but still store, so duplicates are last-write-wins like extern + // decorators) if the same auto decorator is applied twice on the same node. if ("decorators" in target) { - const sameDecorators = (target as any).decorators.filter( - (x: any) => - x.definition?.name === `@${node.id.sv}` && - x.node?.kind === SyntaxKind.DecoratorExpression && - x.node?.parent === (target as any).node, - ); - if (sameDecorators.length > 1) { - context.program.reportDiagnostic( - createDiagnostic({ - code: "duplicate-decorator", - format: { decoratorName: `@${node.id.sv}` }, - target: context.decoratorTarget, - }), - ); - return; - } + validateDecoratorUniqueOnNode(context, target, impl); } // Store as key-value record { paramName: value } @@ -2200,6 +2187,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } context.program.stateMap(stateKey).set(target, data); }; + // The function name drives the `@` text in the duplicate-decorator + // diagnostic; mirror the extern `$name` convention so the helper strips it. + Object.defineProperty(impl, "name", { value: `$${node.id.sv}` }); + return impl; } function checkFunctionDeclaration( diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 4eafe169c65..2998c176116 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -331,6 +331,26 @@ describe("compiler: checker: decorators", () => { ]); }); + it("duplicate auto decorators on same node are last-write-wins", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` + auto dec myLabel(target: Model, label: valueof string); + + #suppress "duplicate-decorator" "testing last-write-wins" + @myLabel("first") + @myLabel("second") + model Foo {} + `, + autoDecOptions, + ); + + // Decorators execute in reverse source order, so the source-first + // application runs last and wins (both applications still store). + const Foo = program.getGlobalNamespaceType().models.get("Foo")!; + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "myLabel", Foo), { label: "first" }); + }); + it("augment decorator overwrites auto decorator value (last-write-wins)", async () => { const { program } = await Tester.using("TypeSpec.Reflection").compile( ` diff --git a/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx b/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx index e74663dd875..96aab35f02b 100644 --- a/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx +++ b/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx @@ -60,6 +60,7 @@ function AutoDecoratorAccessor(props: Readonly) { // Decorators with args — generate `get*` function let returnType; + let body; if (params.length === 1) { const param = params[0]; returnType = ( @@ -68,6 +69,9 @@ function AutoDecoratorAccessor(props: Readonly) { {" | undefined"} ); + // Single-arg: storage is a uniform record, but the typed accessor unwraps to + // the bare value to preserve parity with hand-written extern getters. + body = code`return ${typespecCompiler.getAutoDecoratorValue}(program, "${fqn}", ${decorator.target.name})?.["${param.name}"] as any;`; } else { // Multi-arg — return type is an interface with named properties returnType = ( @@ -84,6 +88,7 @@ function AutoDecoratorAccessor(props: Readonly) { {" } | undefined"} ); + body = code`return ${typespecCompiler.getAutoDecoratorValue}(program, "${fqn}", ${decorator.target.name}) as any;`; } return ( @@ -96,7 +101,7 @@ function AutoDecoratorAccessor(props: Readonly) { ]} returnType={returnType} > - {code`return ${typespecCompiler.getAutoDecoratorValue}(program, "${fqn}", ${decorator.target.name}) as any;`} + {body} ); } 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 42f17129251..9526a8cac5c 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -464,7 +464,20 @@ export function isMyFlag(program: Program, target: Model): boolean { ${autoImportLine(["Model", "Program"], ["getAutoDecoratorValue"])} export function getMyLabel(program: Program, target: Model): string | undefined { - return getAutoDecoratorValue(program, "myLabel", target) as any; + return getAutoDecoratorValue(program, "myLabel", target)?.["label"] as any; +} + `, + }); + }); + + it("generate accessor for single-rest-arg auto decorator unwraps to array", async () => { + await expectSignatures({ + code: `auto dec myTags(target: Model, ...tags: valueof string[]);`, + expected: ` +${autoImportLine(["Model", "Program"], ["getAutoDecoratorValue"])} + +export function getMyTags(program: Program, target: Model): readonly string[] | undefined { + return getAutoDecoratorValue(program, "myTags", target)?.["tags"] as any; } `, }); From b8eb823fca7302c5bc27dbce874e27f7dadb9106 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Jun 2026 08:35:36 -0400 Subject: [PATCH 12/13] test(compiler): cover auto decorator gaps and reject auto on functions - Add tests: hasAutoDecorator true/false, getAutoDecoratorValue undefined when absent, `is` inheritance, non-Model (ModelProperty) target, auto rejected on model/function declarations, namespaced tspd accessor FQN - Restrict function declarations to extern|internal modifiers so the auto modifier is no longer silently accepted on functions --- packages/compiler/src/core/modifiers.ts | 2 +- .../compiler/test/checker/decorators.test.ts | 96 +++++++++++++++++++ .../decorators-signatures.test.ts | 28 ++++++ 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/core/modifiers.ts b/packages/compiler/src/core/modifiers.ts index 85c3706e222..a3f1e1c2106 100644 --- a/packages/compiler/src/core/modifiers.ts +++ b/packages/compiler/src/core/modifiers.ts @@ -50,7 +50,7 @@ const SYNTAX_MODIFIERS: Readonly { ]); }); + it("errors if auto modifier is used on a model declaration", async () => { + const diagnostics = await DecTester.diagnose(` + auto model Foo {} + `); + expectDiagnostics(diagnostics, { + code: "invalid-modifier", + message: "Modifier 'auto' cannot be used on declarations of type 'model'.", + }); + }); + + it("errors if auto modifier is used on a function declaration", async () => { + const diagnostics = await DecTester.diagnose(` + auto extern fn foo(): void; + `); + expectDiagnostics( + diagnostics.filter((d) => d.code === "invalid-modifier"), + { + code: "invalid-modifier", + message: "Modifier 'auto' cannot be used on declarations of type 'function'.", + }, + ); + }); + it("errors if rest parameter type is not an array expression", async () => { const diagnostics = await DecTester.diagnose(` extern dec testDec(target: unknown, ...rest: string); @@ -403,6 +426,79 @@ describe("compiler: checker: decorators", () => { const targets = getAutoDecoratorTargets(program, "tracked"); strictEqual(targets.size, 2); }); + + it("hasAutoDecorator reflects whether the decorator was applied", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` + auto dec tracked(target: Model); + + @tracked model Foo {} + model Bar {} + `, + autoDecOptions, + ); + + const ns = program.getGlobalNamespaceType(); + const { hasAutoDecorator } = await import("../../src/lib/auto-decorator.js"); + strictEqual(hasAutoDecorator(program, "tracked", ns.models.get("Foo")!), true); + strictEqual(hasAutoDecorator(program, "tracked", ns.models.get("Bar")!), false); + }); + + it("getAutoDecoratorValue returns undefined when not applied", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` + auto dec myLabel(target: Model, label: valueof string); + + model Bar {} + `, + autoDecOptions, + ); + + const Bar = program.getGlobalNamespaceType().models.get("Bar")!; + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + strictEqual(getAutoDecoratorValue(program, "myLabel", Bar), undefined); + }); + + it("auto decorator is inherited through `is`", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` + auto dec myLabel(target: Model, label: valueof string); + + @myLabel("base") + model Base {} + + model Derived is Base {} + `, + autoDecOptions, + ); + + const ns = program.getGlobalNamespaceType(); + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "myLabel", ns.models.get("Base")!), { + label: "base", + }); + deepStrictEqual(getAutoDecoratorValue(program, "myLabel", ns.models.get("Derived")!), { + label: "base", + }); + }); + + it("auto decorator works on a non-Model target (ModelProperty)", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` + auto dec field(target: ModelProperty, name: valueof string); + + model Foo { + @field("id") + prop: string; + } + `, + autoDecOptions, + ); + + const prop = program.getGlobalNamespaceType().models.get("Foo")!.properties.get("prop")!; + const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "field", prop), { name: "id" }); + }); }); describe("usage", () => { 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 9526a8cac5c..7ec7c02b96c 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -496,6 +496,34 @@ export function getMyMeta(program: Program, target: Model): { name: string; vers }); }); + it("generates accessor with fully-qualified name for namespaced auto decorator", async () => { + const [{ program }] = await Tester.compileAndDiagnose( + ` + namespace MyLib { + auto dec myLabel(target: Model, label: valueof string); + } + `, + { + compilerOptions: { + parseOptions: { comments: true, docs: true }, + configFile: { + projectRoot: ".", + kind: "project", + features: ["auto-decorators"], + diagnostics: [], + outputDir: "tsp-output", + }, + } as any, + }, + ); + expectDiagnosticEmpty(program.diagnostics.filter((x) => x.code !== "missing-implementation")); + const files = await generateExternDecorators(program, "test-lib", { + prettierConfig: { printWidth: 160, plugins: [] }, + }); + const all = Object.values(files).join("\n"); + expect(all).toContain(`getAutoDecoratorValue(program, "MyLib.myLabel", target)?.["label"]`); + }); + it("does not generate $decorators type for auto decorators", async () => { const result = await generateDecoratorSignatures(`auto dec myFlag(target: Model);`); expect(result).not.toContain("Decorators"); From 738502ba54d6e579e5fad81b3028edc116a9e35f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 16 Jun 2026 11:34:38 -0400 Subject: [PATCH 13/13] reorganize --- packages/compiler/src/core/auto-decorator.ts | 106 ++++++++++++++++++ packages/compiler/src/core/checker.ts | 42 +------ packages/compiler/src/index.ts | 10 +- packages/compiler/src/lib/auto-decorator.ts | 57 ---------- .../compiler/test/checker/decorators.test.ts | 28 ++--- 5 files changed, 126 insertions(+), 117 deletions(-) create mode 100644 packages/compiler/src/core/auto-decorator.ts delete mode 100644 packages/compiler/src/lib/auto-decorator.ts diff --git a/packages/compiler/src/core/auto-decorator.ts b/packages/compiler/src/core/auto-decorator.ts new file mode 100644 index 00000000000..7423c266855 --- /dev/null +++ b/packages/compiler/src/core/auto-decorator.ts @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT License. + +import { validateDecoratorUniqueOnNode } from "./decorator-utils.js"; +import type { Program } from "./program.js"; +import { getFullyQualifiedSymbolName } from "./type-utils.js"; +import type { DecoratorContext, DecoratorDeclarationStatementNode, Sym, Type } from "./types.js"; + +/** + * Get the state key for an auto decorator given its fully-qualified name. + * Uses `dec:` prefix so the state key is based on decorator identity, + * not declaration style — allows seamless migration from auto to extern. + * @internal + */ +export function getAutoDecoratorStateKey(decoratorFqn: string): symbol { + return Symbol.for(`dec:${decoratorFqn}`); +} + +/** + * Build the auto-generated implementation for an `auto dec` declaration. + * + * The returned function stores its arguments as a uniform `{ paramName: value }` + * record in the program state map keyed by the decorator's fully-qualified name, + * and warns (last-write-wins, like extern decorators) on duplicate application. + * @internal + */ +export function createAutoDecoratorImplementation( + symbol: Sym, + node: DecoratorDeclarationStatementNode, +): (ctx: DecoratorContext, target: Type, ...args: unknown[]) => void { + const fqn = getFullyQualifiedSymbolName(symbol); + const stateKey = getAutoDecoratorStateKey(fqn); + const paramNames = node.parameters.map((p) => p.id.sv); + const lastParamIsRest = + node.parameters.length > 0 && node.parameters[node.parameters.length - 1].rest; + + const impl = (context: DecoratorContext, target: Type, ...args: unknown[]) => { + // Warn (but still store, so duplicates are last-write-wins like extern + // decorators) if the same auto decorator is applied twice on the same node. + if ("decorators" in target) { + validateDecoratorUniqueOnNode(context, target, impl); + } + + // Store as key-value record { paramName: value } + const data: Record = {}; + if (lastParamIsRest) { + // Non-rest params first + for (let i = 0; i < paramNames.length - 1; i++) { + data[paramNames[i]] = args[i]; + } + // Rest param collects remaining args as an array + data[paramNames[paramNames.length - 1]] = args.slice(paramNames.length - 1); + } else { + for (let i = 0; i < paramNames.length; i++) { + data[paramNames[i]] = args[i]; + } + } + context.program.stateMap(stateKey).set(target, data); + }; + // The function name drives the `@` text in the duplicate-decorator + // diagnostic; mirror the extern `$name` convention so the helper strips it. + Object.defineProperty(impl, "name", { value: `$${node.id.sv}` }); + return impl; +} + +/** + * Check if an auto decorator has been applied to a target. + * @param program - The current program. + * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). + * @param target - The type to check. + */ +export function hasAutoDecorator(program: Program, decoratorFqn: string, target: Type): boolean { + const key = getAutoDecoratorStateKey(decoratorFqn); + return program.stateMap(key).has(target); +} + +/** + * Get the stored value for an auto decorator applied to a target. + * Always returns a record of `{ paramName: value }` (empty record `{}` for no-arg decorators). + * @param program - The current program. + * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). + * @param target - The type to get the value for. + * @returns The stored record, or `undefined` if the decorator was not applied. + */ +export function getAutoDecoratorValue( + program: Program, + decoratorFqn: string, + target: Type, +): Record | undefined { + const key = getAutoDecoratorStateKey(decoratorFqn); + return program.stateMap(key).get(target) as Record | undefined; +} + +/** + * Get all targets that have a specific auto decorator applied, along with their stored values. + * @param program - The current program. + * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). + * @returns A map of target types to their stored values. + */ +export function getAutoDecoratorTargets( + program: Program, + decoratorFqn: string, +): Map { + const key = getAutoDecoratorStateKey(decoratorFqn); + return program.stateMap(key); +} diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 8d39c606c18..666481b8be8 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1,16 +1,15 @@ import { Realm } from "../experimental/realm.js"; -import { getAutoDecoratorStateKey } from "../lib/auto-decorator.js"; import { docFromCommentDecorator, getIndexer } from "../lib/intrinsic/decorators.js"; import { $ } from "../typekit/index.js"; import { DuplicateTracker } from "../utils/duplicate-tracker.js"; import { MultiKeyMap, Mutable, createRekeyableMap, isArray, mutate } from "../utils/misc.js"; +import { createAutoDecoratorImplementation } from "./auto-decorator.js"; import { createSymbol, getSymNode } from "./binder.js"; import { createChangeIdentifierCodeFix } from "./compiler-code-fixes/change-identifier.codefix.js"; import { createModelToObjectValueCodeFix, createTupleToArrayValueCodeFix, } from "./compiler-code-fixes/convert-to-value.codefix.js"; -import { validateDecoratorUniqueOnNode } from "./decorator-utils.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; import { compilerAssert, @@ -2154,45 +2153,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return decoratorType; } - function createAutoDecoratorImplementation( - symbol: Sym, - node: DecoratorDeclarationStatementNode, - ): (ctx: DecoratorContext, target: Type, ...args: unknown[]) => void { - const fqn = getFullyQualifiedSymbolName(symbol); - const stateKey = getAutoDecoratorStateKey(fqn); - const paramNames = node.parameters.map((p) => p.id.sv); - const lastParamIsRest = - node.parameters.length > 0 && node.parameters[node.parameters.length - 1].rest; - - const impl = (context: DecoratorContext, target: Type, ...args: unknown[]) => { - // Warn (but still store, so duplicates are last-write-wins like extern - // decorators) if the same auto decorator is applied twice on the same node. - if ("decorators" in target) { - validateDecoratorUniqueOnNode(context, target, impl); - } - - // Store as key-value record { paramName: value } - const data: Record = {}; - if (lastParamIsRest) { - // Non-rest params first - for (let i = 0; i < paramNames.length - 1; i++) { - data[paramNames[i]] = args[i]; - } - // Rest param collects remaining args as an array - data[paramNames[paramNames.length - 1]] = args.slice(paramNames.length - 1); - } else { - for (let i = 0; i < paramNames.length; i++) { - data[paramNames[i]] = args[i]; - } - } - context.program.stateMap(stateKey).set(target, data); - }; - // The function name drives the `@` text in the duplicate-decorator - // diagnostic; mirror the extern `$name` convention so the helper strips it. - Object.defineProperty(impl, "name", { value: `$${node.id.sv}` }); - return impl; - } - function checkFunctionDeclaration( ctx: CheckContext, node: FunctionDeclarationStatementNode, diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index eada5814a79..53c2240423c 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -1,4 +1,9 @@ export { resolveCompilerOptions, ResolveCompilerOptionsOptions } from "./config/index.js"; +export { + getAutoDecoratorTargets, + getAutoDecoratorValue, + hasAutoDecorator, +} from "./core/auto-decorator.js"; export { Checker, CreateTypeProps, @@ -105,11 +110,6 @@ export { NodeHost } from "./core/node-host.js"; export { isNumeric, Numeric } from "./core/numeric.js"; export type { CompilerOptions } from "./core/options.js"; export { getPositionBeforeTrivia } from "./core/parser-utils.js"; -export { - getAutoDecoratorTargets, - getAutoDecoratorValue, - hasAutoDecorator, -} from "./lib/auto-decorator.js"; export { $defaultVisibility, $discriminator, diff --git a/packages/compiler/src/lib/auto-decorator.ts b/packages/compiler/src/lib/auto-decorator.ts deleted file mode 100644 index 9ffb34d99a4..00000000000 --- a/packages/compiler/src/lib/auto-decorator.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation -// Licensed under the MIT License. - -import type { Program } from "../core/program.js"; -import type { Type } from "../core/types.js"; - -/** - * Get the state key for an auto decorator given its fully-qualified name. - * Uses `dec:` prefix so the state key is based on decorator identity, - * not declaration style — allows seamless migration from auto to extern. - * @internal - */ -export function getAutoDecoratorStateKey(decoratorFqn: string): symbol { - return Symbol.for(`TypeSpec.dec:${decoratorFqn}`); -} - -/** - * Check if an auto decorator has been applied to a target. - * @param program - The current program. - * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). - * @param target - The type to check. - */ -export function hasAutoDecorator(program: Program, decoratorFqn: string, target: Type): boolean { - const key = getAutoDecoratorStateKey(decoratorFqn); - return program.stateMap(key).has(target); -} - -/** - * Get the stored value for an auto decorator applied to a target. - * Always returns a record of `{ paramName: value }` (empty record `{}` for no-arg decorators). - * @param program - The current program. - * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). - * @param target - The type to get the value for. - * @returns The stored record, or `undefined` if the decorator was not applied. - */ -export function getAutoDecoratorValue( - program: Program, - decoratorFqn: string, - target: Type, -): Record | undefined { - const key = getAutoDecoratorStateKey(decoratorFqn); - return program.stateMap(key).get(target) as Record | undefined; -} - -/** - * Get all targets that have a specific auto decorator applied, along with their stored values. - * @param program - The current program. - * @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec"). - * @returns A map of target types to their stored values. - */ -export function getAutoDecoratorTargets( - program: Program, - decoratorFqn: string, -): Map { - const key = getAutoDecoratorStateKey(decoratorFqn); - return program.stateMap(key); -} diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index c453da1370e..1bbb721a681 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -221,7 +221,7 @@ describe("compiler: checker: decorators", () => { const Foo = program.getGlobalNamespaceType().models.get("Foo")!; ok(Foo, "Foo should exist"); - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); deepStrictEqual(getAutoDecoratorValue(program, "myFlag", Foo), {}); }); @@ -237,7 +237,7 @@ describe("compiler: checker: decorators", () => { ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); deepStrictEqual(getAutoDecoratorValue(program, "myLabel", Foo), { label: "hello" }); }); @@ -253,7 +253,7 @@ describe("compiler: checker: decorators", () => { ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); const value = getAutoDecoratorValue(program, "myMeta", Foo) as any; deepStrictEqual(value, { name: "test", version: 42 }); }); @@ -272,7 +272,7 @@ describe("compiler: checker: decorators", () => { ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); deepStrictEqual(getAutoDecoratorValue(program, "MyLib.myLabel", Foo), { label: "world" }); }); @@ -308,7 +308,7 @@ describe("compiler: checker: decorators", () => { ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); deepStrictEqual(getAutoDecoratorValue(program, "tags", Foo), { tags: ["a", "b", "c"] }); }); @@ -324,7 +324,7 @@ describe("compiler: checker: decorators", () => { ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); deepStrictEqual(getAutoDecoratorValue(program, "route", Foo), { path: "/foo", tags: ["tag1", "tag2"], @@ -370,7 +370,7 @@ describe("compiler: checker: decorators", () => { // Decorators execute in reverse source order, so the source-first // application runs last and wins (both applications still store). const Foo = program.getGlobalNamespaceType().models.get("Foo")!; - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); deepStrictEqual(getAutoDecoratorValue(program, "myLabel", Foo), { label: "first" }); }); @@ -388,7 +388,7 @@ describe("compiler: checker: decorators", () => { ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); deepStrictEqual(getAutoDecoratorValue(program, "myLabel", Foo), { label: "second" }); }); @@ -404,7 +404,7 @@ describe("compiler: checker: decorators", () => { ); const Foo = program.getGlobalNamespaceType().models.get("Foo")!; - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); deepStrictEqual(getAutoDecoratorValue(program, "myDec", Foo), { required: "hello", optional: undefined, @@ -422,7 +422,7 @@ describe("compiler: checker: decorators", () => { autoDecOptions, ); - const { getAutoDecoratorTargets } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorTargets } = await import("../../src/core/auto-decorator.js"); const targets = getAutoDecoratorTargets(program, "tracked"); strictEqual(targets.size, 2); }); @@ -439,7 +439,7 @@ describe("compiler: checker: decorators", () => { ); const ns = program.getGlobalNamespaceType(); - const { hasAutoDecorator } = await import("../../src/lib/auto-decorator.js"); + const { hasAutoDecorator } = await import("../../src/core/auto-decorator.js"); strictEqual(hasAutoDecorator(program, "tracked", ns.models.get("Foo")!), true); strictEqual(hasAutoDecorator(program, "tracked", ns.models.get("Bar")!), false); }); @@ -455,7 +455,7 @@ describe("compiler: checker: decorators", () => { ); const Bar = program.getGlobalNamespaceType().models.get("Bar")!; - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); strictEqual(getAutoDecoratorValue(program, "myLabel", Bar), undefined); }); @@ -473,7 +473,7 @@ describe("compiler: checker: decorators", () => { ); const ns = program.getGlobalNamespaceType(); - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); deepStrictEqual(getAutoDecoratorValue(program, "myLabel", ns.models.get("Base")!), { label: "base", }); @@ -496,7 +496,7 @@ describe("compiler: checker: decorators", () => { ); const prop = program.getGlobalNamespaceType().models.get("Foo")!.properties.get("prop")!; - const { getAutoDecoratorValue } = await import("../../src/lib/auto-decorator.js"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); deepStrictEqual(getAutoDecoratorValue(program, "field", prop), { name: "id" }); }); });