Recipe quality triage: behaviour fixes and design questions (A1–E4)

Open
#211 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
25/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
java

Research direction

Choose one item from section B–E and search the named recipe and test paths under src/main/java/org/openrewrite/java/dependencies/ and src/test/java/org/openrewrite/java/dependencies/; the issue gives item-specific locations and reproduction details. Start with the matching probe or reproduction, then add a test for the selected behavior before changing it. Done means the test verifies correct expected output and the recipe no longer causes the described behavior; section A items require a maintainer decision before work starts.

Written by the indexing model from the issue text.

Description

recipe

We triaged every recipe in this repo against the recipe review checklist we used for openrewrite/rewrite-spring#1201, openrewrite/rewrite-testing-frameworks#1211 and openrewrite/rewrite-migrate-java#1330. The mechanical fixes are tracked in #210. This issue tracks the rest: recipes that produce wrong output, miss common shapes, or need a design decision.

The wrappers here are used widely: other recipe modules reference ChangeDependency, AddDependency, UpgradeDependencyVersion and RemoveDependency about 1,600 times. A change in semantics therefore reaches far beyond this repo, which is why several items below are questions rather than fixes.

Each item is self-contained, so you can pick one up on its own or point an agent at it ("work on item B3 of this issue"). Please reference the item ID in your PR title or description. When you open the PR, tick the item's box and add the PR number after its title, e.g. (#1234). If the PR covers only some bullets, also add "Fixed in #1234." to those bullets. Untick the box if the PR is closed without merging.

How to pick up an item
  • Line numbers are against main at 74e6bdb and may have drifted, so search by symbol if needed.
  • Paths are abbreviated:
    • M/ = src/main/java/org/openrewrite/java/dependencies/
    • T/ = src/test/java/org/openrewrite/java/dependencies/
    • CSV = src/main/resources/migrations.csv
  • Status tags:
    • PROBED means a throwaway test reproduced the behaviour; the snippet is included where one helps.
    • CONFIRMED means the code path was traced but not run.
    • SUSPECTED means it was inferred from reading. Write the probe test first. If it doesn't reproduce, close the item with a note.
  • Tests resolve real POMs from Maven Central. A 429 from Central looks like "Recipe was expected to make a change but made no changes", so confirm a test passes on unmodified main before blaming the recipe. The withToolingApi() tests survive the rate limit if their build.gradle uses maven { url 'https://maven-central.storage-download.googleapis.com/maven2' } instead of mavenCentral().
  • When a recipe can't preserve the build's resolution, prefer bailing out (making no change) over guessing.
  • Every fix comes with a test. Expected output must be correct, not just a record of today's behaviour.
  • Items in A need a maintainer decision first. Comment on the issue before starting.
  • Items marked quick win only make a recipe skip more or report better, so they need no decision.
Start here: the most harmful items
  1. B1–B4, B6: RemoveRedundantDependencies deletes declarations the build still needs. All of these were probed. The causes are optional dependencies, deep exclusions, classifiers, version mediation and strictly pins.
  2. D1: DependencyList(validateResolvable=true) validates the project's own coordinates instead of each dependency's.
  3. C1: RemoveDependency with unlessUsing empties a root subprojects {} block that a subproject still uses.
  4. E3: RelocatedDependencyCheck(changeDependencies=true) jumps to latest.release, across majors and javax→jakarta. It also invents coordinates that don't exist.
  5. B5: RemoveRedundantDependencies drops an implementation dependency that a compileOnly dependency provides.
  6. D2: DependencyList drops Gradle subprojects that share group, name and version, and emits nothing on a second run of the same instance.

A. Design decisions (decide first)

  • A1 Map Maven scope and Gradle configuration consistently, and expose the delegates' missing options

    Details
    • Today:

      • M/AddDependency:203-208 maps scope to a Gradle configuration, but never configuration to a Maven scope.
      • M/DependencyInsight:78 passes null as the Gradle configuration.
      • M/RemoveDependency:110-111, M/ExcludeDependency:92-98 and M/FindDependency:70-73 map neither way.
      • The search recipes behave the same way (see A3).
    • Effect:

      • PROBED: DependencyInsight(.., scope = "compile") marks a Gradle build that only has testImplementation "com.google.guava:guava:29.0-jre".
      • CONFIRMED: AddDependency with only configuration: testImplementation adds a Maven dependency at the scope inferred from onlyIfUsing, or compile.
      • CONFIRMED: a Maven-only scope on RemoveDependency or ExcludeDependency applies to every Gradle configuration, and the reverse.
    • Delegate options not exposed (CONFIRMED):

      • Gradle onlyForConfigurations (M/UpgradeTransitiveDependencyVersion:169 passes null)
      • Maven onlyDirect and Gradle configuration in DependencyInsight (M/DependencyInsight:78-80); ModuleHasDependency gained onlyDirect in #199
      • Maven type vs Gradle extension in M/AddDependency:209-221, which aren't mapped onto each other
      • releasesOnly, onlyIfUsing and addToRootPom in UpgradeTransitiveDependencyVersion, which are silent no-ops on Gradle
    • Decide:

      • Either translate both ways: Scope.asGradleConfigurationName for declaring recipes, and resolvable classpaths for DependencyInsight (compile→compileClasspath, test→testRuntimeClasspath).
      • Or document scope as Maven-only everywhere.

      Adding an option needs a new @JsonCreator constructor that keeps the old arity, because of how many YAML recipes use these.

  • A2 AddDependency behaves differently on Maven and Gradle

    Details
    • Transitives (PROBED): AddDependency("com.google.guava", "failureaccess", "1.0.1", ..) on a project that has guava:29.0-jre adds the dependency on Maven but not on Gradle. The Gradle delegate ignores acceptTransitive and familyPattern: it declares them but never reads them, and always calls removeConfigurationsContainingTransitiveDependency.
    • No version (PROBED): Maven gets latest.release (3.21.0), while Gradle gets implementation "org.apache.commons:commons-lang3", which only resolves if a platform manages the version. Related: #182.
    • Multi-module (SUSPECTED): without onlyIfUsing, Maven adds the dependency once to the parent in the repository. Gradle adds it to every build.gradle, including a plugin-less root script, where implementation doesn't exist.
    • Decide:
      • Either fix the Gradle delegate upstream in openrewrite/rewrite so it honours acceptTransitive, pass latest.release unless a platform manages the dependency, and align the multi-module targeting.
      • Or document each difference and mark the affected options "For Maven only".
  • A3 ModuleHasDependency, RepositoryHasDependency and DoesNotIncludeDependency vs their core counterparts

    Details
    • Drift:

      • Core org.openrewrite.maven.search.ModuleHasDependency and org.openrewrite.gradle.search.ModuleHasDependency do the same scan. They lack invertMarking, the requested-dependency fallback (#179), and the marking of files without a JavaProject (#147). The core Gradle one does have a configuration option.
      • Fixes keep landing here (#150, #174, #176, #179, #199) and not in core.
    • Parity (CONFIRMED):

      • M/search/ModuleHasDependency:62-68,143-167: scope only applies to Maven. On Gradle, scope=test matches implementation dependencies, and there is no configuration option.
      • M/search/RepositoryHasDependency: no onlyDirect and no configuration.
      • M/search/DoesNotIncludeDependency:75-80: drops onlyDirect/scope for Gradle and configuration for Maven.
    • Decide:

      • Either move invertMarking and the requested-dependency fallback into core and make these thin delegates.
      • Or declare these canonical and deprecate the core pair.

      Either way, make the options symmetric (see A1). Related: openrewrite/rewrite#6468.

  • A4 Version policy, chains and data source for RelocatedDependencyCheck (#219)

    Details
    • Depends on: the decisions behind E3 and E4, which together decide whether changeDependencies=true is safe to recommend.
    • Decide:
      • Keep the old version when the target publishes it (a true relocation), and otherwise only mark; or keep latest.release.
      • Keep the group-only fallback, verify its targets, or drop it.
      • Fix the data upstream in oga-maven-plugin, or keep a local override layer that survives regeneration.
  • A5 Replace internal/* with core's org.openrewrite.maven.tree.Version (#223)

    Details
    • Where:

      • M/internal/Version, VersionParser and StaticVersionComparator, copied from Gradle in cdd48ed (2023).
      • Used only by M/search/FindMinimumDependencyVersion:87,108-114,179-191 and M/search/FindMinimumJUnitVersion:83,141-153.
    • Problem (PROBED): these classes order Maven versions differently from the resolver that core uses:

      Comparison internal core Version
      1.0.0-SP1 vs 1.0.0 < >
      Hoxton.SR1 vs Hoxton.RELEASE < >
      5.3.0.RELEASE vs 5.3.0 < =
      1.0.0.Final vs 1.0.0 < =
      2.0 vs 2.0.0 < =

      StaticVersionComparator also returns 0 for different strings (1.1.1 vs 1-1-1), so acc.merge keeps whichever one it sees first.

    • Fix: use new Version(a).compareTo(new Version(b)), and delete internal/ and T/internal/StaticVersionComparatorTest. Because this changes which version counts as the minimum, check with downstream users of the two search recipes first.

  • A6 FindMinimumJUnitVersion: JUnit 6, and repository-wide answers (#224)

    Details
    • Where: M/search/FindMinimumJUnitVersion:51,109-110,138.
    • Problem (CONFIRMED): junit-jupiter-api:6.x counts as "JUnit 5", and valid = {"4", "5"} can't express 6. The answer is computed for the whole repository, so one module still on JUnit 4 decides for all of them.
    • Decide: key the families on the major version (4/5/6) and allow "6". Consider answering per JavaProject. Check how rewrite-testing-frameworks uses this before changing the semantics.

B. RemoveRedundantDependencies removes declarations the build needs

This recipe deletes declarations, so a false positive changes the classpath. B1–B4 share a root cause: redundancy is decided by resolving the provider's POM in isolation. B4 proposes a design that fixes all four together. Prior art: openrewrite/rewrite#8235 and openrewrite/rewrite#8336, and #191, #194 and #195 here.

  • B1 Maven: the provider's <optional> dependencies count as transitive (quick win)

    Details
    • Where: M/RemoveRedundantDependencies:158-166. The provider is resolved as a root, and ResolvedPom only drops optional dependencies at depth ≥ 1. The comment at :157 is inaccurate for the same reason.
    • Problem (PROBED): with commons-compress:1.26.0 and an explicit org.tukaani:xz:1.9, RemoveRedundantDependencies("org.apache.commons", "commons-compress") deletes xz. xz is optional in commons-compress, so it disappears from the classpath.
    • Fix: skip the provider's own optional dependencies when collecting, or adopt B4.
  • B2 Maven: project exclusions are only applied to the provider's direct dependencies

    Details
    • Where: M/RemoveRedundantDependencies:173-180,104,134.
    • Problem (PROBED): with spring-boot-starter-json:3.2.0 excluding jackson-core, and an explicit jackson-core:2.15.3, the recipe still deletes jackson-core. The exclusion is what makes the explicit declaration necessary.
      • SUSPECTED: wildcard exclusions only work as far as getEffectiveExclusions() lists them, which mediation can prune (see the recipe's own comment at :194-196).
    • Fix: resolve through a synthetic root that carries the declared exclusions, or adopt B4.
  • B3 Classifier and type are ignored (quick win)

    Details
    • Where: M/RemoveRedundantDependencies:297-313 (matches on GA and version only), :259-260,287-288 (RemoveDependency by GA).
    • Problem (PROBED): jackson-core:2.17.0 with classifier sources is deleted because jackson-databind provides the plain jar. The same applies to tests/test-jar and native classifiers.
      • SUSPECTED: when the GA is declared both with and without a classifier, both declarations are removed.
    • Fix: never treat a declaration with a classifier or a non-jar type as redundant.
  • B4 Removal can change the mediated version

    Details
    • Where: M/RemoveRedundantDependencies:147-171,304-306.
    • Problem (PROBED): Maven picks the nearest version, and the first declared one wins at the same depth. Once the direct declaration is gone, an earlier sibling can win:
      <dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-yaml</artifactId><version>2.15.0</version></dependency>
      <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.17.0</version></dependency>
      <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId><version>2.17.0</version></dependency>
      <!-- RemoveRedundantDependencies("com.fasterxml.jackson.core", "jackson-databind") deletes jackson-core: 2.17.0 → 2.15.0 -->
      
    • Fix (design): decide redundancy by re-resolving the project's own POM without the candidate. Remove the candidate only if its resolved GAV, scope, classifier and type stay the same in every scope. This replaces the provider-POM approach and its scope and exclusion bookkeeping, and fixes B1–B3 too.
  • B5 Gradle: a compileOnly provider makes an implementation declaration look redundant

    Details
    • Where:
      • M/RemoveRedundantDependencies:93-111,242-266
      • :259-261: RemoveDependency(g, a, null, null, null) removes the dependency from every configuration.
      • :344-357: getBroaderGradleScopes is a hard-coded table, and is missing compileOnly, testCompileOnly, annotationProcessor and the *Classpath configurations.
    • Problem (PROBED): with compileOnly 'jackson-databind:2.17.0' and implementation 'jackson-core:2.17.0', the recipe deletes jackson-core, which then disappears from runtimeClasspath. These cases were probed and are fine:
      • a testImplementation provider with an implementation child
      • a runtimeOnly provider with an implementation child
      • an implementation provider with an api child
    • Fix: only remove a declaration when the provider is declared in the same configuration, or in one that it extends from. Use GradleDependencyConfiguration.getExtendsFrom(), and pass the configuration to RemoveDependency.
  • B6 Gradle: rich versions are removed (quick win)

    Details
    • Where: M/RemoveRedundantDependencies:202-207. constrainsResolution only catches ranges, + and latest..
    • Problem (PROBED): implementation('com.fasterxml.jackson.core:jackson-core') { version { strictly '2.17.0' } } is removed next to jackson-databind:2.17.0, which drops the pin. requested.getVersion() only carries the constraint when the strict version is itself a range.
      • SUSPECTED: the same applies to !!, require/reject, force = true and because.
    • Fix: keep any declaration that has a closure or version {} block, a !! suffix, or force. Read this from the LST via the GradleDependency trait, because the marker can't tell.
  • B7 Download failures are silent, and Maven Central is always added

    Details
    • Where: M/RemoveRedundantDependencies:167-170,209-216.
    • Problem (CONFIRMED):
      • A 429 or an unreachable private repository looks the same as "nothing redundant", in tests and in production.
      • withMavenCentral adds Central to every project, which is wrong for air-gapped builds without a mirrorOf * mirror.
    • Fix: report the failure through ctx.getOnError() or a Markup.warn on the build file. Use the project's effective repositories as they are, since MavenPomDownloader already applies the settings mirrors.
  • B8 Gradle: the accumulator key and the source of metadata

    Details
    • Where: M/RemoveRedundantDependencies:90,243 key projects by group:name; :158-159 resolves the provider's POM.
    • Problem (SUSPECTED):
      • :a:core and :b:core share their transitives, the same issue as D2.
      • With Gradle Module Metadata (variants, capabilities, platforms, as Jackson publishes), the provider's POM is not what Gradle resolves.
    • Fix: key by GradleProject.getPath(). Longer term, read the provider's children from the marker's own resolved graph instead of re-resolving a POM.

C. Wrapper and search recipe bugs

  • C1 RemoveDependency with unlessUsing empties root subprojects {} blocks that a subproject uses

    Details
    • Where: M/RemoveDependency:118-127. Only Maven got the descendant check, anyDescendantPreservesDependency at :146-169 (#183).
    • Problem (PROBED): the root declares subprojects { dependencies { implementation "com.google.guava:guava:29.0-jre" } }, and the child uses com.google.common.math.IntMath. With unlessUsing = "com.google.common..*", the root block is still emptied, because the root project doesn't use the type itself.
      • The same applies to allprojects {}, convention plugins and apply from: scripts.
      • Build files without a JavaProject marker are skipped silently (:120). That is safe, but undocumented.
      rewriteRun(
        spec -> spec.beforeRecipe(withToolingApi()).parser(JavaParser.fromJavaVersion().classpath("guava"))
          .recipe(new RemoveDependency("com.google.guava", "guava", "com.google.common..*", null, null)),
        mavenProject("root",
          settingsGradle("rootProject.name = 'root'\ninclude 'child'"),
          buildGradle("""
            subprojects {
                apply plugin: "java-library"
                repositories { mavenCentral() }
                dependencies { implementation "com.google.guava:guava:29.0-jre" }
            }
            """),
          mavenProject("child",
            srcMainJava(java("import com.google.common.math.IntMath; class A { boolean b() { return IntMath.isPrime(5); } }")),
            buildGradle("")))
      ); // root dependencies {} is emptied
      
    • Fix: treat a declaration in a cross-project block as in use when any project in the build uses the type. At minimum, bail out when the declaration sits in such a block.
  • C2 RepositoryHasDependency ignores build files without a JavaProject marker (quick win)

    Details
    • Where: M/search/RepositoryHasDependency:93-99.
    • Problem (CONFIRMED): a repository-wide check doesn't need a module marker. Root aggregator POMs and some LST producers have no JavaProject marker, so their dependencies never set the flag.
    • Fix: call hasDependency(tree) without the JavaProject gate.
  • C3 FindMinimumDependencyVersion: what version filters, which configurations are scanned, and duplicate rows

    Details
    • Where: M/search/FindMinimumDependencyVersion:89-93,110-115,136-140,153-175.
    • Problem:
      • CONFIRMED (:110-115): version filters the computed minimum, not the dependencies. With 1.0 in test and 2.5 in main, version=2.x finds nothing, although the option promises "Match only dependencies with the specified version" (:57).
      • SUSPECTED (:89-93,136-140): every Gradle configuration is scanned, including checkstyle, spotbugs, kotlinCompilerClasspath and annotationProcessor. A plugin's own Jackson can become "the oldest Jackson in use".
      • CONFIRMED (:153-175): DependenciesInUse gets one row per scope and match. Maven compile dependencies therefore repeat under provided, runtime and test.
        • sourceSet is "unknown" for pom.xml, and probably for build.gradle too.
        • count is always 1.
        • The marker lands on the whole build file rather than the declaration.
    • Fix:
      • Decide which version semantics is meant, and align the code and the text.
      • Restrict Gradle to the *CompileClasspath and *RuntimeClasspath configurations.
      • Deduplicate the rows per project, GAV and scope.
      • Mark the declaration where there is one.
  • C4 FindDuplicateClasses only reports source sets that contain Java

    Details
    • Where: M/search/FindDuplicateClasses:66-68,75-79.
    • Problem (SUSPECTED):
      • A source set with only Kotlin or Groovy sources is never reported, because JavaVisitor.visitCompilationUnit isn't reached for a K.CompilationUnit or a G.CompilationUnit.
      • Two modules with the same projectName share the deduplication key, so only one of them is reported.
    • Fix: use a TreeVisitor<SourceFile> that reads JavaSourceSet from any source file, and key on JavaProject plus the source set name.

D. Reports and diagnostics

  • D1 DependencyList(validateResolvable=true) checks the project's own coordinates

    Details
    • Where: M/DependencyList:131-141 (Gradle) and :166-182 (Maven). Both branches call downloadMetadata(new GroupArtifact(<project group>, <project artifact>), ..) instead of using the dependency's coordinates.
    • Problem (PROBED):
      • A pom com.test.notpublished:test that depends on slf4j-api:2.0.9 reports slf4j-api as unresolvable: com.test.notpublished:test failed. Unable to download metadata.
      • Conversely, a missing dependency of a published project is reported as fine.
      • The same false failures land in MavenMetadataFailures.
      • T/DependencyListTest:139-201 only passes because its project and its dependency share coordinates (:177,184,192).
      • The Gradle branch passes null settings, while the Maven branch merges the marker's settings with the context's.
    • Fix: download dep.getGav() against dep.getRepository() plus the project repositories, with the same settings handling on both sides. Deduplicate by GAV, because includeTransitive multiplies the requests. Change the test so the project and dependency coordinates differ.
    • Related: #187, which also notes that the metadata cache has no TTL. "Unavailable since the LST was produced" therefore only holds for a cold cache.
  • D2 DependencyList drops Gradle subprojects, and keeps state on the recipe instance

    Details
    • Where: M/DependencyList:78,97. seenGradleProjects is a transient field of a @Value recipe, keyed by group:name:version.
    • Problem (PROBED):
      • With include 'a:core', 'b:core' sharing a group and version, only a/core gets rows.
      • Running the same instance twice, the second run emits no rows at all.
      • SUSPECTED: wherever one instance is reused across repositories, later projects named app with an unspecified version disappear.
    • Fix: key on the project path (gradle.getPath()) and keep the set per run, as an ExecutionContext message or the accumulator of a ScanningRecipe. The field exists to deduplicate script plugins that share the build script's marker (:74-77), and that still needs to work.
  • D3 DependencyList produces no rows for builds without the standard JVM configurations

    Details
    • Where: M/DependencyList:99,220-231.
    • Problem (SUSPECTED): scope() maps only to compileClasspath, runtimeClasspath and testRuntimeClasspath. Android (debugCompileClasspath), Kotlin Multiplatform (jvmCompileClasspath) and java-platform projects are skipped silently. Maven Compile also includes provided, which the option text doesn't say.
    • Decide: fall back to the variant configurations, add a configuration option, or document the limitation.
  • D4 DependencyResolutionDiagnostic reports HTTP 200 for repositories that answered with an error

    Details
    • Where: M/DependencyResolutionDiagnostic:211-227. In core, MavenPomDownloader.normalizeRepository treats any server response (401, 403, 404, 5xx) as "reachable" without notifying the listener.
    • Problem (PROBED): https://repo1.maven.org/this-path-does-not-exist gets pingHttpCode=200. A repository that answers 401 for lack of credentials shows 200 too. The column promises "The HTTP response code returned by the repository".
    • Fix: probe the status through the HttpSender (as #187 does), or leave the column empty when the code is unknown. Alternatively, ask core to expose the reachability status.
  • D5 DependencyResolutionDiagnostic says "unknown reason" for repositories that already failed in this run (quick win)

    Details
    • Where: M/DependencyResolutionDiagnostic:160-172, with the accumulator at :92-98.
    • Problem (PROBED): only repositoryAccessFailed is overridden. Core calls repositoryAccessFailedPreviously when it has cached a failure for the repository, or when the endpoint is already unreachable.
      • Probe: with two repositories on the same dead host, one row gets UnknownHostException and the other "Repository unreachable for unknown reason".
      • The HashSets make it random which of the two rows gets which message.
    • Fix:
      • Override repositoryAccessFailedPreviously, keeping the first failure per URI.
      • Use LinkedHashSets.
      • Drop the redundant foundGradle (:93).
  • D6 DependencyResolutionDiagnostic reads caches, although it promises current conditions

    Details
    • Where: the description at M/DependencyResolutionDiagnostic:55-56; record at :139-202.
    • Problem:
      • CONFIRMED from core's source: normalizeRepository caches its results, and downloads return cached POMs, cached 404s included. With the CLI's shared POM cache, or after other dependency recipes in the same run, the columns can come from cache without any network access.
      • PROBED: the file: local repository row reports MavenDownloadingException whenever ~/.m2 doesn't have jackson-core:2.16.0.
      • There is no column for the GAV that was tried.
    • Fix:
      • Probe against a fresh InMemoryMavenPomCache, or through the raw HttpSender.
      • Skip the resolution check for file: repositories.
      • Add a GAV column.
      • Soften the description.
    • Related: #187.
  • D7 DependencyResolutionDiagnostic ignores Kotlin DSL builds, and script plugins duplicate its rows (quick win)

    Details
    • Where: M/DependencyResolutionDiagnostic:247-268 (a GroovyIsoVisitor on G.CompilationUnit) and :277 (endsWith("build.gradle")).
    • Problem:
      • PROBED: a build.gradle.kts without a GradleProject marker gets no warning.
      • CONFIRMED: GradleDependencyConfigurationErrors is never populated for .kts builds.
      • CONFIRMED: script plugins share the build script's marker, so they re-emit the same configuration-error rows.
    • Fix: use a TreeVisitor over SourceFile with an IsBuildGradle-style check that includes .kts, and deduplicate by gp.getPath(). Test with a hand-built GradleProject marker, because the tooling API can't populate configuration errors (T/DependencyResolutionDiagnosticTest:75-76).

E. Relocations: RelocatedDependencyCheck and migrations.csv

  • E1 RelocatedDependencyCheck misses Kotlin DSL, platforms, catalogs and parents (#219)

    Details
    • Where: M/RelocatedDependencyCheck:126-150 (a GroovyIsoVisitor with DependencyHandlerSpec *(..)) and :229-252.
    • Problem:
      • PROBED: buildGradleKts with implementation("commons-lang:commons-lang:2.6") gets no marker and no row. Fixed in #219.
      • CONFIRMED: also missed are:
        • platform(..) and enforcedPlatform(..). Fixed in #219.
        • libs.versions.toml. Fixed in #219.
        • plugins { id .. }
        • Maven <parent> and <extensions>. Fixed in #219 (marked, not changed).
        • ${property} coordinates, whose raw text is looked up. Fixed in #219.
      • CONFIRMED: group: someVar and "${someVar}" look up the variable name (:181-192), which can never match.
    • Fix: use the GradleDependency trait, which covers Groovy and Kotlin, string and map notation, and platforms. Read resolved Maven values via getResolutionResult().getPom().getValue(..). Add <parent>.
  • E2 RelocatedDependencyCheck(changeDependencies=true) neither changes nor marks Maven plugins (#219)

    Details
    • Where: M/RelocatedDependencyCheck:244-249 → :254-268.
    • Problem (PROBED): for a relocated <plugin>, maybeUpdate schedules ChangeDependency, which never touches <plugin> coordinates, and it also skips the SearchResult. The result is no change and no marker, only a data table row.
    • Fix: use ChangePluginGroupIdAndArtifactId, or keep the marker. Decide this together with E3, since the probed plugin relocation is itself wrong.
  • E3 changeDependencies=true: latest.release, one hop, and invented coordinates (#219)

    Details
    • Where: M/RelocatedDependencyCheck:217-219,261-263 ("latest.release") and :273-280 (the group-only fallback).
    • Problem (CONFIRMED):
      • Version: the change always moves to latest.release, across majors and namespaces. Examples are javax.servlet-api → jakarta.servlet-api 6.x, Jackson 2 → tools.jackson 3.x and httpclient → httpclient5. A probe of the existing Gradle test moved mysql-connector-java:8.0.31 to com.mysql:mysql-connector-j:26.10.0.
      • Chains: only one hop is followed. 55 rows point at a coordinate that is itself relocated, e.g. javax.servlet:servlet-api → javax.servlet:javax.servlet-api → jakarta.servlet:jakarta.servlet-api, so reaching the end takes N runs.
      • Invented coordinates: the group-only fallback produces artifacts that don't exist, and every one of these returns 404 on Central:
        • org.apache.groovy:groovy-eclipse-compiler, which T/RelocatedDependencyCheckTest:196-237 expects
        • com.github.spotbugs:jsr305
        • tools.jackson.datatype:jackson-datatype-jdk8, merged into databind in Jackson 3
      • SUSPECTED: ChangeDependency is a ScanningRecipe run via doAfterVisit(..getVisitor()), so its scanner never runs.
    • Fix: see A4.
      • Follow chains to the end, with cycle protection.
      • Verify group-only targets against metadata, or drop the fallback.
      • Pick a version policy.
      • Run ChangeDependency as a proper sub-recipe.
  • E4 migrations.csv is stale and contains successors that aren't relocations (#219)

    Details
    • No longer refreshed (CONFIRMED): 0da925f deleted the weekly migrations.yml workflow, because it pushed straight to main. The CSV has been frozen since b74cf0c (2026-10-12). It is regenerated wholesale, so data fixes must go upstream to jonathanlermitage/oga-maven-plugin, or into a local override layer.
    • Successors flagged as relocations (PROBED): every Jackson 2 user gets a marker saying "Relocated to tools.jackson.core:jackson-databind" (CSV :715-723). Jackson 2 is maintained. Similar rows are rxjava2 (:708), commons-httpclient (:131) and log4j:log4j (:711).
    • Prose targets: 32 rows, CSV :679-705,710-711.
      • They come from og-unofficial-definitions.json, where ParseDefinitionMigrations:71 keeps only the first proposal.
      • Unofficial and official rows aren't distinguished. M2 in #210 stops the crash.
    • Targets that return 404 on Central: 24 artifact rows, e.g.:
      • :13 cactus:cactus-ant-13
      • :107,110,121 com.azure:azure-keyvault*
      • :283 jakarta.websocket-api:jakarta.websocket-client-api (wrong group)
      • :352-355 org.opensymphony.quartz:*
      • :659-660 org.eclipse.jetty.http2:jetty-fcgi-*
    • Conflicting duplicates, where the last row wins:
      • jca:jca (:301-302)
      • org.hibernate:hibernate-infinispan (:466-467)
      • io.quee.ktx.framework:ktx-business-framework (:227-228)
      • net.ju-n:net-ju-n-parent (:335-336)
    • Fix:
      • Restore the refresh as a job that opens a PR. Out of scope for #219.
      • Add a source column (official or unofficial), and skip unofficial rows in change mode. Fixed in #219.
      • Filter out non-coordinate targets when parsing. Fixed in #219.
      • Report the 404, duplicate and successor rows upstream, or keep a local deny-list. Local deny-list in #219; not yet reported upstream.
Dominant language
Java
Stars
18
Forks
12
Avg merge
55m
Merged PRs (30d)
28

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from openrewrite/rewrite-java-dependencies

All issues in openrewrite/rewrite-java-dependencies

Similar issues

More Java issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.