feat(jvm): pom.xml parser — literal versions and same-file properties only - #95
Open
justin13888 wants to merge 23 commits into
Open
feat(jvm): pom.xml parser — literal versions and same-file properties only#95justin13888 wants to merge 23 commits into
justin13888 wants to merge 23 commits into
Conversation
A Maven POM is declarative XML, so the dependencies it states can be read
without running anything. `<dependencies>` directly under `<project>` yields
one `groupId:artifactId` per entry, with the byte span of the `<version>`
text so `--fix` can rewrite it where it is written.
`${property}` resolves against `<properties>` in the same file, following the
Gradle catalog's rule for the same reason: a property used by exactly one
dependency carries the `<properties>` span that governs it, while one several
dependencies share carries none, because a single line cannot be rewritten to
two versions.
`<parent>` inheritance, `<dependencyManagement>`, and BOM imports stay out:
resolving any of them can require fetching the parent POM from a registry,
which would make this a resolution engine rather than a parser. A dependency
those rules leave unresolved is reported with no constraint instead of being
dropped, so a POM that inherits half its versions is never presented as
depending on only the other half.
The notice surface a Gradle build script uses is a directory scan keyed on a
file name, and it does not fit here: a `pom.xml` is perfectly readable, and it
is individual entries within one that are not. Rather than add a second
mechanism, an unresolvable dependency is reported through the vocabulary that
already describes exactly this state — `PackageSource::Inherited` with no
constraint, the shape a Cargo member's unresolved `dep.workspace = true`
already has. It is never fetched, positioned, or fixed, and `list` already
renders it as `(unresolved)`.
`sample-maven/pom.xml` covers all of it in one file: a literal version, a
property used once, a property two dependencies share, a version supplied by
the `<parent>`, and a `${revision}` that is Maven's rather than the file's,
beside a `<dependencyManagement>` entry and a plugin dependency that must not
leak into the artifact's list. The CLI test asserts the unresolvable entry is
present with a null constraint, which is what "reported rather than skipped"
has to mean at the surface a user sees.
… into feat/84-pom-xml-parser
`DependencyStatus` had no word for "nothing was learned about this". A dependency that names no version this tool can resolve fell into `Local`, which states something quite different and quite false: that there is no registry behind the package at all. That is what `local` means for a Cargo `path = "../x"` and for a Maven `<scope>system</scope>` jar, and applying it to `spring-boot-starter-web` — an artifact on Maven Central whose version this run simply did not read — is a claim, not a shrug. `Undetermined` is that word. It is deliberately named for the fact rather than for the ecosystem that surfaced it, because two situations produce the same fact: a manifest that defers its version somewhere this parser does not follow, and an ecosystem that publishes no registry to compare a version against at all. The second has no producer yet. `PackageSource::Inherited` and the two `Item` predicates that gate on it still described a Cargo `workspace = true` and nothing else, though three parsers now emit the variant. They say what the variant means instead.
`MAX_PROPERTY_HOPS = 8` is a count of hops, but the loop spent one iteration on arriving at the first property, which is not a hop. An eight-hop chain that terminates perfectly well therefore returned nothing, and the dependency reading it was reported with no version. It fails safe, so nothing was ever mis-stated — but the constant and the code disagreed about what the number meant, and the doc comment now says which one it is: the longest chain that resolves is `MAX_PROPERTY_HOPS + 1` properties.
`text_of` took the first text node's value and dropped only the *span* when there were several, so `<version>1.0<!--x-->.0</version>` yielded the constraint `1.0` — a version the file never declares — and reported it as checkable. dependable then asked Maven Central about the artifact and evaluated it against a constraint of its own invention. `--fix` could not corrupt the file, because the span was already dropped, so this was a wrong claim rather than a wrong write. It is still a wrong claim. Every text node is now concatenated, which is the value Maven itself reads. The sibling spellings were already correct and stay so: `1.0.0` and a `CDATA` section both yield `1.0.0` with the span dropped, since the source bytes are not the value's bytes and an offset into one is not an offset into the other. A test pins all three together.
…ties line
Sole ownership of a `<properties>` line was decided by counting only the
references made from the one top-level `<dependencies>` list. A property that
list reads once but a `<profiles>` block, a `<dependencyManagement>` entry, or
a plugin's own `<version>` also reads was still marked rewritable, and the
rewrite reached all of them.
Demonstrated with `<lib.version>32.1.3-jre</lib.version>` read by the
top-level `guava` and by a profile-only `guava-gwt`: `fix --all` printed
"Updated 1 dependency", rewrote the line, and silently moved `guava-gwt` — a
different artifact, never fetched, never validated, never named in the fix
record.
Sole ownership is a fact about the line, not about `<dependencies>`, so the
count now walks the whole document. A `<properties>` value that is *entirely*
one `${…}` is a link in a chain rather than a reader of it and is skipped, or
no chained property could ever be rewritten; a composed value is a reader and
is counted.
This is the `count_version_refs` rule from `gradle_catalog`, which repairs the
identical defect for a `[versions]` alias a `[plugins]` entry shares.
A POM that declares every one of its dependencies inside `<profiles>` listed as `(0 dependencies)`. Excluding conditional dependencies is defensible — a profile applies on a JDK version or an activated property, so its dependencies are not this project's as written, and reading them would state as fact something that holds only under a condition this file does not evaluate. A list that reads as complete and is not is the failure the POM parser exists to prevent, and being silent is what made this one that. `ParsedManifest` gains `notices`: what the parser saw and deliberately did not interpret, in the parser's own words. Not an error and not a warning about any dependency in `items` — the words go on stderr, where `list` and `check` both already put manifest-level notes, so the same sentence reaches a reader whichever `--format` they chose and no machine-readable document changes shape. The POM parser is the only one with anything to declare so far. Every other parser leaves it empty.
`evaluate_item` sent every non-checkable, non-git item to
`DependencyStatus::Local`, so an `Inherited` entry that never found a version
landed there through the wildcard arm. On a `<parent>`-inheriting POM — the
dominant real-world shape, along with same-file `<dependencyManagement>` —
that produced a whole table of `local` rows:
org.springframework.boot:spring-boot-starter-web — — local
org.springframework.boot:spring-boot-starter-test — — local
Totals: 2 skipped
`local` is what this tool prints for a Cargo `path = "../x"` and a Maven
`<scope>system</scope>` jar, and it means there is no registry behind the
package. `spring-boot-starter-web` is on Maven Central; only its version went
unread. `list` calls the same entry `(unresolved)`, so the two commands
contradicted each other, and `"status": "LOCAL"` was the wrong token for a CI
consumer to read. It now reports `Undetermined`, which claims nothing.
An unresolved Cargo `workspace = true` reaches the same arm and is the same
mistake, so it is corrected with it.
A check also said none of this out loud: stderr was empty, and a reader
looking at a column of dashes had to infer that nothing had been read. A
manifest-level warning now names the dependencies and where their versions
come from. It stays separate from `undeclared_inheritance` on purpose: a
Cargo member inheriting a name its root never declared is a manifest Cargo
refuses to build, while a POM deferring to its `<parent>` is ordinary, valid,
and extremely common — the same status, two different things to tell a reader.
Parser notices are forwarded into the same warnings, so a `<profiles>` block
`check` did not read is announced exactly as `list` announces it.
…ped one The status now reaches every surface a `check` speaks through: the table (a colour of its own, since a gap in the report is not the same as a row there was nothing to say about), the totals line, `--format json`'s per-result token and its `summary.undetermined` counter, and `--format text`. The totals count it apart from `skipped`. A `path` dependency was passed over deliberately and there is nothing a stricter run could ever learn about it; an undetermined one is a version this run failed to read, and folding the two together is how a POM that yielded nothing came to print "2 skipped". --fail-on --------- `--fail-on any` no longer exits 0 over a manifest whose versions were never read; `--fail-on vulnerable` and `--fail-on outdated` still do. The two narrow gates ask a specific question — is anything vulnerable, is anything behind — and an unread version answers neither. Failing a build that asked about vulnerabilities because a POM defers to its `<parent>` would make the flag mean something other than what it says. `--fail-on any` asks the general one: is every dependency checked and current. A dependency whose version was never read is not current, it is unestablished, and exiting 0 asserts something the run never determined — which is exactly how a parent-inheriting POM used to go green while dependable had read nothing at all. An unreadable version is admittedly not a vulnerability, so the failure never travels alone: `check` names the dependencies on stderr, and a job that fails says what to fix.
`list --format json` emitted `"source": "inherited"` beside `"inherited": false` on the same object. The boolean was filled in only from Cargo workspace resolution, so every entry the Gradle-catalog and POM parsers mark inherited arrived contradicting the field next to it, and a consumer reading both got two answers. Both fields describe one fact — this dependency's version is declared somewhere other than its own entry — so the boolean is now read from the source. The workspace-root list still contributes, because a root declaring a crate by `path` replaces `source` outright and the fact that it was inherited would otherwise be lost. No schema change: the key, its type, and its meaning for a Cargo manifest are all unchanged.
`Summary::checkable` subtracted only the path and git dependencies, so an undetermined one sat below the line as if the run had checked it and found it behind. On a parent-inheriting POM that silently depressed the up-to-date percentage toward nothing. It is now subtracted too, and counted in a field of its own beside `local` and `git`. SARIF emits no finding for it, which is the same decision `Error` gets and for the same reason: nothing was learned, so there is nothing to report about the code — and there is no line to pin a finding to either, since a dependency that defers its version elsewhere records no span. The CLI's manifest-level warning is where that belongs. The HTML badge and the TUI's detail pane both give it a colour of its own rather than the muted one the deliberately-skipped statuses share. The TUI *tree* still shows a green `ok` for a non-Cargo package, for an unrelated reason that predates this branch and spans csproj, mix, and Gradle as well: `direct_graph` zeroes the version. Filed as #96 and deliberately not touched here.
The existing Maven fixture exercised `list --format json` only, which is why `check` calling every unresolvable entry `local` went unseen: `list` renders the parser's output directly, and the mislabel happened a stage later. These drive the binary end to end against a `<parent>`-only POM and a same-file `<dependencyManagement>` one, hermetically — the JVM registry points at a port nothing listens on, so the connection is refused at once and nothing here depends on what a registry would have said. What is under test is decided before any request is made. Covered: the status in `--format json`, in the table, and in `--format text`; the `summary.undetermined` counter; that `--fail-on any` fails while `--fail-on outdated` and `--fail-on vulnerable` stay green; that the warning naming the dependencies reaches stderr; that a `<scope>system</scope>` jar is still `LOCAL` and still clean, so the distinction between "no registry" and "nothing read" is pinned from both sides; that a profiles-only POM says why it lists nothing; and that `source` and `inherited` agree on every object.
… into feat/84-pom-xml-parser
`<groupId>${project.groupId}</groupId>` is the standard way a multi-module
build names a sibling module, and it names a Maven built-in that is not a
`<properties>` entry — so the coordinate could not be resolved and the whole
dependency was dropped. A POM declaring three dependencies, two of them
siblings, listed as depending on one, with nothing on stderr to say otherwise.
A missing `<groupId>` element went the same way.
That is the silent omission this parser already refuses for a `<version>` a
`<parent>` supplies, reached through the other half of the coordinate — and the
asymmetry was inside one function: an unresolvable version was reported with an
empty constraint, an unresolvable group deleted the entry even though the
artifact was named.
Each half is now read as text first and resolved second: the resolved value
where this file states one, the literal as written where it does not. An entry
either half of which is unresolved reports no constraint either, since a
coordinate this file cannot state is one nothing can be fetched for. It lands
in `Undetermined`, beside the parent-deferred ones. Only a `<dependency>`
naming neither half is skipped, having stated nothing to report.
…ucted A comment, a CDATA section, or a character reference inside `<version>` costs the entry its byte-faithful span, and the span-less path rebuilt the item with `PackageSource::Inherited` hardcoded — discarding the `Local` that `<scope>system</scope>` had already established. `Inherited` is checkable, so the run went and asked Maven Central about a jar sitting at a path on this machine, and reported `ERROR` for it: a false statement of exactly the kind this branch set out to remove, pointed the other way. Two dependencies identical but for a comment inside the version, both `<scope>system</scope>`, disagreed about their own status and about whether `--fail-on any` passed. The entry's own source now survives: only a registry entry becomes `Inherited`, which is the one case the variant describes.
Concatenating every text node is what reads `<version>1.0<!--x-->.0</version>`
as the `1.0.0` Maven reads, and only the ends of the join were trimmed. Broken
over lines the same construct keeps the pretty-printer's indentation between
its halves:
<version>
1.0
<!--x-->
.0
</version>
which joined to `1.0\n \n .0` — a value nobody wrote, that Maven
would reject, and that `is_checkable` nonetheless waved through to the fetch
layer as a real constraint.
Stitching the halves together would state a version the file does not, so a
joined value with whitespace inside it now states nothing at all: the
dependency is reported unresolved, which is what it is. The single-line form is
untouched and still reads whole.
…read
Reporting an unread version as `Undetermined` rather than `Local` changed what
`--fail-on any` does to a Cargo package that declares `serde = { workspace =
true }` with no workspace root above it: `Local` is on that flag's clean list,
`Undetermined` is not, so an existing user's job flipped green to red. With
nothing on stderr, because `undeclared_inheritance` only runs once a root has
been found and `deferred_versions` speaks only for POMs.
The status is right — the crate is on crates.io and this run read no version
for it — but it broke the promise `result.rs` makes about itself: that a run
says why alongside one.
So the walk running out is now reported the way finding the wrong root already
was, with every such entry named once on stderr. Cargo only, since `workspace =
true` is the only spelling that promises a root there might be none of; a POM
deferring to its `<parent>` has none to be missing, and stays
`deferred_versions`' story to tell.
`§1`'s status table listed eight statuses and `Undetermined` was not among them, so an executive summary reading "6 dependencies" sat over rows summing to 5 — and a POM whose only dependency defers to its `<parent>` reported one dependency over a table accounting for none of it. `Summary` has carried the count since the status was added; `SummaryView` never passed it to the template, and an undeclared template variable renders as the empty string rather than failing, so the row would have been blank even once added. §3 and the ecosystem table were right all along — it is the summary above them that did not add up. No golden held an `Undetermined` result, which is why three byte-for-byte fixtures did not notice. The full report now carries one, and the test asserts the rendered row carries its count as well as its heading, plus that the rows account for every dependency the summary totals.
… into feat/84-pom-xml-parser
justin13888
changed the base branch from
feat/82-gradle-version-catalogs
to
master
September 1, 2026 18:59
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 #84.
The second declarative JVM slice. It reuses everything #82 built —
Ecosystem::Jvm,MavenCentralFetcher,semver/maven.rs, the[jvm]config — and adds only a parser.What it reads
<dependencies>directly under<project>, with literal<version>elements.<dependencyManagement>,<build><plugins>, and<profiles>are deliberately notread: none of the three is a dependency of the project as written.
${property}interpolated against<properties>in the same file, including a${a}→${b}→ literal chain (bounded, so a cycle terminates).--fixrewrites in place. This is the onestructural difference from the
csprojprecedent: a csproj version is an attribute(
Attribute::range_value), a POM version is element text, so the span comes fromNode::range()on the text node, trimmed of the whitespace a pretty-printed POMputs around it.
<scope>and<optional>, which are stated rather than guessed:test→Dev,<optional>true</optional>→Optional,system→PackageSource::Local(a jarat a path on this machine, which no registry has heard of).
groupId:artifactId— thesame string a dependency on it would use.
Following the Gradle catalog's rule, a property used by exactly one dependency carries
the
<properties>span that governs it and is rewritable; one shared by several isPackageSource::Inherited— resolved and checked, never written to — because a singleline cannot be rewritten to two different versions.
Out of scope
<parent>inheritance,<dependencyManagement>, and BOM imports. Resolving any ofthem correctly can require fetching the parent POM from a registry, which makes it a
resolution engine rather than a parser — a separate decision, not a bigger version of
this one. Maven's built-in properties (
${project.version},${revision}) are not<properties>entries and are not resolved either.The required behaviour, and where it landed
The issue requires that a POM whose version cannot be resolved be reported as unread
rather than skipped silently.
The
unreadable_manifestsnotice surface #82 added does not fit: it is a directoryscan keyed on a file name, and a
pom.xmlis perfectly readable — it is individualentries within one that are not. Rather than build a second mechanism, an unresolvable
dependency is reported through the vocabulary that already exists for exactly this
state:
PackageSource::Inheritedwith an empty constraint, which is the shape a Cargomember's unresolved
dep.workspace = truealready has. It is not checkable, has noposition, is never fixed, and
output/list.rsalready renders it as(unresolved).The Cargo-specific warnings around that shape (
undeclared_inheritance,report_inherited_skips) are both gated on a workspace root, andPomXmlhas none —so a POM gets a manifest-level warning of its own instead. See Review repairs.
The alternative — the
csprojparser'sif value.contains('$') { continue }— wouldreport a POM that inherits half its versions as depending on only the other half.
Stacking
Stacked on #91 (issue #82), which is itself stacked on #90. Base is
feat/82-gradle-version-catalogs. Merge order: #90 → #91 → this.Review repairs
An independent review found one High and three Medium defects, plus four Low. All are
repaired here except the two recorded under Unresolved review notes below. The base
branch was merged in first (
feat/82-gradle-version-catalogs, 4 commits, no conflicts).A second review round found one further High and four further Mediums, all repaired —
see Second review round at the end of this section. The repaired base was merged in
first again (
feat/82-gradle-version-catalogsatba58e02, 5 commits, no conflicts).The new status:
DependencyStatus::UndeterminedHigh.
evaluate_itemsent every non-checkable, non-git item toDependencyStatus::Localthrough a wildcard arm, soPackageSource::Inheritedwith anempty constraint landed there. On a
<parent>-inheriting POM — the dominant real-worldshape, along with same-file
<dependencyManagement>— that produced a table oflocalrows:
localis what this tool prints for a Cargopath = "../x"and a Maven<scope>system</scope>jar, and it means one thing: there is no registry behind thispackage. Said of
spring-boot-starter-web, which is on Maven Central, it is a plainfalse statement.
listcalled the same entry(unresolved), so the two commandscontradicted each other, and
"status": "LOCAL"was the wrong token for a CI consumerto read.
DependencyStatus::Undetermined— labelundetermined, tokenUNDETERMINED— meanswhether this dependency is current could not be determined, and no claim is made
either way. It is deliberately named for that fact rather than for Maven, and it is
distinct from all three of its neighbours on purpose:
LocalErrorUpToDateUndeterminedTwo situations produce it. The manifest names a real package but states no version this
tool can resolve — a POM deferring to
<parent>,<dependencyManagement>, or anundeclared property; a Cargo member inheriting a name its root never declares. Or the
ecosystem publishes no registry to compare against at all, so currency is not merely
unread but unknowable.
#85 (Swift) is expected to reuse this variant verbatim for that second case. Nothing
about it is Maven-shaped: it carries no ecosystem, no manifest kind, and no assumption
that a version exists somewhere to be found. The one thing it does expect of a producer
is that the run says why alongside it — see the warning below.
It is threaded through
check's table (its own colour, not the muted one the skippedstatuses share),
--format json(per-result token plus an additivesummary.undeterminedcounter),--format text,dependable-report's summarycounters, SARIF, the HTML badge, and the TUI's detail pane. Each surface was checked
rather than left to a
_ =>arm.--fail-on: undetermined is clean under two gates and not underanyMedium.
--fail-on anyexited 0 over a POM where zero versions had been read,because
Localwas on the clean list. The decision, and why:--fail-on vulnerable— clean. The gate asks whether anything is vulnerable. Anunread version answers neither yes nor no, and failing a build that asked about
vulnerabilities because a POM defers to its
<parent>would make the flag meansomething other than what it says.
--fail-on outdated— clean. Same reasoning: the gate names a specific finding,and this is the absence of a finding rather than one.
--fail-on any— not clean, exits 1. This gate asks the general question: isevery dependency checked and current. A dependency whose version was never read is not
current; it is unestablished, and exiting 0 asserts something the run never
determined. It is grouped with the failures rather than with
LocalandGit, whichare clean because they were skipped on purpose — there is no registry behind them,
so there is nothing a stricter run could ever learn.
An unreadable version is admittedly not a vulnerability, and failing silently on one
would be its own defect. So the failure never travels alone:
checknow emits amanifest-level warning naming the dependencies and where their versions come from
("N dependencies take their version from a
<parent>,<dependencyManagement>, or aproperty this file does not declare … nothing was checked: a:b, c:d"). Before this,
stderr was empty. It stays separate from Cargo's
undeclared_inheritance, because aCargo member inheriting a name its root never declared is a manifest Cargo refuses to
build, while a POM deferring to its
<parent>is ordinary and valid — the same status,two different things to tell a reader.
The totals line counts
undeterminedapart fromskippedfor the same reason.A
<profiles>block is announced, not parsedMedium. A POM with all its dependencies inside
<profiles>listed as(0 dependencies). Excluding conditional dependencies stays the decision — a profileapplies on a JDK version or an activated property, so its dependencies are not this
project's as written. Being silent about it is what made a list read as complete when it
was not.
ParsedManifestgainsnotices: what a parser saw and deliberately did not interpret,in its own words. They go on stderr, where
listandcheckboth already putmanifest-level notes, so the same sentence reaches a reader whichever
--formattheychose and no machine-readable document changes shape. The POM parser is the only one
with anything to declare so far.
--fixno longer moves a version it never countedLow. Sole ownership of a
<properties>line was decided by counting references fromthe one top-level
<dependencies>list. A property that list reads once but a<profiles>block, a<dependencyManagement>entry, or a plugin<version>also readswas still marked rewritable. Demonstrated:
<lib.version>32.1.3-jre</lib.version>readby top-level
guavaand by profile-onlyguava-gwt—fix --allprinted "Updated 1dependency" and silently moved
guava-gwt, never fetched, never validated, never namedin the fix record.
Ownership is a fact about the line, not about
<dependencies>, so the count now walksthe whole document — the same rule
count_version_refsapplies ingradle_catalog.rsfor a
[versions]alias a[plugins]entry shares. A<properties>value that isentirely one
${…}is a chain link rather than a reader and is skipped, or no chainedproperty could ever be rewritten; a composed value is a reader and is counted.
Two smaller repairs
text_oftook the first text node anddropped only the span when there were several, so
<version>1.0<!--x-->.0</version>yielded the constraint
1.0— a version the file never declares — and reported it ascheckable, whereupon dependable queried Maven Central and evaluated against it. Every
text node is now concatenated, which is the value Maven itself reads. The sibling
spellings were already correct and are now pinned by a test:
1.0.0and aCDATAsection both yield
1.0.0with the span dropped.MAX_PROPERTY_HOPSfollows the number it documents. The loop spent one iterationarriving at the first property, which is not a hop, so an eight-hop chain that
terminates returned nothing. Fails safe, but the constant and the code disagreed about
what the number meant; both now say the same thing.
sourceandinheritedno longer contradict each other.list --format jsonemitted
"source": "inherited"beside"inherited": false, because the boolean wasfilled in only from Cargo workspace resolution. Both describe one fact, so the boolean
is read from the source. No schema change. The
PackageSource::Inherited,Item::is_checkable, andItem::has_positiondoc comments, which still described aCargo
workspace = trueand nothing else though three parsers now emit the variant,say what the variant means instead.
Tests
Every repair has a regression test that was confirmed to fail with the repair reverted
and the test kept.
fixture_maven.rspreviously exercisedlist --format jsononly —which is exactly why the
checkmislabel went unseen, sincelistrenders the parser'soutput directly and the mislabel happened a stage later. It now drives the binary end to
end against a
<parent>-only POM and a same-file<dependencyManagement>one,hermetically: the JVM registry points at a port nothing listens on, so nothing under
test depends on what a registry would have said. A
<scope>system</scope>jar isasserted to still be
LOCALand still clean, pinning the "no registry" / "nothing read"distinction from both sides.
Second review round
The repaired base (
ba58e02) was merged in by merge commit before any of this;git mergereported no conflicts.A dependency whose
<groupId>cannot be resolved was dropped silentlyHigh.
<groupId>${project.groupId}</groupId>is the standard idiom for a siblingmodule in a multi-module build, and it names a Maven built-in this file does not state.
The coordinate could not be resolved, so the whole entry was dropped — a POM declaring
three dependencies, two of them siblings, listed as depending on one, with empty stderr.
A missing
<groupId>element behaved the same.That is the silent omission this PR exists to prevent, reached through the other half of
the coordinate, and the asymmetry sat inside one function: an unresolvable
<version>was reported anyway with an empty constraint, an unresolvable
<groupId>deleted theentry even though
artifactIdwas known.Each half is now read as text first and resolved second: the resolved value where this
file states one, the literal as written where it does not. Either half unresolved means
no constraint either, since a coordinate this file cannot state is one nothing can be
fetched for — so the entry lands in
Undetermined, beside the parent-deferred ones.Only a
<dependency>naming neither half is skipped, having stated nothing to report.Tests:
a_group_this_file_cannot_resolve_is_reported_not_droppedanda_missing_coordinate_half_is_still_reported_under_the_otherinpom_xml.rs,a_group_this_file_cannot_resolve_is_still_listedend to end infixture_maven.rs(3 dependencies listed, 2
UNDETERMINED).a_coordinate_may_be_stated_by_propertycodified the old drop and was updated to assert the report.
A detached Cargo package failed
--fail-on anywith nothing on stderrMedium, regression. Reporting an unread version as
Undeterminedrather thanLocalchanged what--fail-on anydoes to aCargo.tomldeclaringserde = { workspace = true }with no workspace root above it:Localis on thatflag's clean list,
Undeterminedis not, so an existing Rust user's CI flipped green tored — with nothing anywhere saying why, because
undeclared_inheritanceonly ran once aroot had been found and
deferred_versionsspeaks only for POMs. That broke the promiseresult.rsmakes about itself, that a run says why alongside an undetermined result.The status is right; the silence was not. The walk running out is now reported the way
finding the wrong root already was, naming every such entry once on stderr. Cargo only,
since
workspace = trueis the only spelling that promises a root there might be noneof; a POM deferring to its
<parent>has none to be missing.Test:
a_package_inheriting_from_no_workspace_at_all_says_soincli_workspace.rs,which asserts exit 1 and that stderr names
serdeand the missing root.inherited()discarded the entry's source, sending asystemjar to a registryMedium. A comment, CDATA section, or character reference inside
<version>costs theentry its byte-faithful span, and the span-less path rebuilt the item with
PackageSource::Inheritedhardcoded — discarding theLocalthat<scope>system</scope>had already established.
Inheritedis checkable, so the run asked Maven Central about ajar sitting at a path on this machine and reported
ERROR, failing--fail-on any. Twodependencies identical but for a comment disagreed about their own status.
The entry's own source now survives; only a registry entry becomes
Inherited, which isthe one case the variant describes.
Test:
a_system_scoped_jar_stays_local_when_its_version_is_reconstructed, in bothpom_xml.rsandfixture_maven.rs— the latter pinningplainsysandcmtsysto thesame
LOCALand--fail-on anystaying green. The pre-existinga_system_scoped_jar_is_still_localcovered only the byte-faithful spelling.The HTML report's status table omitted
UndeterminedMedium. §1's status table listed eight statuses and
Undeterminedwas not amongthem, so an executive summary reading "6 dependencies" sat over rows summing to 5, and a
POM whose only dependency defers to its
<parent>reported one dependency over a tableaccounting for none of it. §3 and the ecosystem table were right all along.
Summaryhas carried the count since the status was added;SummaryViewnever passed itto the template — and an undeclared template variable renders as the empty string rather
than failing, so the row would have rendered blank even once added. Both halves are fixed.
No golden held an
Undeterminedresult, which is why three byte-for-byte fixtures did notnotice. The full-report fixture now carries one, and the test additionally asserts the
rendered row carries its count as well as its heading, and that the rows account for
every dependency the summary totals.
A version split by a comment across lines yielded a whitespace-laden constraint
Medium. Concatenating every text node is what reads
<version>1.0<!--x-->.0</version>as the1.0.0Maven reads, and only the ends of thejoin were trimmed. Broken over lines the same construct keeps the pretty-printer's
indentation between its halves, joining to
1.0\n \n .0— a value nobodywrote, that Maven would reject, and that
is_checkablewaved through to the fetch layeras a real constraint.
Stitching the halves together would state a version the file does not, so a joined value
with whitespace inside it now states nothing at all: the dependency is reported
unresolved, which is what it is. The single-line form is untouched.
Test:
a_version_split_across_lines_states_no_versioninpom_xml.rs.Validation
Nothing added is network-dependent, so
mise run teststays hermetic. (FORCE_COLORbreaks an unrelated
treetest on every branch — a known environment artifact, notthis change.)
Unresolved review notes
Findings deliberately not repaired here. The first two are from the first review
round; the next several are from the second; the last two are from the third, and are
listed in round order rather than severity order, so a re-reviewer can see what each
round left behind.
Also noted, outside both lists:
--format textpads the status token to 8 characters andUNDETERMINEDis 12, so that column no longer aligns when an undetermined result ispresent.
The TUI shows a green
okfor a non-Cargo packagecrates/dependable-tui/src/ui/tree.rs—status_badge, fed bydirect_graph.direct_graphzeroes the version for a non-Cargo ecosystem, so the badgeis computed against an empty version and can render
okfor a package whose currencywas never established.
a version the tree does not have.
direct_graphshould carry the real declared version perecosystem rather than zeroing it, so the badge is derived from the same data the check
used.
as well as Maven, so it is neither caused by nor confined to the POM parser, and
fixing it would put a cross-ecosystem TUI change inside a parser PR. Filed as fix(tui): a dependency whose version was never read renders as up to date #96
and left to it. The status threading in this PR reaches the TUI's detail pane, which
renders
Undeterminedcorrectly; only the tree badge is affected.A second
<dependencies>under<project>is silently droppedcrates/dependable-core/src/parsers/pom_xml.rs,read_dependencies—child()returns the first match only.<dependencies>elements directly under<project>has thesecond one's dependencies read as zero, with no notice.
<dependencies>tag and the entriesin the second block never appear in a list or a check.
<dependencies>child, or emit aparser notice when more than one is present.
<dependencies>under<project>outright — such a POM does not build, so no POM that is actually in usecan hit it. Repairing it would mean either changing what the parser reads or adding a
notice for a file Maven itself refuses, neither of which improves any real project's
output.
profile_noticecounts more than profile-scoped dependenciescrates/dependable-core/src/parsers/pom_xml.rs,profile_notice.<dependency>under a profile's<dependencyManagement>and<build><plugins>, not just the profile's own<dependencies>.is not listed", naming something that was never a dependency of this project.
<dependency>whose parent<dependencies>isa direct child of a
<profile>.nothing is dropped and no status is wrong — and the fix belongs with the wider
<profiles>handling rather than inside a round of correctness repairs.PackageSource::Inheritedis used for any version needing reconstructioncrates/dependable-core/src/parsers/pom_xml.rs,item→inherited.<version>1.0<!--x-->.0</version>reportsinherited: truethough theversion is declared on that entry, contradicting the doc this PR added.
a version it states itself.
elsewhere"; they are currently the same signal.
system-jar repair above but isnot cured by it, and separating the two concepts changes a public field's meaning
across three parsers. Tracked as refactor(core): PackageSource::Inherited now means four different things #98.
child()is namespace-blindcrates/dependable-core/src/parsers/pom_xml.rs,child.<x:dependencies>preceding the real one wins, and asecond sibling
<dependencies>is dropped.can reach it. (This subsumes the duplicate-
<dependencies>note above.)count_property_refsignores${…}in attributescrates/dependable-core/src/parsers/pom_xml.rs,count_property_refs.${…}reference in thedocument" is slightly overstated.
fewer reader, which could make a shared
<properties>line look sole-owned.no version, so the undercount is theoretical; the claim is the part worth tightening,
and that belongs with the wider
<profiles>work.<properties><project.version>shadows Maven's built-incrates/dependable-core/src/parsers/pom_xml.rs,read_properties.<properties>entry literally namedproject.versionis resolved as anordinary property, where Maven's built-in takes precedence.
does not.
project.*and other reserved prefixes as<properties>keys.handling built-in precedence properly is the start of a resolution engine — the line
this parser deliberately does not cross.
Undeterminedproduces no GitHub Actions annotationcrates/dependable/src/output/github.rs,level_of.level_ofreturnsNoneforUndetermined, so under--fail-on anya jobexits 1 with zero annotations.
<parent>-inheriting POM in CI fails the job with nothing annotated inthe diff view — though the manifest-level warning is still on stderr.
Level::Notice, whose title is already literally"dependable: dependency could not be checked".
defect, and the stderr warning this PR added means the run is no longer silent about
it. Worth its own change alongside the annotation work generally.
An unresolvable coordinate is reported as an unread version
crates/dependable-core/src/parsers/pom_xml.rs,read_dependencies(the
knowngate) →unresolved→inherited(entry, ""); surfaced bydeferred_versionsincrates/dependable-fetch/src/check.rs.versionis forced toSource::Unknownwhatever version sits beside it, so the entry becomesPackageSource::Inheritedwith an emptyversion_constraint. Two things follow.The version the file plainly states is discarded — a
<dependency>with<groupId>${project.groupId}</groupId>and a literal<version>2.0.13</version>reports no constraint at all, and the
2.0.13the reader can see in their own fileappears nowhere in the output. And because
deferred_versionsselects on exactlyInherited && version_constraint.is_empty(), that entry is swept into a messagethat says it "takes its version from a
<parent>,<dependencyManagement>, or aproperty this file does not declare, so no version was read for it" — which is
false twice over: the version was read, and the parent is not why it is
unreported. The coordinate is what could not be resolved.
standard
${project.groupId}idiom, pinning a literal version on each. Every suchline reports
(unresolved)with no version, and stderr explains the absence with a<parent>story that does not apply — pointing a reader at their parent POM to fixsomething their parent POM has nothing to do with.
Keep reading the version independently of the coordinate, so a stated version is
reported even when the name is not fetchable, and give an unresolvable coordinate
its own notice ("the coordinate could not be resolved, so nothing was fetched")
rather than borrowing
deferred_versions'. That likely means a thirdSourcevariant, or a flag on
Declared, so the two conditions stop sharing one signal.PackageSource::Inheritedoverload already tracked as refactor(core): PackageSource::Inherited now means four different things #98 — "no version to report" and "no version to
rewrite" and now "no coordinate to fetch" are all one state — and repairing it
properly means adding a distinguishing signal to a core type and threading a new
notice through the fetch layer and both output surfaces. That is a change to the
parser's contract rather than a round of correctness repairs, and it should land
with refactor(core): PackageSource::Inherited now means four different things #98 rather than being half-made here. Nothing is silently dropped in the
meantime: the dependency is still listed, still reported
Undetermined, and stillcounted — only its stated version and the reason given are wrong.
detached_inheritanceasserts a workspace lookup that never rancrates/dependable-fetch/src/check.rs,check_inner'selse { warnings.extend(detached_inheritance(...)) }branch, reached fromChecker::check_manifest.elsebranch fires wheneverworkspaceisNone, and it does notdistinguish "the walk upwards ran and found nothing" from "no walk was ever
attempted".
Checker::check_manifest— the content-only IDE API — always passesNone, by construction: it is given a buffer with no path behind it, so there is notree to search. Its warning nonetheless states "no workspace root was found above
this manifest", asserting the result of a search that did not happen.
check_manifeston an openCargo.tomlbuffer from a perfectly ordinary workspace member. Every
serde.workspace = trueline produces a warning claiming the member is detached from any workspace root —
which is untrue of the file on disk, and would send a user looking for a missing
root that is sitting one directory up.
Checker::check_pathon the same fileresolves the root and emits nothing.
from
Option::None. Either passcheck_innera value that separates "looked up,found nothing" from "not looked up" (an enum, or a separate flag), or move the
detached_inheritancecall into thecheck_pathside that actually performed thewalk, leaving
check_manifestto report the entries asUndeterminedwithoutclaiming to know why.
check_path, which is the CLIpath and the one this branch's
a_package_inheriting_from_no_workspace_at_all_says_sotest covers; the defect is confined to a library API the CLI never calls, so no
dependablerun can reach it. The fix changescheck_inner's signature — a sharedseam both public entry points go through — which is a wider blast radius than the
bug, and it belongs with the workspace-resolution work rather than inside the POM
parser's PR. The IDE consumer is not silently misled about what is unchecked, only
about why.