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 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..778672b2020 --- /dev/null +++ b/.chronus/changes/data-decorators-2026-3-30.md @@ -0,0 +1,16 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Added `auto` decorator modifier for declaring decorators that auto-store their arguments as metadata without requiring a JavaScript implementation. + +```typespec +auto dec label(target: Model, value: valueof string); + +@label("my-model") +model Foo {} +``` + +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 new file mode 100644 index 00000000000..113435de2cc --- /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 `auto` decorators (e.g., `isMyFlag`, `getMyLabel`). 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 de61f2ecd8a..666481b8be8 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3,6 +3,7 @@ import { docFromCommentDecorator, getIndexer } from "../lib/intrinsic/decorators 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 { @@ -113,6 +114,7 @@ import { ModelProperty, ModelPropertyNode, ModelStatementNode, + ModifierFlags, Namespace, NamespaceStatementNode, NeverType, @@ -2118,8 +2120,19 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); const name = node.id.sv; - const implementation = symbol.value; - if (implementation === undefined) { + const isAuto = (node.modifierFlags & ModifierFlags.Auto) !== 0; + if (isAuto && !isCompilerFeatureEnabled(program, "auto-decorators", node)) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "auto-decorator-disabled", + target: node, + }), + ); + } + let implementation = symbol.value; + if (isAuto) { + implementation = createAutoDecoratorImplementation(symbol, node); + } else if (implementation === undefined) { reportCheckerDiagnostic(createDiagnostic({ code: "missing-implementation", target: node })); } const decoratorType: Decorator = createType({ @@ -2130,6 +2143,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: isAuto ? "auto" : "extern", }); namespace.decoratorDeclarations.set(name, decoratorType); @@ -6896,9 +6910,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/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 9008e0f6be6..31a97d8cbc1 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -269,6 +269,13 @@ const diagnostics = { "Function 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": { severity: "error", messages: { @@ -564,6 +571,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 4473b051a7c..a3f1e1c2106 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,10 +46,11 @@ 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, + }), + ); + } } } @@ -134,6 +167,8 @@ function modifierToFlag(modifier: Modifier): ModifierFlags { return ModifierFlags.Extern; case SyntaxKind.InternalKeyword: return ModifierFlags.Internal; + case SyntaxKind.AutoKeyword: + return ModifierFlags.Auto; default: compilerAssert(false, `Unknown modifier kind: ${(modifier as Modifier).kind}`); } @@ -145,6 +180,8 @@ function getTextForModifier(modifier: Modifier): string { return "extern"; case SyntaxKind.InternalKeyword: return "internal"; + case SyntaxKind.AutoKeyword: + return "auto"; default: compilerAssert(false, `Unknown modifier kind: ${(modifier as Modifier).kind}`); } @@ -158,6 +195,9 @@ function getNamesOfModifierFlags(flags: ModifierFlags): string[] { if (flags & ModifierFlags.Internal) { names.push("internal"); } + if (flags & ModifierFlags.Auto) { + names.push("auto"); + } return names; } diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 63ed0076f13..340c3e4ceea 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -24,6 +24,7 @@ import { AnyKeywordNode, ArrayLiteralNode, AugmentDecoratorStatementNode, + AutoKeywordNode, BlockComment, BooleanLiteralNode, CallExpressionNode, @@ -461,6 +462,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.ConstKeyword: case Token.ExternKeyword: case Token.InternalKeyword: + case Token.AutoKeyword: 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.AutoKeyword: 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 parseAutoKeyword(): AutoKeywordNode { + const pos = tokenPos(); + parseExpected(Token.AutoKeyword); + return { + kind: SyntaxKind.AutoKeyword, + ...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.AutoKeyword: + return parseAutoKeyword(); 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.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 67fd2454300..d940a6b4dd5 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, + AutoKeyword, /** @internal */ __EndModifierKeyword, /////////////////////////////////////////////////////////////// @@ -194,7 +195,6 @@ export enum Token { ImplKeyword, SatisfiesKeyword, FlagKeyword, - AutoKeyword, PartialKeyword, PrivateKeyword, PublicKeyword, @@ -310,6 +310,7 @@ export const TokenDisplay = getTokenDisplayTable([ [Token.NeverKeyword, "'never'"], [Token.UnknownKeyword, "'unknown'"], [Token.ExternKeyword, "'extern'"], + [Token.AutoKeyword, "'auto'"], // Reserved keywords [Token.StatemachineKeyword, "'statemachine'"], @@ -342,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'"], @@ -383,6 +383,7 @@ export const Keywords: ReadonlyMap = new Map([ ["never", Token.NeverKeyword], ["unknown", Token.UnknownKeyword], ["extern", Token.ExternKeyword], + ["auto", Token.AutoKeyword], ["internal", Token.InternalKeyword], // Reserved keywords @@ -416,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], @@ -455,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 60ee8493360..50c36efbf4b 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -758,6 +758,8 @@ export interface Decorator extends BaseType { target: MixedFunctionParameter; parameters: MixedFunctionParameter[]; implementation: (ctx: DecoratorContext, target: Type, ...args: unknown[]) => void; + /** How this decorator was declared. */ + declarationKind: "extern" | "auto"; } /** @@ -1212,6 +1214,7 @@ export enum SyntaxKind { CallExpression, ScalarConstructor, InternalKeyword, + AutoKeyword, FunctionTypeExpression, } @@ -1778,6 +1781,10 @@ export interface InternalKeywordNode extends BaseNode { readonly kind: SyntaxKind.InternalKeyword; } +export interface AutoKeywordNode extends BaseNode { + readonly kind: SyntaxKind.AutoKeyword; +} + export interface VoidKeywordNode extends BaseNode { readonly kind: SyntaxKind.VoidKeyword; } @@ -1834,11 +1841,12 @@ export const enum ModifierFlags { None, Extern = 1 << 1, Internal = 1 << 2, + Auto = 1 << 3, - All = Extern | Internal, + All = Extern | Internal | Auto, } -export type Modifier = ExternKeywordNode | InternalKeywordNode; +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 adf307c6c6a..aa4fafd56ad 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.AutoKeyword: + return "auto"; case SyntaxKind.VoidKeyword: return "void"; case SyntaxKind.NeverKeyword: diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 503951be71b..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, diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index cd46988fc77..1bbb721a681 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -111,16 +111,55 @@ describe("compiler: checker: decorators", () => { }); }); - it("errors if decorator is missing extern 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 required modifier 'extern'.", + message: + "Declaration of type 'dec' is missing one of the required modifiers: 'extern' or 'auto'.", }); }); + it("errors if both extern and auto modifiers are used", async () => { + const diagnostics = await DecTester.diagnose(` + auto extern dec testDec(target: unknown); + `); + expectDiagnostics(diagnostics, [ + { + code: "invalid-modifier", + message: "Modifiers 'extern' and 'auto' cannot be used together.", + }, + { + code: "auto-decorator-disabled", + }, + ]); + }); + + 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); @@ -142,6 +181,326 @@ 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( + ` + auto dec myFlag(target: Model); + `, + autoDecOptions, + ); + + const dec = program.getGlobalNamespaceType().decoratorDeclarations.get("myFlag"); + ok(dec); + strictEqual(dec.declarationKind, "auto"); + ok(dec.implementation, "should have auto-generated implementation"); + }); + + it("auto decorator with no args stores empty record in stateMap", async () => { + 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"); + const { getAutoDecoratorValue } = await import("../../src/core/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "myFlag", Foo), {}); + }); + + it("auto decorator with single arg stores as key-value record in stateMap", async () => { + 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/core/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "myLabel", Foo), { label: "hello" }); + }); + + it("auto decorator with multiple args stores named record in stateMap", async () => { + 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/core/auto-decorator.js"); + const value = getAutoDecoratorValue(program, "myMeta", Foo) as any; + deepStrictEqual(value, { name: "test", version: 42 }); + }); + + it("auto decorator in namespace uses FQN for state key", async () => { + const { program } = await Tester.using("TypeSpec.Reflection").compile( + ` + namespace MyLib { + 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/core/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "MyLib.myLabel", Foo), { label: "world" }); + }); + + it("internal auto dec is valid", async () => { + const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose( + ` + internal auto dec myDec(target: unknown); + `, + autoDecOptions, + ); + strictEqual(diagnostics.length, 0); + }); + + it("emits error without feature flag", async () => { + const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose(` + auto dec myFlag(target: Model); + `); + expectDiagnostics(diagnostics, { + 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( + ` + 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/core/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( + ` + 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/core/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( + ` + auto dec myFlag(target: Model); + + @myFlag + @myFlag + model Foo {} + `, + autoDecOptions, + ); + 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("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/core/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( + ` + 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/core/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( + ` + 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/core/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( + ` + auto dec tracked(target: Model); + + @tracked model Foo {} + @tracked model Bar {} + `, + autoDecOptions, + ); + + const { getAutoDecoratorTargets } = await import("../../src/core/auto-decorator.js"); + 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/core/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/core/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/core/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/core/auto-decorator.js"); + deepStrictEqual(getAutoDecoratorValue(program, "field", prop), { name: "id" }); + }); + }); + describe("usage", () => { let calledArgs: any[] | undefined; const UsageTester = Tester.files({ 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 c4ac8e5900e..1f18484ac0f 100644 --- a/packages/compiler/test/formatter/formatter.test.ts +++ b/packages/compiler/test/formatter/formatter.test.ts @@ -3030,6 +3030,28 @@ internal extern dec foo(target: Type, arg1: StringLiteral); }); }); + it("format auto dec", async () => { + await assertFormat({ + code: ` +auto dec foo(target: Type, arg1: StringLiteral); +`, + expected: ` +auto dec foo(target: Type, arg1: StringLiteral); +`, + }); + }); + + it("format internal auto dec", async () => { + await assertFormat({ + code: ` +internal auto dec foo(target: Type, arg1: StringLiteral); +`, + expected: ` +internal auto 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..4e20f08e7d9 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);", + "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 9dfa8f645fa..dde07fe87b5 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.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/html-program-viewer/src/react/type-config.ts b/packages/html-program-viewer/src/react/type-config.ts index 566bca5a228..be1ce7e5f21 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/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..96aab35f02b --- /dev/null +++ b/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx @@ -0,0 +1,107 @@ +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; + let body; + if (params.length === 1) { + const param = params[0]; + returnType = ( + <> + + {" | 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 = ( + <> + {"{"} + + {(param) => ( + <> + {" "} + {param.name}: + + )} + + {" } | undefined"} + + ); + body = code`return ${typespecCompiler.getAutoDecoratorValue}(program, "${fqn}", ${decorator.target.name}) as any;`; + } + + return ( + + {body} + + ); +} 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..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 @@ -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.isAuto, + ); const hasFunctions = entities.some((e) => e.kind === "Function"); return ( <> - + e.kind === "Decorator"); + 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"); 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..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,6 +24,8 @@ export const typespecCompiler = createPackage({ "Numeric", "ScalarValue", "DecoratorValidatorCallbacks", + "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 0fd36a7f075..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,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", + 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 cb01eb3d7ca..1c94fec5d30 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 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 27ff5c8eb74..7ec7c02b96c 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -14,7 +14,16 @@ 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")); @@ -423,3 +432,116 @@ function importLine(imports: string[]) { const all = new Set(["DecoratorContext", "DecoratorValidatorCallbacks", ...imports]); return `import type { ${[...all].sort().join(", ")} } from "@typespec/compiler";`; } + +function autoImportLine(typeImports: string[], valueImports: 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("auto decorator accessors", () => { + it("generate accessor for no-arg auto decorator (boolean flag)", async () => { + await expectSignatures({ + code: `auto dec myFlag(target: Model);`, + expected: ` +${autoImportLine(["Model", "Program"], ["hasAutoDecorator"])} + +export function isMyFlag(program: Program, target: Model): boolean { + return hasAutoDecorator(program, "myFlag", target); +} + `, + }); + }); + + it("generate accessor for single-arg auto decorator", async () => { + await expectSignatures({ + code: `auto dec myLabel(target: Model, label: valueof string);`, + expected: ` +${autoImportLine(["Model", "Program"], ["getAutoDecoratorValue"])} + +export function getMyLabel(program: Program, target: Model): string | undefined { + 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; +} + `, + }); + }); + + it("generate accessor for multi-arg auto decorator", async () => { + await expectSignatures({ + code: `auto dec myMeta(target: Model, name: valueof string, version: valueof int32);`, + expected: ` +${autoImportLine(["Model", "Program"], ["getAutoDecoratorValue"])} + +export function getMyMeta(program: Program, target: Model): { name: string; version: number } | undefined { + return getAutoDecoratorValue(program, "myMeta", target) as any; +} + `, + }); + }); + + 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"); + expect(result).not.toContain("$myFlag"); + }); + + it("generates both extern and auto decorator outputs when mixed", async () => { + const result = await generateDecoratorSignatures(` + extern dec externDec(target: Model); + auto dec dataFlag(target: Model); + `); + // Verify extern decorator parts + expect(result).toContain("ExternDecDecorator"); + expect(result).toContain("externDec: ExternDecDecorator"); + // Verify auto decorator parts + expect(result).toContain("isDataFlag"); + 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 71c3ae95d09..02e41b1301d 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,76 @@ 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 [auto decorators](#auto-decorators) which require no JavaScript implementation at all. + +## Auto decorators + +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) +auto dec tracked(target: unknown); + +// A single value +auto dec label(target: Model, value: valueof string); + +// Multiple values (stored as a named record) +auto dec serviceInfo(target: Model, name: valueof string, version: valueof int32); +``` + +### How data is stored + +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 `{}` (empty record) +- **One or more parameters**: stores `{ paramName: value, ... }`, e.g. `{ name: "hello", version: 1 }` + +### Reading auto decorator values + +The compiler provides a generic API to read auto decorator values without any generated code: + +```ts +import { hasAutoDecorator, getAutoDecoratorValue } from "@typespec/compiler"; + +// Check if a flag decorator was applied +if (hasAutoDecorator(program, "MyLib.tracked", type)) { + // ... +} + +// Get the stored record +const label = getAutoDecoratorValue(program, "MyLib.label", type) as { value: string }; + +// Get a multi-arg record +const info = getAutoDecoratorValue(program, "MyLib.serviceInfo", type) as { + name: string; + version: number; +}; +``` + +### Generated typed accessors + +When using `tspd gen-extern-signature`, typed accessor functions are generated for auto decorators: + +```ts +// Generated for: auto dec tracked(target: Model); +export function isTracked(program: Program, target: Model): boolean; + +// Generated for: auto dec label(target: Model, value: valueof string); +export function getLabel(program: Program, target: Model): string | undefined; +``` + +### Combining with `internal` + +Auto decorators can be combined with the `internal` modifier: + +```typespec +internal auto dec myInternalFlag(target: Model); +``` + +:::note +`auto` and `extern` are mutually exclusive — a decorator is either auto-implemented (auto) 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..b779393173b 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 [auto decorators](../extending-typespec/create-decorators.md#auto-decorators) which require no JavaScript implementation: + +```typespec +auto dec label(target: Model, value: valueof string); +```