|
5 | 5 | from __future__ import annotations |
6 | 6 |
|
7 | 7 | import ast |
| 8 | +import json |
8 | 9 | from ast import ( |
9 | 10 | And, |
10 | 11 | AnnAssign, |
@@ -2174,3 +2175,113 @@ def refined_field_index(self, access_path: list[str]) -> int: |
2174 | 2175 |
|
2175 | 2176 | def _refined_field_name(self, idx: int) -> str: |
2176 | 2177 | return f"{TMP_VAR_PREFIX}.__refined_field__.{idx}" |
| 2178 | + |
| 2179 | + |
| 2180 | +class PyreflyTypeInfo: |
| 2181 | + """Loads and indexes a pyrefly type trace JSON file. |
| 2182 | +
|
| 2183 | + The JSON file contains: |
| 2184 | + - type_table: array of type entries (class, literal, callable) |
| 2185 | + - locations: array of {loc: {start_line, start_col, end_line, end_col}, type: index} |
| 2186 | +
|
| 2187 | + Locations map source positions to type_table indices. Positions use |
| 2188 | + 1-based lines and 0-based columns (end_col is exclusive), matching |
| 2189 | + Python AST conventions. |
| 2190 | + """ |
| 2191 | + |
| 2192 | + def __init__(self, json_path: str) -> None: |
| 2193 | + with open(json_path) as f: |
| 2194 | + data = json.load(f) |
| 2195 | + self._type_table: list[dict[str, object]] = data["type_table"] |
| 2196 | + self._locations: dict[tuple[int, int, int, int], int] = {} |
| 2197 | + for entry in data["locations"]: |
| 2198 | + loc = entry["loc"] |
| 2199 | + key = ( |
| 2200 | + loc["start_line"], |
| 2201 | + loc["start_col"], |
| 2202 | + loc["end_line"], |
| 2203 | + loc["end_col"], |
| 2204 | + ) |
| 2205 | + self._locations[key] = entry["type"] |
| 2206 | + |
| 2207 | + def _type_to_str(self, type_index: int) -> str: |
| 2208 | + """Convert a type_table entry to a Python annotation string.""" |
| 2209 | + entry = self._type_table[type_index] |
| 2210 | + kind = entry["kind"] |
| 2211 | + if kind == "literal": |
| 2212 | + return "" |
| 2213 | + elif kind == "class": |
| 2214 | + qname = str(entry["qname"]) |
| 2215 | + args = entry.get("args", []) |
| 2216 | + assert isinstance(args, list) |
| 2217 | + if not args: |
| 2218 | + return qname |
| 2219 | + arg_strs = [self._type_to_str(a) for a in args] |
| 2220 | + if any(not s for s in arg_strs): |
| 2221 | + return qname |
| 2222 | + return f"{qname}[{', '.join(arg_strs)}]" |
| 2223 | + elif kind == "callable": |
| 2224 | + params = entry.get("params", []) |
| 2225 | + assert isinstance(params, list) |
| 2226 | + ret = entry.get("return_type") |
| 2227 | + param_strs = [self._type_to_str(p) for p in params] |
| 2228 | + ret_str = self._type_to_str(ret) if isinstance(ret, int) else "" |
| 2229 | + if any(not s for s in param_strs) or not ret_str: |
| 2230 | + return "" |
| 2231 | + return f"Callable[[{', '.join(param_strs)}], {ret_str}]" |
| 2232 | + return "" |
| 2233 | + |
| 2234 | + def lookup(self, node: AST) -> str: |
| 2235 | + """Look up the type string for an AST node by its source position.""" |
| 2236 | + key = ( |
| 2237 | + node.lineno, # pyre-ignore[16] |
| 2238 | + node.col_offset, # pyre-ignore[16] |
| 2239 | + node.end_lineno, # pyre-ignore[16] |
| 2240 | + node.end_col_offset, # pyre-ignore[16] |
| 2241 | + ) |
| 2242 | + type_index = self._locations.get(key) |
| 2243 | + if type_index is None: |
| 2244 | + return "" |
| 2245 | + return self._type_to_str(type_index) |
| 2246 | + |
| 2247 | + |
| 2248 | +class PyreflyTypeBinder(TypeBinder): |
| 2249 | + """TypeBinder that uses pyrefly type inference to set types on expression nodes. |
| 2250 | +
|
| 2251 | + For each expression node, looks up the pyrefly-inferred type, |
| 2252 | + parses it as an annotation, resolves it, and sets it on the node. |
| 2253 | + """ |
| 2254 | + |
| 2255 | + def __init__( |
| 2256 | + self, |
| 2257 | + symbols: SymbolVisitor, |
| 2258 | + filename: str, |
| 2259 | + compiler: Compiler, |
| 2260 | + module_name: str, |
| 2261 | + optimize: int, |
| 2262 | + type_info: PyreflyTypeInfo, |
| 2263 | + enable_patching: bool = False, |
| 2264 | + ) -> None: |
| 2265 | + super().__init__( |
| 2266 | + symbols, filename, compiler, module_name, optimize, enable_patching |
| 2267 | + ) |
| 2268 | + self._type_info = type_info |
| 2269 | + |
| 2270 | + def visit(self, node: AST, *args: object) -> NarrowingEffect | None: |
| 2271 | + ret = super().visit(node, *args) |
| 2272 | + if isinstance(node, ast.expr): |
| 2273 | + type_str = self._type_info.lookup(node) |
| 2274 | + if type_str: |
| 2275 | + annotation_node = ast.parse(type_str, "", "eval").body |
| 2276 | + comp_type = self.module.resolve_annotation( |
| 2277 | + annotation_node, self.context_qualname |
| 2278 | + ) |
| 2279 | + if comp_type is not None: |
| 2280 | + declared_type = comp_type.instance |
| 2281 | + self.set_type(node, declared_type) |
| 2282 | + if isinstance(node, Name) and isinstance(node.ctx, ast.Store): |
| 2283 | + try: |
| 2284 | + self.declare_local(node.id, declared_type) |
| 2285 | + except TypedSyntaxError: |
| 2286 | + pass # already declared, just update the type |
| 2287 | + return ret |
0 commit comments