modwire 1.0.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.
- modwire/__init__.py +15 -0
- modwire/_version.py +24 -0
- modwire/architecture/__init__.py +10 -0
- modwire/architecture/analyzers.py +140 -0
- modwire/architecture/matching.py +58 -0
- modwire/architecture/policy.py +63 -0
- modwire/architecture/render.py +98 -0
- modwire/architecture/violations.py +24 -0
- modwire/definitions.py +101 -0
- modwire/extraction.py +73 -0
- modwire/extractors/__init__.py +5 -0
- modwire/extractors/base.py +177 -0
- modwire/extractors/loader.py +31 -0
- modwire/extractors/php.py +170 -0
- modwire/extractors/python.py +113 -0
- modwire/extractors/scripts/php_extractor.php +816 -0
- modwire/extractors/scripts/python_extractor.py +398 -0
- modwire/extractors/scripts/typescript_extractor.js +1030 -0
- modwire/extractors/typescript.py +48 -0
- modwire/graph.py +56 -0
- modwire-1.0.0.dist-info/METADATA +111 -0
- modwire-1.0.0.dist-info/RECORD +25 -0
- modwire-1.0.0.dist-info/WHEEL +5 -0
- modwire-1.0.0.dist-info/licenses/LICENSE +21 -0
- modwire-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def line_span(node: ast.AST) -> int:
|
|
11
|
+
return node.end_lineno - node.lineno + 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize_module_path(value: str) -> str:
|
|
15
|
+
return value.replace(".", "/").strip("/")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def import_path(node: ast.ImportFrom) -> str:
|
|
19
|
+
return f"{'.' * node.level}{node.module or ''}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def normalized_import_path(
|
|
23
|
+
import_value: str,
|
|
24
|
+
is_relative: bool,
|
|
25
|
+
file_path: Path,
|
|
26
|
+
sources_root: Path,
|
|
27
|
+
) -> str:
|
|
28
|
+
if not is_relative:
|
|
29
|
+
return normalize_module_path(import_value)
|
|
30
|
+
|
|
31
|
+
level = len(import_value) - len(import_value.lstrip("."))
|
|
32
|
+
module = import_value[level:]
|
|
33
|
+
package_dir = file_path.parent
|
|
34
|
+
for _ in range(max(level - 1, 0)):
|
|
35
|
+
package_dir = package_dir.parent
|
|
36
|
+
|
|
37
|
+
package_path = package_dir.relative_to(sources_root).as_posix()
|
|
38
|
+
module_path = normalize_module_path(module)
|
|
39
|
+
return "/".join(part for part in (package_path, module_path) if part)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def argument_counts(
|
|
43
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
44
|
+
*,
|
|
45
|
+
exclude_receiver: bool,
|
|
46
|
+
) -> tuple[int, int]:
|
|
47
|
+
positional_args = [*node.args.posonlyargs, *node.args.args]
|
|
48
|
+
positional_required_count = len(positional_args) - len(node.args.defaults)
|
|
49
|
+
parameters = [
|
|
50
|
+
(arg.arg, index >= positional_required_count)
|
|
51
|
+
for index, arg in enumerate(positional_args)
|
|
52
|
+
]
|
|
53
|
+
parameters.extend(
|
|
54
|
+
(arg.arg, default is not None)
|
|
55
|
+
for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults, strict=True)
|
|
56
|
+
)
|
|
57
|
+
if node.args.vararg is not None:
|
|
58
|
+
parameters.append((node.args.vararg.arg, True))
|
|
59
|
+
if node.args.kwarg is not None:
|
|
60
|
+
parameters.append((node.args.kwarg.arg, True))
|
|
61
|
+
|
|
62
|
+
if exclude_receiver and parameters and parameters[0][0] in {"self", "cls"}:
|
|
63
|
+
parameters = parameters[1:]
|
|
64
|
+
|
|
65
|
+
return len(parameters), sum(1 for _, is_optional in parameters if is_optional)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def has_decorator(node: ast.FunctionDef | ast.AsyncFunctionDef, name: str) -> bool:
|
|
69
|
+
for decorator in node.decorator_list:
|
|
70
|
+
if isinstance(decorator, ast.Name) and decorator.id == name:
|
|
71
|
+
return True
|
|
72
|
+
if isinstance(decorator, ast.Attribute) and decorator.attr == name:
|
|
73
|
+
return True
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def node_name(node: ast.AST) -> str:
|
|
78
|
+
if isinstance(node, ast.Name):
|
|
79
|
+
return node.id
|
|
80
|
+
if isinstance(node, ast.Attribute):
|
|
81
|
+
parent_name = node_name(node.value)
|
|
82
|
+
return f"{parent_name}.{node.attr}" if parent_name else node.attr
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def method_is_abstract(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
87
|
+
return has_decorator(node, "abstractmethod")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def class_is_abstract(node: ast.ClassDef) -> bool:
|
|
91
|
+
return any(node_name(base) in {"ABC", "abc.ABC"} for base in node.bases) or any(
|
|
92
|
+
isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
93
|
+
and method_is_abstract(child)
|
|
94
|
+
for child in node.body
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def visibility_intent(name: str) -> str:
|
|
99
|
+
if name.startswith("__") and not (name.startswith("__") and name.endswith("__")):
|
|
100
|
+
return "private"
|
|
101
|
+
if name.startswith("_") and not (name.startswith("__") and name.endswith("__")):
|
|
102
|
+
return "protected"
|
|
103
|
+
return "public"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def annotation_is_optional(node: ast.AST | None) -> bool:
|
|
107
|
+
if node is None:
|
|
108
|
+
return False
|
|
109
|
+
if isinstance(node, ast.Constant) and node.value is None:
|
|
110
|
+
return True
|
|
111
|
+
if isinstance(node, ast.Name):
|
|
112
|
+
return node.id in {"None", "Optional"}
|
|
113
|
+
if isinstance(node, ast.Attribute):
|
|
114
|
+
return node.attr == "Optional"
|
|
115
|
+
if isinstance(node, ast.Subscript):
|
|
116
|
+
return annotation_is_optional(node.value) or annotation_is_optional(node.slice)
|
|
117
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
118
|
+
return annotation_is_optional(node.left) or annotation_is_optional(node.right)
|
|
119
|
+
if isinstance(node, ast.Tuple):
|
|
120
|
+
return any(annotation_is_optional(element) for element in node.elts)
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def value_is_none(node: ast.AST | None) -> bool:
|
|
125
|
+
return isinstance(node, ast.Constant) and node.value is None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def optional_constructor_parameters(
|
|
129
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
130
|
+
) -> set[str]:
|
|
131
|
+
positional_args = [*node.args.posonlyargs, *node.args.args]
|
|
132
|
+
defaults_by_name = {}
|
|
133
|
+
if node.args.defaults:
|
|
134
|
+
defaults_by_name = {
|
|
135
|
+
arg.arg: default
|
|
136
|
+
for arg, default in zip(
|
|
137
|
+
positional_args[-len(node.args.defaults) :],
|
|
138
|
+
node.args.defaults,
|
|
139
|
+
strict=True,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
optional_names = {
|
|
143
|
+
arg.arg
|
|
144
|
+
for arg in [*positional_args, *node.args.kwonlyargs]
|
|
145
|
+
if annotation_is_optional(arg.annotation)
|
|
146
|
+
}
|
|
147
|
+
optional_names.update(
|
|
148
|
+
name for name, default in defaults_by_name.items() if value_is_none(default)
|
|
149
|
+
)
|
|
150
|
+
optional_names.update(
|
|
151
|
+
arg.arg
|
|
152
|
+
for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults, strict=True)
|
|
153
|
+
if value_is_none(default)
|
|
154
|
+
)
|
|
155
|
+
return optional_names
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def class_properties(node: ast.ClassDef) -> list[dict[str, object]]:
|
|
159
|
+
properties: dict[str, bool] = {}
|
|
160
|
+
|
|
161
|
+
def add_property(name: str, is_optional: bool) -> None:
|
|
162
|
+
properties[name] = properties.get(name, False) or is_optional
|
|
163
|
+
|
|
164
|
+
for child in node.body:
|
|
165
|
+
if isinstance(child, ast.AnnAssign):
|
|
166
|
+
target = child.target
|
|
167
|
+
if isinstance(target, ast.Name):
|
|
168
|
+
add_property(
|
|
169
|
+
target.id,
|
|
170
|
+
annotation_is_optional(child.annotation)
|
|
171
|
+
or value_is_none(child.value),
|
|
172
|
+
)
|
|
173
|
+
continue
|
|
174
|
+
if isinstance(child, ast.Assign):
|
|
175
|
+
for target in child.targets:
|
|
176
|
+
if isinstance(target, ast.Name):
|
|
177
|
+
add_property(target.id, value_is_none(child.value))
|
|
178
|
+
continue
|
|
179
|
+
if not isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
optional_parameter_names = optional_constructor_parameters(child)
|
|
183
|
+
for descendant in ast.walk(child):
|
|
184
|
+
if isinstance(descendant, ast.AnnAssign):
|
|
185
|
+
target = descendant.target
|
|
186
|
+
if (
|
|
187
|
+
isinstance(target, ast.Attribute)
|
|
188
|
+
and isinstance(target.value, ast.Name)
|
|
189
|
+
and target.value.id in {"self", "cls"}
|
|
190
|
+
):
|
|
191
|
+
add_property(
|
|
192
|
+
target.attr,
|
|
193
|
+
annotation_is_optional(descendant.annotation)
|
|
194
|
+
or value_is_none(descendant.value),
|
|
195
|
+
)
|
|
196
|
+
continue
|
|
197
|
+
if not isinstance(descendant, ast.Assign):
|
|
198
|
+
continue
|
|
199
|
+
is_optional = value_is_none(descendant.value) or (
|
|
200
|
+
isinstance(descendant.value, ast.Name)
|
|
201
|
+
and descendant.value.id in optional_parameter_names
|
|
202
|
+
)
|
|
203
|
+
for target in descendant.targets:
|
|
204
|
+
if (
|
|
205
|
+
isinstance(target, ast.Attribute)
|
|
206
|
+
and isinstance(target.value, ast.Name)
|
|
207
|
+
and target.value.id in {"self", "cls"}
|
|
208
|
+
):
|
|
209
|
+
add_property(target.attr, is_optional)
|
|
210
|
+
|
|
211
|
+
return [
|
|
212
|
+
{"name": name, "is_optional": is_optional}
|
|
213
|
+
for name, is_optional in properties.items()
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def method_definition(node: ast.FunctionDef | ast.AsyncFunctionDef) -> dict[str, object]:
|
|
218
|
+
declared_args, optional_args = argument_counts(
|
|
219
|
+
node,
|
|
220
|
+
exclude_receiver=not has_decorator(node, "staticmethod"),
|
|
221
|
+
)
|
|
222
|
+
return {
|
|
223
|
+
"name": node.name,
|
|
224
|
+
"visibility": "public",
|
|
225
|
+
"visibility_intent": visibility_intent(node.name),
|
|
226
|
+
"line_count": line_span(node),
|
|
227
|
+
"declared_args": declared_args,
|
|
228
|
+
"optional_args": optional_args,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def class_definition(node: ast.ClassDef) -> dict[str, object]:
|
|
233
|
+
return {
|
|
234
|
+
"name": node.name,
|
|
235
|
+
"visibility": "public",
|
|
236
|
+
"visibility_intent": visibility_intent(node.name),
|
|
237
|
+
"methods": [
|
|
238
|
+
method_definition(child)
|
|
239
|
+
for child in node.body
|
|
240
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
241
|
+
],
|
|
242
|
+
"properties": class_properties(node),
|
|
243
|
+
"line_count": line_span(node),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def abstract_class_definition(node: ast.ClassDef) -> dict[str, object]:
|
|
248
|
+
methods = [
|
|
249
|
+
method_definition(child)
|
|
250
|
+
for child in node.body
|
|
251
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
252
|
+
]
|
|
253
|
+
abstract_method_names = {
|
|
254
|
+
child.name
|
|
255
|
+
for child in node.body
|
|
256
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
257
|
+
and method_is_abstract(child)
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
"name": node.name,
|
|
261
|
+
"visibility": "public",
|
|
262
|
+
"visibility_intent": visibility_intent(node.name),
|
|
263
|
+
"abstract_methods": [
|
|
264
|
+
method for method in methods if method["name"] in abstract_method_names
|
|
265
|
+
],
|
|
266
|
+
"concrete_methods": [
|
|
267
|
+
method for method in methods if method["name"] not in abstract_method_names
|
|
268
|
+
],
|
|
269
|
+
"properties": class_properties(node),
|
|
270
|
+
"line_count": line_span(node),
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def function_definition(node: ast.FunctionDef | ast.AsyncFunctionDef) -> dict[str, object]:
|
|
275
|
+
declared_args, optional_args = argument_counts(node, exclude_receiver=False)
|
|
276
|
+
return {
|
|
277
|
+
"name": node.name,
|
|
278
|
+
"visibility": "public",
|
|
279
|
+
"visibility_intent": visibility_intent(node.name),
|
|
280
|
+
"line_count": line_span(node),
|
|
281
|
+
"declared_args": declared_args,
|
|
282
|
+
"optional_args": optional_args,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def code_line_count(content: str) -> int:
|
|
287
|
+
return sum(
|
|
288
|
+
1
|
|
289
|
+
for line in content.splitlines()
|
|
290
|
+
if line.strip() and not line.strip().startswith("#")
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def import_join_key(import_path_value: str) -> str:
|
|
295
|
+
return normalize_module_path(import_path_value).rsplit("/", 1)[0]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def collect_imports(
|
|
299
|
+
tree: ast.Module,
|
|
300
|
+
path: Path,
|
|
301
|
+
sources_root: Path,
|
|
302
|
+
) -> list[dict[str, object]]:
|
|
303
|
+
imports = []
|
|
304
|
+
for statement_id, node in enumerate(ast.walk(tree), start=1):
|
|
305
|
+
if isinstance(node, ast.Import):
|
|
306
|
+
for alias in node.names:
|
|
307
|
+
imports.append(
|
|
308
|
+
{
|
|
309
|
+
"path": alias.name,
|
|
310
|
+
"is_relative": False,
|
|
311
|
+
"normalized_path": normalized_import_path(
|
|
312
|
+
alias.name,
|
|
313
|
+
False,
|
|
314
|
+
path,
|
|
315
|
+
sources_root,
|
|
316
|
+
),
|
|
317
|
+
"imported_name": "",
|
|
318
|
+
"is_aliased": alias.asname is not None,
|
|
319
|
+
"crossing_type": "module",
|
|
320
|
+
"file_barrier_crossed": True,
|
|
321
|
+
"statement_id": statement_id,
|
|
322
|
+
"join_key": import_join_key(alias.name),
|
|
323
|
+
"uses_joined_import": False,
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
continue
|
|
327
|
+
if isinstance(node, ast.ImportFrom):
|
|
328
|
+
node_path = import_path(node)
|
|
329
|
+
for alias in node.names:
|
|
330
|
+
imports.append(
|
|
331
|
+
{
|
|
332
|
+
"path": node_path,
|
|
333
|
+
"is_relative": node.level > 0,
|
|
334
|
+
"normalized_path": normalized_import_path(
|
|
335
|
+
node_path,
|
|
336
|
+
node.level > 0,
|
|
337
|
+
path,
|
|
338
|
+
sources_root,
|
|
339
|
+
),
|
|
340
|
+
"imported_name": alias.name,
|
|
341
|
+
"is_aliased": alias.asname is not None,
|
|
342
|
+
"crossing_type": "symbol",
|
|
343
|
+
"file_barrier_crossed": True,
|
|
344
|
+
"statement_id": statement_id,
|
|
345
|
+
"join_key": node_path,
|
|
346
|
+
"uses_joined_import": True,
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
return imports
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def extract_file(path: Path, sources_root: Path) -> dict[str, object]:
|
|
353
|
+
content = path.read_text(encoding="utf-8")
|
|
354
|
+
tree = ast.parse(content, filename=str(path))
|
|
355
|
+
class_nodes = [node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]
|
|
356
|
+
classes = [
|
|
357
|
+
class_definition(node)
|
|
358
|
+
for node in class_nodes
|
|
359
|
+
if not class_is_abstract(node)
|
|
360
|
+
]
|
|
361
|
+
abstract_classes = [
|
|
362
|
+
abstract_class_definition(node)
|
|
363
|
+
for node in class_nodes
|
|
364
|
+
if class_is_abstract(node)
|
|
365
|
+
]
|
|
366
|
+
functions = [
|
|
367
|
+
function_definition(node)
|
|
368
|
+
for node in tree.body
|
|
369
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
"imports": collect_imports(tree, path, sources_root),
|
|
374
|
+
"classes": classes,
|
|
375
|
+
"interfaces": [],
|
|
376
|
+
"types": [],
|
|
377
|
+
"abstract_classes": abstract_classes,
|
|
378
|
+
"functions": functions,
|
|
379
|
+
"line_count": len(content.splitlines()),
|
|
380
|
+
"code_line_count": code_line_count(content),
|
|
381
|
+
"public_symbol_count": sum(
|
|
382
|
+
1
|
|
383
|
+
for node in tree.body
|
|
384
|
+
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef))
|
|
385
|
+
and not node.name.startswith("_")
|
|
386
|
+
),
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def main() -> int:
|
|
391
|
+
file_path = Path(sys.argv[1]).resolve()
|
|
392
|
+
sources_root = Path(sys.argv[2]).resolve() if len(sys.argv) > 2 else Path.cwd()
|
|
393
|
+
print(json.dumps(extract_file(file_path, sources_root)))
|
|
394
|
+
return 0
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
if __name__ == "__main__":
|
|
398
|
+
raise SystemExit(main())
|