From 5f8dbbb55e6fd75894fefc72b1aaad4ce1af4515 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 8 Jan 2025 09:33:56 -0500 Subject: [PATCH 1/2] [core] Fix visibility projection handling --- packages/compiler/src/core/projector.ts | 27 +++++- .../compiler/src/core/visibility/lifecycle.ts | 18 +++- packages/compiler/test/visibility.test.ts | 95 +++++++++++++++++++ 3 files changed, 135 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/core/projector.ts b/packages/compiler/src/core/projector.ts index 60821d1b72..35b3152839 100644 --- a/packages/compiler/src/core/projector.ts +++ b/packages/compiler/src/core/projector.ts @@ -109,7 +109,28 @@ export function createProjector( type: Type | Value | IndeterminateEntity, ): Type | Value | IndeterminateEntity { if (isValue(type)) { - return type; + if (type.valueKind === "EnumValue") { + // This is a hack. We project the enum itself, so we need to project enum values in order to prevent incoherence + // between projections that reference enum values and the enum/members themselves. If we project the enummember + // in the value and it results in something other than an enum member, we will give up and just return the value + // as is. Otherwise we'll return the projected member. + + const projectedType = projectType(type.type); + const projectedMember = projectType(type.value); + + if (projectedMember.kind === "EnumMember") { + return { + entityKind: "Value", + valueKind: "EnumValue", + type: projectedType, + value: projectedMember, + }; + } else { + return type; + } + } else { + return type; + } } if (type.entityKind === "Indeterminate") { return { entityKind: "Indeterminate", type: projectType(type.type) as any }; @@ -571,8 +592,8 @@ export function createProjector( const args: DecoratorArgument[] = []; for (const arg of dec.args) { const jsValue = - typeof arg.jsValue === "object" && arg.jsValue !== null && "kind" in arg.jsValue - ? projectType(arg.jsValue as any) + typeof arg.jsValue === "object" && arg.jsValue !== null && "entityKind" in arg.jsValue + ? projectType(arg.jsValue as Type | Value) : arg.jsValue; args.push({ ...arg, value: projectType(arg.value), jsValue }); } diff --git a/packages/compiler/src/core/visibility/lifecycle.ts b/packages/compiler/src/core/visibility/lifecycle.ts index 0d6addf627..4d1e914a44 100644 --- a/packages/compiler/src/core/visibility/lifecycle.ts +++ b/packages/compiler/src/core/visibility/lifecycle.ts @@ -3,6 +3,7 @@ import { compilerAssert } from "../diagnostics.js"; import type { Program } from "../program.js"; +import { isProjectedProgram } from "../projected-program.js"; import type { Enum, EnumMember } from "../types.js"; /** @@ -30,9 +31,22 @@ export function getLifecycleVisibilityEnum(program: Program): Enum { compilerAssert(type!.kind === "Enum", "Expected `TypeSpec.Visibility.Lifecycle` to be an enum"); - LIFECYCLE_ENUM_CACHE.set(program, type); + if (isProjectedProgram(program)) { + const projectedType = program.projector.projectType(type); - return type; + compilerAssert( + projectedType.entityKind === "Type" && projectedType.kind === "Enum", + "Expected `TypeSpec.Visibility.Lifecycle` to be an Enum (projected)", + ); + + LIFECYCLE_ENUM_CACHE.set(program, projectedType); + + return projectedType; + } else { + LIFECYCLE_ENUM_CACHE.set(program, type); + + return type; + } } /** diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index 837464c1fe..50276b52f0 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -17,6 +17,7 @@ import { isVisible, Model, ModelProperty, + projectProgram, removeVisibilityModifiers, resetVisibilityModifiersForClass, sealVisibilityModifiers, @@ -1010,7 +1011,101 @@ describe("compiler: visibility core", () => { strictEqual(idRead.type, idReadCreate.type); }); }); + + describe("projection compatibility", () => { + it("allows @defaultVisibility to be used on enums that are projected", async () => { + const { Example, Bar } = (await runner.compile(` + @defaultVisibility(Example.A) + @test enum Example { + A, + B, + } + + @test + model Bar { + @visibility(Example.B) + x: string; + } + + #suppress "projections-are-experimental" + projection Bar#test { + to { + } + } + `)) as { Example: Enum; Bar: Model }; + + const projected = projectProgram(runner.program, [ + { + projectionName: "test", + arguments: [], + }, + ]); + + const visibility = getVisibilityForClass(runner.program, Bar.properties.get("x")!, Example); + + strictEqual(visibility.size, 1); + ok(visibility.has(Example.members.get("B")!)); + + const ProjectedBar = projected.projector.projectType(Bar) as Model; + const ProjectedExample = projected.projector.projectType(Example) as Enum; + + const projectedX = ProjectedBar.properties.get("x")!; + + strictEqual(projectedX.decorators.length, 1); + + const projectedVisibility = getVisibilityForClass(projected, projectedX, ProjectedExample); + + strictEqual(projectedVisibility.size, 1); + ok(projectedVisibility.has(ProjectedExample.members.get("B")!)); + }); + + it("correctly makes projected properties invisible", async () => { + const { Example } = (await runner.compile(` + @test + model Example { + @invisible(Lifecycle) + x: string; + } + + #suppress "projections-are-experimental" + projection Example#test { + to { + } + } + `)) as { Example: Model }; + + const projected = projectProgram(runner.program, [ + { + projectionName: "test", + arguments: [], + }, + ]); + + const visibility = getVisibilityForClass( + runner.program, + Example.properties.get("x")!, + getLifecycleVisibilityEnum(runner.program), + ); + + strictEqual(visibility.size, 0); + + const projectedExample = projected.projector.projectType(Example) as Model; + + const projectedX = projectedExample.properties.get("x")!; + + strictEqual(projectedX.decorators.length, 1); + + const projectedVisibility = getVisibilityForClass( + projected, + projectedX, + getLifecycleVisibilityEnum(projected), + ); + + strictEqual(projectedVisibility.size, 0); + }); + }); }); + function validateCreateOrUpdateTransform( props: { c: ModelProperty | undefined; From 37757e5bc979426ece5fef1cefd833fe332175d4 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 8 Jan 2025 09:37:04 -0500 Subject: [PATCH 2/2] Chronus --- ...temple-visibility-projection-fix-2025-0-8-9-35-1.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .chronus/changes/witemple-visibility-projection-fix-2025-0-8-9-35-1.md diff --git a/.chronus/changes/witemple-visibility-projection-fix-2025-0-8-9-35-1.md b/.chronus/changes/witemple-visibility-projection-fix-2025-0-8-9-35-1.md new file mode 100644 index 0000000000..d66d434357 --- /dev/null +++ b/.chronus/changes/witemple-visibility-projection-fix-2025-0-8-9-35-1.md @@ -0,0 +1,10 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Enum-driven visibility decorators and projections now interact correctly. + +Projections now project EnumValue values to preserve consistency with projected Enum/EnumMember types using a best-effort +strategy.