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
11 changes: 11 additions & 0 deletions .chronus/changes/stub-property-type-2026-4-28-8-11-36.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
changeKind: feature
packages:
- "@typespec/mutator-framework"
---

Support replacing member references with alternate types during mutation.

```ts
return engine.replaceAndMutateReference(referenceTypes[0], alternateType, options, halfEdge);
```
24 changes: 24 additions & 0 deletions packages/mutator-framework/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,30 @@ class NullableReferencedModelMutation extends SimpleModelMutation<SimpleMutation

Mutation nodes also have a `replace` method. Returning a new mutation from MutationInfo makes the resulting Mutations look "as if" the source type graph were shaped differently. This is useful for doing things like normalizations of the type graph. The structure of the Mutations mimic this new structure. When you `replace` on a mutation node, the Mutation stays the same, but the mutated type graph is changed. This is useful for doing things like renaming things or swapping scalars in situations where you want to see both the source type and the mutated type in order to compare them.

#### Replacing a member's underlying type from `mutationInfo`

When `replaceAndMutateReference` is called from a member's `mutationInfo` (e.g.
a `ModelProperty` or `UnionVariant` overriding `mutationInfo`), the parent's
half-edge expects a member-kind tail (`ModelProperty` / `UnionVariant`) but
`newType` is typically a different kind such as `Model`, `Union`, or `Scalar`.

The engine detects this mismatch (via `MutationHalfEdge.expectedTailKind`,
declared by the built-in `Simple*Mutation` member-yielding edges) and wraps
`newType` in a synthetic member of the expected kind named after the original
reference. The synthetic member's normal mutation flow then mutates `newType`
recursively through its own type edge, so:

- `parentMutation.properties.get("prop")` still returns a `ModelProperty`
mutation (the synthetic), keeping the parent's slot kind-correct.
- `propMutation.mutatedType.type` points at the mutated `newType`.
- Nested replacements (e.g. an alternate model whose own properties are also
replaced) compose naturally because each level wraps independently.

If the half-edge does not declare `expectedTailKind` (the default for "type"
edges and any user-defined half-edge), `newType` is mutated directly through
the half-edge — useful for routing alternate types to a separate edge in
custom engines.

## Mutation Caching

Mutations are automatically cached and reused. When you call `engine.mutate()`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,18 @@ it("clones synthetic mutation nodes", async () => {
expect(propNode.mutatedType.type === model).toBe(false);
expect(propNode.mutatedType.type === typeNode.mutatedType).toBe(true);
});

it("creates a literal mutation node for StringTemplate types", async () => {
const { Foo, program } = await runner.compile(t.code`
model ${t.model("Foo")} {
prop: "Start \${123} end";
}
`);
const engine = getEngine(program);
const prop = Foo.properties.get("prop")!;

expect(prop.type.kind).toBe("StringTemplate");
const node = engine.getMutationNode(prop.type);
expect(node).toBeDefined();
expect(node.sourceType).toBe(prop.type);
});
Loading
Loading