leyline-code 0.8.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. leyline/__init__.py +3 -0
  2. leyline/adapters/__init__.py +19 -0
  3. leyline/adapters/csharp.py +1026 -0
  4. leyline/adapters/generic.py +844 -0
  5. leyline/adapters/python.py +929 -0
  6. leyline/adapters/typescript.py +1512 -0
  7. leyline/affected.py +509 -0
  8. leyline/agent_skills.py +311 -0
  9. leyline/change.py +500 -0
  10. leyline/channels.py +1782 -0
  11. leyline/cli.py +1029 -0
  12. leyline/cluster.py +149 -0
  13. leyline/context.py +531 -0
  14. leyline/coupling.py +485 -0
  15. leyline/coverage.py +376 -0
  16. leyline/diagrams.py +680 -0
  17. leyline/diff.py +1078 -0
  18. leyline/drift.py +747 -0
  19. leyline/exact.py +410 -0
  20. leyline/explain.py +973 -0
  21. leyline/export.py +673 -0
  22. leyline/fncoupling.py +328 -0
  23. leyline/formats.py +923 -0
  24. leyline/grade.py +207 -0
  25. leyline/incremental.py +931 -0
  26. leyline/indexer.py +3733 -0
  27. leyline/learnings.py +743 -0
  28. leyline/loop.py +584 -0
  29. leyline/model.py +142 -0
  30. leyline/outline.py +1066 -0
  31. leyline/packing.py +86 -0
  32. leyline/patterns.py +478 -0
  33. leyline/pr.py +1249 -0
  34. leyline/props.py +255 -0
  35. leyline/query.py +683 -0
  36. leyline/quick.py +853 -0
  37. leyline/related.py +266 -0
  38. leyline/removal.py +231 -0
  39. leyline/renames.py +231 -0
  40. leyline/rereview.py +375 -0
  41. leyline/roslyn_refs/Program.cs +179 -0
  42. leyline/roslyn_refs/RoslynRefs.csproj +16 -0
  43. leyline/rules.py +171 -0
  44. leyline/schema.sql +178 -0
  45. leyline/scip_pb2.py +97 -0
  46. leyline/server.py +1230 -0
  47. leyline/skills/leyline-adversarial-review/SKILL.md +121 -0
  48. leyline/skills/leyline-ask/SKILL.md +57 -0
  49. leyline/skills/leyline-change-impact/SKILL.md +79 -0
  50. leyline/skills/leyline-explain-flow/SKILL.md +70 -0
  51. leyline/skills/leyline-explore-module/SKILL.md +55 -0
  52. leyline/skills/leyline-pr-review/SKILL.md +77 -0
  53. leyline/skills/leyline-quick-change/SKILL.md +63 -0
  54. leyline/skills/leyline-spec/SKILL.md +179 -0
  55. leyline/skills/leyline-tour/SKILL.md +46 -0
  56. leyline/spec.py +1819 -0
  57. leyline/store.py +560 -0
  58. leyline/tours.py +352 -0
  59. leyline/verdicts.py +390 -0
  60. leyline/viewer/viewer.html +1622 -0
  61. leyline_code-0.8.1.dist-info/METADATA +1325 -0
  62. leyline_code-0.8.1.dist-info/RECORD +65 -0
  63. leyline_code-0.8.1.dist-info/WHEEL +4 -0
  64. leyline_code-0.8.1.dist-info/entry_points.txt +2 -0
  65. leyline_code-0.8.1.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,844 @@
1
+ """One adapter for every language that has a tree-sitter grammar.
2
+
3
+ Nothing here knows a language. A language is one row of LANGUAGES: its name, its file extensions and
4
+ the grammar package to load. What the adapter reads comes from two language-neutral sources:
5
+
6
+ - the grammar's own *tags query* where it ships one (the queries code hosts use for "go to
7
+ definition"): which nodes define a function, a method or a type, and which are calls;
8
+ - otherwise the *shape* of the syntax tree: grammars name their nodes alike (`function_declaration`,
9
+ `class_definition`, `call_expression`), so a node whose type reads like a definition and that has a
10
+ name is taken as one.
11
+
12
+ That gives declarations, nesting, call sites with whatever the text shows of the receiver, type
13
+ mentions, imports, tests by naming convention, and `main` as an entry point. It does not give what
14
+ needs a language's rules: the type of a receiver, which overload, field reads and writes. Calls are
15
+ therefore linked by name and scope, and the indexer says which links are guesses. A SCIP index, when
16
+ there is one, replaces the guesses with the compiler's answer.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import importlib
22
+ import re
23
+ from typing import Optional
24
+
25
+ from ..model import CallSite, Edge, FileResult, ImportRef, Node, TypeRef
26
+
27
+ VERSION = "0.1"
28
+
29
+ # language, extensions, grammar package, function in that package that returns the grammar
30
+ LANGUAGES = [
31
+ ("go", (".go",), "tree_sitter_go", "language"),
32
+ ("rust", (".rs",), "tree_sitter_rust", "language"),
33
+ ("java", (".java",), "tree_sitter_java", "language"),
34
+ ("kotlin", (".kt", ".kts"), "tree_sitter_kotlin", "language"),
35
+ ("swift", (".swift",), "tree_sitter_swift", "language"),
36
+ ("c", (".c", ".h"), "tree_sitter_c", "language"),
37
+ ("cpp", (".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"), "tree_sitter_cpp", "language"),
38
+ ("ruby", (".rb",), "tree_sitter_ruby", "language"),
39
+ ("php", (".php",), "tree_sitter_php", "language_php"),
40
+ ("scala", (".scala", ".sc"), "tree_sitter_scala", "language"),
41
+ ("lua", (".lua",), "tree_sitter_lua", "language"),
42
+ ("elixir", (".ex", ".exs"), "tree_sitter_elixir", "language"),
43
+ ("dart", (".dart",), "tree_sitter_dart", "language"),
44
+ ("zig", (".zig",), "tree_sitter_zig", "language"),
45
+ ("haskell", (".hs",), "tree_sitter_haskell", "language"),
46
+ ("ocaml", (".ml",), "tree_sitter_ocaml", "language_ocaml"),
47
+ ("julia", (".jl",), "tree_sitter_julia", "language"),
48
+ ("bash", (".sh", ".bash"), "tree_sitter_bash", "language"),
49
+ ("gdscript", (".gd",), "tree_sitter_gdscript", "language"),
50
+ # These three also have hand-written adapters, which are used unless the generic one is asked for.
51
+ ("python", (".py",), "tree_sitter_python", "language"),
52
+ ("csharp", (".cs",), "tree_sitter_c_sharp", "language"),
53
+ ("typescript", (".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"), "tree_sitter_typescript", "language_typescript"),
54
+ ("typescript", (".tsx", ".jsx"), "tree_sitter_typescript", "language_tsx"),
55
+ ]
56
+
57
+ # -- the shape of a syntax tree ------------------------------------------------------------------
58
+ _CALLABLE = re.compile(
59
+ r"^(local_|generator_|abstract_|anonymous_)?(function|method|constructor|destructor|func|fun|fn|def|macro|subroutine|"
60
+ r"procedure|init|deinit|getter|setter|operator|secondary_constructor)"
61
+ r"(_definition|_declaration|_item|_statement|_signature|_declarator)?$")
62
+ _NOT_CALLABLE = {"function_declarator", "method_signature", "function_signature", "anonymous_function", "function_type",
63
+ "abstract_function_declaration" if False else "function_pointer_declarator", "method_elem"}
64
+ _TYPE = re.compile(
65
+ r"^(abstract_)?(class|interface|struct|enum|trait|protocol|object|record|union|type_alias|typealias|module|"
66
+ r"mixin|actor|annotation_type|type|data_type|newtype|companion_object)"
67
+ r"(_definition|_declaration|_item|_specifier|_statement|_spec)?$")
68
+ _NOT_TYPE = {"type", "module", "class", "object", "interface", "struct", "union", "enum", "record", "type_declaration", "trait"}
69
+ _KEEP_TYPE = {("ruby", "class"), ("ruby", "module"), ("elixir", "module")}
70
+ # A block that adds members to a type declared elsewhere: `impl T { }`, `extension T { }`.
71
+ _CONTAINER = {"impl_item", "extension_declaration", "extension", "implementation", "class_implementation"}
72
+ # `const f = () => ...`, `let f = |x| ...`, `local f = function() ... end`, `val f = { x -> ... }`: a name given a function.
73
+ _BINDING = re.compile(r"^(variable_declarator|let_declaration|const_item|static_item|short_var_declaration|init_declarator|"
74
+ r"val_definition|var_definition|property_declaration|variable_declaration|assignment|assignment_expression|"
75
+ r"assignment_statement|local_variable_declaration|pair|public_field_definition|field_definition|"
76
+ r"variable_assignment|binding|value_definition|let_binding|local_function|lexical_binding)$")
77
+ _FUNC_VALUE = re.compile(r"^(arrow_function|function_expression|function|lambda|lambda_expression|closure_expression|"
78
+ r"anonymous_function|func_literal|function_definition|fun|fn|block_lambda|annotated_lambda|"
79
+ r"closure|generator_function|anonymous_method_expression|function_literal)$")
80
+ # A call that declares a test: it("does x", () => ...), test("x") { ... }, it "x" do ... end, t.Run("x", func ...).
81
+ TEST_CALLS = {"it", "test", "specify", "scenario", "Run", "should", "xit", "fit", "bench", "Describe" if False else "it_behaves_like"}
82
+ SUITE_CALLS = {"describe", "context", "suite", "feature", "Describe", "Context", "When"}
83
+ _CALL = re.compile(r"^(call|call_expression|method_invocation|invocation_expression|function_call|function_call_expression|"
84
+ r"member_call_expression|scoped_call_expression|macro_invocation|method_call|command|new_expression|"
85
+ r"object_creation_expression|instance_expression|navigation_call|constructor_invocation|"
86
+ r"jsx_self_closing_element|jsx_opening_element)$")
87
+ _NEW = {"new_expression", "object_creation_expression", "instance_expression", "composite_literal", "struct_expression"}
88
+ _IMPORT = re.compile(r"^(import|use|using|include|require|preproc_include|import_from|namespace_use|extern_crate|open)"
89
+ r"(_statement|_declaration|_directive|_header|_clause|_spec|_list)?$")
90
+ _BASES = re.compile(r"superclass|super_class|super_interfaces|base_list|base_class_clause|class_heritage|extends|implements|"
91
+ r"delegation_specifier|inheritance|type_list|trait_bounds|supertype|argument_list")
92
+ _FIELD = re.compile(r"^(field_declaration|property_declaration|field_definition|public_field_definition|struct_field|"
93
+ r"field|variable_declaration|val_definition|var_definition|instance_variable_declaration)$")
94
+ _NAME_TYPES = ("identifier", "type_identifier", "field_identifier", "simple_identifier", "constant", "name",
95
+ "property_identifier", "namespace_identifier", "scoped_identifier", "qualified_identifier", "word", "variable_name")
96
+ _KEYWORDS = {"if", "for", "while", "switch", "return", "sizeof", "typeof", "catch", "match", "defined", "await", "assert"}
97
+ TEST_PATH = re.compile(r"(^|/)(tests?|spec|specs|__tests__|testing)(/|$)|(_test|_spec|Test|Tests|Spec|\.test|\.spec)\.[A-Za-z]+$"
98
+ r"|(^|/)test_[^/]+$")
99
+ TEST_MARK = re.compile(r"#\[(tokio::)?test|@Test\b|@ParameterizedTest|\[Test\]|\[Fact\]|\[Theory\]|@pytest|func Test|\btest\s+\"")
100
+ # Method names so common on built-in and library objects that sharing one says nothing about which function is meant.
101
+ COMMON_METHODS = frozenset("""new get set add remove push pop put clear size len length next close open read write run start stop
102
+ init main string to_string toString equals hash clone copy map filter each find contains insert delete update append
103
+ format print println printf error errorf log debug info warn ok err unwrap expect iter into from as_ref as_str
104
+ is_empty isEmpty keys values join split trim send recv lock unlock wait name value index count first last sort parse""".split())
105
+
106
+
107
+ def _text(node) -> str:
108
+ return node.text.decode("utf8", "replace") if node is not None else ""
109
+
110
+
111
+ def _name_of(node):
112
+ """The node that names a definition."""
113
+ n = node.child_by_field_name("name")
114
+ if n is not None:
115
+ return n
116
+ decl = node.child_by_field_name("declarator")
117
+ hops = 0
118
+ while decl is not None and hops < 6: # C: function_definition > function_declarator > identifier
119
+ inner = decl.child_by_field_name("declarator") or decl.child_by_field_name("name")
120
+ if inner is None:
121
+ return decl if decl.type in _NAME_TYPES else next((c for c in decl.named_children if c.type in _NAME_TYPES), None)
122
+ decl, hops = inner, hops + 1
123
+ return next((c for c in node.named_children if c.type in _NAME_TYPES), None)
124
+
125
+
126
+ def _last_name(node):
127
+ """The rightmost name in a callee expression: `a.b.c` -> c."""
128
+ if node is None:
129
+ return None
130
+ if node.type in _NAME_TYPES and not node.named_child_count:
131
+ return node
132
+ for field in ("name", "field", "property", "method", "attribute", "function", "member"):
133
+ c = node.child_by_field_name(field)
134
+ if c is not None and c is not node:
135
+ hit = _last_name(c)
136
+ if hit is not None:
137
+ return hit
138
+ for c in reversed(node.named_children):
139
+ if c.type in _NAME_TYPES or c.named_child_count:
140
+ hit = _last_name(c)
141
+ if hit is not None:
142
+ return hit
143
+ return None
144
+
145
+
146
+ class Generic:
147
+ GENERIC = True
148
+
149
+ def __init__(self, language: str, extensions: tuple, package: str, fn: str):
150
+ self.LANGUAGE, self.EXTENSIONS, self.package, self.fn = language, extensions, package, fn
151
+ self.NAME = f"generic-{language}"
152
+ self.VERSION = VERSION
153
+ self.COMMON_METHODS = COMMON_METHODS
154
+ self._parser = self._query = None
155
+ self._shape: dict = {} # node type -> (definition kind, is a call, is a base list), see _collect
156
+ self._io_shape: dict = {} # node type -> (is an import, is a field), see parse
157
+ self.mode = None # "tags" or "shape", once loaded
158
+ self._loaded = False
159
+
160
+ def available(self) -> bool:
161
+ try:
162
+ importlib.import_module(self.package)
163
+ return True
164
+ except ImportError:
165
+ return False
166
+
167
+ def _load(self) -> None:
168
+ if self._loaded:
169
+ return
170
+ self._loaded = True
171
+ from tree_sitter import Language, Parser, Query
172
+ mod = importlib.import_module(self.package)
173
+ lang = Language(getattr(mod, self.fn)())
174
+ self._parser = Parser(lang)
175
+ self.mode = "shape"
176
+ tags = getattr(mod, "TAGS_QUERY", None)
177
+ if tags is None and hasattr(mod, "_get_query"):
178
+ try:
179
+ tags = mod._get_query("TAGS_QUERY", "tags.scm")
180
+ except Exception:
181
+ tags = None
182
+ if tags:
183
+ try:
184
+ self._query = Query(lang, tags)
185
+ self.mode = "tags"
186
+ except Exception:
187
+ self._query = None
188
+
189
+ @staticmethod
190
+ def module_path(rel_path: str, keep_ext: bool = False) -> str:
191
+ return (rel_path if keep_ext else re.sub(r"\.[A-Za-z0-9]+$", "", rel_path)).replace("/", ".") # see typescript.module_path
192
+
193
+ # -- reading one file ---------------------------------------------------------------------------
194
+ def _collect(self, root, use_tags: bool):
195
+ """(definitions, calls, type mentions, containers), each a list of tuples over tree nodes."""
196
+ defs, calls, refs, containers = [], [], [], []
197
+ if use_tags and self._query is not None:
198
+ from tree_sitter import QueryCursor
199
+ for _, caps in QueryCursor(self._query).matches(root):
200
+ name = (caps.get("name") or [None])[0]
201
+ if name is None:
202
+ continue
203
+ for key, nodes in caps.items():
204
+ node = nodes[0]
205
+ if key.startswith("definition."):
206
+ kind = key.split(".", 1)[1]
207
+ if kind in ("function", "method", "macro", "constructor"):
208
+ defs.append(("callable", kind, name, node))
209
+ elif kind in ("class", "interface", "type", "module", "struct", "enum", "trait", "namespace", "object"):
210
+ if kind == "module" and node.child_by_field_name("body") is None and node.named_child_count <= 1:
211
+ continue # `mod geo;` names a file, it is not a type
212
+ defs.append(("type", kind, name, node))
213
+ elif key == "reference.call":
214
+ # some queries tag a call's argument list; the call is the node around it
215
+ calls.append((name, node.parent if node.type in ("argument_list", "arguments") and node.parent is not None else node))
216
+ elif key == "reference.implementation" and node.type in _CONTAINER:
217
+ # `impl Trait for Type`: the block's methods belong to Type; the query names the trait
218
+ own = _last_name(node.child_by_field_name("type"))
219
+ containers.append((node, _text(own if own is not None else name)))
220
+ refs.append((name, node, "base") if node.child_by_field_name("trait") is not None
221
+ and name.start_byte == node.child_by_field_name("trait").start_byte else (name, node, "skip"))
222
+ elif key.startswith("reference."):
223
+ role = "base" if key == "reference.implementation" or _BASES.search(node.type) or (
224
+ node.parent is not None and _BASES.search(node.parent.type)) else \
225
+ "instantiate" if node.type in _NEW or (node.parent is not None and node.parent.type in _NEW) else "local"
226
+ refs.append((name, node, role))
227
+ lang = self.LANGUAGE
228
+ shape = self._shape
229
+ stack = [root]
230
+ while stack:
231
+ n = stack.pop()
232
+ t = n.type
233
+ # What the rules below make of a node type, worked out once per type rather than by four regular
234
+ # expressions on every node of every file.
235
+ got = shape.get(t)
236
+ if got is None:
237
+ got = shape[t] = (1 if _CALLABLE.match(t) and t not in _NOT_CALLABLE else
238
+ 2 if (_TYPE.match(t) and t not in _NOT_TYPE) or (lang, t) in _KEEP_TYPE else
239
+ 3 if _BINDING.match(t) else 0,
240
+ _CALL.match(t) is not None, bool(_BASES.search(t)) and t != "argument_list")
241
+ decl, is_call, is_bases = got
242
+ if True: # the shape rules run alongside the tags query: many queries cover only part of a language
243
+ if decl == 1:
244
+ nm = _name_of(n)
245
+ if nm is not None and _text(nm) not in _KEYWORDS:
246
+ defs.append(("callable", t.split("_")[0], nm, n))
247
+ elif decl == 2:
248
+ nm = _name_of(n)
249
+ if nm is not None:
250
+ defs.append(("type", t.split("_")[0], nm, n))
251
+ elif decl == 3:
252
+ value = n.child_by_field_name("value") or n.child_by_field_name("right") or \
253
+ (n.named_children[-1] if n.named_child_count >= 2 else None)
254
+ while value is not None and value.type in ("parenthesized_expression", "expression_list") and value.named_children:
255
+ value = value.named_children[0]
256
+ target = n.child_by_field_name("name") or n.child_by_field_name("left") or n.child_by_field_name("pattern") \
257
+ or n.child_by_field_name("key") or n.child_by_field_name("declarator") or (n.named_children[0] if n.named_children else None)
258
+ if value is not None and _FUNC_VALUE.match(value.type) and target is not None and target is not value:
259
+ nm = target if target.type in _NAME_TYPES and not target.named_child_count else _last_name(target)
260
+ if nm is not None and re.fullmatch(r"[A-Za-z_$][\w$]*", _text(nm)):
261
+ defs.append(("callable", "function", nm, n))
262
+ if is_call:
263
+ callee = n.child_by_field_name("function") or n.child_by_field_name("name") or n.child_by_field_name("method") \
264
+ or n.child_by_field_name("constructor") or n.child_by_field_name("type") or n.child_by_field_name("macro") \
265
+ or (n.named_children[0] if n.named_children else None)
266
+ nm = _last_name(callee)
267
+ if t.startswith("jsx_") and (nm is None or not _text(nm)[:1].isupper()):
268
+ nm = None # <div> is markup; <Panel> renders a component
269
+ if nm is not None and _text(nm) not in _KEYWORDS:
270
+ calls.append((nm, n))
271
+ word = _text(nm)
272
+ if word in TEST_CALLS or word in SUITE_CALLS:
273
+ title = _first_string(n)
274
+ if title is not None and _has_function(n):
275
+ defs.append(("test" if word in TEST_CALLS else "suite", word, title, n))
276
+ if t in _NEW:
277
+ refs.append((nm, n, "instantiate"))
278
+ elif is_bases and n.parent is not None and (
279
+ _TYPE.match(n.parent.type) or (lang, n.parent.type) in _KEEP_TYPE or n.parent.type in ("class", "type_spec")):
280
+ inner = [n]
281
+ while inner:
282
+ m = inner.pop()
283
+ if m.type in ("type_identifier", "identifier", "simple_identifier", "constant", "user_type") and not m.named_child_count:
284
+ refs.append((m, n, "base"))
285
+ else:
286
+ inner.extend(m.named_children)
287
+ if t == "token_tree":
288
+ # A macro's arguments, which the grammar leaves as raw tokens: a name directly followed by a
289
+ # parenthesized group (or by `!` and one) is still a call.
290
+ kids = n.children
291
+ for i, c in enumerate(kids[:-1]):
292
+ nxt = kids[i + 1] if kids[i + 1].type != "!" or i + 2 >= len(kids) else kids[i + 2]
293
+ if c.type == "identifier" and nxt.type == "token_tree" and _text(nxt)[:1] == "(" and \
294
+ (i == 0 or _text(kids[i - 1]) not in ("fn", "struct", "enum", "macro_rules", "mod")):
295
+ calls.append((c, nxt))
296
+ if t in _CONTAINER and not any(c[0].start_byte == n.start_byte for c in containers):
297
+ target = n.child_by_field_name("type") or n.child_by_field_name("name") or _name_of(n)
298
+ nm = _last_name(target)
299
+ if nm is not None:
300
+ containers.append((n, _text(nm)))
301
+ # Only named nodes are read; unnamed ones (punctuation, keywords) are not put on the stack at all.
302
+ stack.extend(n.named_children)
303
+ return defs, calls, refs, containers
304
+
305
+ def parse(self, repo: str, rel_path: str, file_id: str, src: bytes, module: str = "", keep_ext: bool = False) -> FileResult:
306
+ self._load()
307
+ res = FileResult()
308
+ tree = self._parser.parse(src)
309
+ root = tree.root_node
310
+ mod = self.module_path(rel_path, keep_ext)
311
+ lang = self.LANGUAGE
312
+ prefix = f"{repo}:{lang}:{mod}"
313
+ top_id = f"{prefix}.<module>"
314
+ test_file = bool(TEST_PATH.search(rel_path))
315
+ defs, calls, refs, containers = self._collect(root, True)
316
+
317
+ # One definition per span; a method wins over a function when a grammar tags the node as both.
318
+ best: dict = {}
319
+ for kind, native, name, node in defs:
320
+ key = (node.start_byte, node.end_byte)
321
+ if key not in best or native == "method":
322
+ best[key] = (kind, native, name, node)
323
+ ordered = sorted(best.values(), key=lambda d: (d[3].start_byte, -d[3].end_byte))
324
+ containers.sort(key=lambda c: (c[0].start_byte, -c[0].end_byte))
325
+ made: list = [] # (start, end, id, kind, qualified name)
326
+ force_top = [False]
327
+ open_defs: list = []
328
+ ids: dict[str, int] = {}
329
+ type_ids: dict[str, str] = {}
330
+
331
+ def enclosing(start, end, kinds=("callable", "type", "test")):
332
+ for s, e, i, k, q in reversed(open_defs):
333
+ if s <= start and end <= e and k in kinds:
334
+ return s, e, i, k, q
335
+ return None
336
+
337
+ suites: list = []
338
+ for kind, native, name_node, node in ordered:
339
+ if kind in ("test", "suite"):
340
+ title = re.sub(r"^[\"'`]+|[\"'`]+$", "", _text(name_node)).strip()
341
+ while suites and suites[-1][1] <= node.start_byte:
342
+ suites.pop()
343
+ if kind == "suite":
344
+ suites.append((node.start_byte, node.end_byte, title))
345
+ continue
346
+ full = " > ".join([x[2] for x in suites] + [title])
347
+ while open_defs and open_defs[-1][1] <= node.start_byte:
348
+ open_defs.pop()
349
+ outer = enclosing(node.start_byte, node.end_byte, ("callable",))
350
+ parent = outer[2] if outer else top_id
351
+ tid = f"{parent}/test:{re.sub(r'[^A-Za-z0-9]+', '-', full).strip('-').lower()[:120]}"
352
+ ids[tid] = ids.get(tid, 0) + 1
353
+ if ids[tid] > 1:
354
+ tid = f"{tid}-{ids[tid]}"
355
+ test_parent_top = outer is None
356
+ res.nodes.append(Node(id=tid, kind="test", name=title[:200], parent_id=parent, language=lang, path=rel_path,
357
+ span_start=node.start_point[0] + 1, span_end=node.end_point[0] + 1,
358
+ attrs={"framework": "by convention", "runner": native, "full_name": full}))
359
+ entry = (node.start_byte, node.end_byte, tid, "test", full)
360
+ made.append(entry)
361
+ open_defs.append(entry)
362
+ if test_parent_top:
363
+ force_top[0] = True
364
+ continue
365
+ name = _text(name_node).strip()
366
+ if not name or len(name) > 120 or "\n" in name:
367
+ continue
368
+ name = re.split(r"[.:]+", name)[-1] if kind == "callable" else name.split("::")[-1].split(".")[-1]
369
+ if native == "macro":
370
+ name += "!"
371
+ while open_defs and open_defs[-1][1] <= node.start_byte:
372
+ open_defs.pop()
373
+ outer = enclosing(node.start_byte, node.end_byte)
374
+ owner_name = None
375
+ cont = next((c for c in reversed(containers) if c[0].start_byte <= node.start_byte and node.end_byte <= c[0].end_byte
376
+ and not _same(c[0], node)), None)
377
+ recv = node.child_by_field_name("receiver") # Go: func (t *T) Run()
378
+ if kind == "callable" and recv is not None:
379
+ owner_name = next((_text(x) for x in _walk(recv) if x.type == "type_identifier"), None)
380
+ elif kind == "callable" and cont is not None and (outer is None or outer[0] <= cont[0].start_byte):
381
+ owner_name = cont[1]
382
+ parent_id, qual = (outer[2], outer[4]) if outer else (file_id, "")
383
+ if owner_name and (outer is None or outer[3] != "type"):
384
+ qual = f"{qual}.{owner_name}" if qual and not qual.endswith(owner_name) else owner_name
385
+ if owner_name in type_ids:
386
+ parent_id = type_ids[owner_name]
387
+ qname = f"{qual}.{name}" if qual else name
388
+ nid = f"{prefix}.{qname}"
389
+ ids[nid] = ids.get(nid, 0) + 1
390
+ if ids[nid] > 1:
391
+ nid = f"{nid}~{ids[nid]}" # an overload, or the same name declared again
392
+ body = node.child_by_field_name("body")
393
+ head = src[node.start_byte:body.start_byte if body is not None else node.end_byte].decode("utf8", "replace")
394
+ head = re.sub(r"\s+", " ", head.split("\n\n")[0]).strip()[:240]
395
+ # What is written before the definition and belongs to it: its attributes and annotations (Rust's #[test]
396
+ # sits beside the function, not in it), not the end of the definition above (another's @Test).
397
+ start, prev = node.start_byte, node.prev_named_sibling
398
+ while prev is not None and re.search(r"attribute|annotation|decorator|comment|pragma", prev.type):
399
+ start, prev = prev.start_byte, prev.prev_named_sibling
400
+ before = src[start:node.start_byte].decode("utf8", "replace")
401
+ is_method = kind == "callable" and (native == "method" or bool(owner_name) or (outer is not None and outer[3] == "type"))
402
+ anonymous = kind == "callable" and not owner_name and outer is not None and _in_anonymous(node, outer[0])
403
+ if anonymous:
404
+ is_method = False # `new Base() { void run() {} }`: a member of a class with no name, not of the outer type
405
+ type_id = parent_id if is_method and parent_id != file_id and (outer is None or outer[3] == "type" or owner_name) else None
406
+ attrs = {"signature": head, "native_kind": native,
407
+ "visibility": "private" if re.search(r"\b(private|fileprivate)\b", head) or name.startswith("_") else "public"}
408
+ if kind == "callable":
409
+ params = node.child_by_field_name("parameters")
410
+ plist = [p for p in params.named_children if p.type != "comment"] if params is not None else []
411
+ # The receiver written out (Go's func (c *T), Rust's &self, Python's self): a bare call in the
412
+ # body is then never a call on the receiver.
413
+ explicit_self = recv is not None or bool(plist and ("self" in plist[0].type or
414
+ re.fullmatch(r"&?(mut )?(self|cls)", _text(plist[0]).strip())))
415
+ is_test = (test_file and re.match(r"(?i)test", name) is not None) or bool(TEST_MARK.search(before + " " + head[:40]))
416
+ attrs.update({"argc_min": 0, "argc_max": 99, "is_static": not is_method, "is_virtual": is_method,
417
+ "type_id": type_id, "owner_name": owner_name, "is_test": is_test or None,
418
+ "framework": "by convention" if is_test else None,
419
+ "params_seen": len(plist) if params is not None else None, "explicit_self": explicit_self or None,
420
+ "param_types": _param_types(plist[1:] if explicit_self and recv is None else plist) or None,
421
+ # declared in `impl Trait for T` or in an anonymous class: it implements something declared
422
+ # elsewhere, and a method of the type's own of the same name is the one called
423
+ "via_base": True if anonymous or (cont is not None and owner_name and
424
+ cont[0].child_by_field_name("trait") is not None) else None,
425
+ "body_line": body.start_point[0] + 1 if body is not None else None})
426
+ ret = node.child_by_field_name("return_type") or node.child_by_field_name("result") or \
427
+ node.child_by_field_name("returns") or node.child_by_field_name("type")
428
+ if ret is not None:
429
+ names = _type_names(_text(ret))
430
+ own = owner_name or (outer[4].rsplit(".", 1)[-1] if outer is not None and outer[3] == "type" else None)
431
+ attrs["returns_names"] = [own if x == "Self" and own else x for x in names][:4] or None
432
+ tparams = node.child_by_field_name("type_parameters")
433
+ if tparams is not None:
434
+ attrs["type_params"] = re.findall(r"[A-Za-z_]\w*", re.sub(r"(:|extends|super)[^,>]*", "", _text(tparams)))
435
+ else:
436
+ attrs.update({"namespace": qual, "is_abstract": native in ("interface", "trait", "protocol")})
437
+ type_ids.setdefault(name, nid)
438
+ res.nodes.append(Node(id=nid, kind=kind, name=name, parent_id=parent_id, language=lang, path=rel_path,
439
+ span_start=node.start_point[0] + 1, span_end=node.end_point[0] + 1, attrs=attrs))
440
+ entry = (node.start_byte, node.end_byte, nid, kind, qname)
441
+ made.append(entry)
442
+ open_defs.append(entry)
443
+ if kind == "callable" and name == "main" and (parent_id == file_id or "static" in head) and not test_file:
444
+ res.nodes.append(Node(id=nid + "#entry", kind="entry_point", name=f"{rel_path} (main)", parent_id=file_id,
445
+ language=lang, path=rel_path, span_start=node.start_point[0] + 1,
446
+ span_end=node.end_point[0] + 1, attrs={"trigger": "cli", "address": rel_path}))
447
+ res.edges.append(Edge("exposes", nid + "#entry", nid))
448
+
449
+ made.sort()
450
+ starts = [m[0] for m in made]
451
+
452
+ def inside(pos, kinds):
453
+ """The innermost definition of one of these kinds around a byte position."""
454
+ import bisect
455
+ i = bisect.bisect_right(starts, pos) - 1
456
+ hit = None
457
+ while i >= 0:
458
+ s, e, nid, k, _ = made[i]
459
+ if s <= pos < e and k in kinds:
460
+ return made[i]
461
+ if hit is None and pos - s > 400000:
462
+ break
463
+ i -= 1
464
+ return None
465
+
466
+ top_used = False
467
+ seen_calls = set()
468
+ kinds_of = {n.id: n.attrs.get("native_kind") for n in res.nodes}
469
+ parents = {n.id: n.parent_id for n in res.nodes}
470
+ ctors: dict = {} # type id -> its first constructor's entry
471
+ for m in made:
472
+ if m[3] == "callable" and kinds_of.get(m[2]) == "constructor":
473
+ ctors.setdefault(parents.get(m[2]), m)
474
+ sites, site_at = [], {}
475
+ for name_node, node in calls:
476
+ name = _text(name_node).strip()
477
+ if not name or not re.fullmatch(r"[A-Za-z_$@][\w$?]*", name) or name in _KEYWORDS:
478
+ continue
479
+ key = (name_node.start_byte, name)
480
+ if key in seen_calls:
481
+ continue
482
+ seen_calls.add(key)
483
+ home = inside(name_node.start_byte, ("callable", "test"))
484
+ if home is not None and home[0] == node.start_byte and home[1] == node.end_byte:
485
+ if home[3] == "test": # the call that declares a test is made by what surrounds it
486
+ outer = [m for m in made if m[0] <= node.start_byte and node.end_byte <= m[1] and m is not home and m[3] in ("callable", "test")]
487
+ home = max(outer, key=lambda m: m[0]) if outer else None
488
+ else:
489
+ continue
490
+ owner = inside(name_node.start_byte, ("type",))
491
+ if home is None and owner is not None and _in_field(node, owner[0]):
492
+ # A field initialiser (private Formatter fmt = Formatter.create()) runs when the object is made: in
493
+ # the type's constructor, or the type itself when it declares none.
494
+ home = ctors.get(owner[2]) or owner
495
+ src_id = home[2] if home else top_id
496
+ top_used = top_used or home is None
497
+ enclosing_type = owner[2] if owner else None
498
+ if home is not None:
499
+ n = next((x for x in res.nodes if x.id == home[2]), None) if False else None
500
+ receiver = _receiver(src, name_node.start_byte)
501
+ if src[name_node.end_byte:name_node.end_byte + 1] == b"!" and lang in ("rust",):
502
+ name += "!" # a macro: only a macro of that name can answer it
503
+ args = node.child_by_field_name("arguments") or (node if node.type in ("argument_list", "arguments") else None)
504
+ argc = len([a for a in args.named_children if a.type != "comment"]) if args is not None else 0
505
+ if node.type == "token_tree":
506
+ inner = node.children[1:-1]
507
+ argc = sum(1 for x in inner if x.type == ",") + 1 if inner else 0
508
+ site = CallSite(src_id, name, receiver, None, argc, name_node.start_point[0] + 1, enclosing_type,
509
+ name_node.start_point[1])
510
+ res.calls.append(site)
511
+ sites.append((site, node, args))
512
+ site_at[(node.start_byte, node.end_byte)] = site
513
+ for site, node, args in sites:
514
+ # What the text shows of an argument's type, and of the value a call is made on when it is
515
+ # itself an expression: `new Foo().run()`, `a.make().run()` (resolved by the indexer).
516
+ if args is not None and args.type != "token_tree":
517
+ hints = tuple(_arg_hint(a, site_at) for a in args.named_children if a.type != "comment")
518
+ site.args = hints if any(h is not None for h in hints) else ()
519
+ if site.receiver == "?":
520
+ obj = _callee_object(node)
521
+ if obj is not None and obj.type in _NEW:
522
+ tnode = obj.child_by_field_name("type") or obj.child_by_field_name("constructor") or \
523
+ (obj.named_children[0] if obj.named_children else None)
524
+ site.receiver_type = _type_head(_text(tnode)) if tnode is not None else None
525
+ elif obj is not None and (obj.start_byte, obj.end_byte) in site_at:
526
+ site.chain = site_at[(obj.start_byte, obj.end_byte)]
527
+ by_id = {n.id: n for n in res.nodes}
528
+ for c in res.calls: # a method written outside its type's block still belongs to the type
529
+ if c.enclosing_type is None and c.src_id in by_id and by_id[c.src_id].attrs.get("type_id"):
530
+ c.enclosing_type = by_id[c.src_id].attrs["type_id"]
531
+
532
+ seen_refs = set()
533
+ for name_node, node, role in refs:
534
+ name = _text(name_node).split("::")[-1].split(".")[-1].strip()
535
+ if role == "skip" or not re.fullmatch(r"[A-Za-z_][\w]*", name):
536
+ continue
537
+ home = inside(name_node.start_byte, ("callable", "type"))
538
+ if home is None or (home[0] <= name_node.start_byte and _named_here(by_id.get(home[2]), name, name_node, role)):
539
+ continue
540
+ if (home[2], name, role) not in seen_refs:
541
+ seen_refs.add((home[2], name, role))
542
+ res.type_refs.append(TypeRef(home[2], [name], role, name_node.start_point[0] + 1))
543
+
544
+ # Imports and fields come from the tree's shape in either mode.
545
+ stack = [root]
546
+ kinds = self._io_shape
547
+ while stack:
548
+ n = stack.pop()
549
+ t = n.type
550
+ got = kinds.get(t)
551
+ if got is None:
552
+ got = kinds[t] = (_IMPORT.match(t) is not None, _FIELD.match(t) is not None)
553
+ if got[0]:
554
+ for target in _import_targets(n):
555
+ res.imports.append(ImportRef(file_id, target))
556
+ continue
557
+ if got[1]:
558
+ home = inside(n.start_byte, ("callable", "type"))
559
+ if home is not None and home[3] == "type":
560
+ nm = _name_of(n)
561
+ nm = _last_name(nm) if nm is not None else None
562
+ fname = _text(nm).strip() if nm is not None else ""
563
+ fid = f"{home[2]}.{fname}"
564
+ if re.fullmatch(r"[A-Za-z_@$][\w$]*", fname) and fid not in by_id:
565
+ tnode = n.child_by_field_name("type")
566
+ field = Node(id=fid, kind="field", name=fname, parent_id=home[2], language=lang, path=rel_path,
567
+ span_start=n.start_point[0] + 1, span_end=n.end_point[0] + 1,
568
+ attrs={"native_kind": "field", "declared_type": _text(tnode)[:80] or None,
569
+ "type_name": (re.findall(r"[A-Z]\w*", _text(tnode)) or [None])[-1] if tnode is not None else None,
570
+ "visibility": "private" if "private" in _text(n)[:40] else "public", "is_mutable": True})
571
+ by_id[fid] = field
572
+ res.nodes.append(field)
573
+ res.edges.append(Edge("has_field", home[2], fid))
574
+ continue
575
+ stack.extend(n.named_children)
576
+
577
+ if top_used or force_top[0]:
578
+ res.nodes.append(Node(id=top_id, kind="callable", name="<module>", parent_id=file_id, language=lang, path=rel_path,
579
+ span_start=1, span_end=root.end_point[0] + 1,
580
+ attrs={"signature": f"module {rel_path}", "visibility": "public", "native_kind": "module_body",
581
+ "argc_min": 0, "argc_max": 0}))
582
+ return res
583
+
584
+
585
+ def _type_head(text: str) -> Optional[str]:
586
+ """The type a declaration names, without what decorates it: `Class<T>` -> Class, `&'a mut io::Read` -> Read,
587
+ `java.io.Reader` -> Reader, `[]string` -> string."""
588
+ t = text
589
+ while True:
590
+ u = re.sub(r"<[^<>]*>|\[[^\[\]]*\]", "", t)
591
+ if u == t:
592
+ break
593
+ t = u
594
+ t = re.sub(r"'\w+|\b(mut|dyn|impl|const|final|in|out|ref)\b|\.\.\.|[*&?]", " ", t)
595
+ words = re.findall(r"[A-Za-z_]\w*", t)
596
+ return words[-1] if words else None
597
+
598
+
599
+ def _param_types(plist) -> list:
600
+ """Each parameter's type head, in order (Go's `a, b int` is two of them); None where none is written.
601
+ A trailing `...` marks a parameter that takes the rest of the arguments."""
602
+ out = []
603
+ for p in plist:
604
+ tnode = p.child_by_field_name("type")
605
+ if tnode is None and ("spread" in p.type or "variadic" in p.type):
606
+ tnode = next((c for c in p.named_children if "type" in c.type), None)
607
+ head = _type_head(_text(tnode)) if tnode is not None else None
608
+ names = p.children_by_field_name("name") if tnode is not None else []
609
+ rest = "..." in _text(p) or "spread" in p.type or "variadic" in p.type
610
+ for _ in range(max(1, len(names))):
611
+ out.append((head or "") + ("..." if rest else ""))
612
+ return out if any(out) else []
613
+
614
+
615
+ _LITERALS = [(re.compile(r"char"), "char"), (re.compile(r"string|template"), "string"),
616
+ (re.compile(r"^(true|false)$|bool"), "bool"), (re.compile(r"float|double|decimal_floating"), "float"),
617
+ (re.compile(r"int|number|decimal|hex|octal|binary"), "int"), (re.compile(r"^(null|nil|none|null_literal|nil_literal)$"), "null")]
618
+
619
+
620
+ def _type_names(text: str) -> list:
621
+ """The names in a written type, outer first, without package qualifiers: `*pkg.Command` -> [Command]."""
622
+ text = re.sub(r"\b[a-z_]\w*\s*(::|\.)\s*", "", text)
623
+ return [w for w in re.findall(r"[A-Za-z_]\w*", text) if w not in ("mut", "dyn", "impl", "const", "final", "ref", "in", "out")]
624
+
625
+
626
+ def _in_field(node, type_start: int) -> bool:
627
+ """A node sits in a field's declaration (its initialiser) below the type that declares the field. Other code
628
+ in a type's body (Ruby's has_many, a Python class body) runs when the type is defined, not made."""
629
+ p = node
630
+ while p is not None and p.start_byte >= type_start:
631
+ if _FIELD.match(p.type):
632
+ return True
633
+ p = p.parent
634
+ return False
635
+
636
+
637
+ def _in_anonymous(node, outer_start: int) -> bool:
638
+ """A definition sits in an object-creation expression's body (Java's anonymous class) below its outer type."""
639
+ p = node.parent
640
+ while p is not None and p.start_byte > outer_start:
641
+ if p.type in _NEW or p.type in ("enum_constant", "enum_entry"): # new Base() { }, an enum constant's own body
642
+ return True
643
+ p = p.parent
644
+ return False
645
+
646
+
647
+ def _callee_object(call):
648
+ """The expression a method call is made on: `a.b()` -> a, `x.f().g()` -> x.f()."""
649
+ callee = call.child_by_field_name("function") or call
650
+ if callee.type in ("generic_function", "generic_name"):
651
+ callee = callee.child_by_field_name("function") or callee
652
+ obj = None
653
+ for f in ("object", "operand", "value", "receiver", "expression"):
654
+ obj = callee.child_by_field_name(f)
655
+ if obj is not None:
656
+ break
657
+ while obj is not None and obj.type == "parenthesized_expression" and obj.named_children:
658
+ obj = obj.named_children[0]
659
+ return obj
660
+
661
+
662
+ def _arg_hint(a, calls=None):
663
+ """What an argument shows of its type: a literal's kind, a constructed or cast-to type, a class literal, a
664
+ lambda, `$name` for a variable or `$a.b` for a field whose declared type the indexer looks up, or the
665
+ call whose declared return type it is."""
666
+ while a.type in ("unary_expression", "reference_expression", "parenthesized_expression", "argument", "value_argument") \
667
+ and a.named_children and not (a.type == "unary_expression" and _text(a)[:1] in "-+!~"):
668
+ a = a.named_children[-1]
669
+ t = a.type
670
+ if calls and (a.start_byte, a.end_byte) in calls and t not in _NEW:
671
+ return calls[(a.start_byte, a.end_byte)]
672
+ if t in _NAME_TYPES and not a.named_child_count:
673
+ return "$" + _text(a)
674
+ if t in ("field_access", "selector_expression", "field_expression", "member_access_expression", "member_expression") \
675
+ and re.fullmatch(r"[A-Za-z_]\w*\s*\.\s*[A-Za-z_]\w*", _text(a)):
676
+ return "$" + re.sub(r"\s+", "", _text(a))
677
+ if "cast" in t or t == "as_expression":
678
+ tnode = a.child_by_field_name("type")
679
+ return _type_head(_text(tnode)) if tnode is not None else None
680
+ if t in ("ternary_expression", "conditional_expression"):
681
+ for f in ("consequence", "alternative"):
682
+ branch = a.child_by_field_name(f)
683
+ h = _arg_hint(branch, calls) if branch is not None else None
684
+ if h is not None and h != "null":
685
+ return h
686
+ return None
687
+ if t == "unary_expression" and _text(a)[:1] == "!":
688
+ return "bool"
689
+ if t == "unary_expression" and _text(a)[:1] in "-+" and a.named_children:
690
+ return _arg_hint(a.named_children[-1], calls)
691
+ if t in ("binary_expression", "binary_operator"):
692
+ op = a.child_by_field_name("operator")
693
+ op = _text(op) if op is not None else next((_text(c) for c in a.children if not c.is_named), "")
694
+ if op in ("==", "!=", "<", ">", "<=", ">=", "&&", "||", "and", "or", "===", "!=="):
695
+ return "bool"
696
+ if op == "+" and any(_arg_hint(c) == "string" for c in a.named_children):
697
+ return "string" # "a" + b, a + " " + b
698
+ return None
699
+ if t == "class_literal":
700
+ return "Class"
701
+ if t in _NEW or t == "composite_literal":
702
+ tnode = a.child_by_field_name("type") or a.child_by_field_name("name") or (a.named_children[0] if a.named_children else None)
703
+ return _type_head(_text(tnode)) if tnode is not None else None
704
+ if _FUNC_VALUE.match(t):
705
+ return "fn"
706
+ if t.endswith(("literal", "string", "integer", "float", "number")) or t in ("true", "false", "null", "nil", "none", "string_literal"):
707
+ return next((kind for pat, kind in _LITERALS if pat.search(t)), None)
708
+ return None
709
+
710
+
711
+ def _first_string(call):
712
+ """The first argument when it is a string literal (the test's title)."""
713
+ args = call.child_by_field_name("arguments") or next((c for c in call.named_children if "argument" in c.type), None)
714
+ first = None
715
+ for c in (args.named_children if args is not None else call.named_children[1:]):
716
+ if c.type != "comment":
717
+ first = c
718
+ break
719
+ while first is not None and first.type in ("argument", "value_argument", "string_content") and first.named_children:
720
+ first = first.named_children[0]
721
+ if first is not None and ("string" in first.type or first.type in ("template_string",)):
722
+ return first
723
+ return None
724
+
725
+
726
+ def _has_function(call) -> bool:
727
+ for c in _walk(call):
728
+ if c is not call and (_FUNC_VALUE.match(c.type) or c.type in ("do_block", "block", "lambda_literal", "statement_block")):
729
+ if c.parent is not None and (c.parent is call or "argument" in c.parent.type or c.parent.parent is call):
730
+ return True
731
+ return False
732
+
733
+
734
+ def _same(a, b) -> bool:
735
+ return a.start_byte == b.start_byte and a.end_byte == b.end_byte
736
+
737
+
738
+ def _walk(node):
739
+ stack = [node]
740
+ while stack:
741
+ n = stack.pop()
742
+ yield n
743
+ stack.extend(reversed(n.children))
744
+
745
+
746
+ def _named_here(node: Optional[Node], name: str, name_node, role: str) -> bool:
747
+ """A definition's own name is not a mention of some other type."""
748
+ return node is not None and node.name == name and role != "base" and name_node.start_point[0] + 1 == node.span_start
749
+
750
+
751
+ _RECV = re.compile(rb"([A-Za-z_@$][\w$]*)?\s*(\)|\])?\s*(\?\.|\.|->|::|:|&\.)\s*$")
752
+
753
+
754
+ _WS = b" \t\n\r\x0b\x0c"
755
+ _WORD = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_$@"
756
+
757
+
758
+ def _receiver(src: bytes, pos: int) -> Optional[str]:
759
+ """What the text shows the call is made on: None for a bare call, `this`, `base`, a name, or ? for an expression."""
760
+ text = src[max(0, pos - 80):pos]
761
+ # _RECV tried at every position of the 80 bytes was a fifth of all parsing time. A match ends in one of the
762
+ # operators and can only cover the name, bracket and spaces just before it, so the search starts there:
763
+ # it finds the same match, since no match can start earlier.
764
+ s = text.rstrip(_WS)
765
+ if s[-1:] not in (b".", b">", b":"):
766
+ return None
767
+ s = s[:-2] if s[-2:-1] and s[-2:-1] in b"?.->:&" else s[:-1]
768
+ s = s.rstrip(_WS)
769
+ if s[-1:] in (b")", b"]"):
770
+ s = s[:-1]
771
+ m = _RECV.search(text, len(s.rstrip(_WS).rstrip(_WORD)))
772
+ if not m:
773
+ return None
774
+ if m.group(3) == b":" and not src[max(0, pos - 80):pos].rstrip().endswith(b":"):
775
+ return None
776
+ if m.group(3) == b":": # Lua's obj:method(); elsewhere a lone colon is a label or a type
777
+ if not m.group(1) or m.group(2) or src[pos - 1:pos] != b":":
778
+ return None # `Args: ExactArgs(2)`, `{key: f()}`: a key, with space before the value
779
+ if m.group(2) or not m.group(1):
780
+ return "?"
781
+ name = m.group(1).decode("utf8", "replace")
782
+ if name in ("this", "self", "Self", "@", "me"):
783
+ return "this"
784
+ if name in ("super", "base", "parent"):
785
+ return "base"
786
+ return name
787
+
788
+
789
+ def _import_targets(node) -> list[str]:
790
+ """What an import names, as written: quoted paths (JS, Go, C), or dotted / ::-separated ones.
791
+ `from a.b import c, d` names a.b, a.b.c and a.b.d; `use a::b::{c, d}` names a::b, a::b::c and a::b::d."""
792
+ strings, stack = [], [node]
793
+ while stack:
794
+ n = stack.pop()
795
+ if n.type in ("string", "string_literal", "interpreted_string_literal", "system_lib_string", "raw_string_literal",
796
+ "string_content", "string_fragment") and not any(c.type.startswith("string") for c in n.named_children):
797
+ strings.append(_text(n).strip("\"'<>`"))
798
+ else:
799
+ stack.extend(reversed(n.children))
800
+ if strings:
801
+ return [s for s in strings if s and len(s) < 200]
802
+ text = re.sub(r"/\*.*?\*/|//[^\n]*|#[^\n]*", " ", _text(node))
803
+ text = re.sub(r"\s+", " ", text).strip().rstrip(";")
804
+ path = r"\.*[A-Za-z_][\w]*(?:(?:\.|::)[A-Za-z_*][\w]*)*|\.+"
805
+ m = re.match(rf"from ({path}) import \(?(.+?)\)?$", text)
806
+ if m:
807
+ base, names = m.group(1), [x.strip().split(" as ")[0] for x in m.group(2).split(",")]
808
+ sep = "" if base.endswith(".") else "."
809
+ return [base] + [base + sep + n for n in names if re.fullmatch(r"\w+", n)][:20]
810
+ m = re.match(rf"(?:pub(?:\([\w ]+\))? )?use ({path})(?:::)?\{{(.+)\}}", text)
811
+ if m:
812
+ base = m.group(1).rstrip(":")
813
+ return [base] + [f"{base}::{n.strip().split(' as ')[0]}" for n in m.group(2).split(",") if re.fullmatch(r"\s*\w+.*", n)][:20]
814
+ text = re.sub(r"^(?:pub(?:\([\w ]+\))? )?(?:import|use|using|include|require|open|extern crate|static|namespace)\b ?", "", text)
815
+ text = re.sub(r"^(?:static|type|typeof|func|function|const|global::) ?", "", text)
816
+ out = []
817
+ for part in text.split(","):
818
+ m = re.search(path, part.strip())
819
+ if m and m.group(0) not in ("as", "from"):
820
+ out.append(m.group(0))
821
+ return out[:12]
822
+
823
+
824
+ def adapters(skip: tuple = ()) -> list:
825
+ """One adapter per language whose grammar package is installed. `skip`: languages someone else handles."""
826
+ out, seen = [], set()
827
+ for language, exts, package, fn in LANGUAGES:
828
+ if language in skip:
829
+ continue
830
+ a = Generic(language, exts, package, fn)
831
+ if a.available():
832
+ out.append(a)
833
+ seen.add(language)
834
+ return out
835
+
836
+
837
+ def missing(extensions: set) -> dict:
838
+ """Extensions in a repo that a grammar exists for but is not installed: {package: [extensions]}."""
839
+ out: dict = {}
840
+ for language, exts, package, fn in LANGUAGES:
841
+ hit = sorted(set(exts) & extensions)
842
+ if hit and not Generic(language, exts, package, fn).available():
843
+ out.setdefault(package.replace("_", "-"), []).extend(hit)
844
+ return out