Skip to content

Resolve Maven pin properties through profiles and nesting - #2020

Draft
Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
fix/maven-discovery-false-attestationfrom
fix/maven-discovery-property-overrides
Draft

Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
fix/maven-discovery-false-attestationfrom
fix/maven-discovery-property-overrides

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #2005, including its review fix (9765178). This closes the two Maven false-attestation gaps #2005 left open in VEX discovery. It also gives VEX discovery and the vendored planner one property resolver.

1. Profile <properties> redefining a pin's property

Take a live <version>${v}</version> whose build resolution is the Socket pin. If a profile's <properties> redefine v, or any property v names, Maven resolves something else while that profile is active. Maven activates any number of profiles together, so combinations count too. Vendored mode refuses to pin over such a property (version_override), but discovery attested it.

  • parse_pom_with reads every profile's <properties>. PomScope::resolve_in_build then lists each value a combination of profiles gives the version.
    • Only the profiles that define a name reachable from the version are combined, at most 8; past that it fails closed.
    • Each combination is resolved as one property view: the profiles are merged over the pom's own, and a later profile wins.
    • User properties and model references that the model answers stay fixed.
  • Each PomDep carries those values as version_alternatives, smallest combination first. Each alternative records the profile ids that give it.
  • In discovery, the resolutions(d) helper plugs these into Stop attesting Maven pins a profile or user property overrides #2005's helpers in three places:
    • unpatched_classifier_copy: classifier copies and their classifier-keyed management.
    • profile_override, profile declarations: every version they can take is checked against the pin.
    • profile_override, live declarations: live main-jar declarations are walked too, with the same main_jar_declarations scope and management-governs filter.
  • A live pin that some combination moves keeps the ref, marked unattested with the existing UnattestedKind::MavenProfileUnpatched. The detail names the profiles and the version they give. This applies to hosted pins and the pre-v5 vendored wiring alike.
  • A <profiles> declaration's own ${v} reads its own profile first.

2. Property versions resolved one level deep

${a} with <a>${b}</a> now resolves through every property it names. That includes ${project.version}, which is bound raw (for example ${revision}) and resolved wherever it is read. Both of #2005's views use it:

  • version (as the pom spells it): the model, then the declaration's own profile, then the root <properties>.
  • build_version: .mvn/maven.config -D user properties first, at every level.

Limits: at most 16 levels, and a cycle, an undefined name or an unterminated ${ never resolves. The version then stays as written, which is never a pin.

One resolver: formats::maven::properties (pure)

  • lookup_property / PomLayer: Maven's lookup order. The model comes first, then user properties, then each pom's properties with its active profiles merged in (nearest pom first), then the bare ${version}.
  • profile_combinations: the shared enumeration of profile combinations.
  • interpolate_strict / interpolate_lenient: one expansion with explicit cycle detection, depth 16, 1024 lookups and 4096 bytes. Strict is for versions. Lenient, for coordinates and model references, keeps an unresolved ${…} visible for may_name.
  • read_properties: the <properties> parser.

PomScope and the vendored Reactor::lookup_with / version_override both use this module. Vendoring now weighs the same profile combinations VEX does, where it used to try one profile at a time, and fails closed past the same bound. The old round-based coordinate interpolator and interpolate_each are gone. resolve_in_build takes the interpolation function, so #2026 can reuse it for coordinate alternatives.

Review findings

All 9 are confirmed and fixed. Each behavior finding has a test that failed before 854c3f682.

  1. Model references moved by properties (false attestation). Model references are bound raw and resolved with each read's user properties and profiles. So a profile <revision> moves ${project.version} (unattested). A -Drevision=<pin> supplied only by maven.config now makes the spelled and built versions differ, so it is diagnosed, never a ref. Test: model_references_move_with_the_properties_they_are_spelled_through.
  2. Attributed or spaced property tags were skipped (false attestation). An element's name now ends at whitespace or /, closing tags may carry whitespace, and <v/> defines an empty value. Tests: profile_properties_with_attributes_are_read and properties::tests::properties_read_attributed_and_spaced_tags.
  3. Nested references in a profile value resolved against the root. Each combination is now one merged view. Test: profile_values_resolve_through_their_own_profile.
  4. A profile <properties> parse error failed the whole scope. with_user_properties no longer scans profiles, so the hosted rewriter is unaffected. Discovery records the error and withholds only ${…} pins (MovedBy::Unreadable); a literal pin is still attested. Test: unreadable_profile_properties_withhold_only_property_pins, plus a formats assertion.
  5. The cross-product budget collapsed multi-profile poms. It is replaced by relevance-filtered combinations, each on its own budget, with a hard bound of 8 combined profiles. Test: many_profiles_that_keep_the_pin_are_attested (5 profiles × a 5-deep chain → attested).
  6. The detail reported the lexicographically first alternative. It now names the smallest moving combination's profile ids and the version they give. Test: profile_property_detail_names_the_profile_and_version.
  7. Two precedence implementations. Both now use lookup_property and profile_combinations. Test: vendor::jvm::maven_reactor::tests::profiles_that_move_the_version_only_together_are_weighed, which failed before because the planner tried one profile at a time.
  8. Two interpolators. There is now one expansion with strict and keep-unresolved modes. Coordinate alternatives are left for Withhold Maven pins whose coordinates a profile redefines #2026.
  9. Cleanups. resolutions() drops its from-profile flag (the classifier check derives it), PomScope::fixed and the redundant guard are gone, and readers that never weigh profiles don't scan them.

Two behavior changes come from sharing the resolver:

  • The vendored planner now weighs profile combinations and fails closed above 8 relevant profiles.
  • Its ${project.*} lookups fall through to user and pom properties when the model has no value, as in Maven.

Tests

Earlier tests:

  • profile_properties_redefining_a_pin_property_are_not_attested
  • profile_declaration_reads_its_own_profile_properties
  • version_property_resolves_through_nested_properties
  • vendored_ref_under_profile_properties_is_not_attested
  • formats::maven::tests::{version_properties_resolve_recursively_and_fail_closed, profile_properties_give_version_alternatives, interpolation_is_bounded}

Changed expectation after the rebase onto #2005's review fix. In version_property_resolves_through_nested_properties, a pin supplied only by -Dp1=<pin> in maven.config, through a nested property, now expects no attestation. A Socket version that only maven.config supplies is not pom.xml wiring, so it is diagnosed, never a ref.

Runs on 854c3f682:

  • cargo test -p socket-patch-core --lib -- vex::discover:: formats::maven vendor::jvm:: passes 726.
  • patch::redirect:: together with crawlers::maven passes 953.
  • The golden corpus is unchanged (committed_fixture_corpus_matches_golden passes).
  • cargo test -p socket-patch-cli --lib vex_sources passes 14.
  • cargo clippy --locked --workspace --all-features -- -D warnings and cargo fmt --all -- --check are clean.

Not done


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: merge conflict needs a decision (left unmerged, nothing pushed).

This PR now conflicts with its base fix/maven-discovery-false-attestation (#2005) after 9765178 ("Address review: Maven discovery bypass checks match Maven precedence"). Both sides changed the same version-resolution behavior in incompatible ways, in formats/maven/mod.rs (6 hunks), vex/discover/maven.rs (3 hunks) and CLI_CONTRACT.md (2 hunks):

  • Stop attesting Maven pins a profile or user property overrides #2005 (base) splits PomDep::version (one-level, root <properties> only — what the hosted rewriter writes and rolls back) from a new build_version (user properties first, project.*/pom.* exempt), and reports a pin that .mvn/maven.config moves under a new UnattestedKind::MavenConfigOverride (vex_maven_config_override). It also narrows profile <dependencyManagement> to "only where management governs the pin" and drops the profile-classifier case from MavenProfileUnpatched (moved to MavenClassifierUnpatched).
  • This PR makes PomDep::version the full strict interpolation (user properties, model, own profile, root; 16 deep) with version_text and version_alternatives, adds fixed / profile_properties to PomScope, and folds user-property and profile-property moves into MavenProfileUnpatched.

Question: should this PR be rebuilt on #2005's split — keep version = pom-spelled for the rewriter, put the strict/recursive interpolation into build_version (plus version_alternatives from profile <properties>), keep MavenConfigOverride for .mvn/maven.config moves and use MavenProfileUnpatched only for profile-driven moves? Or should #2005's split be replaced by this PR's single version? Once you choose, the conflict can be resolved mechanically. Since #2026 is stacked on this branch, it may be simpler for the author's own stack tooling to restack it.


Generated by Claude Code

Closes the two Maven false-attestation gaps #2005 left open in VEX
discovery.

1. Profile <properties> redefining a pin's property. A live
   <version>${v}</version> that the root <properties> resolve to the
   Socket pin resolves something else whenever an active profile's
   <properties> redefine v (or a property v names): Maven merges active
   profile properties over the pom's own before interpolating. Vendored
   mode refuses to pin over such a property (version_override); discovery
   attested it. PomScope now reads each profile's <properties>, and every
   PomDep carries version_alternatives: the other versions its
   build_version takes under any profile or set of profiles active
   together (user properties and project.* model references stay fixed; a
   value some set leaves unresolved, cyclic or too deep counts as the text
   as written). profile_override now walks live main-jar declarations too
   (same main_jar_declarations scope and management-governs filter): one
   whose build version is the pin but has an alternative off it marks the
   ref unattested with the existing UnattestedKind::MavenProfileUnpatched
   (CLI note and CLI_CONTRACT.md updated), in hosted and the pre-v5
   vendored wiring alike. Profile declarations and the shared classifier
   check (unpatched_classifier_copy) count every alternative as a version
   the declaration may resolve. A <profiles> declaration's own ${v} reads
   its own profile's <properties> first.

2. Property versions resolved one level deep. ${a} with <a>${b}</a> now
   resolves through every property it names, in both views PomDep keeps:
   `version` (as the pom spells it: model, own profile, root properties)
   and `build_version` (.mvn/maven.config user properties first at every
   level; project.* stays the model's). Limits are the vendored reactor's:
   at most 16 levels, and a cycle, undefined name or unterminated ${ never
   resolves (the version stays as written, which is never a pin). A partly
   literal version (${a}.${b}-x) resolves too, and ${project.version}
   resolves from the model in both views.

The vendored reactor's interpolator (interpolate_by and the duplicate
body of Reactor::interpolate_with) moves to formats/maven as the pure
interpolate_strict, built on interpolate_each, which enumerates the
values a name may take (capped at 16 results). Both now detect cycles
explicitly and bound work (1024 lookups, 4096 bytes), so a pom whose
properties each name the next many times over fails closed instead of
expanding exponentially. Reactor::interpolate drops its always-zero
depth argument. Vendored behavior is otherwise unchanged.

Tests (each fails before this change):
- vex::discover::maven::tests::profile_properties_redefining_a_pin_property_are_not_attested
- vex::discover::maven::tests::profile_declaration_reads_its_own_profile_properties
- vex::discover::maven::tests::version_property_resolves_through_nested_properties
  (16-deep chain resolves, 17 does not; two cycles; nested user overrides)
- vex::discover::maven::tests::vendored_ref_under_profile_properties_is_not_attested
- formats::maven::tests::{version_properties_resolve_recursively_and_fail_closed,
  profile_properties_give_version_alternatives, interpolation_is_bounded}
The golden corpus is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) force-pushed the fix/maven-discovery-property-overrides branch from d7b4b40 to ec89bd9 Compare November 4, 2026 21:39
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Nov 4, 2026
The base branch was force-pushed (review fixes added; MavenConfigOverride
split out of profile overrides, build_version beside version, classifier
handling moved into unpatched_classifier_copy). Resolved against the old
base commit d7b4b40 so only this PR's changes are re-applied:

- coordinate_override now runs from bypasses after profile_override, and
  reads build_version (as the base's live-declaration checks do) and
  is_executable_classifier instead of the removed list.
- coordinate_alternatives reads the no-profile baseline with user
  properties (lookup's new build flag), matching lookup_any.
- CLI_CONTRACT and the profile note keep both sides' wording.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011f9aePp9J8NPBFw7TPjhfT
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Nov 5, 2026
Closes the last Maven property false-attestation gap in VEX discovery
after #2020: a <groupId> / <artifactId> written as ${prop} that a
profile's <properties> redefine.

Coordinates were read only through the lenient interpolator, from the
root <properties> (with .mvn/maven.config user properties over them),
so discovery attested two shapes the build may not resolve:

(a) The declaration carrying the pin names another package whenever a
    profile redefining its coordinate property is active; the pin then
    no longer applies to the patched GA, which resolves from its other
    declarations or transitively, unpinned.
(b) Another declaration (live, or inside a profile whose own properties
    retarget it) becomes the patched GA under a profile, at its own
    version off the pin.

formats/maven: interpolate_each_keeping is interpolate_each with the
lenient reading's semantics (an undefined name, cycle, chain past 16
levels or unterminated ${ stays as written, so may_name reads it as any
text; None only past the work bounds). PomDep now carries
coordinate_alternatives: every (groupId, artifactId) pair the
declaration may name under any profile or set of profiles (its own
profile's properties first for a profile declaration), minus what it
reads with no profile active. User properties and project.* model
references stay fixed, as for versions. ScopedDep.group / artifact
(the lenient base the hosted rewriter also reads) are unchanged; they
gain the raw texts.

vex/discover/maven: bypasses() also runs coordinate_override (after
profile_override), reported as the existing MavenProfileUnpatched, for
(a) the live main-jar declaration carrying the pin (build_version ==
pin) with any alternative, and (b) any declaration with an alternative
that may be the GA's main jar (as main_jar_declarations reads one:
literal, or may_shadow; pom / sources / javadoc types skipped) at a
version other than the pin among its resolutions(), or literally an
executable classifier copy off the pin or with no version of its own.
As in profile_override, a managed declaration counts only where
management governs the pin, and a main jar without a version takes the
live pin and does not withhold. Applies to hosted and the pre-v5
vendored wiring. Coordinates resolve in the build's view (user
properties first). CLI note and CLI_CONTRACT.md updated.

vendor/jvm/maven_reactor: the v5 vendored planner had the same gap.
coordinates_elsewhere enumerates inheritors and every profile on the
chain (one at a time, as version_override does). (a) The existing
coordinates_overridden check now weighs profiles too, not only
inheritors: a declaration a profile moves off the GA is left as is and
its root unpinned. (b) New warn_moved_coordinates: a declaration that
names another artifact in its pom but the GA in an inheritor or under a
profile is warned (coordinates_overridden) and, when versioned, unpins
its root and is uncertain, so attestable_checked no longer attests it.
This also covers the inheritor form of (b), which was likewise unseen.

Tests (each fails before this change):
- vex::discover::maven::tests::profile_properties_moving_the_pinned_coordinates_are_not_attested
- vex::discover::maven::tests::profile_properties_turning_a_declaration_into_the_pinned_ga_are_not_attested
- vendor::jvm::maven_reactor::tests::a_profile_moving_the_declared_coordinates_is_left_alone
- vendor::jvm::maven_reactor::tests::a_profile_turning_a_declaration_into_the_patched_artifact_is_weighed
New API: formats::maven::tests::{keeping_interpolation_leaves_unresolved_references,
profile_properties_give_coordinate_alternatives}.
Guard: vex::discover::maven::tests::unrelated_coordinate_properties_do_not_withhold.
User-property variants (a chain through -D, -D beating the profile) are
in each. The golden corpus is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on the profile / nested property resolution (#2020):

1. Model references moved by properties (false attestation). The model
   (${project.version} from <version>${revision}</version>) was bound
   already resolved from the root properties, and profiles were assumed
   unable to change it. Model references are now bound raw and resolved
   wherever they are read, with that read's user properties and active
   profiles: a profile <revision> moves ${project.version} (unattested),
   and a -Drevision=<pin> only .mvn/maven.config supplies makes the
   spelled and built versions differ, so it is diagnosed, never a ref.
2. Attributed / spaced property tags (false attestation). read_properties
   took the whole tag as the name, so <v combine.self="override"> and
   <v > were skipped. The name now ends at whitespace or '/', closing tags
   may carry whitespace, and <v/> defines an empty value.
3. Nested references in a profile value resolved against the root. Each
   combination of profiles is now resolved as one property view (the
   profiles merged over the pom's, a later one winning), so a profile's
   ${w} reads its own <w> first.
4. Profile <properties> parse errors failed the whole scope, including
   the hosted rewriter's. PomScope::with_user_properties no longer reads
   profile properties; parse_pom_with calls read_profiles(), and an
   unreadable one withholds every ${...} pin (MovedBy::Unreadable) while
   the rest of the pom is read.
5. The shared cross-product budget collapsed ordinary multi-profile poms.
   interpolate_each and its 16-result / 1024-lookup product are gone:
   profile_combinations walks the names reachable from the version,
   keeps only the profiles that define one (at most 8, else fail closed),
   and every combination of those is resolved on its own budget.
6. The detail named the lexicographically first alternative. Alternatives
   are listed smallest combination first and carry the profile ids
   (MovedBy), and the detail names them and the version they give.
7. Two precedence implementations. formats::maven::properties now holds
   lookup_property (Maven's order: model, user properties, each pom's
   properties with its active profiles merged in, bare ${version}),
   PomLayer and profile_combinations; PomScope and the vendored
   Reactor::lookup_with / version_override both use them, so vendoring
   now weighs the same combinations VEX does (it previously tried one
   profile at a time) and fails closed past the same bound.
8. Two interpolators. The round-based coordinate interpolate is gone:
   coordinates and model references go through the same expansion as
   versions, in a keep-unresolved mode (interpolate_lenient). Coordinate
   alternatives stay for #2026; PomScope::resolve_in_build takes the
   interpolation so it can reuse it.
9. Cleanups: resolutions() drops its from-profile flag (the classifier
   check derives it), PomScope::fixed and the profile-set guard are gone,
   and readers that never weigh profiles do not scan them.

Tests (each failed before this commit):
- vex::discover::maven::tests::model_references_move_with_the_properties_they_are_spelled_through
- vex::discover::maven::tests::profile_properties_with_attributes_are_read
- vex::discover::maven::tests::profile_values_resolve_through_their_own_profile
- vex::discover::maven::tests::unreadable_profile_properties_withhold_only_property_pins
- vex::discover::maven::tests::many_profiles_that_keep_the_pin_are_attested
- vex::discover::maven::tests::profile_property_detail_names_the_profile_and_version
- vendor::jvm::maven_reactor::tests::profiles_that_move_the_version_only_together_are_weighed
plus formats::maven::properties unit tests and extended
formats::maven::tests::profile_properties_give_version_alternatives.
The golden corpus is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Nov 6, 2026
Closes the last Maven property false-attestation gap in VEX discovery
after #2020: a <groupId> / <artifactId> written as ${prop} that a
profile's <properties> redefine.

Coordinates were read only with no profile active, so discovery
attested two shapes the build may not resolve:

(a) The declaration carrying the pin names another package whenever a
    profile redefining its coordinate property is active; the pin then
    no longer applies to the patched GA, which resolves from its other
    declarations or transitively, unpinned.
(b) Another declaration (live, or inside a profile whose own properties
    retarget it) becomes the patched GA under a profile, at its own
    version off the pin.

Both readers now resolve coordinates through #2020's shared helpers
(formats::maven::properties): the groupId and artifactId are joined
with COORDINATE_SEPARATOR (U+0001, which XML 1.0 forbids) so one
profile_combinations walk and one interpolation per combination read
both under the same active profiles, through interpolate_keeping (the
keep-unresolved interpolate_lenient as an Interpolate), and
split_coordinates splits the result.

formats/maven: PomScope::coordinate_alternatives runs the joined text
through resolve_in_build; PomDep carries group_text / artifact_text and
coordinate_alternatives (CoordinateAlternative: group, artifact,
MovedBy), every pair some combination gives other than the no-profile
reading (ScopedDep.group / artifact, unchanged), plus a profile
declaration's own-profile reading. More than 8 relevant profiles or an
unreadable profile fail closed (the text as written, which may_name
reads as any text).

vex/discover/maven: bypasses() runs coordinate_override after
profile_override, reported as MavenProfileUnpatched (detail names the
profiles), for (a) the live main-jar declaration carrying the pin
(build_version == pin) with any alternative, and (b) any declaration
with an alternative that may be the GA's main jar (literal or
may_shadow; pom / sources / javadoc types skipped) at a version other
than the pin among its resolutions(), or literally an executable
classifier copy off the pin or with no version of its own. As in
profile_override, a managed declaration counts only where management
governs the pin, and a versionless main jar takes the live pin. CLI
note and CLI_CONTRACT.md updated.

vendor/jvm/maven_reactor: the v5 planner had the same gap.
interpolate_with and profile_override take the Interpolate to use
(interpolate_strict for versions, as before), and coordinates_elsewhere
reads the joined coordinates in each inheritor and under each profile
combination on the chain through them. (a) The existing
coordinates_overridden check now weighs profiles, not only inheritors.
(b) New warn_moved_coordinates: a declaration that names another
artifact in its pom but may be the GA in an inheritor or under a profile
combination is warned (coordinates_overridden) and, when versioned,
unpins its root and is uncertain, so attestable_checked no longer
attests it. This also covers the inheritor form of (b).

Tests (each fails before this change):
- vex::discover::maven::tests::profile_properties_moving_the_pinned_coordinates_are_not_attested
- vex::discover::maven::tests::profile_properties_turning_a_declaration_into_the_pinned_ga_are_not_attested
- vendor::jvm::maven_reactor::tests::a_profile_moving_the_declared_coordinates_is_left_alone
- vendor::jvm::maven_reactor::tests::a_profile_turning_a_declaration_into_the_patched_artifact_is_weighed
- formats::maven::tests::profile_properties_give_coordinate_alternatives
Guard: vex::discover::maven::tests::unrelated_coordinate_properties_do_not_withhold.
User-property variants (a chain through -D, -D beating the profile) are
in each. The golden corpus is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…property-overrides

Resolve the classifier-copy conflict by keeping both sides: this branch's
per-resolution profile flags (flagged/resolutions) and the base's rule
that a managed entry naming the GA only through an undefined property
never stands in for the copy's version alone.

Co-Authored-By: Claude <noreply@anthropic.com>
…ation' into fix/maven-discovery-property-overrides

# Conflicts:
#	crates/socket-patch-cli/CLI_CONTRACT.md
#	crates/socket-patch-core/src/vex/discover/maven.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants