refactor(core): split a lockfile-supplied version out of PackageSource::Inherited - #103
Open
justin13888 wants to merge 9 commits into
Open
refactor(core): split a lockfile-supplied version out of PackageSource::Inherited#103justin13888 wants to merge 9 commits into
justin13888 wants to merge 9 commits into
Conversation
`Inherited` had grown to mean four different things, one of which — a version read out of a lockfile with no manifest declaration behind it anywhere — is not inheritance at all. `Locked` names that case. It shares `Inherited`'s mechanics exactly: checkable, no position, never rewritable. Both `is_checkable` and `has_position` name it explicitly, because `PackageSource` is `#[non_exhaustive]` and every match on it carries a wildcard arm — a variant left out of `has_position` silently gains line 1 of a file that never declared it and becomes rewritable. The new test pins both halves.
A `Package.resolved` pin is not inherited from anything: `Package.swift` is a program this crate declines to read, so no manifest declares the package at all. Calling it `Inherited` sent a consumer looking for a central declaration to bump that does not exist. Behaviour is unchanged — `Locked` is checkable, has no position, and is never rewritable, exactly as before. `unfetchable` gains the matching arm, since `Locked` would otherwise fall to the wildcard and report `Local` — "there is no registry for this" — of a package that is published and merely has no version recorded. The three inheritance-warning predicates and `resolve_workspace_inheritance` keep reading `Inherited` by name and so no longer speak for Swift, which is right: none of them has a root to name for a locked entry.
`list --format json` gains a sixth `source` token. `inherited` stays `false` beside it: the flag exists to send a consumer to the file where the version can be bumped, and a lockfile pin has no such file. Stays within `dependable.list/v1`. The document's *shape* is unchanged, and the token sets inside it were already open — `source` has always fallen back to `"unknown"` for a variant the CLI does not name, so no consumer could safely match them exhaustively. The schema comment now says so. `check --format json` keeps `inherited_from` off a locked entry for the same reason, and the workspace note on stderr keeps reading `Inherited` by name rather than "skipped by fix", because it has no root to point a Swift reader at. `annotation` deliberately gains no arm: a locked entry states a version, so it is not unresolved, and its `kind` already says the useful thing.
`a_swift_pin_is_reported_as_locked_and_never_as_inherited` fails on the previous behaviour: it asserts `"source": "locked"` with `"inherited": false` on a `Package.resolved` pin, and that no Swift entry claims to inherit. The two halves are computed by different functions from the same field, so either can drift back alone. The Maven fixture keeps asserting `"source": "inherited"` with `"inherited": true`, because POM entries are unchanged; its comment now states the implication runs one way only, and that a locked entry is the deliberate non-inheritor.
…into refactor/98-locked-package-source
`pin_item` chose `Locked` on any `Some(version)`, with no emptiness filter —
unlike the name path two blocks above, which filters one. A `Package.resolved`
carrying `"state": { "version": "" }` therefore produced `Locked` with an empty
`version_constraint`, and `dependable list` printed
github.com/apple/swift-nio — (indirect)
a bare `—` with nothing to explain it. `Locked` has no annotation arm precisely
because a lockfile pin always states a version, and `locked_note` stays silent
when there is no locked version to name, so the line said nothing at all. That
is the exact output the `Inherited` `(unresolved)` arm exists to prevent, for
the reason its own comment gives: a bare dash reads like a parse failure.
Filter the emptiness at the producer rather than adding a renderer arm, so
`Locked` with an empty constraint is unrepresentable instead of merely
unrendered. The degenerate pin now falls through to the branch/revision state
and the `Git` arm it already had, printing `635b25 (git)`.
SwiftPM writes `null`, never `""`, for a versionless pin, so this needs a
hand-edited or third-party-generated file to reach.
`a_swift_dependency_has_no_position_and_is_never_rewritable` had gained a `continue` for non-checkable pins, added only so the `source == Locked` assertion inside the loop would hold. It also dropped the `!has_position()`, `!is_rewritable()`, `version_line == 0` and zero-width-span assertions for the branch pin and the local package — which the doc comment still claimed to cover. That coverage is the point: those two pins carry `version_line: 0` like every other, so a `has_position` that stopped consulting `is_checkable` would hand them line 1 of `Package.swift` in SARIF and in GitHub annotations. Dropping the `is_checkable() &&` conjunct leaves the narrowed test green and the surviving `a_branch_pin_and_a_local_package_report_what_they_always_did`, which asserts only `!is_checkable()`, green too. Make the `Locked` assertion conditional inside the loop body instead, so every pin is asserted on. With the conjunct removed the test now fails on `sample-helpers`.
The `dependable.list/v1` doc comment now says the schema pins the document's shape and that the token sets inside those fields are open. `README.md` still gave `source` as a closed enumeration and omitted the `unknown` fallback that argument rests on, so the two contradicted each other. State the open-set policy in the README and name `unknown`. Record the narrower reading of the `Registry`/`Locked` boundary on the variant itself: the test is whether a manifest declares the dependency, not where the version string came from. A `Cargo.toml` entry whose exact version came out of `Cargo.lock` stays `Registry`, because it was declared and `--fix` rewrites the declaration. Issue #98 was ambiguous between the two readings; a lockfile-first reader added later needs the settled one. In `build_workspace_graph`, give `Locked` its own arm. The correctness argument for the wildcard was carried entirely by a comment, and a comment is the one thing a `#[non_exhaustive]` enum will not check. Also close the blank line that had separated that comment from the `match` it explains.
…into refactor/98-locked-package-source
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #98.
PackageSource::Inheritedhad four producers meaning four different things. Thissplits out the one that is not inheritance at all.
parsers/cargo_toml.rsworkspace = true, resolved against[workspace.dependencies]Inheritedparsers/gradle_catalog.rsversion.refalias shared by several librariesInheritedparsers/pom_xml.rsInheritedlockfiles/swift_package_resolved.rsLockedLockedmeans the version came from a lockfile rather than any manifest. It keepsInherited's mechanics exactly —is_checkable() && !has_position(), the"check it, never rewrite it" behaviour — and differs only in what it tells a
consumer.
"inherited"invites going to the central declaration and bumping it;a Swift pin has no such declaration, because
Package.swiftis a Swift programthis crate declines to read and
Package.resolvedis the only place the versionis written.
Why POM stays
InheritedThe issue calls it a separate question and it is. An unresolvable POM version is
already carried at the result level:
DependencyStatus::Undetermined, plus theper-manifest
deferred_versionsnotice on stderr naming the entries and thereason. Nothing at the item level is missing, and a third variant would be a
second way to say what those two already say. What is genuinely wrong there —
that "inherited" is a poor word for "unreadable" — is a naming question about
POM's own resolution, not a data-model gap, and it does not share
Locked'sdistinguishing fact (a lockfile). Reversed by: evidence that a consumer needs to
tell an unresolvable POM entry from a resolved Gradle alias from the item alone,
without reading the result.
The audit
PackageSourceis#[non_exhaustive]and everymatchon it already carries awildcard, so adding a variant compiles cleanly everywhere and the blast radius is
silent drift. Every site, checked by hand:
Changed — the load-bearing invariant
dependable-core/src/item.rsis_checkable()—LockedjoinsInherited'sarm (
!version_constraint.is_empty()). Without it a Swift pin falls to thewildcard, stops being checkable, and a Swift project reports nothing.
dependable-core/src/item.rshas_position()— wasself.source != Inherited,a
!=against one variant. Now!matches!(source, Inherited | Locked). This isthe sharpest one: left alone,
Lockedwould regain a position, point everyreporter at line 1 of a
Package.swiftthat declares nothing, and becomerewritable by
--fix. Both functions now name the exempt variants explicitlyand the doc says why, so a source added later starts out without a position.
dependable-fetch/src/check.rsunfetchable()—Lockedgets its ownUndeterminedarm. The wildcard isLocal, which says "there is no registryfor this"; of a published Swift package with no version recorded that is simply
false, and it is the exact inversion
DependencyStatus's own doc warns against.dependable/src/output/list.rssource_token()— new"locked"token. Thewildcard is
"unknown", which would have hidden the whole ecosystem.dependable-core/src/lockfiles/swift_package_resolved.rs— the producer moves.Audited, deliberately unchanged
dependable/src/output/list.rs, theinherited: boolDTO field — readssource == Inherited, so a locked entry isfalse. Correct and intended: thefield exists to send a consumer to the file where the version can be bumped.
The field doc now says so, and the two fields still cannot contradict each other.
dependable/src/output/list.rsannotation()— no arm added on purpose. Alocked entry states a version so it is not
(unresolved), and it falls throughto its
kind, which isIndirect— the honest annotation, and the one italready rendered. Hardcoding
(indirect)on the source would be wrong for anyfuture lockfile reader whose entries are not indirect.
dependable/src/output/json.rsinherited_from— staysInherited-only.Naming the workspace root as the source of a locked constraint would point at a
declaration that is not there.
dependable/src/runner.rsreport_inherited_skips— staysInherited-only.It is gated on
report.workspace_rootand exists to name the file to editinstead; a locked entry has no such file. Swift already gets its own per-project
"nothing here can be checked" notice.
dependable-core/src/parsers/cargo_workspace.rs— the!= Inheritedguard nowalso excludes
Locked, which is right: a lockfile already supplied the versionand no root above it promised one.
dependable-fetch/src/check.rsundeclared_inheritance,detached_inheritance,deferred_versions— all three keepsource == Inherited && constraint.is_empty().Each accuses a specific file (a workspace root, a missing root, a
<parent>);a locked entry has no such file to accuse.
detached_inheritanceanddeferred_versionsare additionally kind-gated to Cargo and POM.dependable-fetch/src/tree.rs— the source match reaches only Cargo manifests,so
Lockedcannot arrive; the_ => registry+wildcard would be right for itregardless.
dependable-report/src/sarif.rsanddependable/src/output/github.rs— bothgate line emission on
has_position(), so both are correct by construction oncehas_positionis. This is why that function is the invariant worth testing.dependable-tui— holds noPackageSourcelogic at all.JSON schema change
list --format jsongains a sixthsourcetoken,"locked". A locked entryreports
"inherited": false— stated in the DTO doc and asserted in a test.It stays
dependable.list/v1. The document's shape (which fields exist, whattype each holds) is unchanged, and the token sets inside those fields were already
open:
source_tokenhas always fallen back to"unknown"for a variant it doesnot name, so a consumer matching exhaustively on it was never safe. The
SCHEMAconstant's doc now records that reading. Reversed by: treating any token-set
addition as breaking, which would mean the constant should have bumped for
"jsr"too. README'ssourceparagraph is updated.What falsifies a regression
Two tests, one per layer:
item::tests::a_locked_item_is_checkable_and_has_no_position— holdsis_checkable() && !has_position() && !is_rewritable()forLocked. This isthe pair that stops the variant silently drifting back through a wildcard.
fixture_swift::a_swift_pin_is_reported_as_locked_and_never_as_inherited— runsthe binary and asserts
"source": "locked"with"inherited": falseonswift-nio, and that no Swift entry claims to inherit. Fails on the previous
behaviour. The two assertions are one fact stated twice on purpose:
sourceandinheritedare computed by different functions, and either can drift alone.a_swift_dependency_has_no_position_and_is_never_rewritableadditionally nowasserts every checkable Swift pin is
Locked.Judgement calls
is_checkableguardsLockedon a non-empty constraint rather thanreturning
trueunconditionally. Today the Swift reader only producesLockedwith a version (a branch pin becomes
Git), so the two are indistinguishable inpractice; the guard keeps
LockedandInheritedreading identically and meansa future lockfile reader that records no version does not fetch on a blank.
Reversed by: a lockfile reader for which "no version recorded" should still be
queried by name.
Lockedsits besideInheritedon the enum rather than replacing it.Adding is additive for library consumers;
PackageSourceis#[non_exhaustive]so no downstream match breaks.
Review repairs
An independent re-derivation of the
PackageSourceaudit found no site this PRmissed and confirmed each "deliberately unchanged" decision against a concrete
wrong-output scenario. Three findings came back, all low; each was reproduced
before it was fixed.
Base merge
feat/85-swift-package-resolvedhad moved on by four repair commits(
0ab4e6a→e060ef4), several of them incrates/dependable-core/src/lockfiles/swift_package_resolved.rs, which thisbranch also changes.
origin/feat/85-swift-package-resolvedis merged in at128549c. The merge was clean — no conflicts, in that file or anywhere — andall three gates were re-run on the merge commit before any repair started. What
arrived from the base and matters here:
read_lockfileapplying!read_lockfilesonly to a lockfile that is not a dependency source,
split_authoritydecidingport-vs-path from the presence of a URL scheme,
DEP003moving its fact out ofproperties.statusintodependencyListUnread, andSummary::manifests_unread/ManifestView::dependencies_unread.1. A versionless locked pin lost its explanation (
c59d66c)pin_itemchoseLockedon anySome(version), with no emptiness filter —unlike the name path two blocks above, which filters one. A
Package.resolvedcarrying
"state": { "version": "" }therefore producedLockedwith an emptyversion_constraint.Before,
dependable liston such a project:A bare
—with nothing to explain it.Lockedhas no annotation arm preciselybecause a lockfile pin always states a version, and
locked_notestays silentwith no locked version to name. That is the exact output the
Inherited(unresolved)arm exists to prevent, for the reason its own comment gives: abare dash reads like a parse failure.
After:
Fixed at the producer rather than by adding a renderer arm, so
Lockedwith anempty constraint is unrepresentable instead of merely unrendered: the degenerate
pin falls through to the branch/revision state and the
Gitarmpin_itemalready had. SwiftPM writes
null, never"", for a versionless pin, so thisneeds a hand-edited or third-party-generated file to reach.
Falsified by two new tests in the Swift reader. With the filter removed:
The second walks four pin shapes — a real version, an empty one, an empty one
beside a branch, and a local package — and asserts the invariant over all of
them, so it holds for shapes the first test does not name.
2. A test narrowing dropped the Git and Local assertions (
3217776)a_swift_dependency_has_no_position_and_is_never_rewritablehad gained acontinuefor non-checkable pins, added only so thesource == Lockedassertion inside the loop would hold. It also silently dropped
!has_position(),!is_rewritable(),version_line == 0and the zero-width-span assertion forthe branch pin and the local package — which the test's own doc comment still
claimed to cover.
That coverage is the point. Both pins carry
version_line: 0, so ahas_positionthat stopped consultingis_checkablewould hand them line 1 ofPackage.swiftin SARIF and in GitHub annotations. Removing theis_checkable() &&conjunct fromItem::has_position— a plausiblesimplification once the exclusion list reads
Inherited | Locked— left thenarrowed suite fully green (
20 passed; 0 failed), including the survivinga_branch_pin_and_a_local_package_report_what_they_always_did, which assertsonly
!is_checkable().The
Lockedassertion is now a guarded assertion inside the loop body ratherthan a
continue, so every pin is asserted on. With the conjunct removed therestored test fails:
3. Comment detached from its subject, and the decisions (
3055745)In
build_workspace_grapha blank line separated the extended comment from thematch item.sourceit explains; it is closed.dependable.list/v1doc comment declares that the schema pins the document's shape and that the
token sets inside those fields are open.
README.mdgavesourceas a closedenumeration and omitted the
unknownfallback that argument rests on. It nowstates the open-set policy, names
unknown, and tells consumers to match thetokens they care about and default the rest.
Registry/Lockedboundary is the narrower reading, stated on thevariant. The test is whether a manifest declares the dependency, not where
the version string came from: a
Cargo.tomlentry whose exact version came outof
Cargo.lockstaysRegistry, because it was declared and--fixrewritesthe declaration. Issue refactor(core): PackageSource::Inherited now means four different things #98's phrasing was ambiguous between the two readings,
and a lockfile-first reader added later — a
Gemfile.lock, apoetry.lock—needs the settled one; the doc now says so and names that case.
Lockedgets its own arm inbuild_workspace_graph. The correctnessargument for the wildcard was carried entirely by a comment, and a comment is
the one thing a
#[non_exhaustive]enum will not check. No broadercompile-time scheme: that is a larger design question and out of scope.
Gates
Base was 948 passed; the merge brought it to 950 and the two new reader tests to
952. No test was deleted or weakened.
Carried forward over the base's second repair round
feat/85-swift-package-resolvedgained two fixes after this branch last merged it: a numeric guard on the port split inswift_package_resolved.rs, and an "unread dependency list" heading in the HTML report and thechecktable. Merged in (never rebased); nothing on this branch changed.Both branches touch
swift_package_resolved.rs, but in different regions — the base's guard is insplit_authority, this branch's change ispin_item'spin.version.clone().filter(|v| !v.is_empty())— so the merge was clean. Both survive:split_authoritycarries.filter(|i| authority[i + 1..].bytes().all(|b| b.is_ascii_digit()))pin_itemcarries the empty-version filter, andgrep -rn "PackageSource::Locked"still shows exactly one producer —swift_package_resolved.rs, behind that filter. Every other hit is a consumer (check.rs,tree.rs,list.rs,item.rs) or a test.list --format jsonover thesample-swiftfixture still reports"source": "locked"with"inherited": falsefor all four version-pinned entries.Fixture sweep
check --no-vuln --format json,fix --dry-run,list --depth 4,treeandtree --format jsonwere run over all 18 fixtures on both branches — 180 stdout/stderr captures each, byte-identical throughout.list --format jsondiffers onsample-swiftalone, and only insource/inherited(inherited/true->locked/false) for the four version-pinned entries in each of its two manifests. Nothing else moved.Gates
952 before; the base's two new tests take it to 954. No test was lost in the merge.