Skip to content

Expand star imports in ChangePackage when they would create ambiguity#7202

Draft
steve-aom-elliott wants to merge 9 commits intomainfrom
fix/changepackage-ambiguous-star-imports
Draft

Expand star imports in ChangePackage when they would create ambiguity#7202
steve-aom-elliott wants to merge 9 commits intomainfrom
fix/changepackage-ambiguous-star-imports

Conversation

@steve-aom-elliott
Copy link
Copy Markdown
Contributor

@steve-aom-elliott steve-aom-elliott commented Mar 30, 2026

Summary

  • When ChangePackage renames a star import (e.g., import javax.validation.constraints.*import jakarta.validation.constraints.*), it now detects if the new package contains types whose simple names clash with types from other star imports (e.g., org.hibernate.validator.constraints.* also has NotBlank)
  • If ambiguity is detected, only the changed star import is expanded into explicit imports for the types actually referenced in the source, leaving other star imports untouched
  • Uses JavaSourceSet classpath to enumerate types in each package, then walks AST identifiers to determine which types are actually referenced

Test plan

  • New test: changePackageExpandsStarImportWhenItWouldCreateAmbiguity — uses real javax.validation, jakarta.validation, and org.hibernate.validator types; verifies star import is expanded to explicit imports when another star import contains a type with the same simple name
  • New test: changePackagePreservesStarImportWhenNoAmbiguity — verifies star import is preserved when no ambiguity exists (no hibernate-validator on classpath)
  • Existing ChangePackageTest suite passes

Future work

AddImport has a related gap when star-import folding kicks in. If the import layout style folds multiple explicit imports from the same package into a star import (e.g., 3+ jakarta.validation.constraints imports fold to jakarta.validation.constraints.*), and another existing star import (org.hibernate.validator.constraints.*) contains a type with the same simple name, the result is ambiguous. A fix would check via classpath whether folding to a star import would create cross-package name collisions, and if so, keep the conflicting type as an explicit import.

Example:

// Before ChangeType javax→jakarta (with star threshold of 3)
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.*;

// After (BROKEN — folded to star, NotBlank now ambiguous)
import jakarta.validation.constraints.*;
import org.hibernate.validator.constraints.*;

// After (CORRECT — NotBlank stays explicit)
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.hibernate.validator.constraints.*;

When ChangePackage renames a star import (e.g. import origpkg.* to
import newpkg.*), the new package may contain types whose simple names
clash with types from other star imports in the same file. This creates
ambiguous imports that fail to compile.

This change converts ChangePackage to a ScanningRecipe so it can first
collect which types exist in which packages across all source files,
then use that information to detect ambiguity. When a changed star
import would create ambiguity, it is expanded into explicit imports for
only the types actually used from that package.

Fixes moderneinc/customer-requests#1733
@steve-aom-elliott steve-aom-elliott force-pushed the fix/changepackage-ambiguous-star-imports branch from a6ee247 to c7942f8 Compare March 30, 2026 18:22
Move annotation interface definitions out of source specs and into
parser dependsOn, so the tests focus on the consumer file's import
behavior rather than asserting changes to the definition files.
Replace synthetic test packages with real javax/jakarta/hibernate validation
types from recipeDependencies. Use JavaSourceSet.build with classpath to
populate the source set marker. Switch from typesInUse to AST identifier
walking for collecting referenced types, since typesInUse does not include
annotation types resolved through star imports.
- Use javax.validation:validation-api:1.1 (no NotBlank), jakarta.validation-api:2.0.2,
  and org.hibernate:hibernate-validator:5.4.3 (has NotBlank) so @notblank unambiguously
  resolves from org.hibernate.validator.constraints
- Fix hasAmbiguity to also check the original (pre-rename) package on the classpath,
  since the classpath may not yet have types under the new package name
- Regenerate type table for updated parserClasspath versions
Use classpathFromResources + srcMainJava + addTypesToSourceSet instead of
manual JavaSourceSet.build and classpath. Move validation and hibernate
deps to testParserClasspath since they're only needed by ChangePackageTest.
@steve-aom-elliott
Copy link
Copy Markdown
Contributor Author

Future work: Recipe-level type table lookup (Option 1)

A higher-level migration recipe (e.g., a javax-to-jakarta composite) could declare the target dependency as a recipeDependency. The type table for that JAR would then be available at recipe runtime. ChangePackage could use it to look up types in the new package rather than relying solely on the JavaSourceSet marker (which reflects the pre-migration classpath).

Example

A composite migration recipe already declares ChangeDependency and ChangePackage together. If it also declared the target dependency as a recipe dependency:

// In the migration recipe module's build.gradle.kts
recipeDependencies {
    parserClasspath("jakarta.validation:jakarta.validation-api:3.0.2")
}

Then at recipe runtime, ChangePackage could call classpathFromResources or equivalent to load the type table for jakarta.validation-api and check what types exist in jakarta.validation.constraints — rather than falling back to checking the original package (javax.validation.constraints) as a proxy.

What's needed

  • A mechanism for ChangePackage to discover and load type tables that are bundled with the recipe JAR (not just the ones from the LST's source set)
  • ChangePackage is generic — it doesn't know which JAR contains the target package. The composite recipe or a new option/marker would need to pass that context down
  • This builds on existing infrastructure (type tables, recipeDependencies) so the incremental cost is relatively low

@steve-aom-elliott
Copy link
Copy Markdown
Contributor Author

Future work: Worker-level dependency resolution (Option 2)

When ChangeDependency modifies a build file, the execution engine could detect this, resolve the new JAR, and update JavaSourceSet markers before downstream recipes like ChangePackage run.

Inter-recipe signaling

Currently recipes are independent transforms — they modify source files but don't communicate side effects to the runner. ChangeDependency would need to signal that the classpath changed. Two approaches:

  • ChangeDependency places a new marker (e.g., ClasspathChanged(oldGav, newGav)) on the build file
  • The execution engine diffs build file state between recipe steps and detects dependency changes

Dependency resolution in the worker

The worker would need to resolve a GAV to an actual JAR at recipe runtime. The Moderne worker already connects to artifact repositories (Artifactory, Maven Central) for LST retrieval, but that's a different code path. It would need:

  • Maven-style coordinate resolution (group:artifact:version → JAR download)
  • Repository configuration (which repos to search — the agent already has this for LSTs)
  • Caching, since multiple ChangeDependency calls in a single migration could hit the same repos repeatedly

JavaSourceSet rebuild

After downloading the new JAR:

  • Scan it for types (like JavaSourceSet.build already does with ClassGraph)
  • Remove types belonging to the old dependency's packages
  • Add types from the new JAR
  • Update the JavaSourceSet marker on all source files in the affected source set

Ordering becomes load-bearing

This only works if ChangeDependency runs before ChangePackage. Today recipe ordering is the author's responsibility, but this would make it a correctness requirement. The engine might need to enforce or at least document this constraint.

Transitive dependencies

When javax.validation:validation-apijakarta.validation:jakarta.validation-api, the transitive closure may also change. Resolving transitives is significantly more complex — essentially reimplementing Maven/Gradle dependency resolution. Could be scoped to direct dependencies only as a first pass.

Platform boundary

This works naturally in the Moderne worker/CLI where there's infrastructure for artifact resolution. For the OSS plugins (rewrite-maven-plugin, rewrite-gradle-plugin), the build tool's own resolver could potentially be used instead, since those plugins run inside a real build. But it's a different implementation path for each.

Performance

Resolving and scanning JARs adds latency per dependency change. For a large migration like javax → jakarta with dozens of ChangeDependency calls, this could be significant. Parallel resolution and caching would be important.

Realistic v1 scope

A pragmatic first version could:

  • Only handle direct dependency changes (no transitive resolution)
  • Only in the Moderne worker/CLI (not OSS plugins initially)
  • ChangeDependency emits a ClasspathChanged marker
  • Between recipe steps, the engine checks for that marker, resolves the new JAR, and updates JavaSourceSet
  • ChangePackage reads the updated JavaSourceSet as it already does

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

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant