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.
- leyline/__init__.py +3 -0
- leyline/adapters/__init__.py +19 -0
- leyline/adapters/csharp.py +1026 -0
- leyline/adapters/generic.py +844 -0
- leyline/adapters/python.py +929 -0
- leyline/adapters/typescript.py +1512 -0
- leyline/affected.py +509 -0
- leyline/agent_skills.py +311 -0
- leyline/change.py +500 -0
- leyline/channels.py +1782 -0
- leyline/cli.py +1029 -0
- leyline/cluster.py +149 -0
- leyline/context.py +531 -0
- leyline/coupling.py +485 -0
- leyline/coverage.py +376 -0
- leyline/diagrams.py +680 -0
- leyline/diff.py +1078 -0
- leyline/drift.py +747 -0
- leyline/exact.py +410 -0
- leyline/explain.py +973 -0
- leyline/export.py +673 -0
- leyline/fncoupling.py +328 -0
- leyline/formats.py +923 -0
- leyline/grade.py +207 -0
- leyline/incremental.py +931 -0
- leyline/indexer.py +3733 -0
- leyline/learnings.py +743 -0
- leyline/loop.py +584 -0
- leyline/model.py +142 -0
- leyline/outline.py +1066 -0
- leyline/packing.py +86 -0
- leyline/patterns.py +478 -0
- leyline/pr.py +1249 -0
- leyline/props.py +255 -0
- leyline/query.py +683 -0
- leyline/quick.py +853 -0
- leyline/related.py +266 -0
- leyline/removal.py +231 -0
- leyline/renames.py +231 -0
- leyline/rereview.py +375 -0
- leyline/roslyn_refs/Program.cs +179 -0
- leyline/roslyn_refs/RoslynRefs.csproj +16 -0
- leyline/rules.py +171 -0
- leyline/schema.sql +178 -0
- leyline/scip_pb2.py +97 -0
- leyline/server.py +1230 -0
- leyline/skills/leyline-adversarial-review/SKILL.md +121 -0
- leyline/skills/leyline-ask/SKILL.md +57 -0
- leyline/skills/leyline-change-impact/SKILL.md +79 -0
- leyline/skills/leyline-explain-flow/SKILL.md +70 -0
- leyline/skills/leyline-explore-module/SKILL.md +55 -0
- leyline/skills/leyline-pr-review/SKILL.md +77 -0
- leyline/skills/leyline-quick-change/SKILL.md +63 -0
- leyline/skills/leyline-spec/SKILL.md +179 -0
- leyline/skills/leyline-tour/SKILL.md +46 -0
- leyline/spec.py +1819 -0
- leyline/store.py +560 -0
- leyline/tours.py +352 -0
- leyline/verdicts.py +390 -0
- leyline/viewer/viewer.html +1622 -0
- leyline_code-0.8.1.dist-info/METADATA +1325 -0
- leyline_code-0.8.1.dist-info/RECORD +65 -0
- leyline_code-0.8.1.dist-info/WHEEL +4 -0
- leyline_code-0.8.1.dist-info/entry_points.txt +2 -0
- 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
|