From 5b2001c0c4259f5657929f31ac1deacb72e944ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:26:59 +0000 Subject: [PATCH 1/4] Initial plan From dd75523c8cefac5b57e7235cd8c78e0390716326 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:40:03 +0000 Subject: [PATCH 2/4] fix: FilterVisibility now applies visibility filter to inherited types (extends) When filtering a model by visibility, the filter is now also applied to base models in the inheritance chain. This ensures that properties with incompatible visibility in base types are properly filtered out. Closes #11036 Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- ...er-visibility-extends-2026-6-22-18-35-0.md | 7 + packages/compiler/src/lib/visibility.ts | 8 + packages/compiler/test/visibility.test.ts | 175 ++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 .chronus/changes/fix-filter-visibility-extends-2026-6-22-18-35-0.md diff --git a/.chronus/changes/fix-filter-visibility-extends-2026-6-22-18-35-0.md b/.chronus/changes/fix-filter-visibility-extends-2026-6-22-18-35-0.md new file mode 100644 index 00000000000..3ce8c7649f4 --- /dev/null +++ b/.chronus/changes/fix-filter-visibility-extends-2026-6-22-18-35-0.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +`FilterVisibility` now applies the visibility filter to inherited types (via `extends`), filtering out invisible properties from base models in the inheritance chain. diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index dd8965ab487..1da7727b9b4 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -748,6 +748,14 @@ function createVisibilityFilterMutator( } } + if (model.baseModel) { + const mutatedBase = cachedMutateSubgraph(program, self, model.baseModel); + if (mutatedBase.type !== model.baseModel) { + clone.baseModel = mutatedBase.type as Model; + modified = true; + } + } + if (options.decoratorFn) { clone.decorators = clone.decorators.filter( (d) => diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index b4c6911a06c..2d6af032e8b 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -1596,6 +1596,181 @@ describe("compiler: visibility core", () => { ok(!arrA.properties.has("invisible")); }); + it("applies visibility filter to base model (extends)", async () => { + const { UserCreate, program } = await Tester.compile(t.code` + model Base { + @visibility(Lifecycle.Read) id: string; + } + + model Auditable extends Base { + @visibility(Lifecycle.Read) created_at: utcDateTime; + @visibility(Lifecycle.Read) updated_at: utcDateTime; + } + + model User extends Auditable { + name: string; + } + + @test model ${t.model("UserCreate")} is FilterVisibility; + `); + + ok(UserCreate); + + // User's own property 'name' has default visibility (all), so it should be visible + ok(UserCreate.properties.has("name")); + + // The base model should be transformed (Auditable -> AuditableCreate) + const auditableCreate = UserCreate.baseModel; + ok(auditableCreate); + strictEqual(auditableCreate.name, "AuditableCreate"); + + // Auditable's read-only properties should be filtered out + ok(!auditableCreate.properties.has("created_at")); + ok(!auditableCreate.properties.has("updated_at")); + + // The base of Auditable (Base) should also be transformed + const baseCreate = auditableCreate.baseModel; + ok(baseCreate); + strictEqual(baseCreate.name, "BaseCreate"); + + // Base's read-only property should be filtered out + ok(!baseCreate.properties.has("id")); + }); + + it("does not transform base model when all properties are visible", async () => { + const { ChildFiltered, program } = await Tester.compile(t.code` + model Parent { + name: string; + } + + model Child extends Parent { + @visibility(Lifecycle.Read) id: string; + description: string; + } + + @test model ${t.model("ChildFiltered")} is FilterVisibility; + `); + + ok(ChildFiltered); + + // Child's 'description' is visible (default visibility), 'id' is not + ok(ChildFiltered.properties.has("description")); + ok(!ChildFiltered.properties.has("id")); + + // Parent has no properties with restricted visibility, so it should remain unchanged + const base = ChildFiltered.baseModel; + ok(base); + strictEqual(base.name, "Parent"); + ok(base.properties.has("name")); + }); + + it("applies visibility filter across multiple layers where base is unchanged", async () => { + const { Result, program } = await Tester.compile(t.code` + model GrandParent { + gp_all: string; + } + + model Parent extends GrandParent { + parent_all: string; + } + + model Child extends Parent { + @visibility(Lifecycle.Read) child_read: string; + child_all: string; + } + + @test model ${t.model("Result")} is FilterVisibility; + `); + + ok(Result); + + // Child's properties + ok(Result.properties.has("child_all")); + ok(!Result.properties.has("child_read")); + + // Neither Parent nor GrandParent have visibility-restricted properties, + // so the entire base chain should remain unchanged + const parentBase = Result.baseModel; + ok(parentBase); + strictEqual(parentBase.name, "Parent"); + ok(parentBase.properties.has("parent_all")); + + const gpBase = parentBase.baseModel; + ok(gpBase); + strictEqual(gpBase.name, "GrandParent"); + ok(gpBase.properties.has("gp_all")); + }); + + it("applies visibility filter across multiple inheritance layers", async () => { + const { Result, program } = await Tester.compile(t.code` + model GrandParent { + @visibility(Lifecycle.Read) gp_read: string; + gp_all: string; + } + + model Parent extends GrandParent { + parent_all: string; + } + + model Child extends Parent { + @visibility(Lifecycle.Read) child_read: string; + child_all: string; + } + + @test model ${t.model("Result")} is FilterVisibility; + `); + + ok(Result); + + // Child's properties + ok(Result.properties.has("child_all")); + ok(!Result.properties.has("child_read")); + + // Parent is transformed because its base (GrandParent) is transformed + const parentFiltered = Result.baseModel; + ok(parentFiltered); + strictEqual(parentFiltered.name, "ParentCreate"); + ok(parentFiltered.properties.has("parent_all")); + + // GrandParent needs transformation (has read-only property) + const gpFiltered = parentFiltered.baseModel; + ok(gpFiltered); + strictEqual(gpFiltered.name, "GrandParentCreate"); + ok(gpFiltered.properties.has("gp_all")); + ok(!gpFiltered.properties.has("gp_read")); + }); + + it("applies visibility filter to base model via applyVisibilityFilter", async () => { + const { User, program } = await Tester.compile(t.code` + model Base { + @visibility(Lifecycle.Read) id: string; + } + + model ${t.model("User")} extends Base { + name: string; + } + `); + + const fnContext = { program } satisfies Pick; + const lifecycle = getLifecycleVisibilityEnum(program); + const filtered = applyVisibilityFilter( + fnContext, + User, + anyFilter(lifecycle.members.get("Create")!), + "{name}Create", + ); + + ok(filtered); + strictEqual(filtered.name, "UserCreate"); + ok(filtered.properties.has("name")); + + // Base model should be transformed + const baseFiltered = filtered.baseModel; + ok(baseFiltered); + strictEqual(baseFiltered.name, "BaseCreate"); + ok(!baseFiltered.properties.has("id")); + }); + it("does not duplicate encodedName metadata", async () => { const diagnostics = await Tester.diagnose(` model SomeModel { From a7de7adac4eef0e9d851076e310c71b2c678f59a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:52:25 +0000 Subject: [PATCH 3/4] test: add self-referential model test to prevent infinite regress Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- packages/compiler/test/visibility.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index 2d6af032e8b..cecf6768b24 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -1771,6 +1771,23 @@ describe("compiler: visibility core", () => { ok(!baseFiltered.properties.has("id")); }); + it("does not produce infinite regress for self-referential models", async () => { + const { Result } = await Tester.compile(t.code` + model LinkedNode { + value: string; + @visibility(Lifecycle.Read) id: string; + next?: LinkedNode; + } + + @test model ${t.model("Result")} is FilterVisibility; + `); + + ok(Result); + ok(Result.properties.has("value")); + ok(Result.properties.has("next")); + ok(!Result.properties.has("id")); + }); + it("does not duplicate encodedName metadata", async () => { const diagnostics = await Tester.diagnose(` model SomeModel { From 1e6c9a41c8f51c556813264d85dd1d0d5baf8528 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 23:07:53 +0000 Subject: [PATCH 4/4] test: update self-referential test to extend model with self-link Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- packages/compiler/test/visibility.test.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index cecf6768b24..a2092d372ef 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -1779,13 +1779,23 @@ describe("compiler: visibility core", () => { next?: LinkedNode; } - @test model ${t.model("Result")} is FilterVisibility; + model Child extends LinkedNode { + extra: string; + } + + @test model ${t.model("Result")} is FilterVisibility; `); ok(Result); - ok(Result.properties.has("value")); - ok(Result.properties.has("next")); - ok(!Result.properties.has("id")); + ok(Result.properties.has("extra")); + + // Base model (LinkedNode) should be filtered and renamed + const base = Result.baseModel; + ok(base); + strictEqual(base.name, "LinkedNodeCreate"); + ok(base.properties.has("value")); + ok(base.properties.has("next")); + ok(!base.properties.has("id")); }); it("does not duplicate encodedName metadata", async () => {