Skip to content

Keep an aliased import's alias when ChangeType retargets it - #8409

Merged
jkschneider merged 3 commits into
mainfrom
changetype-alias-detection-via-importservice
Aug 7, 2026
Merged

Keep an aliased import's alias when ChangeType retargets it#8409
jkschneider merged 3 commits into
mainfrom
changetype-alias-detection-via-importservice

Conversation

@knutwannheden

@knutwannheden knutwannheden commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Motivation

ChangeType reuses an aliased import's alias when it retargets a type, so that import a.b.Original as MyAlias becomes import x.y.Target as MyAlias and references to MyAlias keep resolving (#3558, #3560). Verifying that behaviour across languages turned up three ways it goes wrong outside Kotlin.

In Python, imports can appear inside a function body or under if TYPE_CHECKING:. The alias capture considered every J.Import it visited, so such an import produced a file-level aliased import while the nested one stayed put and went on shadowing it wherever it applied. The file gained a line that changed no behaviour.

In Groovy, the alias was found but silently discarded on the way out: import java.util.ArrayList as MyList became import java.util.LinkedList, leaving MyList unresolved and the file broken.

Python's AddImport also treated a name bound by an existing import as a reference to that name, so only_if_referenced was satisfied by the import itself and a redundant second import was added.

Examples

Python keeps the alias, and usages need no rewriting:

# ChangeType("typing.List", "builtins.list")
from typing import List as L

def foo(items: L[str]) -> L[int]:
    pass
from builtins import list as L

def foo(items: L[str]) -> L[int]:
    pass

A function-local import is left alone. Previously the file gained a from builtins import list as L line above def foo, while the local import stayed and kept shadowing it inside the function:

# ChangeType("typing.List", "builtins.list") — output identical to input
def foo():
    from typing import List as L
    items: L[int] = []
    return items

The same holds for if TYPE_CHECKING:, where the added line would sit outside the guard that exists to keep the import off the runtime path:

# ChangeType("typing.List", "builtins.list") — output identical to input
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from typing import List as L

def foo(items: L[int]) -> None:
    pass

Groovy keeps the alias instead of dropping it:

// ChangeType("java.util.ArrayList", "java.util.LinkedList")
import java.util.LinkedList as MyList

class A {
    MyList type
}

Summary

  • Restrict ChangeType's alias capture to imports with no enclosing J.Block. Java, Kotlin, and Groovy have no nested imports and are unaffected; Python's function-local and if TYPE_CHECKING: imports are now skipped, since the file-level import that would replace them binds a wider scope than the original.
  • Set the alias on the J.Import that Java's AddImport constructs. It has reached the visitor since Update AddImport with alias to support Kotlin alias import #3560 — threaded through maybeAddImport/addImportVisitor so Kotlin's own AddImport could receive it — but was never applied to the import Java's implementation builds, so Groovy lost it. JavaPrinter emits no alias, so a Java file prints identically whatever the import holds. The non-aliased add that ChangeType also queues is skipped, because by then the type is explicitly imported.
  • Ignore identifiers inside import statements in Python's AddImport reference check, matching RemoveImport's collector and Java's AddImport.

Test plan

  • New Python RPC tests in tests/rpc/test_java_recipes.py covering the aliased from-import, a re-exported import named by its canonical type (collections.abc.Iterable is attributed typing.Iterable), the standalone import <module> as <alias> statement, and the function-local and if TYPE_CHECKING: imports left unchanged. Both nested cases were confirmed to fail without the guard.
  • New Groovy changeImportKeepsAlias in ChangeTypeAdaptabilityTest, which fails on main and passes here.
  • New test_add_import.py case for a name occurring only inside an import statement.
  • Full module suites, all green: rewrite-java-test (2167 tests), rewrite-kotlin (1309, including changeImportAlias, changeTypeWithGenericArgumentAlias, addImportAlias, updateImportAlias), rewrite-groovy (678), and Python (1803 across tests/python, tests/recipes, tests/rpc).
  • RequirementsTxtParserTest.markerContainsDependenciesFromFreeze fails on this branch, but fails identically on a clean checkout of main — it resolves live packages from PyPI and is unrelated to these changes.

@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Aug 6, 2026
@knutwannheden
knutwannheden force-pushed the changetype-alias-detection-via-importservice branch 2 times, most recently from aebda7d to 895a051 Compare August 6, 2026 12:17
@knutwannheden knutwannheden changed the title Detect aliased imports in ChangeType via ImportService Keep an aliased import's alias when ChangeType retargets it Aug 6, 2026
ChangeType reuses an aliased import's alias for the target import so that
references to the alias keep resolving. It considered every J.Import it
visited, including those nested in a block — a function-local import or one
under `if TYPE_CHECKING:` in Python. Those bind a narrower scope than the
file-level import added in their place, which the nested binding then goes
on shadowing, so the file gained a line that changed no behaviour.

Consider only imports with no enclosing block. Java, Kotlin, and Groovy have
no nested imports and are unaffected.
Groovy parses import aliases and its printer emits them, but it has no
ImportService override and so adds imports through Java's AddImport, which
dropped the alias: `import java.util.ArrayList as MyList` became
`import java.util.LinkedList`, leaving the bound name unresolved.

The alias has reached the visitor since #3560, which threaded it through
maybeAddImport and addImportVisitor so Kotlin's own AddImport could receive
it, but it was never applied to the J.Import that Java's implementation
constructs. Set it there. JavaPrinter emits no alias, so a Java file prints
identically whatever the import holds, and the non-aliased add ChangeType
also queues is skipped because the type is by then explicitly imported.
Identifiers inside import statements are bindings rather than uses, but the
check counted them, so a module named only by an existing import satisfied
only_if_referenced and a second, redundant import was added. RemoveImport's
collector and Java's AddImport already draw this distinction.
@knutwannheden
knutwannheden force-pushed the changetype-alias-detection-via-importservice branch from 895a051 to 1495d9e Compare August 6, 2026 12:54
@jkschneider
jkschneider merged commit 61574b6 into main Aug 7, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Aug 7, 2026
@jkschneider
jkschneider deleted the changetype-alias-detection-via-importservice branch August 7, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants