Skip to content

Commit de26134

Browse files
authored
[undocumented] Optionally export line-level information about references (#14805)
When run with `--export-ref-info`, store line-level reference information in the cache, in a JSON file with a `.refs.json` extension. It includes the line numbers and targets of each RefExpr in the program. This only works properly if incremental mode is disabled. The target can either be a fullname, or `*.name` for an `name` attribute reference where the type of the object is unknown. This is an undocumented, experimental feature that may be useful for certain tools, but it shouldn't be used in production use cases.
1 parent 494802f commit de26134

File tree

4 files changed

+64
-0
lines changed

4 files changed

+64
-0
lines changed

mypy/build.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2410,6 +2410,10 @@ def finish_passes(self) -> None:
24102410
manager.report_file(self.tree, self.type_map(), self.options)
24112411

24122412
self.update_fine_grained_deps(self.manager.fg_deps)
2413+
2414+
if manager.options.export_ref_info:
2415+
write_undocumented_ref_info(self, manager.metastore, manager.options)
2416+
24132417
self.free_state()
24142418
if not manager.options.fine_grained_incremental and not manager.options.preserve_asts:
24152419
free_tree(self.tree)
@@ -2941,6 +2945,7 @@ def dispatch(sources: list[BuildSource], manager: BuildManager, stdout: TextIO)
29412945
dump_all_dependencies(
29422946
manager.modules, manager.all_types, manager.options.python_version, manager.options
29432947
)
2948+
29442949
return graph
29452950

29462951

@@ -3616,3 +3621,22 @@ def is_silent_import_module(manager: BuildManager, path: str) -> bool:
36163621
is_sub_path(path, dir)
36173622
for dir in manager.search_paths.package_path + manager.search_paths.typeshed_path
36183623
)
3624+
3625+
3626+
def write_undocumented_ref_info(state: State, metastore: MetadataStore, options: Options) -> None:
3627+
# This exports some dependency information in a rather ad-hoc fashion, which
3628+
# can be helpful for some tools. This is all highly experimental and could be
3629+
# removed at any time.
3630+
3631+
from mypy.refinfo import get_undocumented_ref_info_json
3632+
3633+
if not state.tree:
3634+
# We need a full AST for this.
3635+
return
3636+
3637+
_, data_file, _ = get_cache_names(state.id, state.xpath, options)
3638+
ref_info_file = ".".join(data_file.split(".")[:-2]) + ".refs.json"
3639+
assert not ref_info_file.startswith(".")
3640+
3641+
deps_json = get_undocumented_ref_info_json(state.tree)
3642+
metastore.write(ref_info_file, json.dumps(deps_json))

mypy/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,8 @@ def add_invertible_flag(
10171017
add_invertible_flag(
10181018
"--allow-empty-bodies", default=False, help=argparse.SUPPRESS, group=internals_group
10191019
)
1020+
# This undocumented feature exports limited line-level dependency information.
1021+
internals_group.add_argument("--export-ref-info", action="store_true", help=argparse.SUPPRESS)
10201022

10211023
report_group = parser.add_argument_group(
10221024
title="Report generation", description="Generate a report in the specified format."

mypy/options.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,9 @@ def __init__(self) -> None:
339339
self.disable_recursive_aliases = False
340340
# Deprecated reverse version of the above, do not use.
341341
self.enable_recursive_aliases = False
342+
# Export line-level, limited, fine-grained dependency information in cache data
343+
# (undocumented feature).
344+
self.export_ref_info = False
342345

343346
self.disable_bytearray_promotion = False
344347
self.disable_memoryview_promotion = False

mypy/refinfo.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Find line-level reference information from a mypy AST (undocumented feature)"""
2+
3+
from __future__ import annotations
4+
5+
from mypy.nodes import LDEF, MemberExpr, MypyFile, NameExpr, RefExpr
6+
from mypy.traverser import TraverserVisitor
7+
8+
9+
class RefInfoVisitor(TraverserVisitor):
10+
def __init__(self) -> None:
11+
super().__init__()
12+
self.data: list[dict[str, object]] = []
13+
14+
def visit_name_expr(self, expr: NameExpr) -> None:
15+
super().visit_name_expr(expr)
16+
self.record_ref_expr(expr)
17+
18+
def visit_member_expr(self, expr: MemberExpr) -> None:
19+
super().visit_member_expr(expr)
20+
self.record_ref_expr(expr)
21+
22+
def record_ref_expr(self, expr: RefExpr) -> None:
23+
fullname = None
24+
if expr.kind != LDEF and "." in expr.fullname:
25+
fullname = expr.fullname
26+
elif isinstance(expr, MemberExpr) and not expr.fullname:
27+
fullname = f"*.{expr.name}"
28+
if fullname is not None:
29+
self.data.append({"line": expr.line, "column": expr.column, "target": fullname})
30+
31+
32+
def get_undocumented_ref_info_json(tree: MypyFile) -> list[dict[str, object]]:
33+
visitor = RefInfoVisitor()
34+
tree.accept(visitor)
35+
return visitor.data

0 commit comments

Comments
 (0)