entrygraph 0.1.0__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.
- entrygraph/__init__.py +87 -0
- entrygraph/__main__.py +8 -0
- entrygraph/_version.py +24 -0
- entrygraph/api.py +549 -0
- entrygraph/cli/__init__.py +0 -0
- entrygraph/cli/main.py +387 -0
- entrygraph/cli/render.py +136 -0
- entrygraph/data/sinks/csharp.toml +106 -0
- entrygraph/data/sinks/go.toml +87 -0
- entrygraph/data/sinks/java.toml +92 -0
- entrygraph/data/sinks/javascript.toml +112 -0
- entrygraph/data/sinks/lib_javascript.toml +34 -0
- entrygraph/data/sinks/lib_python.toml +39 -0
- entrygraph/data/sinks/php.toml +125 -0
- entrygraph/data/sinks/python.toml +160 -0
- entrygraph/data/sinks/ruby.toml +102 -0
- entrygraph/data/sinks/rust.toml +68 -0
- entrygraph/db/__init__.py +0 -0
- entrygraph/db/engine.py +34 -0
- entrygraph/db/meta.py +70 -0
- entrygraph/db/models.py +176 -0
- entrygraph/db/queries.py +155 -0
- entrygraph/detect/__init__.py +0 -0
- entrygraph/detect/entrypoints/__init__.py +22 -0
- entrygraph/detect/entrypoints/base.py +124 -0
- entrygraph/detect/entrypoints/configs.py +139 -0
- entrygraph/detect/entrypoints/csharp.py +156 -0
- entrygraph/detect/entrypoints/golang.py +158 -0
- entrygraph/detect/entrypoints/java.py +187 -0
- entrygraph/detect/entrypoints/javascript.py +211 -0
- entrygraph/detect/entrypoints/php.py +133 -0
- entrygraph/detect/entrypoints/python.py +335 -0
- entrygraph/detect/entrypoints/ruby.py +147 -0
- entrygraph/detect/entrypoints/rust.py +153 -0
- entrygraph/detect/frameworks.py +369 -0
- entrygraph/detect/manifests.py +234 -0
- entrygraph/detect/taint.py +224 -0
- entrygraph/errors.py +27 -0
- entrygraph/extract/__init__.py +0 -0
- entrygraph/extract/base.py +51 -0
- entrygraph/extract/csharp.py +502 -0
- entrygraph/extract/golang.py +342 -0
- entrygraph/extract/ir.py +105 -0
- entrygraph/extract/java.py +329 -0
- entrygraph/extract/javascript.py +400 -0
- entrygraph/extract/php.py +426 -0
- entrygraph/extract/python.py +390 -0
- entrygraph/extract/registry.py +43 -0
- entrygraph/extract/ruby.py +321 -0
- entrygraph/extract/rust.py +482 -0
- entrygraph/fs/__init__.py +0 -0
- entrygraph/fs/hashing.py +78 -0
- entrygraph/fs/lang.py +134 -0
- entrygraph/fs/walker.py +167 -0
- entrygraph/graph/__init__.py +0 -0
- entrygraph/graph/adjacency.py +146 -0
- entrygraph/graph/cte.py +123 -0
- entrygraph/graph/scoring.py +101 -0
- entrygraph/kinds.py +51 -0
- entrygraph/parsing/__init__.py +0 -0
- entrygraph/parsing/parsers.py +49 -0
- entrygraph/parsing/queries.py +39 -0
- entrygraph/pipeline/__init__.py +0 -0
- entrygraph/pipeline/scanner.py +506 -0
- entrygraph/pipeline/worker.py +49 -0
- entrygraph/pipeline/writer.py +41 -0
- entrygraph/py.typed +0 -0
- entrygraph/queries/csharp/calls.scm +4 -0
- entrygraph/queries/csharp/definitions.scm +29 -0
- entrygraph/queries/csharp/imports.scm +4 -0
- entrygraph/queries/go/calls.scm +2 -0
- entrygraph/queries/go/definitions.scm +24 -0
- entrygraph/queries/go/imports.scm +1 -0
- entrygraph/queries/java/calls.scm +2 -0
- entrygraph/queries/java/definitions.scm +14 -0
- entrygraph/queries/java/imports.scm +2 -0
- entrygraph/queries/javascript/calls.scm +4 -0
- entrygraph/queries/javascript/definitions.scm +4 -0
- entrygraph/queries/javascript/imports.scm +6 -0
- entrygraph/queries/php/calls.scm +8 -0
- entrygraph/queries/php/definitions.scm +24 -0
- entrygraph/queries/php/imports.scm +1 -0
- entrygraph/queries/python/calls.scm +2 -0
- entrygraph/queries/python/definitions.scm +11 -0
- entrygraph/queries/python/imports.scm +2 -0
- entrygraph/queries/ruby/calls.scm +4 -0
- entrygraph/queries/ruby/definitions.scm +20 -0
- entrygraph/queries/ruby/imports.scm +7 -0
- entrygraph/queries/rust/calls.scm +5 -0
- entrygraph/queries/rust/definitions.scm +26 -0
- entrygraph/queries/rust/imports.scm +4 -0
- entrygraph/resolve/__init__.py +0 -0
- entrygraph/resolve/externals.py +61 -0
- entrygraph/resolve/hierarchy.py +152 -0
- entrygraph/resolve/resolver.py +275 -0
- entrygraph/resolve/symbol_table.py +48 -0
- entrygraph/results.py +138 -0
- entrygraph-0.1.0.dist-info/METADATA +204 -0
- entrygraph-0.1.0.dist-info/RECORD +102 -0
- entrygraph-0.1.0.dist-info/WHEEL +4 -0
- entrygraph-0.1.0.dist-info/entry_points.txt +2 -0
- entrygraph-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Java extractor: .scm queries harvest nodes, this shaper builds the IR.
|
|
2
|
+
|
|
3
|
+
Module paths are the fully-qualified class path — a leading ``src/main/java/``
|
|
4
|
+
or ``src/test/java/`` root is stripped, ``.java`` dropped, ``/`` -> ``.`` — so
|
|
5
|
+
``src/main/java/com/example/UserController.java`` -> ``com.example.UserController``.
|
|
6
|
+
Java has no package concept in the IR sense (one type per module path), so
|
|
7
|
+
``is_package`` is always False.
|
|
8
|
+
|
|
9
|
+
Annotations are captured on ``RawSymbol.decorators`` as raw source text
|
|
10
|
+
(``@GetMapping("/users")``) and also emitted as ``RawReference(kind="decorator")``
|
|
11
|
+
so entrypoint rules and the resolver can match them. Supertypes/interfaces are
|
|
12
|
+
emitted as ``RawReference(kind="inherit")``. External calls with unknown
|
|
13
|
+
receivers (``Runtime.getRuntime().exec(...)``) resolve to ``java:*.exec``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
19
|
+
|
|
20
|
+
from entrygraph.extract.base import FileContext, node_text, span_of, truncate
|
|
21
|
+
from entrygraph.extract.ir import FileExtraction, RawImport, RawReference, RawSymbol
|
|
22
|
+
from entrygraph.kinds import SymbolKind
|
|
23
|
+
from entrygraph.parsing.queries import captures, load_query
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
26
|
+
from tree_sitter import Node, Tree
|
|
27
|
+
|
|
28
|
+
_SCOPE_TYPES = frozenset({"class_declaration", "interface_declaration"})
|
|
29
|
+
_ANNOTATION_TYPES = frozenset({"marker_annotation", "annotation"})
|
|
30
|
+
_JAVA_ROOTS = ("src/main/java/", "src/test/java/")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class JavaExtractor:
|
|
34
|
+
language_ids: ClassVar[tuple[str, ...]] = ("java",)
|
|
35
|
+
|
|
36
|
+
def module_path_for(self, repo_relative_path: str) -> tuple[str, bool]:
|
|
37
|
+
path = repo_relative_path
|
|
38
|
+
for root in _JAVA_ROOTS:
|
|
39
|
+
if path.startswith(root):
|
|
40
|
+
path = path[len(root):]
|
|
41
|
+
break
|
|
42
|
+
path = path.removesuffix(".java")
|
|
43
|
+
return ".".join(p for p in path.split("/") if p) or "_root", False
|
|
44
|
+
|
|
45
|
+
def extract(self, tree: "Tree", ctx: FileContext) -> FileExtraction:
|
|
46
|
+
root = tree.root_node
|
|
47
|
+
out = FileExtraction(
|
|
48
|
+
path=ctx.path, language=ctx.language, module_path=ctx.module_path,
|
|
49
|
+
parse_ok=not root.has_error, error_count=1 if root.has_error else 0,
|
|
50
|
+
)
|
|
51
|
+
self._definitions(root, ctx, out)
|
|
52
|
+
self._imports(root, ctx, out)
|
|
53
|
+
self._calls(root, ctx, out)
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
# ---------------- definitions ----------------
|
|
57
|
+
|
|
58
|
+
def _definitions(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
59
|
+
caps = captures(load_query("java", "definitions"), root)
|
|
60
|
+
|
|
61
|
+
for node in caps.get("def.class", []):
|
|
62
|
+
self._add_type(node, ctx, out, SymbolKind.CLASS)
|
|
63
|
+
for node in caps.get("def.interface", []):
|
|
64
|
+
self._add_type(node, ctx, out, SymbolKind.INTERFACE)
|
|
65
|
+
|
|
66
|
+
for node in caps.get("def.method", []):
|
|
67
|
+
name_node = node.child_by_field_name("name")
|
|
68
|
+
if name_node is None:
|
|
69
|
+
continue
|
|
70
|
+
name = node_text(name_node)
|
|
71
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
72
|
+
modifiers = self._modifiers(node)
|
|
73
|
+
annotations = self._annotations(node)
|
|
74
|
+
out.symbols.append(
|
|
75
|
+
RawSymbol(
|
|
76
|
+
kind=SymbolKind.METHOD, name=name, qualified_name=qname,
|
|
77
|
+
span=span_of(node), parent_qualified_name=parent_q,
|
|
78
|
+
signature=self._signature(node),
|
|
79
|
+
decorators=annotations,
|
|
80
|
+
modifiers=modifiers,
|
|
81
|
+
is_exported="public" in modifiers,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
self._emit_annotation_refs(node, qname, out)
|
|
85
|
+
|
|
86
|
+
for node in caps.get("def.field", []):
|
|
87
|
+
scope = self._nearest_scope(node)
|
|
88
|
+
if scope is None:
|
|
89
|
+
continue # only class/interface members are symbols
|
|
90
|
+
declarator = node.child_by_field_name("declarator")
|
|
91
|
+
if declarator is None:
|
|
92
|
+
continue
|
|
93
|
+
name_node = declarator.child_by_field_name("name")
|
|
94
|
+
if name_node is None:
|
|
95
|
+
continue
|
|
96
|
+
name = node_text(name_node)
|
|
97
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
98
|
+
modifiers = self._modifiers(node)
|
|
99
|
+
is_const = "static" in modifiers and "final" in modifiers and name.isupper()
|
|
100
|
+
out.symbols.append(
|
|
101
|
+
RawSymbol(
|
|
102
|
+
kind=SymbolKind.CONSTANT if is_const else SymbolKind.FIELD,
|
|
103
|
+
name=name, qualified_name=qname, span=span_of(node),
|
|
104
|
+
parent_qualified_name=parent_q,
|
|
105
|
+
signature=truncate(node_text(node)),
|
|
106
|
+
decorators=self._annotations(node),
|
|
107
|
+
modifiers=modifiers,
|
|
108
|
+
is_exported="public" in modifiers,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _add_type(self, node: "Node", ctx: FileContext, out: FileExtraction, kind: SymbolKind) -> None:
|
|
113
|
+
name_node = node.child_by_field_name("name")
|
|
114
|
+
if name_node is None:
|
|
115
|
+
return
|
|
116
|
+
name = node_text(name_node)
|
|
117
|
+
qname, parent_q = self._qualify(node, name, ctx)
|
|
118
|
+
extends, interfaces = self._supertypes(node)
|
|
119
|
+
modifiers = self._modifiers(node)
|
|
120
|
+
out.symbols.append(
|
|
121
|
+
RawSymbol(
|
|
122
|
+
kind=kind, name=name, qualified_name=qname, span=span_of(node),
|
|
123
|
+
parent_qualified_name=parent_q, signature=self._signature(node),
|
|
124
|
+
decorators=self._annotations(node), bases=[*extends, *interfaces],
|
|
125
|
+
modifiers=modifiers, is_exported="public" in modifiers,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
for base in extends:
|
|
129
|
+
out.references.append(
|
|
130
|
+
RawReference(
|
|
131
|
+
kind="inherit", callee_text=base, callee_name=base.rsplit(".", 1)[-1],
|
|
132
|
+
receiver_text=None, span=span_of(node), caller_qualified_name=qname,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
for iface in interfaces:
|
|
136
|
+
out.references.append(
|
|
137
|
+
RawReference(
|
|
138
|
+
kind="implement", callee_text=iface, callee_name=iface.rsplit(".", 1)[-1],
|
|
139
|
+
receiver_text=None, span=span_of(node), caller_qualified_name=qname,
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
self._emit_annotation_refs(node, qname, out)
|
|
143
|
+
|
|
144
|
+
# ---------------- imports ----------------
|
|
145
|
+
|
|
146
|
+
def _imports(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
147
|
+
caps = captures(load_query("java", "imports"), root)
|
|
148
|
+
for node in caps.get("import", []):
|
|
149
|
+
scoped = next((c for c in node.named_children if c.type == "scoped_identifier"), None)
|
|
150
|
+
if scoped is None:
|
|
151
|
+
continue
|
|
152
|
+
full = node_text(scoped)
|
|
153
|
+
wildcard = any(c.type == "asterisk" for c in node.named_children)
|
|
154
|
+
if wildcard:
|
|
155
|
+
out.imports.append(
|
|
156
|
+
RawImport(module=full, imported_name="*", alias="*", span=span_of(node))
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
last = full.rsplit(".", 1)[-1]
|
|
160
|
+
out.imports.append(
|
|
161
|
+
RawImport(module=full, imported_name=last, alias=last, span=span_of(node))
|
|
162
|
+
)
|
|
163
|
+
for imp in out.imports:
|
|
164
|
+
if imp.module:
|
|
165
|
+
out.framework_signals.append(("import", imp.module))
|
|
166
|
+
|
|
167
|
+
# ---------------- calls / annotations ----------------
|
|
168
|
+
|
|
169
|
+
def _calls(self, root: "Node", ctx: FileContext, out: FileExtraction) -> None:
|
|
170
|
+
caps = captures(load_query("java", "calls"), root)
|
|
171
|
+
|
|
172
|
+
for node in caps.get("call", []):
|
|
173
|
+
name_node = node.child_by_field_name("name")
|
|
174
|
+
if name_node is None:
|
|
175
|
+
continue
|
|
176
|
+
callee_name = node_text(name_node)
|
|
177
|
+
obj = node.child_by_field_name("object")
|
|
178
|
+
if obj is not None:
|
|
179
|
+
receiver = node_text(obj)
|
|
180
|
+
callee_text = f"{receiver}.{callee_name}"
|
|
181
|
+
else:
|
|
182
|
+
receiver = None
|
|
183
|
+
callee_text = callee_name
|
|
184
|
+
args = node.child_by_field_name("arguments")
|
|
185
|
+
out.references.append(
|
|
186
|
+
RawReference(
|
|
187
|
+
kind="call", callee_text=callee_text, callee_name=callee_name,
|
|
188
|
+
receiver_text=receiver, span=span_of(node),
|
|
189
|
+
caller_qualified_name=self._caller(node, ctx),
|
|
190
|
+
arg_count=len(args.named_children) if args is not None else 0,
|
|
191
|
+
arg_preview=truncate(node_text(args)) if args is not None else None,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
for node in caps.get("new", []):
|
|
196
|
+
type_node = node.child_by_field_name("type")
|
|
197
|
+
if type_node is None:
|
|
198
|
+
continue
|
|
199
|
+
type_text = node_text(type_node).split("<", 1)[0].strip()
|
|
200
|
+
callee_name = type_text.rsplit(".", 1)[-1]
|
|
201
|
+
args = node.child_by_field_name("arguments")
|
|
202
|
+
out.references.append(
|
|
203
|
+
RawReference(
|
|
204
|
+
kind="call", callee_text=type_text, callee_name=callee_name,
|
|
205
|
+
receiver_text=None, span=span_of(node),
|
|
206
|
+
caller_qualified_name=self._caller(node, ctx),
|
|
207
|
+
arg_count=len(args.named_children) if args is not None else 0,
|
|
208
|
+
arg_preview=truncate(node_text(args)) if args is not None else None,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# ---------------- helpers ----------------
|
|
213
|
+
|
|
214
|
+
def _scope_chain(self, node: "Node", ctx: FileContext) -> list[str]:
|
|
215
|
+
parts, current = [], node.parent
|
|
216
|
+
while current is not None:
|
|
217
|
+
if current.type in _SCOPE_TYPES:
|
|
218
|
+
name = current.child_by_field_name("name")
|
|
219
|
+
if name is not None:
|
|
220
|
+
parts.append(node_text(name))
|
|
221
|
+
current = current.parent
|
|
222
|
+
parts.reverse()
|
|
223
|
+
# The conventional top-level public type shares the file's stem; the
|
|
224
|
+
# module path already encodes it (com.example.UserController), so drop
|
|
225
|
+
# the duplicate leading segment to avoid ...UserController.UserController.
|
|
226
|
+
top = ctx.module_path.rsplit(".", 1)[-1]
|
|
227
|
+
if parts and parts[0] == top:
|
|
228
|
+
parts = parts[1:]
|
|
229
|
+
return parts
|
|
230
|
+
|
|
231
|
+
def _nearest_scope(self, node: "Node") -> str | None:
|
|
232
|
+
current = node.parent
|
|
233
|
+
while current is not None:
|
|
234
|
+
if current.type in _SCOPE_TYPES:
|
|
235
|
+
return current.type
|
|
236
|
+
current = current.parent
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
def _qualify(self, node: "Node", name: str, ctx: FileContext) -> tuple[str, str | None]:
|
|
240
|
+
chain = self._scope_chain(node, ctx)
|
|
241
|
+
top = ctx.module_path.rsplit(".", 1)[-1]
|
|
242
|
+
# The top-level type named after the file collapses onto the module path
|
|
243
|
+
# itself (com.example.UserController), so its members qualify cleanly as
|
|
244
|
+
# com.example.UserController.getUser and parent to that same qname.
|
|
245
|
+
if not chain and name == top:
|
|
246
|
+
# this node *is* the top-level type
|
|
247
|
+
return ctx.module_path, None
|
|
248
|
+
enclosed_in_top = self._enclosed_in_top_type(node, top)
|
|
249
|
+
parent_q = ".".join([ctx.module_path, *chain]) if (chain or enclosed_in_top) else None
|
|
250
|
+
return ".".join([ctx.module_path, *chain, name]), parent_q
|
|
251
|
+
|
|
252
|
+
def _enclosed_in_top_type(self, node: "Node", top: str) -> bool:
|
|
253
|
+
current = node.parent
|
|
254
|
+
while current is not None:
|
|
255
|
+
if current.type in _SCOPE_TYPES:
|
|
256
|
+
name = current.child_by_field_name("name")
|
|
257
|
+
return name is not None and node_text(name) == top
|
|
258
|
+
current = current.parent
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
def _caller(self, node: "Node", ctx: FileContext) -> str | None:
|
|
262
|
+
current = node.parent
|
|
263
|
+
while current is not None:
|
|
264
|
+
if current.type == "method_declaration":
|
|
265
|
+
name = current.child_by_field_name("name")
|
|
266
|
+
if name is not None:
|
|
267
|
+
return self._qualify(current, node_text(name), ctx)[0]
|
|
268
|
+
current = current.parent
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
def _signature(self, node: "Node") -> str:
|
|
272
|
+
return truncate(node_text(node).split("{", 1)[0].strip(), 120)
|
|
273
|
+
|
|
274
|
+
def _modifiers_node(self, node: "Node") -> "Node | None":
|
|
275
|
+
return next((c for c in node.named_children if c.type == "modifiers"), None)
|
|
276
|
+
|
|
277
|
+
def _modifiers(self, node: "Node") -> list[str]:
|
|
278
|
+
mods = self._modifiers_node(node)
|
|
279
|
+
if mods is None:
|
|
280
|
+
return []
|
|
281
|
+
return [
|
|
282
|
+
node_text(c) for c in mods.children
|
|
283
|
+
if c.type not in _ANNOTATION_TYPES and node_text(c).strip()
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
def _annotations(self, node: "Node") -> list[str]:
|
|
287
|
+
mods = self._modifiers_node(node)
|
|
288
|
+
if mods is None:
|
|
289
|
+
return []
|
|
290
|
+
return [node_text(c) for c in mods.children if c.type in _ANNOTATION_TYPES]
|
|
291
|
+
|
|
292
|
+
def _emit_annotation_refs(self, node: "Node", owner_qname: str, out: FileExtraction) -> None:
|
|
293
|
+
mods = self._modifiers_node(node)
|
|
294
|
+
if mods is None:
|
|
295
|
+
return
|
|
296
|
+
for child in mods.children:
|
|
297
|
+
if child.type not in _ANNOTATION_TYPES:
|
|
298
|
+
continue
|
|
299
|
+
name_node = child.child_by_field_name("name")
|
|
300
|
+
if name_node is None:
|
|
301
|
+
continue
|
|
302
|
+
callee_text = node_text(name_node)
|
|
303
|
+
out.references.append(
|
|
304
|
+
RawReference(
|
|
305
|
+
kind="decorator", callee_text=callee_text,
|
|
306
|
+
callee_name=callee_text.rsplit(".", 1)[-1],
|
|
307
|
+
receiver_text=callee_text.rsplit(".", 1)[0] if "." in callee_text else None,
|
|
308
|
+
span=span_of(child), caller_qualified_name=owner_qname,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def _supertypes(self, node: "Node") -> tuple[list[str], list[str]]:
|
|
313
|
+
"""Return (extends supertypes, implements interfaces)."""
|
|
314
|
+
extends: list[str] = []
|
|
315
|
+
superclass = node.child_by_field_name("superclass")
|
|
316
|
+
if superclass is not None:
|
|
317
|
+
for child in superclass.named_children:
|
|
318
|
+
text = node_text(child).split("<", 1)[0].strip()
|
|
319
|
+
if text:
|
|
320
|
+
extends.append(text)
|
|
321
|
+
interfaces: list[str] = []
|
|
322
|
+
iface_node = node.child_by_field_name("interfaces")
|
|
323
|
+
if iface_node is not None:
|
|
324
|
+
for type_list in iface_node.named_children:
|
|
325
|
+
for child in type_list.named_children:
|
|
326
|
+
text = node_text(child).split("<", 1)[0].strip()
|
|
327
|
+
if text:
|
|
328
|
+
interfaces.append(text)
|
|
329
|
+
return extends, interfaces
|