Skip to content

Commit 204705e

Browse files
committed
Use moduleName from ty-types for module-qualified FQNs
classLiteral types now use moduleName to build proper FQNs (e.g. mymodule.MyClass instead of bare MyClass), and bare function calls get a declaring type from their module. Bumps ty-types to 0.0.19.dev20260223122104.
1 parent c99e4f9 commit 204705e

4 files changed

Lines changed: 141 additions & 3 deletions

File tree

rewrite-python/rewrite/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ requires-python = ">=3.10"
2424
dependencies = [
2525
"cbor2>=5.6.5",
2626
"more_itertools>=10.0.0",
27-
"ty-types>=0.0.19.dev20260223102528", # Type inference CLI for Python type attribution
27+
"ty-types>=0.0.19.dev20260223122104", # Type inference CLI for Python type attribution
2828
"parso>=0.7.1,<0.8", # Python 2/3 parser with CST support (0.8+ dropped Python 2.7 grammar)
2929
]
3030

rewrite-python/rewrite/src/rewrite/python/type_mapping.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,12 @@ def _descriptor_to_java_type(self, descriptor: Dict[str, Any]) -> Optional[JavaT
446446

447447
elif kind == 'classLiteral':
448448
class_name = descriptor.get('className', '')
449-
class_type = self._create_class_type(class_name)
449+
module_name = descriptor.get('moduleName')
450+
if module_name and module_name != 'builtins':
451+
fqn = f"{module_name}.{class_name}"
452+
else:
453+
fqn = class_name
454+
class_type = self._create_class_type(fqn)
450455

451456
# Infer Kind from supertypes before resolving them
452457
supertypes = descriptor.get('supertypes', [])
@@ -881,6 +886,10 @@ def _get_declaring_type(self, node: ast.Call) -> Optional[JavaType.FullyQualifie
881886
kind = descriptor.get('kind')
882887
if kind == 'module':
883888
return self._create_class_type(descriptor.get('moduleName', ''))
889+
elif kind in ('function', 'boundMethod'):
890+
module_name = descriptor.get('moduleName')
891+
if module_name and module_name != 'builtins':
892+
return self._create_class_type(module_name)
884893

885894
return self._infer_declaring_type_from_ast(node)
886895

rewrite-python/rewrite/tests/python/all/tree/class_test.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import json
12
import shutil
3+
import subprocess
4+
import tempfile
25

36
import pytest
47

58
from rewrite.java.support_types import JavaType
6-
from rewrite.java.tree import Assignment, Identifier
9+
from rewrite.java.tree import Assignment, ClassDeclaration, Identifier
710
from rewrite.python.tree import CompilationUnit
811
from rewrite.python.visitor import PythonVisitor
912
from rewrite.test import RecipeSpec, python
@@ -14,6 +17,30 @@
1417
)
1518

1619

20+
def _ty_types_has_module_name() -> bool:
21+
"""Check if the installed ty-types CLI provides moduleName on classLiteral descriptors."""
22+
if shutil.which('ty-types') is None:
23+
return False
24+
try:
25+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
26+
f.write('class _C:\n pass\n')
27+
fname = f.name
28+
result = subprocess.run(['ty-types', fname], capture_output=True, text=True, timeout=30)
29+
data = json.loads(result.stdout)
30+
return any(
31+
d.get('kind') == 'classLiteral' and 'moduleName' in d
32+
for d in data.get('types', {}).values()
33+
)
34+
except Exception:
35+
return False
36+
37+
38+
requires_module_name = pytest.mark.skipif(
39+
not _ty_types_has_module_name(),
40+
reason="ty-types CLI does not provide moduleName on classLiteral descriptors"
41+
)
42+
43+
1744
def test_empty():
1845
# language=python
1946
RecipeSpec().rewrite_run(python(
@@ -174,6 +201,41 @@ def __init__(self, value: T) -> None:
174201
assert not errors, "Type attribution errors:\n" + "\n".join(f" - {e}" for e in errors)
175202

176203

204+
@requires_module_name
205+
def test_class_literal_module_qualified_fqn():
206+
"""Verify that a class defined in a module gets a module-qualified FQN on its classLiteral type."""
207+
errors = []
208+
209+
def check_types(source_file):
210+
assert isinstance(source_file, CompilationUnit)
211+
212+
class TypeChecker(PythonVisitor):
213+
def visit_class_declaration(self, class_decl, p):
214+
if not isinstance(class_decl, ClassDeclaration):
215+
return class_decl
216+
cd_type = class_decl.type
217+
if cd_type is None:
218+
errors.append("ClassDeclaration.type is None for MyClass")
219+
elif isinstance(cd_type, JavaType.Class):
220+
fqn = cd_type._fully_qualified_name
221+
# The FQN should contain a module prefix (not just bare 'MyClass')
222+
if '.' not in fqn:
223+
errors.append(f"ClassDeclaration.type fqn '{fqn}' has no module prefix, expected '<module>.MyClass'")
224+
return class_decl
225+
226+
TypeChecker().visit(source_file, None)
227+
228+
# language=python
229+
RecipeSpec(type_attribution=True).rewrite_run(python(
230+
"""\
231+
class MyClass:
232+
pass
233+
""",
234+
after_recipe=check_types,
235+
))
236+
assert not errors, "Type attribution errors:\n" + "\n".join(f" - {e}" for e in errors)
237+
238+
177239
@requires_ty_cli
178240
def test_class_instance_type_attribution():
179241
"""Verify that x = Foo() assigns a type with fqn 'Foo'."""

rewrite-python/rewrite/tests/python/all/tree/method_invocation_test.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import json
12
import shutil
3+
import subprocess
4+
import tempfile
25

36
import pytest
47

@@ -14,6 +17,30 @@
1417
)
1518

1619

20+
def _ty_types_has_module_name() -> bool:
21+
"""Check if the installed ty-types CLI provides moduleName on function descriptors."""
22+
if shutil.which('ty-types') is None:
23+
return False
24+
try:
25+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
26+
f.write('from os.path import join\njoin("a", "b")\n')
27+
fname = f.name
28+
result = subprocess.run(['ty-types', fname], capture_output=True, text=True, timeout=30)
29+
data = json.loads(result.stdout)
30+
return any(
31+
d.get('kind') == 'function' and 'moduleName' in d
32+
for d in data.get('types', {}).values()
33+
)
34+
except Exception:
35+
return False
36+
37+
38+
requires_module_name = pytest.mark.skipif(
39+
not _ty_types_has_module_name(),
40+
reason="ty-types CLI does not provide moduleName on function descriptors"
41+
)
42+
43+
1744
def test_no_select():
1845
# language=python
1946
RecipeSpec().rewrite_run(python("assert len('a')"))
@@ -249,3 +276,43 @@ def identity[T](x: T) -> T:
249276
after_recipe=check_types,
250277
))
251278
assert not errors, "Type attribution errors:\n" + "\n".join(f" - {e}" for e in errors)
279+
280+
281+
@requires_module_name
282+
def test_bare_function_declaring_type_has_module():
283+
"""Verify that a bare function call imported from a module gets a declaring type with the module name."""
284+
errors = []
285+
286+
def check_types(source_file):
287+
assert isinstance(source_file, CompilationUnit)
288+
289+
class TypeChecker(PythonVisitor):
290+
def visit_method_invocation(self, method, p):
291+
if not isinstance(method, MethodInvocation):
292+
return method
293+
if method.name.simple_name != 'join':
294+
return method
295+
if method.method_type is None:
296+
errors.append("MethodInvocation.method_type is None for join()")
297+
else:
298+
dt = method.method_type.declaring_type
299+
if dt is None:
300+
errors.append("method_type.declaring_type is None for join()")
301+
elif not hasattr(dt, '_fully_qualified_name') or 'posixpath' not in dt._fully_qualified_name:
302+
errors.append(
303+
f"declaring_type fqn is '{getattr(dt, '_fully_qualified_name', '?')}', "
304+
f"expected to contain 'posixpath' (os.path module)"
305+
)
306+
return method
307+
308+
TypeChecker().visit(source_file, None)
309+
310+
# language=python
311+
RecipeSpec(type_attribution=True).rewrite_run(python(
312+
"""\
313+
from os.path import join
314+
x = join("a", "b")
315+
""",
316+
after_recipe=check_types,
317+
))
318+
assert not errors, "Type attribution errors:\n" + "\n".join(f" - {e}" for e in errors)

0 commit comments

Comments
 (0)