Skip to content

Commit 287c45a

Browse files
authored
Generate better reference information for member expressions (#14864)
Previously most member expressions only produced the wildcard reference such as `*.attr` when using the (undocumented) `--export-ref-info` flag. Use the type of the object to generate better fullnames, such as `module.Cls.attr`.
1 parent 2e8dcbf commit 287c45a

File tree

2 files changed

+47
-9
lines changed

2 files changed

+47
-9
lines changed

mypy/build.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2413,7 +2413,9 @@ def finish_passes(self) -> None:
24132413
self.update_fine_grained_deps(self.manager.fg_deps)
24142414

24152415
if manager.options.export_ref_info:
2416-
write_undocumented_ref_info(self, manager.metastore, manager.options)
2416+
write_undocumented_ref_info(
2417+
self, manager.metastore, manager.options, self.type_map()
2418+
)
24172419

24182420
self.free_state()
24192421
if not manager.options.fine_grained_incremental and not manager.options.preserve_asts:
@@ -3624,7 +3626,9 @@ def is_silent_import_module(manager: BuildManager, path: str) -> bool:
36243626
)
36253627

36263628

3627-
def write_undocumented_ref_info(state: State, metastore: MetadataStore, options: Options) -> None:
3629+
def write_undocumented_ref_info(
3630+
state: State, metastore: MetadataStore, options: Options, type_map: dict[Expression, Type]
3631+
) -> None:
36283632
# This exports some dependency information in a rather ad-hoc fashion, which
36293633
# can be helpful for some tools. This is all highly experimental and could be
36303634
# removed at any time.
@@ -3639,5 +3643,5 @@ def write_undocumented_ref_info(state: State, metastore: MetadataStore, options:
36393643
ref_info_file = ".".join(data_file.split(".")[:-2]) + ".refs.json"
36403644
assert not ref_info_file.startswith(".")
36413645

3642-
deps_json = get_undocumented_ref_info_json(state.tree)
3646+
deps_json = get_undocumented_ref_info_json(state.tree, type_map)
36433647
metastore.write(ref_info_file, json.dumps(deps_json, separators=(",", ":")))

mypy/refinfo.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,24 @@
22

33
from __future__ import annotations
44

5-
from mypy.nodes import LDEF, MemberExpr, MypyFile, NameExpr, RefExpr
5+
from mypy.nodes import LDEF, Expression, MemberExpr, MypyFile, NameExpr, RefExpr
66
from mypy.traverser import TraverserVisitor
7+
from mypy.typeops import tuple_fallback
8+
from mypy.types import (
9+
FunctionLike,
10+
Instance,
11+
TupleType,
12+
Type,
13+
TypeType,
14+
TypeVarLikeType,
15+
get_proper_type,
16+
)
717

818

919
class RefInfoVisitor(TraverserVisitor):
10-
def __init__(self) -> None:
20+
def __init__(self, type_map: dict[Expression, Type]) -> None:
1121
super().__init__()
22+
self.type_map = type_map
1223
self.data: list[dict[str, object]] = []
1324

1425
def visit_name_expr(self, expr: NameExpr) -> None:
@@ -23,13 +34,36 @@ def record_ref_expr(self, expr: RefExpr) -> None:
2334
fullname = None
2435
if expr.kind != LDEF and "." in expr.fullname:
2536
fullname = expr.fullname
26-
elif isinstance(expr, MemberExpr) and not expr.fullname:
27-
fullname = f"*.{expr.name}"
37+
elif isinstance(expr, MemberExpr):
38+
typ = self.type_map.get(expr.expr)
39+
if typ:
40+
tfn = type_fullname(typ)
41+
if tfn:
42+
fullname = f"{tfn}.{expr.name}"
43+
if not fullname:
44+
fullname = f"*.{expr.name}"
2845
if fullname is not None:
2946
self.data.append({"line": expr.line, "column": expr.column, "target": fullname})
3047

3148

32-
def get_undocumented_ref_info_json(tree: MypyFile) -> list[dict[str, object]]:
33-
visitor = RefInfoVisitor()
49+
def type_fullname(typ: Type) -> str | None:
50+
typ = get_proper_type(typ)
51+
if isinstance(typ, Instance):
52+
return typ.type.fullname
53+
elif isinstance(typ, TypeType):
54+
return type_fullname(typ.item)
55+
elif isinstance(typ, FunctionLike) and typ.is_type_obj():
56+
return type_fullname(typ.fallback)
57+
elif isinstance(typ, TupleType):
58+
return type_fullname(tuple_fallback(typ))
59+
elif isinstance(typ, TypeVarLikeType):
60+
return type_fullname(typ.upper_bound)
61+
return None
62+
63+
64+
def get_undocumented_ref_info_json(
65+
tree: MypyFile, type_map: dict[Expression, Type]
66+
) -> list[dict[str, object]]:
67+
visitor = RefInfoVisitor(type_map)
3468
tree.accept(visitor)
3569
return visitor.data

0 commit comments

Comments
 (0)