Skip to content
Merged
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
13 changes: 13 additions & 0 deletions .chronus/changes/auto-decorator-setter-2026-7-14-11-0-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Add `setAutoDecorator` API to programmatically apply an `auto` decorator to a target, mirroring what the synthesized `auto dec` implementation does when the decorator is written in source. This lets emitters and mutators mark synthetic types without reaching into the program state map directly.

```ts
import { setAutoDecorator } from "@typespec/compiler";

setAutoDecorator(program, "MyLib.myFlag", target);
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

`createTester` now mounts each discovered library's `tspconfig.yaml` into the virtual file system, so experimental features a library opts into (e.g. `auto-decorators`) are honored when compiling against the tester.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/tspd"
---

`tspd gen-extern-signature` now also generates a typed setter (e.g. `setMyFlag`, `setMyLabel`) for each `auto` decorator, alongside the existing `is*`/`get*` readers.
24 changes: 22 additions & 2 deletions packages/compiler/src/core/auto-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export function createAutoDecoratorImplementation(
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;
Expand All @@ -53,14 +52,35 @@ export function createAutoDecoratorImplementation(
data[paramNames[i]] = args[i];
}
}
context.program.stateMap(stateKey).set(target, data);
setAutoDecorator(context.program, fqn, 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;
}

/**
* Programmatically apply an auto decorator to a target, storing its argument values.
*
* Mirrors what the synthesized `auto dec` implementation does when the decorator is
* written in source, so emitters and mutators can mark synthetic types the same way
* without reaching into the program state map directly.
* @param program - The current program.
* @param decoratorFqn - The fully-qualified name of the decorator (e.g., "MyLib.myDec").
* @param target - The type to mark.
* @param value - The stored `{ paramName: value }` record (defaults to `{}` for a no-arg decorator).
*/
export function setAutoDecorator(
program: Program,
decoratorFqn: string,
target: Type,
value: Record<string, unknown> = {},
): void {
const key = getAutoDecoratorStateKey(decoratorFqn);
program.stateMap(key).set(target, value);
}

/**
* Check if an auto decorator has been applied to a target.
* @param program - The current program.
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {
getAutoDecoratorTargets,
getAutoDecoratorValue,
hasAutoDecorator,
setAutoDecorator,
} from "./core/auto-decorator.js";
export {
Checker,
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler/src/testing/tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ async function createTesterFs(base: string, options: TesterOptions) {
resolvePath("node_modules", lib, "package.json"),
(resolved.manifest as any).file.text,
);

// Mount the library's own `tspconfig.yaml` (if any) so that features it opts into
// (e.g. `auto-decorators`) are honored when compiling against the virtual file system.
const tspconfigPath = resolvePath(resolved.path, "tspconfig.yaml");
try {
const tspconfig = await host.readFile(tspconfigPath);
fs.add(resolvePath("node_modules", lib, "tspconfig.yaml"), tspconfig.text);
} catch {
// No library tspconfig.yaml; nothing to mount.
}
}
}

Expand Down
19 changes: 19 additions & 0 deletions packages/compiler/test/checker/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,25 @@ describe("compiler: checker: decorators", () => {
deepStrictEqual(getAutoDecoratorValue(program, "MyLib.myLabel", Foo), { label: "world" });
});

it("setAutoDecorator programmatically marks a target read back by the accessors", async () => {
const { program } = await Tester.using("TypeSpec.Reflection").compile(`model Foo {}`);

const Foo = program.getGlobalNamespaceType().models.get("Foo")!;
const { setAutoDecorator, hasAutoDecorator, getAutoDecoratorValue } =
await import("../../src/core/auto-decorator.js");

// No decorator written in source yet.
strictEqual(hasAutoDecorator(program, "MyLib.myLabel", Foo), false);

setAutoDecorator(program, "MyLib.myLabel", Foo, { label: "world" });
strictEqual(hasAutoDecorator(program, "MyLib.myLabel", Foo), true);
deepStrictEqual(getAutoDecoratorValue(program, "MyLib.myLabel", Foo), { label: "world" });

// Defaults to an empty record for a no-arg mark.
setAutoDecorator(program, "MyLib.myFlag", Foo);
deepStrictEqual(getAutoDecoratorValue(program, "MyLib.myFlag", Foo), {});
});

it("internal auto dec is valid", async () => {
const diagnostics = await Tester.using("TypeSpec.Reflection").diagnose(
`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { it } from "vitest";
import { expect, it } from "vitest";
import { CompilerHost } from "../../src/core/types.js";
import { createTestFileSystem, mockFile } from "../../src/testing/fs.js";
import { resolveVirtualPath } from "../../src/testing/test-utils.js";
Expand Down Expand Up @@ -50,3 +50,28 @@ it("subpath typespec export get added to the test host", async () => {
});
await Tester.compile(`import "mylib/subpath";`);
});

it("mounts a library's tspconfig.yaml so its opted-in features are honored", async () => {
const fs = mkFs({
"package.json": JSON.stringify({ name: "test", version: "1.0.0" }),
"node_modules/mylib/package.json": JSON.stringify({
name: "mylib",
version: "1.0.0",
exports: { ".": { import: "./index.js", typespec: "./main.tsp" } },
}),
"node_modules/mylib/index.js": mockFile.js({}),
"node_modules/mylib/main.tsp": `
namespace MyLib;
auto dec myFlag(target: Reflection.Model);
`,
"node_modules/mylib/tspconfig.yaml": `kind: project\nfeatures:\n - auto-decorators\n`,
});
const Tester = createTester(resolveVirtualPath(""), {
host: fs,
libraries: ["mylib"],
});
const [, diagnostics] = await Tester.compileAndDiagnose(`import "mylib";`);
// Without the mounted tspconfig.yaml the library opt-in would be lost and the
// `auto dec` declaration would report `auto-decorator-disabled`.
expect(diagnostics.filter((d) => d.code === "auto-decorator-disabled")).toHaveLength(0);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { code, For } from "@alloy-js/core";
import { code, For, List } from "@alloy-js/core";
import * as ts from "@alloy-js/typescript";
import { typespecCompiler } from "../external-packages/compiler.js";
import { DecoratorSignature } from "../types.js";
Expand All @@ -12,6 +12,8 @@ export interface AutoDecoratorAccessorsProps {
/**
* Generate typed accessor functions for auto decorators.
* These are thin wrappers around the compiler's generic auto decorator API.
* For each auto decorator both a reader (`is*`/`get*`) and a setter (`set*`)
* are generated.
*/
export function AutoDecoratorAccessors(props: Readonly<AutoDecoratorAccessorsProps>) {
return (
Expand All @@ -29,6 +31,16 @@ interface AutoDecoratorAccessorProps {
}

function AutoDecoratorAccessor(props: Readonly<AutoDecoratorAccessorProps>) {
return (
<List doubleHardline>
<AutoDecoratorReader {...props} />
<AutoDecoratorSetter {...props} />
</List>
);
}

/** Generate the reader (`is*` for no-arg, `get*` for decorators with args). */
function AutoDecoratorReader(props: Readonly<AutoDecoratorAccessorProps>) {
const decorator = props.signature.decorator;
const name = decorator.name.slice(1); // remove @
const capitalizedName = name[0].toUpperCase() + name.slice(1);
Expand Down Expand Up @@ -100,3 +112,62 @@ function AutoDecoratorAccessor(props: Readonly<AutoDecoratorAccessorProps>) {
</ts.FunctionDeclaration>
);
}

/**
* Generate the setter (`set*`). Mirrors what the synthesized `auto dec`
* implementation does when the decorator is written in source, so emitters and
* mutators can programmatically mark synthetic types.
*/
function AutoDecoratorSetter(props: Readonly<AutoDecoratorAccessorProps>) {
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 = <TargetParameterTsType type={decorator.target.type.type} />;

const parameters: { name: string; type: any }[] = [
{ name: "program", type: typespecCompiler.Program },
{ name: decorator.target.name, type: targetType },
];

let body;
if (params.length === 0) {
// No-arg auto decorator — the stored record defaults to `{}`.
body = code`${typespecCompiler.setAutoDecorator}(program, "${fqn}", ${decorator.target.name});`;
} else if (params.length === 1) {
// Single-arg: accept the bare value (parity with the `get*` reader) and
// wrap it into the uniform `{ paramName: value }` storage record.
const param = params[0];
parameters.push({ name: param.name, type: <ParameterTsType constraint={param.type} /> });
body = code`${typespecCompiler.setAutoDecorator}(program, "${fqn}", ${decorator.target.name}, { ${param.name}: ${param.name} });`;
} else {
// Multi-arg: accept the whole `{ paramName: value }` record.
const valueType = (
<>
{"{ "}
<For each={params} joiner="; ">
{(param) => (
<>
{param.name}: <ParameterTsType constraint={param.type} />
</>
)}
</For>
{" }"}
</>
);
parameters.push({ name: "value", type: valueType });
body = code`${typespecCompiler.setAutoDecorator}(program, "${fqn}", ${decorator.target.name}, value);`;
}

return (
<ts.FunctionDeclaration
export
name={`set${capitalizedName}`}
parameters={parameters}
returnType="void"
>
{body}
</ts.FunctionDeclaration>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const typespecCompiler = createPackage({
"DecoratorValidatorCallbacks",
"getAutoDecoratorValue",
"hasAutoDecorator",
"setAutoDecorator",
],
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,26 +462,20 @@ function importLine(imports: string[]) {
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"])}
import { hasAutoDecorator, type Model, type Program, setAutoDecorator } from "@typespec/compiler";

export function isMyFlag(program: Program, target: Model): boolean {
return hasAutoDecorator(program, "myFlag", target);
}

export function setMyFlag(program: Program, target: Model): void {
setAutoDecorator(program, "myFlag", target);
}
`,
});
});
Expand All @@ -490,11 +484,15 @@ export function isMyFlag(program: Program, target: Model): boolean {
await expectSignatures({
code: `auto dec myLabel(target: Model, label: valueof string);`,
expected: `
${autoImportLine(["Model", "Program"], ["getAutoDecoratorValue"])}
import { getAutoDecoratorValue, type Model, type Program, setAutoDecorator } from "@typespec/compiler";

export function getMyLabel(program: Program, target: Model): string | undefined {
return getAutoDecoratorValue(program, "myLabel", target)?.["label"] as any;
}

export function setMyLabel(program: Program, target: Model, label: string): void {
setAutoDecorator(program, "myLabel", target, { label: label });
}
`,
});
});
Expand All @@ -503,11 +501,15 @@ export function getMyLabel(program: Program, target: Model): string | undefined
await expectSignatures({
code: `auto dec myTags(target: Model, ...tags: valueof string[]);`,
expected: `
${autoImportLine(["Model", "Program"], ["getAutoDecoratorValue"])}
import { getAutoDecoratorValue, type Model, type Program, setAutoDecorator } from "@typespec/compiler";

export function getMyTags(program: Program, target: Model): readonly string[] | undefined {
return getAutoDecoratorValue(program, "myTags", target)?.["tags"] as any;
}

export function setMyTags(program: Program, target: Model, tags: readonly string[]): void {
setAutoDecorator(program, "myTags", target, { tags: tags });
}
`,
});
});
Expand All @@ -516,11 +518,15 @@ export function getMyTags(program: Program, target: Model): readonly string[] |
await expectSignatures({
code: `auto dec myMeta(target: Model, name: valueof string, version: valueof int32);`,
expected: `
${autoImportLine(["Model", "Program"], ["getAutoDecoratorValue"])}
import { getAutoDecoratorValue, type Model, type Program, setAutoDecorator } from "@typespec/compiler";

export function getMyMeta(program: Program, target: Model): { name: string; version: number } | undefined {
return getAutoDecoratorValue(program, "myMeta", target) as any;
}

export function setMyMeta(program: Program, target: Model, value: { name: string; version: number }): void {
setAutoDecorator(program, "myMeta", target, value);
}
`,
});
});
Expand Down Expand Up @@ -570,6 +576,8 @@ export function getMyMeta(program: Program, target: Model): { name: string; vers
// Verify auto decorator parts
expect(result).toContain("isDataFlag");
expect(result).toContain("hasAutoDecorator");
expect(result).toContain("setDataFlag");
expect(result).toContain("setAutoDecorator");
// Verify no $decorators type for auto decorators
expect(result).not.toContain("dataFlag: ");
});
Expand Down
Loading