microsoft / microsoft/typespec
Versioning misses a reference to a type removed before the referrer, emitting a dangling ref with no diagnostic
- Dominant language
- Java
- Stars
- 5.9k
- Forks
- 394
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 104
Description
`@typespec/versioning` does not report a reference to a type that was **removed before** the referrer exists, so a versioned spec can emit a dangling `$ref` with no diagnostic.
The symmetric case — target *added after* the referrer — is correctly caught, which is what makes this easy to miss.
## Repro
```tsp
import "@typespec/versioning";
using TypeSpec.Versioning;
@service
@versioned(Versions)
namespace Svc;
enum Versions { v1, v2, v3 }
@removed(Versions.v2)
model Target { x: string; }
@added(Versions.v1)
model Source { t: Target; } // dangling at v2 and v3
op get(): Source;
```
```
$ tsp compile . --no-emit
Compilation completed successfully.
```
`Source` is available at v1, v2 and v3. `Target` is gone from v2 onward. At v2 and v3, `Source.t` references a type that does not exist in that version.
## Control — the mirror case *is* caught
Changing only the `Target` decorator to `@added(Versions.v3)`:
```
main.tsp:14:16 - error @typespec/versioning/incompatible-versioned-reference:
'Svc.Source.t' was added in version 'v1' but referencing type 'Svc.Target' added in version 'v3'.
```
## Cause
`validateAvailabilityForRef` ([`packages/versioning/src/validate.ts#L770-L873`](https://github.com/microsoft/typespec/blob/main/packages/versioning/src/validate.ts#L770-L873)) is a **boundary-event** check, not a pointwise availability check. It walks the shared version key set and only acts when the *source's* availability at that point is `Added` or `Removed`:
```ts
switch (sourceAvail) {
case Availability.Added: /* compare against target */ break;
case Availability.Removed: /* compare against target */ break;
// Availability.Available -> nothing
}
```
Once `Source` is merely `Available` (v2, v3) rather than transitioning, the target's availability is never consulted. In the control case the source's `Added` event at v1 lands on a key where the target is `Unavailable`, so it fires.
The check appears to assume the source's own boundary events are the only places a mismatch can be introduced, but a target can disappear at any later point while the source keeps existing.
## Suggested fix
Make it pointwise: for every key in the shared key set where the source is present (`Added` **or** `Available`), require the target to be present too, rather than only inspecting source transition points.
I have a standalone prototype of the pointwise/containment formulation that flags exactly this case while still producing the existing diagnostics, in https://github.com/timotheeguerin/typespec/pull/32 (`spike/scope-algebra.ts`) — happy to turn it into a real fix if the approach looks right.
## Environment
`@typespec/compiler@1.15.0`, `@typespec/versioning@0.85.0`
Found while spiking https://github.com/microsoft/typespec/issues/10551, where versioning's availability reasoning was the model for cross-scope reference validation.
Contributor guide
Research direction
Start in packages/versioning/src/validate.ts, especially validateAvailabilityForRef around lines 770–873, and run the TypeSpec reproduction from the issue with tsp compile . --no-emit. Verify that references from a still-present source to a removed target produce diagnostics at v2 and v3, while the existing added-after-referrer diagnostic remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 62/100