Skip to content

New rule: banSplitPackages — detect packages split across modules and artifacts #997

Description

@ascheman

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}/<module-name>/.

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-requiresd 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 <level>.

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 New rules: enforce Java Module System descriptor policy #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:
https://vidocq.dev/posts/multi-module-jpms-apps-on-vidocq-one-opens-one-split-package-and-a-fix/.

I'm happy to contribute the implementation.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions