apache / apache/maven-enforcer
New rule: banSplitPackages — detect packages split across modules and artifacts
- Dominant language
- Java
- Stars
- 161
- Forks
- 180
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 11
Description
Follow-up to #995 (the `module-info` module rules: `requireExplicitModules`, `requireMinimalExports`, `banUnjustifiedOpens`).
## Problem
The Java module system requires every package to belong to exactly one module. When two modules on the module path
contain the same package, the boot layer refuses to resolve:
```
java.lang.module.ResolutionException: Module a.consumer contains package com.acme.p,
module a.provider exports package com.acme.p to a.consumer
```
Such a split package is easy to introduce by accident — most often when a build-time code generator, an unpacked or
shaded dependency, or a copied resource puts classes of a *foreign* package into the current project's output. The
failure surfaces late (at packaging, `jlink`, or first boot), and the message describes the symptom rather than the
build step that caused it.
Two further shapes make it worse:
- **Silent variant.** If the project does not `requires` the split module, the resolver never pulls it in — so it
never complains. The build is green and the application boots, but a type or an endpoint that was supposed to come
from that module is silently absent (a 404, a late `LayerInstantiationException`, a `NoClassDefFoundError`).
- **On the classpath.** A split package is legal there and the JVM never complains at all — but it is precisely what
will break a later migration to the module path.
There is currently no Enforcer rule that catches any of these at build time.
## Proposed rule: `banSplitPackages`
Primary target is the **module path**, but the rule is useful on the **classpath** too (as a modularization aid), so
it covers both. Bind it after compilation (e.g. `process-classes`, or `verify`); it fails or warns when a package
produced by the current project also belongs to one of its dependencies.
Detection:
- **Project packages** — enumerate the packages actually present under the compiled output directory (the
directories that hold `.class` files). This is intentionally based on the real output, not the compiled
`module-info` descriptor: the offending classes are frequently added *after* `compile` (generation, copy), and the
descriptor's `ModulePackages` attribute is only finalized at packaging time.
- **Dependency packages** — resolve the project's dependency artifacts with `java.lang.module.ModuleFinder`, whose
`ModuleReference.descriptor().packages()` yields the full package set and transparently handles automatic modules;
fall back to scanning jar entries for plain (non-modular) artifacts.
- **Intersect against the full declared-dependency set**, not only the modules the project `requires`. This is what
lets the rule catch the **silent variant** that the resolver misses: the split provider is a real Maven dependency,
so it is in the set even when the module graph would never pull it in.
- **Report** every package owned by more than one place, naming both owners.
The rule handles the Maven 4 module source hierarchy (POM model 4.1.0), where one project compiles several modules,
each to `${project.build.outputDirectory}//`.
## Severity model
The severity depends on how real the problem is, and is configurable:
| Overlap kind | Default | Rationale |
| --- | --- | --- |
| Project **module** vs. a dependency **module** (incl. the silent, not-`requires`d case) | **error** | An actual `ResolutionException` (or a silent missing type) — a real defect. |
| Any overlap involving a **non-modular / classpath** artifact | **warn** | Legal on the classpath today; matters only for a future move to modules — flag it, don't fail. |
The classpath check is **on by default at `warn`** (at least a warning is emitted); set `classpathSeverity=ignore`
to opt out entirely, or `error` to enforce module-readiness strictly. The module-path/severity defaults can still be
lowered globally through the plugin's per-rule ``.
## Configuration
| Parameter | Default | Meaning |
| --- | --- | --- |
| `classpathSeverity` | `warn` | Severity for overlaps involving non-modular artifacts: `warn`, `error`, or `ignore` (opt-out). Module-vs-module overlaps are always errors. |
| `allowedSplitPackages` | – | Packages permitted to overlap (escape hatch). |
| `ignoredModules` / `ignoredArtifacts` | – | Dependencies to exclude from the check. |
| `message` | – | Custom failure message. |
## Relationship to existing rules
- Complements MojoHaus `banDuplicateClasses` (the classpath duplicate-*class* check); this rule is the
*package*-level counterpart and is module-aware. The real classpath bug is a duplicate class (handled there); a
split package *without* a duplicate class is the module-migration hazard this rule surfaces.
- Sibling of the `module-info` rules proposed in #995 (`requireExplicitModules`, `requireMinimalExports`,
`banUnjustifiedOpens`) and can reuse their `module-info` reader.
## Notes and non-goals
- A guard is a **safety net, not a fix**: the durable cure is placing generated or copied classes in the module that
owns their package.
- Only **declared dependencies** are inspected. A provider pulled in purely at run time (not a Maven dependency)
cannot be seen at build time and is out of scope; an `additionalModulePath` / `scanArtifacts` parameter could let
advanced users feed such paths in.
- On a large legacy classpath the `warn` default may be chatty; `allowedSplitPackages` / `ignoredArtifacts` /
`classpathSeverity=ignore` are the escape hatches.
## Motivation
This surfaced from a real multi-module modular application: a code generator wrote classes into a consumer module's
output, producing a split package that failed `jlink` and boot — and, in one configuration, **silently** returned
404 because the affected module was never resolved. Write-up:
.
I'm happy to contribute the implementation.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.