Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/versioning"
---

Report references that remain available after their target type is removed.
35 changes: 35 additions & 0 deletions packages/versioning/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,24 @@ function findAvailabilityOnOrBeforeVersion(
return undefined;
}

function isFirstUnavailableVersion(
version: string,
avail: Map<string, Availability>,
): boolean {
let previous: Availability | undefined;
for (const [key, current] of avail) {
if (key === version) {
return (
[Availability.Removed, Availability.Unavailable].includes(current) &&
previous !== undefined &&
[Availability.Added, Availability.Available].includes(previous)
);
}
previous = current;
}
return false;
}

function validateAvailabilityForRef(
program: Program,
sourceAvail: Map<string, Availability> | undefined,
Expand Down Expand Up @@ -847,6 +865,23 @@ function validateAvailabilityForRef(
codefixes: getVersionAdditionCodefixes(targetVersion, target, program),
});
}
if (
sourceVal === Availability.Available &&
isFirstUnavailableVersion(key, targetAvail) &&
findAvailabilityAfterVersion(key, Availability.Removed, sourceAvail) === undefined
) {
reportDiagnostic(program, {
code: "incompatible-versioned-reference",
messageId: "doesNotExist",
format: {
sourceName: getTypeName(source),
targetName: getTypeName(target),
version: key,
},
target: source,
codefixes: getVersionRemovalCodeFixes(key, source, program),
});
}
if (
[Availability.Removed].includes(sourceVal) &&
[Availability.Unavailable].includes(targetVal)
Expand Down
48 changes: 48 additions & 0 deletions packages/versioning/test/incompatible-versioning.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,23 @@ describe("versioning: validate incompatible references", () => {
});
});

it("emit diagnostic when referenced type is removed while property remains available", async () => {
const diagnostics = await runner.diagnose(`
@removed(Versions.v2)
model Target {}

@added(Versions.v1)
model Source {
target: Target;
}
`);
expectDiagnostics(diagnostics, {
code: "@typespec/versioning/incompatible-versioned-reference",
message:
"'TestService.Source.target' is referencing type 'TestService.Target' which does not exist in version 'v2'.",
});
});

it("emit diagnostic when using @typeChangedFrom with a type parameter that does not yet exist", async () => {
const diagnostics = await runner.diagnose(`
@test
Expand Down Expand Up @@ -1002,6 +1019,37 @@ describe("versioning: validate incompatible references", () => {
});
});

it("emit diagnostic when dependency mapping skips the target removal version", async () => {
const diagnostics = await Tester.diagnose(`
@versioned(Versions)
namespace VersionedLib {
enum Versions {l1, l2, l3}
@removed(Versions.l2)
model Foo {}
}

@versioned(Versions)
namespace TestService {
enum Versions {
@useDependency(VersionedLib.Versions.l1)
v1,
@useDependency(VersionedLib.Versions.l3)
v2,
@useDependency(VersionedLib.Versions.l3)
v3
}

@added(Versions.v1)
op test(): VersionedLib.Foo;
}
`);
expectDiagnostics(diagnostics, {
code: "@typespec/versioning/incompatible-versioned-reference",
message:
"'TestService.test' is referencing type 'VersionedLib.Foo' which does not exist in version 'v2'.",
});
});

it("doesn't emit diagnostic if all version use the same one", async () => {
// Here Foo was added in v2 which makes it only available in 1 & 2.
const diagnostics = await Tester.diagnose(`
Expand Down