diff --git a/.chronus/changes/allow-spreading-model-previous-version-2024-6-19-18-50-40.md b/.chronus/changes/allow-spreading-model-previous-version-2024-6-19-18-50-40.md new file mode 100644 index 0000000000..ab7ab21daf --- /dev/null +++ b/.chronus/changes/allow-spreading-model-previous-version-2024-6-19-18-50-40.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/versioning" +--- + +Allow spreading a model that has props added in previous version diff --git a/packages/versioning/src/validate.ts b/packages/versioning/src/validate.ts index 57e1ca178a..4d7461025b 100644 --- a/packages/versioning/src/validate.ts +++ b/packages/versioning/src/validate.ts @@ -6,12 +6,15 @@ import { isTemplateInstance, isType, navigateProgram, + type ModelProperty, type Namespace, type Program, type Type, type TypeNameOptions, } from "@typespec/compiler"; import { + $added, + $removed, findVersionedNamespace, getMadeOptionalOn, getMadeRequiredOn, @@ -772,6 +775,30 @@ function validateAvailabilityForRef( } } +function canIgnoreDependentVersioning(type: Type, versioning: "added" | "removed") { + if (type.kind === "ModelProperty") { + return canIgnoreVersioningOnProperty(type, versioning); + } + return false; +} + +function canIgnoreVersioningOnProperty( + prop: ModelProperty, + versioning: "added" | "removed" +): boolean { + if (prop.sourceProperty === undefined) { + return false; + } + + const decoratorFn = versioning === "added" ? $added : $removed; + // Check if the decorator was defined on this property or a source property. If source property ignore. + const selfDecorators = prop.decorators.filter((x) => x.decorator === decoratorFn); + const sourceDecorators = prop.sourceProperty.decorators.filter( + (x) => x.decorator === decoratorFn + ); + return !selfDecorators.some((x) => !sourceDecorators.some((y) => x.node === y.node)); +} + function validateAvailabilityForContains( program: Program, sourceAvail: Map | undefined, @@ -791,7 +818,8 @@ function validateAvailabilityForContains( if (sourceVal === targetVal) continue; if ( [Availability.Added].includes(targetVal) && - [Availability.Removed, Availability.Unavailable].includes(sourceVal) + [Availability.Removed, Availability.Unavailable].includes(sourceVal) && + !canIgnoreDependentVersioning(target, "added") ) { const sourceAddedOn = findAvailabilityOnOrBeforeVersion(key, Availability.Added, sourceAvail); reportDiagnostic(program, { @@ -808,7 +836,8 @@ function validateAvailabilityForContains( } if ( [Availability.Removed].includes(sourceVal) && - [Availability.Added, Availability.Available].includes(targetVal) + [Availability.Added, Availability.Available].includes(targetVal) && + !canIgnoreDependentVersioning(target, "removed") ) { const targetRemovedOn = findAvailabilityAfterVersion(key, Availability.Removed, targetAvail); reportDiagnostic(program, { diff --git a/packages/versioning/test/incompatible-versioning.test.ts b/packages/versioning/test/incompatible-versioning.test.ts index 6495d6b86c..c5bdf569df 100644 --- a/packages/versioning/test/incompatible-versioning.test.ts +++ b/packages/versioning/test/incompatible-versioning.test.ts @@ -412,6 +412,34 @@ describe("versioning: validate incompatible references", () => { expectDiagnosticEmpty(diagnostics); }); + it("succeed when spreading a model that might have add properties added in previous versions", async () => { + const diagnostics = await runner.diagnose(` + model Base { + @added(Versions.v1) name: string; + } + + @added(Versions.v2) + model Child { + ...Base; + } + `); + expectDiagnosticEmpty(diagnostics); + }); + + it("succeed when spreading a model that might have add properties removed after the model", async () => { + const diagnostics = await runner.diagnose(` + model Base { + @removed(Versions.v3) name: string; + } + + @removed(Versions.v2) + model Child { + ...Base; + } + `); + expectDiagnosticEmpty(diagnostics); + }); + it("emit diagnostic when model property was added before model itself", async () => { const diagnostics = await runner.diagnose(` @added(Versions.v3)