Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .chronus/changes/data-decorators-2026-2-30-17-38-53.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: internal
packages:
- "@typespec/html-program-viewer"
---

Data decorators
16 changes: 16 additions & 0 deletions .chronus/changes/data-decorators-2026-3-30.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions .chronus/changes/data-decorators-tspd-2026-3-30.md
Original file line number Diff line number Diff line change
@@ -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`).
106 changes: 106 additions & 0 deletions packages/compiler/src/core/auto-decorator.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = {};
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 `@<name>` 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<string, unknown> | undefined {
const key = getAutoDecoratorStateKey(decoratorFqn);
return program.stateMap(key).get(target) as Record<string, unknown> | 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<Type, unknown> {
const key = getAutoDecoratorStateKey(decoratorFqn);
return program.stateMap(key);
}
21 changes: 18 additions & 3 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -113,6 +114,7 @@ import {
ModelProperty,
ModelPropertyNode,
ModelStatementNode,
ModifierFlags,
Namespace,
NamespaceStatementNode,
NeverType,
Expand Down Expand Up @@ -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({
Expand All @@ -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);
Expand Down Expand Up @@ -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,
};
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/core/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, CompilerFeatureDefinition>;

export type CompilerFeatureName = keyof typeof compilerFeatures;
Expand Down
9 changes: 9 additions & 0 deletions packages/compiler/src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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"}'.`,
},
},
Expand Down
58 changes: 49 additions & 9 deletions packages/compiler/src/core/modifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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][];
}

/**
Expand Down Expand Up @@ -44,10 +46,11 @@ const SYNTAX_MODIFIERS: Readonly<Record<Declaration["kind"], ModifierCompatibili
[SyntaxKind.ConstStatement]: DEFAULT_COMPATIBILITY,
[SyntaxKind.DecoratorDeclarationStatement]: {
allowed: ModifierFlags.All,
required: ModifierFlags.Extern,
required: ModifierFlags.Extern | ModifierFlags.Auto,
mutuallyExclusive: [[ModifierFlags.Extern, ModifierFlags.Auto]],
},
[SyntaxKind.FunctionDeclarationStatement]: {
allowed: ModifierFlags.All,
allowed: ModifierFlags.Extern | ModifierFlags.Internal,
required: ModifierFlags.Extern,
},
};
Expand Down Expand Up @@ -87,21 +90,51 @@ export function checkModifiers(program: Program, node: Declaration): boolean {
}
}

const missingRequiredModifiers = compatibility.required & ~node.modifierFlags;

if (missingRequiredModifiers) {
// There is at least one required modifier missing from this syntax node.
if (compatibility.required && !(node.modifierFlags & compatibility.required)) {
// None of the required modifiers are present.
isValid = false;

for (const missing of getNamesOfModifierFlags(missingRequiredModifiers)) {
const names = getNamesOfModifierFlags(compatibility.required);
if (names.length === 1) {
program.reportDiagnostic(
createDiagnostic({
code: "invalid-modifier",
messageId: "missing-required",
format: { modifier: missing, nodeKind: getDeclarationKindText(node.kind) },
format: { modifier: names[0], nodeKind: getDeclarationKindText(node.kind) },
target: node,
}),
);
} else {
program.reportDiagnostic(
createDiagnostic({
code: "invalid-modifier",
messageId: "missing-required-one-of",
format: {
modifiers: names.map((n) => `'${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,
}),
);
}
}
}

Expand Down Expand Up @@ -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}`);
}
Expand All @@ -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}`);
}
Expand All @@ -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;
}

Expand Down
Loading
Loading