spaghetti-detector 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.
spaghetti/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Spaghetti-code and architectural-smell detector for the Boti workspace.
2
+
3
+ See :mod:`spaghetti.detector` for the scanning implementation; individual
4
+ check functions (``check_long_functions``, ``check_circular_imports``, etc.)
5
+ are accessed from there directly, e.g. ``from spaghetti import detector``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from spaghetti.detector import main
11
+
12
+ __all__ = ["main"]
@@ -0,0 +1,120 @@
1
+ """AST utility functions shared across check modules."""
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from collections.abc import Iterator
6
+
7
+
8
+ def _nesting_depth(node: ast.AST, current: int = 0) -> int:
9
+ """Compute maximum nesting depth from a node."""
10
+ max_depth = current
11
+ for child in ast.iter_child_nodes(node):
12
+ if isinstance(child, (ast.If, ast.For, ast.While, ast.With, ast.Try, ast.ExceptHandler)):
13
+ max_depth = max(max_depth, _nesting_depth(child, current + 1))
14
+ else:
15
+ max_depth = max(max_depth, _nesting_depth(child, current))
16
+ return max_depth
17
+
18
+
19
+ def _cyclomatic_complexity(node: ast.AST) -> int:
20
+ """Approximate McCabe cyclomatic complexity."""
21
+ complexity = 1
22
+ for child in ast.walk(node):
23
+ if isinstance(child, (ast.If, ast.While, ast.For, ast.AsyncFor)):
24
+ complexity += 1
25
+ elif isinstance(child, ast.BoolOp):
26
+ complexity += len(child.values) - 1
27
+ elif isinstance(child, ast.ExceptHandler):
28
+ complexity += 1
29
+ elif isinstance(child, ast.Assert):
30
+ complexity += 1
31
+ elif isinstance(child, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
32
+ complexity += 1
33
+ return complexity
34
+
35
+
36
+ def _line_count(node: ast.AST) -> int:
37
+ """End line - start line + 1."""
38
+ if hasattr(node, "end_lineno") and node.end_lineno is not None:
39
+ return node.end_lineno - node.lineno + 1
40
+ return 0
41
+
42
+
43
+ def _has_return_type_hint(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
44
+ return node.returns is not None
45
+
46
+
47
+ def _param_has_type_hint(arg: ast.arg) -> bool:
48
+ return arg.annotation is not None
49
+
50
+
51
+ def _is_private(name: str) -> bool:
52
+ return name.startswith("_")
53
+
54
+
55
+ def _file_line_count(source: str) -> int:
56
+ return len(source.splitlines())
57
+
58
+
59
+ def _count_own_returns(node: ast.AST) -> int:
60
+ """Count Return statements belonging to ``node``, not to nested defs."""
61
+ count = 0
62
+
63
+ def visit(n: ast.AST) -> None:
64
+ nonlocal count
65
+ for child in ast.iter_child_nodes(n):
66
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
67
+ continue
68
+ if isinstance(child, ast.Return):
69
+ count += 1
70
+ visit(child)
71
+
72
+ visit(node)
73
+ return count
74
+
75
+
76
+ def _dump_stmts(stmts: list[ast.stmt]) -> str:
77
+ return "\n".join(ast.dump(s, annotate_fields=False) for s in stmts)
78
+
79
+
80
+ def _is_trivial_body(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
81
+ """True for stub-like bodies (pass / docstring-only / ellipsis) that shouldn't
82
+ count as meaningful duplication if repeated."""
83
+ body = node.body
84
+ meaningful = [
85
+ s
86
+ for s in body
87
+ if not (
88
+ isinstance(s, ast.Expr)
89
+ and isinstance(s.value, ast.Constant)
90
+ and isinstance(s.value.value, str)
91
+ )
92
+ ]
93
+ if not meaningful:
94
+ return True
95
+ if len(meaningful) == 1 and isinstance(meaningful[0], ast.Pass):
96
+ return True
97
+ if (
98
+ len(meaningful) == 1
99
+ and isinstance(meaningful[0], ast.Expr)
100
+ and isinstance(meaningful[0].value, ast.Constant)
101
+ and meaningful[0].value.value is Ellipsis
102
+ ):
103
+ return True
104
+ if len(meaningful) == 1 and isinstance(meaningful[0], ast.Raise):
105
+ return True
106
+ return False
107
+
108
+
109
+ def _walk_with_class_context(tree: ast.AST) -> Iterator[tuple[ast.AST, str | None]]:
110
+ """Yield (node, enclosing_class_name_or_None) for every node in the tree."""
111
+
112
+ def visit(node: ast.AST, class_name: str | None) -> Iterator[tuple[ast.AST, str | None]]:
113
+ for child in ast.iter_child_nodes(node):
114
+ new_class_name = class_name
115
+ if isinstance(child, ast.ClassDef):
116
+ new_class_name = child.name
117
+ yield child, class_name
118
+ yield from visit(child, new_class_name)
119
+
120
+ yield from visit(tree, None)
@@ -0,0 +1,38 @@
1
+ """Check registries: ALL_CHECKS, SOURCE_CHECKS, PACKAGE_CHECKS."""
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ from spaghetti.checks.ast_per_file import ALL_CHECKS
9
+ from spaghetti.checks.package_level import (
10
+ check_duplicate_functions_pkg,
11
+ check_import_cycles_pkg,
12
+ check_orphan_interfaces_pkg,
13
+ check_sync_async_twins_pkg,
14
+ )
15
+ from spaghetti.checks.text_per_file import check_long_file, check_todo_markers
16
+ from spaghetti.models import Issue
17
+
18
+ __all__ = [
19
+ "ALL_CHECKS",
20
+ "SOURCE_CHECKS",
21
+ "PACKAGE_CHECKS",
22
+ "check_duplicate_functions_pkg",
23
+ "check_orphan_interfaces_pkg",
24
+ "check_sync_async_twins_pkg",
25
+ ]
26
+
27
+ SourceCheck = Callable[[str, Path, str], list[Issue]]
28
+ PackageCheck = Callable[[str, list[tuple[Path, ast.Module]]], list[Issue]]
29
+
30
+ SOURCE_CHECKS: list[SourceCheck] = [
31
+ check_long_file,
32
+ check_todo_markers,
33
+ ]
34
+
35
+ PACKAGE_CHECKS: list[PackageCheck] = [
36
+ check_import_cycles_pkg,
37
+ check_orphan_interfaces_pkg,
38
+ ]