diff --git a/.chronus/changes/auto-decorator-setter-2026-7-14-11-0-0.md b/.chronus/changes/auto-decorator-setter-2026-7-14-11-0-0.md new file mode 100644 index 00000000000..831f769d2f2 --- /dev/null +++ b/.chronus/changes/auto-decorator-setter-2026-7-14-11-0-0.md @@ -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); +``` diff --git a/.chronus/changes/tester-mount-library-tspconfig-2026-7-14-11-0-2.md b/.chronus/changes/tester-mount-library-tspconfig-2026-7-14-11-0-2.md new file mode 100644 index 00000000000..70b623b5b66 --- /dev/null +++ b/.chronus/changes/tester-mount-library-tspconfig-2026-7-14-11-0-2.md @@ -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. diff --git a/.chronus/changes/tspd-auto-decorator-setter-2026-7-14-11-0-1.md b/.chronus/changes/tspd-auto-decorator-setter-2026-7-14-11-0-1.md new file mode 100644 index 00000000000..84bbb9b0a5a --- /dev/null +++ b/.chronus/changes/tspd-auto-decorator-setter-2026-7-14-11-0-1.md @@ -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. diff --git a/packages/compiler/src/core/auto-decorator.ts b/packages/compiler/src/core/auto-decorator.ts index 167d2fc7207..befbf984b0b 100644 --- a/packages/compiler/src/core/auto-decorator.ts +++ b/packages/compiler/src/core/auto-decorator.ts @@ -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; @@ -53,7 +52,7 @@ 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 `@` text in the duplicate-decorator // diagnostic; mirror the extern `$name` convention so the helper strips it. @@ -61,6 +60,27 @@ export function createAutoDecoratorImplementation( 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 = {}, +): 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. diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 0c6a4e03663..3af1f8bc617 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -3,6 +3,7 @@ export { getAutoDecoratorTargets, getAutoDecoratorValue, hasAutoDecorator, + setAutoDecorator, } from "./core/auto-decorator.js"; export { Checker, diff --git a/packages/compiler/src/testing/tester.ts b/packages/compiler/src/testing/tester.ts index dd50e0082a1..e699228ef4e 100644 --- a/packages/compiler/src/testing/tester.ts +++ b/packages/compiler/src/testing/tester.ts @@ -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. + } } } diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 1bbb721a681..ba83ea9d69e 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -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( ` diff --git a/packages/compiler/test/testing/tester-library-discovery.test.ts b/packages/compiler/test/testing/tester-library-discovery.test.ts index 14c5cf01ddd..af7339e40c2 100644 --- a/packages/compiler/test/testing/tester-library-discovery.test.ts +++ b/packages/compiler/test/testing/tester-library-discovery.test.ts @@ -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"; @@ -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); +}); 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 fa2287b904e..0cebb9b02c7 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 @@ -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"; @@ -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) { return ( @@ -29,6 +31,16 @@ interface AutoDecoratorAccessorProps { } function AutoDecoratorAccessor(props: Readonly) { + return ( + + + + + ); +} + +/** Generate the reader (`is*` for no-arg, `get*` for decorators with args). */ +function AutoDecoratorReader(props: Readonly) { const decorator = props.signature.decorator; const name = decorator.name.slice(1); // remove @ const capitalizedName = name[0].toUpperCase() + name.slice(1); @@ -100,3 +112,62 @@ function AutoDecoratorAccessor(props: Readonly) { ); } + +/** + * 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) { + 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 = ; + + 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: }); + body = code`${typespecCompiler.setAutoDecorator}(program, "${fqn}", ${decorator.target.name}, { ${param.name}: ${param.name} });`; + } else { + // Multi-arg: accept the whole `{ paramName: value }` record. + const valueType = ( + <> + {"{ "} + + {(param) => ( + <> + {param.name}: + + )} + + {" }"} + + ); + parameters.push({ name: "value", type: valueType }); + body = code`${typespecCompiler.setAutoDecorator}(program, "${fqn}", ${decorator.target.name}, value);`; + } + + return ( + + {body} + + ); +} 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 afff12fa90d..5a60a0c4258 100644 --- a/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts +++ b/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts @@ -26,6 +26,7 @@ export const typespecCompiler = createPackage({ "DecoratorValidatorCallbacks", "getAutoDecoratorValue", "hasAutoDecorator", + "setAutoDecorator", ], }, }, 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 16001f974fa..e9678c07a78 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -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); +} `, }); }); @@ -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 }); +} `, }); }); @@ -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 }); +} `, }); }); @@ -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); +} `, }); }); @@ -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: "); });