Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
55091f0
feat(core): parse pom.xml literal and same-file property versions
justin13888 Sep 1, 2026
c32ad7f
feat(jvm): report a POM whose versions cannot be resolved
justin13888 Sep 1, 2026
d0bccfa
docs(core): say what a <properties> value is kept as
justin13888 Sep 1, 2026
3a29eea
test(core): an exclusion's coordinate is not the dependency's own
justin13888 Sep 1, 2026
e94a76e
Merge remote-tracking branch 'origin/feat/82-gradle-version-catalogs'…
justin13888 Sep 1, 2026
725ef41
feat(core): report a dependency whose currency could not be determined
justin13888 Sep 1, 2026
949b4f8
fix(core): follow the number of property hops the constant documents
justin13888 Sep 1, 2026
b0b0825
fix(core): read a pom version split by a comment whole
justin13888 Sep 1, 2026
2ed3a0f
fix(core): count every ${…} reference when deciding who owns a proper…
justin13888 Sep 1, 2026
cd03d51
feat(core): say what a parser saw and declined to read
justin13888 Sep 1, 2026
0521ead
fix(fetch): report an unread version as undetermined, not as local
justin13888 Sep 1, 2026
dfcde31
fix(cli): count and gate an undetermined dependency apart from a skip…
justin13888 Sep 1, 2026
fbec08a
fix(cli): agree with source about whether a dependency is inherited
justin13888 Sep 1, 2026
c1b510f
fix(report): keep an unread version out of the up-to-date denominator
justin13888 Sep 1, 2026
eee2821
test(jvm): check a pom that takes its versions from its parent
justin13888 Sep 1, 2026
ae39736
Merge remote-tracking branch 'origin/feat/82-gradle-version-catalogs'…
justin13888 Sep 1, 2026
412f7b4
fix(jvm): report a dependency whose group this POM cannot resolve
justin13888 Sep 1, 2026
f6c72d6
fix(jvm): keep a system-scoped jar local when its version is reconstr…
justin13888 Sep 1, 2026
43e7c12
fix(jvm): refuse a version spliced back together across lines
justin13888 Sep 1, 2026
a84ac1c
fix(fetch): say why a detached package's inherited version was never …
justin13888 Sep 1, 2026
50922c4
fix(report): count an undetermined dependency in the HTML status table
justin13888 Sep 1, 2026
06c7f62
Merge remote-tracking branch 'origin/feat/82-gradle-version-catalogs'…
justin13888 Sep 1, 2026
91fc4f2
Merge remote-tracking branch 'origin/master' into feat/84-pom-xml-parser
justin13888 Sep 1, 2026
8ddd1b1
docs(cli): keep the inheritance ordering rationale with the code it e…
justin13888 Sep 6, 2026
13575bf
fix(cli): decline to rewrite a Maven single-version interval
justin13888 Sep 6, 2026
94f7cd1
fix(report): withhold the up-to-date share when a version went unread
justin13888 Sep 6, 2026
c06007b
fix(fetch): tell an unsearched workspace apart from an absent one
justin13888 Sep 6, 2026
a6a55ce
fix(core): read a POM version independently of its coordinate
justin13888 Sep 6, 2026
adcff2f
fix(core): read a POM element's text the same way in both readers
justin13888 Sep 6, 2026
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
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,21 @@ Or download a prebuilt binary for your platform from the
| Dart / Flutter | `pubspec.yaml` | pub.dev | `pubspec.lock` | 🧪 Experimental |
| C# / .NET | `*.csproj`, `Directory.Packages.props` | NuGet | — | 🧪 Experimental |
| Elixir | `mix.exs` | Hex | `mix.lock` | 🧪 Experimental |
| Kotlin / Java | `gradle/libs.versions.toml` | Maven Central | — | 🧪 Experimental |

Kotlin / Java coverage is the **declarative** half of a Gradle build: the version
catalog. A build script (`build.gradle`, `build.gradle.kts`) is a program, and reading
one means running your build — so a build script found without a catalog beside it is
reported as unread rather than silently skipped, and a handful of catalog entries never
gets presented as a whole dependency list.
| Kotlin / Java | `gradle/libs.versions.toml`, `pom.xml` | Maven Central | — | 🧪 Experimental |

Kotlin / Java coverage is the **declarative** half of a JVM build. For Gradle that is
the version catalog: a build script (`build.gradle`, `build.gradle.kts`) is a program,
and reading one means running your build — so a build script found without a catalog
beside it is reported as unread rather than silently skipped, and a handful of catalog
entries never gets presented as a whole dependency list.

A Maven `pom.xml` is data throughout, so its `<dependencies>` are read directly, with
`${property}` resolved against the `<properties>` of the same file. What a POM defers
to its `<parent>`, to `<dependencyManagement>`, or to an imported BOM is **not**
resolved — doing so correctly can mean fetching the parent POM from a registry, which
is a resolution engine rather than a parser. Those dependencies are still listed, with
no version and nothing claimed about them, so a POM that inherits some of its versions
is never presented as depending on only the rest.

### Lockfiles

Expand Down
69 changes: 58 additions & 11 deletions crates/dependable-core/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,13 @@ impl Item {
/// sources are skipped.
///
/// An [`Inherited`](PackageSource::Inherited) item is checkable only once
/// [`resolve_workspace_inheritance`](crate::resolve_workspace_inheritance) has
/// supplied the workspace root's constraint. Unresolved, the manifest states no
/// version at all, and there is nothing to ask a registry for.
/// something has supplied the constraint declared elsewhere — the workspace root
/// via [`resolve_workspace_inheritance`](crate::resolve_workspace_inheritance) for
/// Cargo, the `[versions]` table for a Gradle catalog, the `<properties>` table
/// for a POM. Without one the manifest states no version at all, and there is
/// nothing to ask a registry for; a check reports such an item as
/// [`Undetermined`](crate::result::DependencyStatus::Undetermined) rather than
/// claiming it has no registry.
#[must_use]
pub fn is_checkable(&self) -> bool {
match self.source {
Expand All @@ -60,7 +64,9 @@ impl Item {
/// parser that declines to record a span also gives the item a source nothing would
/// fetch, so [`is_checkable`](Self::is_checkable) covers all of them but one: a
/// resolved [`Inherited`](PackageSource::Inherited) item is worth checking and still
/// has no home here, because its version string is in the workspace root.
/// has no home here, because the version string it was resolved from belongs to
/// another entry — a workspace root's table, a catalog `[versions]` alias, a shared
/// POM `<properties>` value.
#[must_use]
pub fn has_position(&self) -> bool {
self.is_checkable() && self.source != PackageSource::Inherited
Expand Down Expand Up @@ -153,15 +159,56 @@ pub enum PackageSource {
Local,
/// A git dependency — skipped for version checks.
Git,
/// A Cargo `dep.workspace = true`: the manifest opts into a version declared in
/// the workspace root's `[workspace.dependencies]` and states none of its own.
/// The dependency's version is declared somewhere other than this entry, so
/// there is no version string here to check against or to rewrite.
///
/// Reading `workspace = true` needs no filesystem, so the IO-free parser records it
/// — which is what keeps it distinct from a [`Local`](Self::Local) `path` entry that
/// happens to share a name with a root declaration. Resolving it against the root
/// does need IO, and is [`resolve_workspace_inheritance`](crate::resolve_workspace_inheritance)
/// applied by the caller that has the root in hand.
/// Three parsers emit it, for the same reason and with the same consequences:
///
/// - Cargo's `dep.workspace = true` — the version is in the workspace root's
/// `[workspace.dependencies]`. Reading `workspace = true` needs no
/// filesystem, so the IO-free parser records the fact; *resolving* it does
/// need IO, and is
/// [`resolve_workspace_inheritance`](crate::resolve_workspace_inheritance)
/// applied by the caller that has the root in hand.
/// - A Gradle version catalog entry whose `version.ref` names a `[versions]`
/// alias several entries share.
/// - A Maven POM entry whose version comes from a `<properties>` value several
/// dependencies share, or from a `<parent>` / `<dependencyManagement>` /
/// undeclared property this file does not state.
///
/// What keeps it distinct from [`Local`](Self::Local) is that the package is a
/// real registry package — an entry that merely shares a name with a root
/// `path` declaration is `Local`, and a POM `<scope>system</scope>` jar is
/// `Local`, because neither has a registry at all.
///
/// The constraint tells the two halves apart. Filled in, the version was found
/// elsewhere and the item is checkable — never rewritable, since the string it
/// would rewrite is not this dependency's own. Empty, no version was found at
/// all, and a check reports
/// [`DependencyStatus::Undetermined`](crate::result::DependencyStatus::Undetermined).
Inherited,
/// The entry names a package this manifest cannot identify, whatever version it
/// states beside it.
///
/// A Maven POM is the case that needs it: `<groupId>${project.groupId}</groupId>`
/// names a built-in this file does not state, and a `<dependency>` may omit
/// `<groupId>` altogether and inherit it from a `<parent>`. Either way the
/// coordinate is not a name any registry could answer for, so the entry is never
/// fetched — and a check reports it
/// [`Undetermined`](crate::result::DependencyStatus::Undetermined), because what
/// went unread is which package this is.
///
/// Distinct from [`Inherited`](Self::Inherited), whose name *is* known and whose
/// missing half is the version: the two get different explanations, and an entry
/// whose version is stated right there in the file must never be told it takes
/// that version from somewhere else. Distinct from [`Local`](Self::Local), which
/// asserts there is no registry behind the package rather than that this file
/// could not say which package it is.
///
/// The version, when the entry states one, is still recorded in
/// [`version_constraint`](Item::version_constraint) — it is written in this file,
/// and dropping it would report a manifest as stating less than it does.
Unidentified,
}

#[cfg(test)]
Expand Down
7 changes: 4 additions & 3 deletions crates/dependable-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ pub use parsers::{
AutoTargets, CargoPackageManifest, CargoTarget, CargoTargetKind, CargoTomlParser,
CfgDependencyTable, ComposerJsonParser, CsprojParser, DenoJsonParser, DependencySection,
GoModParser, GradleCatalogParser, MixExsParser, PackageField, PackageJsonParser, Parser,
PnpmWorkspaceParser, ProjectMeta, ProjectRole, PubspecYamlParser, PyprojectTomlParser,
RequirementsTxtParser, WorkspaceDecl, parse, parse_cargo_config, parse_package_manifest,
parse_package_name, parse_project, parse_workspace, resolve_workspace_inheritance,
PnpmWorkspaceParser, PomXmlParser, ProjectMeta, ProjectRole, PubspecYamlParser,
PyprojectTomlParser, RequirementsTxtParser, WorkspaceDecl, parse, parse_cargo_config,
parse_package_manifest, parse_package_name, parse_project, parse_workspace,
resolve_workspace_inheritance,
};
pub use result::{CheckResult, DependencyStatus};
pub use semver::{Evaluation, UnstableFilter, check_version, is_prerelease, to_semver_constraint};
21 changes: 20 additions & 1 deletion crates/dependable-core/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ pub struct ParsedManifest {
pub items: Vec<Item>,
/// Alternate registry declarations (Rust `[registries.*]`).
pub alternate_registries: Vec<AlternateRegistryDecl>,
/// What the parser saw and deliberately did not read, in the parser's own
/// words, ready to print.
///
/// Not errors and not warnings *about* the dependencies in
/// [`items`](Self::items): each one names a construct this parser declines to
/// interpret, so that a list which reads as complete and is not says so.
/// A Maven `<profiles>` block holding dependencies is the motivating case —
/// excluding conditional dependencies is defensible, printing
/// `(0 dependencies)` for a POM that declares twelve of them is not.
///
/// Empty for every parser that has nothing to declare, which is most of them.
pub notices: Vec<String>,
}

/// A declared alternate registry (Rust only).
Expand Down Expand Up @@ -48,6 +60,7 @@ pub enum ManifestKind {
MixExs,
Csproj,
GradleVersionCatalog,
PomXml,
}

impl ManifestKind {
Expand All @@ -65,7 +78,7 @@ impl ManifestKind {
ManifestKind::PubspecYaml => Ecosystem::Dart,
ManifestKind::MixExs => Ecosystem::Elixir,
ManifestKind::Csproj => Ecosystem::CSharp,
ManifestKind::GradleVersionCatalog => Ecosystem::Jvm,
ManifestKind::GradleVersionCatalog | ManifestKind::PomXml => Ecosystem::Jvm,
}
}

Expand Down Expand Up @@ -179,6 +192,7 @@ impl ManifestKind {
"pubspec.yaml" => ManifestKind::PubspecYaml,
"mix.exs" => ManifestKind::MixExs,
"Directory.Packages.props" => ManifestKind::Csproj,
"pom.xml" => ManifestKind::PomXml,
// Gradle reads every `*.versions.toml` under `gradle/` as a catalog;
// `libs` is only the conventional name of the default one.
_ if name.ends_with(".versions.toml") => ManifestKind::GradleVersionCatalog,
Expand Down Expand Up @@ -429,6 +443,7 @@ mod tests {
"gradle/deps.versions.toml",
ManifestKind::GradleVersionCatalog,
),
("services/api/pom.xml", ManifestKind::PomXml),
];
for (path, expected) in cases {
assert_eq!(
Expand Down Expand Up @@ -491,6 +506,7 @@ mod tests {
ManifestKind::MixExs,
ManifestKind::Csproj,
ManifestKind::GradleVersionCatalog,
ManifestKind::PomXml,
] {
assert!(kind.workspace_roots().is_none(), "{kind:?}");
assert!(
Expand Down Expand Up @@ -540,6 +556,9 @@ mod tests {
);
}
assert!(ManifestKind::CargoToml.unreadable_manifests().is_empty());
// A `pom.xml` is data and reads fine; what it cannot resolve is reported
// entry by entry, so there is nothing here to declare unreadable.
assert!(ManifestKind::PomXml.unreadable_manifests().is_empty());
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/dependable-core/src/parsers/cargo_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ impl Parser for CargoTomlParser {
kind: ManifestKind::CargoToml,
items,
alternate_registries,
notices: Vec::new(),
})
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/dependable-core/src/parsers/composer_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ impl Parser for ComposerJsonParser {
kind: ManifestKind::ComposerJson,
items,
alternate_registries: Vec::new(),
notices: Vec::new(),
})
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/dependable-core/src/parsers/csproj.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ impl Parser for CsprojParser {
kind: ManifestKind::Csproj,
items,
alternate_registries: Vec::new(),
notices: Vec::new(),
})
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/dependable-core/src/parsers/deno_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ impl Parser for DenoJsonParser {
kind: ManifestKind::DenoJson,
items,
alternate_registries: Vec::new(),
notices: Vec::new(),
})
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/dependable-core/src/parsers/go_mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ impl Parser for GoModParser {
kind: ManifestKind::GoMod,
items,
alternate_registries: Vec::new(),
notices: Vec::new(),
})
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/dependable-core/src/parsers/gradle_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ impl Parser for GradleCatalogParser {
kind: ManifestKind::GradleVersionCatalog,
items,
alternate_registries: Vec::new(),
notices: Vec::new(),
})
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/dependable-core/src/parsers/mix_exs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ impl Parser for MixExsParser {
kind: ManifestKind::MixExs,
items,
alternate_registries: Vec::new(),
notices: Vec::new(),
})
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/dependable-core/src/parsers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod json_scan;
pub mod mix_exs;
pub mod package_json;
pub mod pnpm_workspace;
pub mod pom_xml;
pub mod position;
pub mod project;
pub mod pubspec_yaml;
Expand All @@ -42,6 +43,7 @@ pub use gradle_catalog::GradleCatalogParser;
pub use mix_exs::MixExsParser;
pub use package_json::PackageJsonParser;
pub use pnpm_workspace::PnpmWorkspaceParser;
pub use pom_xml::PomXmlParser;
pub use project::{ProjectMeta, ProjectRole, parse_project};
pub use pubspec_yaml::PubspecYamlParser;
pub use pyproject_toml::PyprojectTomlParser;
Expand All @@ -68,5 +70,6 @@ pub fn parse(kind: ManifestKind, content: &str) -> Result<ParsedManifest, ParseE
ManifestKind::Csproj => CsprojParser.parse(content),
ManifestKind::MixExs => MixExsParser.parse(content),
ManifestKind::GradleVersionCatalog => GradleCatalogParser.parse(content),
ManifestKind::PomXml => PomXmlParser.parse(content),
}
}
1 change: 1 addition & 0 deletions crates/dependable-core/src/parsers/package_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ impl Parser for PackageJsonParser {
kind: ManifestKind::PackageJson,
items,
alternate_registries: Vec::new(),
notices: Vec::new(),
})
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/dependable-core/src/parsers/pnpm_workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ impl Parser for PnpmWorkspaceParser {
kind: ManifestKind::PnpmWorkspaceYaml,
items,
alternate_registries: Vec::new(),
notices: Vec::new(),
})
}
}
Expand Down
Loading
Loading