Skip to content

Commit

Permalink
Add new sourceModels property to model (#3191)
Browse files Browse the repository at this point in the history
resolves [#2818](#2818)

Add a new `sourceModels` property that keeps track of models used to
construct it. `sourceModels` is an array of a new `SoruceModel` object
which contains metadata on how the model was used:
- is: `model A is B`
- spread: `model A {...B}`
- intersection: `alias A = B & C`
  • Loading branch information
timotheeguerin committed Apr 23, 2024
1 parent 1987a56 commit 5fa3d78
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 14 deletions.
8 changes: 8 additions & 0 deletions .chronus/changes/spread-source-2024-3-18-18-19-45.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: fix
packages:
- "@typespec/compiler"
---

[API] Add new `sourceModels` property to model
8 changes: 8 additions & 0 deletions .chronus/changes/spread-source-2024-3-18-18-49-3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: fix
packages:
- "@typespec/html-program-viewer"
---

Add `sourceModels` property to model view
10 changes: 10 additions & 0 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1585,6 +1585,7 @@ export function createChecker(program: Program): Checker {
properties: properties,
decorators: [],
derivedModels: [],
sourceModels: [],
});

const indexers: ModelIndexer[] = [];
Expand Down Expand Up @@ -1617,6 +1618,7 @@ export function createChecker(program: Program): Checker {
}
}
for (const [_, option] of modelOptions) {
intersection.sourceModels.push({ usage: "intersection", model: option });
const allProps = walkPropertiesInherited(option);
for (const prop of allProps) {
if (properties.has(prop.name)) {
Expand Down Expand Up @@ -2746,13 +2748,15 @@ export function createChecker(program: Program): Checker {
properties: createRekeyableMap<string, ModelProperty>(),
namespace: getParentNamespaceType(node),
decorators,
sourceModels: [],
derivedModels: [],
});
linkType(links, type, mapper);
const isBase = checkModelIs(node, node.is, mapper);

if (isBase) {
type.sourceModel = isBase;
type.sourceModels.push({ usage: "is", model: isBase });
// copy decorators
decorators.push(...isBase.decorators);
if (isBase.indexer) {
Expand Down Expand Up @@ -2840,6 +2844,7 @@ export function createChecker(program: Program): Checker {
namespace: getParentNamespaceType(node),
decorators: [],
derivedModels: [],
sourceModels: [],
});
checkModelProperties(node, properties, type, mapper);
return finishType(type);
Expand Down Expand Up @@ -2920,6 +2925,7 @@ export function createChecker(program: Program): Checker {
parentModel,
mapper
);

if (additionalIndexer) {
if (spreadIndexers) {
spreadIndexers.push(additionalIndexer);
Expand Down Expand Up @@ -3452,6 +3458,8 @@ export function createChecker(program: Program): Checker {
);
}

parentModel.sourceModels.push({ usage: "spread", model: targetType });

const props: ModelProperty[] = [];
// copy each property
for (const prop of walkPropertiesInherited(targetType)) {
Expand Down Expand Up @@ -4842,6 +4850,7 @@ export function createChecker(program: Program): Checker {
decorators: [],
properties: createRekeyableMap(),
derivedModels: [],
sourceModels: [],
});

for (const propNode of node.properties) {
Expand Down Expand Up @@ -6165,6 +6174,7 @@ export function filterModelProperties(
properties,
decorators: [],
derivedModels: [],
sourceModels: [{ usage: "spread", model }],
});

for (const property of walkPropertiesInherited(model)) {
Expand Down
17 changes: 17 additions & 0 deletions packages/compiler/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,13 +265,30 @@ export interface Model extends BaseType, DecoratedType, TemplatedTypeBase {
*/
sourceModel?: Model;

/**
* Models that were used to build this model. This include any model referenced in `model is`, `...` or when intersecting models.
*/
sourceModels: SourceModel[];

/**
* Late-bound symbol of this model type.
* @internal
*/
symbol?: Sym;
}

export interface SourceModel {
/**
* How was this model used.
* - is: `model A is B`
* - spread: `model A {...B}`
* - intersection: `alias A = B & C`
*/
readonly usage: "is" | "spread" | "intersection";
/** Source model */
readonly model: Model;
}

export interface ModelProperty extends BaseType, DecoratedType {
kind: "ModelProperty";
node:
Expand Down
43 changes: 32 additions & 11 deletions packages/compiler/test/checker/intersections.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ok, strictEqual } from "assert";
import { beforeEach, describe, it } from "vitest";
import { Model } from "../../src/core/index.js";
import { beforeEach, describe, expect, it } from "vitest";
import { Model, ModelProperty } from "../../src/core/index.js";
import {
BasicTestRunner,
createTestHost,
Expand All @@ -18,17 +18,38 @@ describe("compiler: intersections", () => {
});

it("intersect 2 models", async () => {
const { Foo } = (await runner.compile(`
@test model Foo {
prop: {a: string} & {b: string};
const { prop } = (await runner.compile(`
model Foo {
@test prop: {a: string} & {b: string};
}
`)) as { Foo: Model };
`)) as { prop: ModelProperty };

const prop = Foo.properties.get("prop")!.type as Model;
strictEqual(prop.kind, "Model");
strictEqual(prop.properties.size, 2);
ok(prop.properties.has("a"));
ok(prop.properties.has("b"));
const propType = prop.type;
strictEqual(propType.kind, "Model");
strictEqual(propType.properties.size, 2);
ok(propType.properties.has("a"));
ok(propType.properties.has("b"));
});

it("keeps reference to source model in sourceModels", async () => {
const { A, B, prop } = (await runner.compile(`
@test model A { one: string }
@test model B { two: string }
model Foo {
@test prop: A & B;
}
`)) as {
A: Model;
B: Model;
prop: ModelProperty;
};
const intersection = prop.type;
strictEqual(intersection.kind, "Model");
expect(intersection.sourceModels).toHaveLength(2);
strictEqual(intersection.sourceModels[0].model, A);
strictEqual(intersection.sourceModels[0].usage, "intersection");
strictEqual(intersection.sourceModels[1].model, B);
strictEqual(intersection.sourceModels[1].usage, "intersection");
});

it("intersection type belong to namespace it is declared in", async () => {
Expand Down
36 changes: 33 additions & 3 deletions packages/compiler/test/checker/model.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { deepStrictEqual, match, ok, strictEqual } from "assert";
import { beforeEach, describe, it } from "vitest";
import { beforeEach, describe, expect, it } from "vitest";
import { isTemplateDeclaration } from "../../src/core/type-utils.js";
import { Model, ModelProperty, Type } from "../../src/core/types.js";
import { Operation, getDoc, isArrayModelType, isRecordModelType } from "../../src/index.js";
Expand Down Expand Up @@ -565,11 +565,10 @@ describe("compiler: models", () => {
});
});

it("keeps reference to source model", async () => {
it("keeps reference to source model in sourceModel", async () => {
testHost.addTypeSpecFile(
"main.tsp",
`
import "./dec.js";
@test model A { }
@test model B is A { };
`
Expand All @@ -578,6 +577,20 @@ describe("compiler: models", () => {
strictEqual(B.sourceModel, A);
});

it("keeps reference to source model in sourceModels", async () => {
testHost.addTypeSpecFile(
"main.tsp",
`
@test model A { }
@test model B is A { };
`
);
const { A, B } = (await testHost.compile("main.tsp")) as { A: Model; B: Model };
expect(B.sourceModels).toHaveLength(1);
strictEqual(B.sourceModels[0].model, A);
strictEqual(B.sourceModels[0].usage, "is");
});

it("copies decorators", async () => {
testHost.addTypeSpecFile(
"main.tsp",
Expand Down Expand Up @@ -851,6 +864,23 @@ describe("compiler: models", () => {
strictEqual(getDoc(testHost.program, Base.properties.get("one")!), "base doc");
});

it("keeps reference to source model in sourceModels", async () => {
testHost.addTypeSpecFile(
"main.tsp",
`
@test model A { one: string }
@test model B { two: string }
@test model C {...A, ...B}
`
);
const { A, B, C } = (await testHost.compile("main.tsp")) as { A: Model; B: Model; C: Model };
expect(C.sourceModels).toHaveLength(2);
strictEqual(C.sourceModels[0].model, A);
strictEqual(C.sourceModels[0].usage, "spread");
strictEqual(C.sourceModels[1].model, B);
strictEqual(C.sourceModels[1].usage, "spread");
});

it("can spread a Record<T>", async () => {
testHost.addTypeSpecFile(
"main.tsp",
Expand Down
1 change: 1 addition & 0 deletions packages/html-program-viewer/src/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ const ModelUI: FunctionComponent<{ type: Model }> = ({ type }) => {
derivedModels: "ref",
properties: "nested",
sourceModel: "ref",
sourceModels: "value",
}}
/>
);
Expand Down

0 comments on commit 5fa3d78

Please sign in to comment.