@@ -2541,6 +2541,17 @@ def _optional_type_ident(type_annotation):
25412541 return _read_text (idents [0 ], source )
25422542 return None
25432543
2544+ def _class_name (class_body ) -> str | None :
2545+ # `class Foo { .. }` -> "Foo"; an unnamed class expression -> None
2546+ owner = class_body .parent if class_body is not None and class_body .type == "class_body" else None
2547+ name = owner .child_by_field_name ("name" ) if owner is not None else None
2548+ return _read_text (name , source ) if name is not None else None
2549+
2550+ def _new_type (new_expr ) -> str | None :
2551+ ctor = new_expr .child_by_field_name ("constructor" )
2552+ return _read_text (ctor , source ) if ctor is not None and ctor .type in ("identifier" , "type_identifier" ) else None
2553+
2554+ assigned : dict [str , dict [str , str ]] = {}
25442555 stack = [root ]
25452556 while stack :
25462557 n = stack .pop ()
@@ -2570,28 +2581,132 @@ def _optional_type_ident(type_annotation):
25702581 # same as a constructor parameter property. It is recorded under its
25712582 # own class only, so two classes in one file with a same-named field
25722583 # of different types never borrow each other's type. Static fields are
2573- # read as `Cls.field`, not `this.field`, so they are skipped .
2584+ # kept apart: only a static method reads them as `this.field`.
25742585 name_n = n .child_by_field_name ("name" ) or n .child_by_field_name ("property" )
2575- owner = n .parent . parent if n . parent is not None else None
2576- owner_name = owner . child_by_field_name ( "name" ) if owner is not None else None
2577- if ( name_n is not None and owner_name is not None
2578- and name_n . type in ( "property_identifier" , "private_property_identifier" )
2579- and not any ( c .type == "static" for c in n . children )):
2586+ owner_name = _class_name ( n .parent )
2587+ if owner_name is not None and any ( c . type == "static" for c in n . children ):
2588+ owner_name += _TS_STATIC_SUFFIX
2589+ if ( name_n is not None
2590+ and name_n .type in ( "property_identifier" , "private_property_identifier" )):
25802591 ann = n .child_by_field_name ("type" )
25812592 value = n .child_by_field_name ("value" )
25822593 tname = None
25832594 if ann is not None :
25842595 tname = _bare_type_ident (ann ) or _optional_type_ident (ann )
25852596 elif value is not None and value .type == "new_expression" :
2586- ctor = value .child_by_field_name ("constructor" )
2587- if ctor is not None and ctor .type in ("identifier" , "type_identifier" ):
2588- tname = _read_text (ctor , source )
2597+ tname = _new_type (value )
25892598 name = _read_text (name_n , source )
2590- fields = field_table .setdefault (_read_text (owner_name , source ), {})
2591- if name and tname and name not in fields :
2592- fields [name ] = tname
2599+ if name and tname :
2600+ if owner_name is not None :
2601+ field_table .setdefault (owner_name , {}).setdefault (name , tname )
2602+ elif name not in table :
2603+ # An unnamed class (`export default class { .. }`) has no
2604+ # node to key its fields by, and its methods have no class
2605+ # either: keep the file table they read.
2606+ table [name ] = tname
2607+ elif t == "method_definition" and field_table is not None :
2608+ # The constructor declares fields too: parameter properties
2609+ # (`constructor(private repo: Repo)`) and `this.repo = repo` /
2610+ # `this.repo = new Repo()`. A declared field type wins over an
2611+ # assignment (merged after the walk).
2612+ mname = n .child_by_field_name ("name" )
2613+ owner_name = _class_name (n .parent )
2614+ accessor = next ((c .type for c in n .children if c .type in ("get" , "set" )), None )
2615+ if mname is not None and owner_name is not None and accessor is not None :
2616+ # `get repo(): Repo` / `set repo(r: Repo)` read and write a
2617+ # `this.repo` of that type, like a field.
2618+ tname = None
2619+ if accessor == "get" :
2620+ ret = n .child_by_field_name ("return_type" )
2621+ tname = (_bare_type_ident (ret ) or _optional_type_ident (ret )) if ret is not None else None
2622+ else :
2623+ params = n .child_by_field_name ("parameters" )
2624+ first = next ((p for p in params .named_children if p .type in ("required_parameter" , "optional_parameter" )), None ) if params is not None else None
2625+ ann = first .child_by_field_name ("type" ) if first is not None else None
2626+ tname = (_bare_type_ident (ann ) or _optional_type_ident (ann )) if ann is not None else None
2627+ if tname :
2628+ field_table .setdefault (owner_name , {}).setdefault (_read_text (mname , source ), tname )
2629+ if (mname is not None and owner_name is not None
2630+ and _read_text (mname , source ) == "constructor" ):
2631+ params = n .child_by_field_name ("parameters" )
2632+ typed : dict [str , str ] = {}
2633+ for p in params .named_children if params is not None else ():
2634+ if p .type not in ("required_parameter" , "optional_parameter" ):
2635+ continue
2636+ pat , ann = p .child_by_field_name ("pattern" ), p .child_by_field_name ("type" )
2637+ if pat is None or pat .type != "identifier" or ann is None :
2638+ continue
2639+ tname = _bare_type_ident (ann ) or _optional_type_ident (ann )
2640+ if not tname :
2641+ continue
2642+ pname = _read_text (pat , source )
2643+ typed [pname ] = tname
2644+ if any (c .type in ("accessibility_modifier" , "readonly" ) for c in p .children ):
2645+ field_table .setdefault (owner_name , {}).setdefault (pname , tname )
2646+ body = n .child_by_field_name ("body" )
2647+ for stmt in body .named_children if body is not None else ():
2648+ expr = stmt .named_children [0 ] if stmt .type == "expression_statement" and stmt .named_children else None
2649+ if expr is None or expr .type != "assignment_expression" :
2650+ continue
2651+ left , right = expr .child_by_field_name ("left" ), expr .child_by_field_name ("right" )
2652+ if left is None or right is None or left .type != "member_expression" :
2653+ continue
2654+ obj , prop = left .child_by_field_name ("object" ), left .child_by_field_name ("property" )
2655+ if obj is None or obj .type != "this" or prop is None :
2656+ continue
2657+ tname = (typed .get (_read_text (right , source )) if right .type == "identifier"
2658+ else _new_type (right ) if right .type == "new_expression" else None )
2659+ if tname :
2660+ assigned .setdefault (owner_name , {}).setdefault (_read_text (prop , source ), tname )
25932661 for c in n .children :
25942662 stack .append (c )
2663+ if field_table is not None :
2664+ for cls , fields in assigned .items ():
2665+ for name , tname in fields .items ():
2666+ field_table .setdefault (cls , {}).setdefault (name , tname )
2667+
2668+ _TS_STATIC_SUFFIX = "\x00 static"
2669+
2670+
2671+ def _ts_attach_class_fields (result : dict , field_table : dict [str , dict [str , str ]], str_path : str ) -> None :
2672+ """Put each class's field types on its node as `_ts_fields`.
2673+
2674+ Node ids are casefolded, so `class UsecaseVisitor` and `const usecaseVisitor`
2675+ in one file are one node, labeled after either name; classes are matched the
2676+ same way, and two classes folding together keep only the fields they type
2677+ alike."""
2678+ def _fold (label : str ) -> str :
2679+ return "" .join (c for c in str (label ) if c .isascii () and c .isalnum ()).lower ()
2680+
2681+ by_key : dict [tuple [str , bool ], dict [str , str ]] = {}
2682+ for cls , fields in field_table .items ():
2683+ static = cls .endswith (_TS_STATIC_SUFFIX )
2684+ merged = by_key .setdefault ((_fold (cls .removesuffix (_TS_STATIC_SUFFIX )), static ), {})
2685+ for name , tname in fields .items ():
2686+ merged [name ] = tname if merged .get (name , tname ) == tname else ""
2687+ if not by_key :
2688+ return
2689+ for node in result .get ("nodes" , []):
2690+ if node .get ("source_file" ) != str_path :
2691+ continue
2692+ for static , attr in ((False , "_ts_fields" ), (True , "_ts_static_fields" )):
2693+ fields = by_key .get ((_fold (node .get ("label" , "" )), static ))
2694+ kept = {k : v for k , v in sorted ((fields or {}).items ()) if v }
2695+ if kept :
2696+ node [attr ] = kept
2697+
2698+
2699+ def _ts_in_static_method (node ) -> bool :
2700+ """Whether ``node`` runs in a `static` method, where `this` is the class.
2701+ Arrow functions keep the enclosing `this`; other functions end the search."""
2702+ while node is not None :
2703+ if node .type == "method_definition" :
2704+ return any (c .type == "static" for c in node .children )
2705+ if node .type in _TS_FUNCTION_NODES and node .type != "arrow_function" :
2706+ return False
2707+ node = node .parent
2708+ return False
2709+
25952710
25962711_TS_FUNCTION_NODES = frozenset ({
25972712 "arrow_function" , "method_definition" , "function_declaration" , "function_expression" ,
@@ -8229,6 +8344,8 @@ def walk_calls(
82298344 "tree_sitter_javascript" , "tree_sitter_typescript" ,
82308345 ):
82318346 rc_entry ["ts_this_field" ] = True
8347+ if _ts_in_static_method (node ):
8348+ rc_entry ["ts_static_this" ] = True
82328349 # Tag the C++ raw_call's language so the cross-file C++ resolver
82338350 # claims it unambiguously: a `.h` file routes to extract_cpp or
82348351 # extract_objc by content, and both resolvers see `.h` in their
@@ -8750,14 +8867,13 @@ def _scan_js_module_dispatch(n) -> None:
87508867 # a call on a typed param (incl. inside a closure) resolve (#1630). The
87518868 # constructor-injection entries are populated during the walk above and win on
87528869 # a name clash (first-binding-wins in the helper).
8753- # Class fields go to their own per-class table (`this.x` only reads the
8754- # caller's class), never into the file-wide name table.
8870+ # Class fields go on their class node (`_ts_fields`), never into the
8871+ # file-wide name table: `this.x` reads the caller's class and its bases, which
8872+ # may live in files a changed-files rebuild does not re-extract.
87558873 ts_field_types : dict [str , dict [str , str ]] = {}
87568874 if config .ts_module in ("tree_sitter_javascript" , "tree_sitter_typescript" ):
87578875 _ts_receiver_type_table (root , source , type_table , ts_field_types )
8758- ts_field_types = {cls : f for cls , f in ts_field_types .items () if f }
8759- if ts_field_types and not type_table :
8760- result ["ts_type_table" ] = {"path" : str_path , "table" : {}, "fields" : ts_field_types }
8876+ _ts_attach_class_fields (result , ts_field_types , str_path )
87618877 if config .ts_module == "tree_sitter_swift" :
87628878 if type_table or swift_factory_bindings :
87638879 result ["swift_type_table" ] = {"path" : str_path , "table" : type_table }
@@ -8769,8 +8885,6 @@ def _scan_js_module_dispatch(n) -> None:
87698885 elif type_table :
87708886 if config .ts_module in ("tree_sitter_javascript" , "tree_sitter_typescript" ):
87718887 result ["ts_type_table" ] = {"path" : str_path , "table" : type_table }
8772- if ts_field_types :
8773- result ["ts_type_table" ]["fields" ] = ts_field_types
87748888 elif config .ts_module == "tree_sitter_cpp" :
87758889 result ["cpp_type_table" ] = {"path" : str_path , "table" : type_table }
87768890 return result
0 commit comments