Skip to content

Add banSplitPackages rule: module-path split-package detection - #1000

Draft
ascheman wants to merge 8 commits into
apache:masterfrom
ascheman:feature/997-ban-split-packages
Draft

Add banSplitPackages rule: module-path split-package detection#1000
ascheman wants to merge 8 commits into
apache:masterfrom
ascheman:feature/997-ban-split-packages

Conversation

@ascheman

Copy link
Copy Markdown

Implements the banSplitPackages rule proposed in #997 — the follow-up announced in #996.

Important

Stacked on #996. The first four commits are #996 (module-info reader + three rules); only the top three commits (feat / test / docs for banSplitPackages) are new here. Once #996 merges I will rebase onto master and the diff collapses to just the new rule. Draft until then.

Rule

A package must belong to exactly one Java module — two modules on the module path containing the same package fail resolution (java.lang.module.ResolutionException), typically at packaging, jlink, or first boot, with a symptom-not-cause message. And if the offending module is not even requiresd, the resolver never pulls it in: the build stays green while a type or endpoint is silently absent at runtime.

  • Project packages come from walking the output directory, not the compiled descriptor — ModulePackages is only finalized at packaging time, and offending classes are frequently added after compile (code generation, copied resources). Both the classic layout and the Maven 4 module source hierarchy are supported (same moduleOutputs() discovery as Add Java module-info rules: requireExplicitModules, requireMinimalExports, banUnjustifiedOpens #996).
  • Dependency packages are read through java.lang.module.ModuleFinder (reflectively — same Java 8 baseline rationale as the reader in Add Java module-info rules: requireExplicitModules, requireMinimalExports, banUnjustifiedOpens #996), which derives automatic-module names and reports complete package sets; artifacts the finder cannot model fall back to a plain class-file scan.
  • The rule intersects against the full declared-dependency set, deliberately not only the requiresd modules — this is what catches the silent variant.
  • Severity model: overlap between two artifacts that both end up on the module path → error. An automatic module counts as "on the module path" only when a project module actually requires its derived name. Overlaps involving a classpath-only artifact are legal today and reported at <classpathSeverity> (default warn) as a modularization hazard.
  • Configuration: classpathSeverity (warn/error/ignore), allowedSplitPackages, ignoredModules, ignoredArtifacts, message.
  • Non-goal: a provider that is not a declared Maven dependency (pure runtime) is out of scope. The rule is a safety net, not a fix — the durable cure is placing generated/copied classes in the module that owns their package.

Design note: dependency resolution

ResolverUtil is widened to public: the enforce mojo declares only requiresDependencyCollection, so artifact files are not resolved by default; the rule resolves them explicitly — the same approach enforceBytecodeVersion already uses.

Tests

  • 15 unit tests (Java 9+ gated; ASM fixtures in test scope only, extended with a JAR fixture writer for explicit/automatic/plain dependency artifacts).
  • Four ITs under maven-enforcer-plugin/src/it/projects/: a classic two-artifact reactor failing on the silent variant, a clean pass, a classpath-overlap warn case (build succeeds, warning names the automatic module), and a Maven 4 module-source-hierarchy fail case (gated 4.0.0-rc-5+ / JDK 17+).
  • Full mvn clean verify -P run-its green on Maven 3.10.0-rc-1 (158 ITs passed); the four new ITs additionally verified on Maven 4.0.0-rc-5.

  • Your pull request should address just one issue, without pulling in other changes.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Each commit in the pull request should have a meaningful subject line and body.
  • Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied.
  • Run mvn verify to make sure basic checks pass.
  • You have run the integration tests successfully (mvn -Prun-its verify).
  • I hereby declare this contribution to be licenced under the Apache License Version 2.0, January 2004

(Replaces #999, which GitHub auto-closed when the head branch was renamed to feature/997-ban-split-packages.)

Gerd Aschemann and others added 8 commits July 17, 2026 00:32
Add a small reader that parses a module-info.class into an immutable
JavaModuleInfo (name, open flag, requires, exports, opens). It delegates
to java.lang.module.ModuleDescriptor via reflection so the code keeps the
plugin's Java 8 source baseline, and guards against a class file newer
than the running JVM with a clear diagnostic instead of a silent failure.
ASM is added in test scope only, to generate module-info.class fixtures;
production stays dependency-free.

Part of apache#995.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Gerd Aschemann <ascheman@apache.org>
Add requireExplicitModules (no automatic-module fallback),
requireMinimalExports (do not export internal/impl packages) and
banUnjustifiedOpens (no unqualified opens or open module). They share an
AbstractModuleInfoRule base whose moduleOutputs() supports both the
classic layout (one module-info.class in the output directory) and the
Maven 4 module source hierarchy (one module per output subdirectory).
Rules bind after compile (e.g. process-classes) since they read the
compiled module-info.class. Unit tests are guarded to Java 9+.

Part of apache#995.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Gerd Aschemann <ascheman@apache.org>
Four classic single-module ITs (one pass, three fail) plus two Maven 4
module source hierarchy ITs (pass and fail). The classic ITs are gated to
JDK 11+; the module-source-hierarchy ITs to Maven 4.0.0-rc-5+ and JDK 17+,
so they skip cleanly on older toolchains. Each binds the enforce goal to
process-classes so module-info.class exists when the rule runs.

Part of apache#995.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Gerd Aschemann <ascheman@apache.org>
Add site pages for requireExplicitModules, requireMinimalExports and
banUnjustifiedOpens, and list them in the built-in rules index. Each page
notes the two supported output layouts and that the enforce execution
must run after compile (e.g. process-classes).

Part of apache#995.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Gerd Aschemann <ascheman@apache.org>
Sort requires/exports/opens (and their targets) read from
ModuleDescriptor sets so diagnostics are stable across JDKs;
null-normalize allowedOpens/allowedExports setters; write test
fixture classes to package-directory layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements the banSplitPackages rule requested in apache#997: a package must
belong to exactly one Java module. The rule intersects the package sets
of the project's compiled output(s) with the full declared-dependency
set - deliberately not only the requires'd modules, which is what
catches the "silent" split the resolver never sees.

* Project packages come from walking the output directory, not the
  compiled descriptor: ModulePackages is only finalized at packaging
  time and offending classes are frequently added after compile
  (generation, copy). Classic and Maven 4 module source hierarchy
  layouts are supported via moduleOutputs().
* Dependency packages are read through java.lang.module.ModuleFinder
  (reflectively, keeping the Java 8 baseline), which derives automatic
  module names and reports complete package sets; artifacts the finder
  cannot model fall back to a plain class-file scan.
* Overlaps between two artifacts that both end up on the module path
  fail the build; an automatic module counts as on the module path only
  when a project module requires its derived name. Overlaps involving a
  classpath-only artifact are reported at <classpathSeverity> (default
  warn) as a modularization hazard.
* Configuration: classpathSeverity, allowedSplitPackages,
  ignoredModules, ignoredArtifacts, message.
* ResolverUtil is made public: the enforcer mojo only performs
  dependency collection, so the rule resolves artifact files explicitly
  (same approach as enforceBytecodeVersion).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four invoker ITs for banSplitPackages:

* ban-split-packages-fail: classic two-artifact reactor; the consumer
  re-declares a dependency package without requiring the provider (the
  silent variant) and the build fails at process-classes.
* ban-split-packages-pass: same shape with disjoint packages.
* ban-split-packages-warn: non-modular consumer overlapping a
  non-required automatic module; the build succeeds with a warning
  (default classpathSeverity).
* ban-split-packages-msh-fail: Maven 4 module source hierarchy variant,
  gated to Maven 4.0.0-rc-5+ / JDK 17+.

The MSH reactor uses a 4.0.0 aggregator and provider POM: the invoker
plugin discovers child POMs for @...@ interpolation only through
<modules>; a 4.1.0 root POM with <subprojects> leaves the children
unfiltered. Only the MSH consumer needs the 4.1.0 model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Site page for the new rule plus its entry in the rules index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@scordio

scordio commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Successfully tested locally on the AssertJ build, see output logs at assertj/assertj#4331 (comment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants