Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new sourceModels property to model #3191

Merged
merged 6 commits into from
Apr 23, 2024
Merged
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
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
Loading