thailint 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.
- src/.ai/layout.yaml +48 -0
- src/__init__.py +49 -0
- src/api.py +118 -0
- src/cli.py +698 -0
- src/config.py +386 -0
- src/core/__init__.py +17 -0
- src/core/base.py +122 -0
- src/core/registry.py +170 -0
- src/core/types.py +83 -0
- src/linter_config/__init__.py +13 -0
- src/linter_config/ignore.py +403 -0
- src/linter_config/loader.py +77 -0
- src/linters/__init__.py +4 -0
- src/linters/file_placement/__init__.py +31 -0
- src/linters/file_placement/linter.py +621 -0
- src/linters/nesting/__init__.py +87 -0
- src/linters/nesting/config.py +50 -0
- src/linters/nesting/linter.py +257 -0
- src/linters/nesting/python_analyzer.py +89 -0
- src/linters/nesting/typescript_analyzer.py +180 -0
- src/orchestrator/__init__.py +9 -0
- src/orchestrator/core.py +188 -0
- src/orchestrator/language_detector.py +81 -0
- thailint-0.1.0.dist-info/LICENSE +21 -0
- thailint-0.1.0.dist-info/METADATA +601 -0
- thailint-0.1.0.dist-info/RECORD +28 -0
- thailint-0.1.0.dist-info/WHEEL +4 -0
- thailint-0.1.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Nesting depth linter package initialization
|
|
3
|
+
|
|
4
|
+
Scope: Exports for nesting depth linter module
|
|
5
|
+
|
|
6
|
+
Overview: Initializes the nesting depth linter package and exposes the main rule class for
|
|
7
|
+
external use. Exports NestingDepthRule as the primary interface for the nesting linter,
|
|
8
|
+
allowing the orchestrator to discover and instantiate the rule. Also exports configuration
|
|
9
|
+
and analyzer classes for advanced use cases. Provides a convenience lint() function for
|
|
10
|
+
direct usage without orchestrator setup. This module serves as the entry point for
|
|
11
|
+
the nesting linter functionality within the thai-lint framework.
|
|
12
|
+
|
|
13
|
+
Dependencies: NestingDepthRule, NestingConfig, PythonNestingAnalyzer, TypeScriptNestingAnalyzer
|
|
14
|
+
|
|
15
|
+
Exports: NestingDepthRule (primary), NestingConfig, PythonNestingAnalyzer, TypeScriptNestingAnalyzer, lint
|
|
16
|
+
|
|
17
|
+
Interfaces: Standard Python package initialization with __all__ for explicit exports, lint() convenience function
|
|
18
|
+
|
|
19
|
+
Implementation: Simple re-export pattern for package interface, convenience function wraps orchestrator
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from .config import NestingConfig
|
|
26
|
+
from .linter import NestingDepthRule
|
|
27
|
+
from .python_analyzer import PythonNestingAnalyzer
|
|
28
|
+
from .typescript_analyzer import TypeScriptNestingAnalyzer
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"NestingDepthRule",
|
|
32
|
+
"NestingConfig",
|
|
33
|
+
"PythonNestingAnalyzer",
|
|
34
|
+
"TypeScriptNestingAnalyzer",
|
|
35
|
+
"lint",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def lint(path: Path | str, config: dict[str, Any] | None = None, max_depth: int = 4) -> list:
|
|
40
|
+
"""Lint a file or directory for nesting depth violations.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
path: Path to file or directory to lint
|
|
44
|
+
config: Configuration dict (optional, uses defaults if not provided)
|
|
45
|
+
max_depth: Maximum allowed nesting depth (default: 4)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of violations found
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> from src.linters.nesting import lint
|
|
52
|
+
>>> violations = lint('src/my_module.py', max_depth=3)
|
|
53
|
+
>>> for v in violations:
|
|
54
|
+
... print(f"{v.file_path}:{v.line} - {v.message}")
|
|
55
|
+
"""
|
|
56
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
57
|
+
project_root = path_obj if path_obj.is_dir() else path_obj.parent
|
|
58
|
+
|
|
59
|
+
orchestrator = _setup_nesting_orchestrator(project_root, config, max_depth)
|
|
60
|
+
violations = _execute_nesting_lint(orchestrator, path_obj)
|
|
61
|
+
|
|
62
|
+
return [v for v in violations if "nesting" in v.rule_id]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _setup_nesting_orchestrator(
|
|
66
|
+
project_root: Path, config: dict[str, Any] | None, max_depth: int
|
|
67
|
+
) -> Any:
|
|
68
|
+
"""Set up orchestrator with nesting config."""
|
|
69
|
+
from src.orchestrator.core import Orchestrator
|
|
70
|
+
|
|
71
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
72
|
+
|
|
73
|
+
if config:
|
|
74
|
+
orchestrator.config["nesting"] = config
|
|
75
|
+
else:
|
|
76
|
+
orchestrator.config["nesting"] = {"max_nesting_depth": max_depth}
|
|
77
|
+
|
|
78
|
+
return orchestrator
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _execute_nesting_lint(orchestrator: Any, path_obj: Path) -> list:
|
|
82
|
+
"""Execute linting on file or directory."""
|
|
83
|
+
if path_obj.is_file():
|
|
84
|
+
return orchestrator.lint_file(path_obj)
|
|
85
|
+
if path_obj.is_dir():
|
|
86
|
+
return orchestrator.lint_directory(path_obj)
|
|
87
|
+
return []
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration schema for nesting depth linter
|
|
3
|
+
|
|
4
|
+
Scope: NestingConfig dataclass with max_nesting_depth setting
|
|
5
|
+
|
|
6
|
+
Overview: Defines configuration schema for nesting depth linter. Provides NestingConfig dataclass
|
|
7
|
+
with max_nesting_depth field (default 4), validation logic, and config loading from YAML/JSON.
|
|
8
|
+
Supports per-file and per-directory config overrides. Validates that max_depth is positive
|
|
9
|
+
integer. Integrates with the orchestrator's configuration system to allow users to customize
|
|
10
|
+
nesting depth limits via .thailint.yaml configuration files.
|
|
11
|
+
|
|
12
|
+
Dependencies: dataclasses, typing
|
|
13
|
+
|
|
14
|
+
Exports: NestingConfig dataclass
|
|
15
|
+
|
|
16
|
+
Interfaces: NestingConfig(max_nesting_depth: int = 4), from_dict class method for loading config
|
|
17
|
+
|
|
18
|
+
Implementation: Dataclass with validation and defaults, matches reference implementation default
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class NestingConfig:
|
|
27
|
+
"""Configuration for nesting depth linter."""
|
|
28
|
+
|
|
29
|
+
max_nesting_depth: int = 4 # Default from reference implementation
|
|
30
|
+
enabled: bool = True
|
|
31
|
+
|
|
32
|
+
def __post_init__(self) -> None:
|
|
33
|
+
"""Validate configuration values."""
|
|
34
|
+
if self.max_nesting_depth <= 0:
|
|
35
|
+
raise ValueError(f"max_nesting_depth must be positive, got {self.max_nesting_depth}")
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_dict(cls, config: dict[str, Any]) -> "NestingConfig":
|
|
39
|
+
"""Load configuration from dictionary.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
config: Dictionary containing configuration values
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
NestingConfig instance with values from dictionary
|
|
46
|
+
"""
|
|
47
|
+
return cls(
|
|
48
|
+
max_nesting_depth=config.get("max_nesting_depth", 4),
|
|
49
|
+
enabled=config.get("enabled", True),
|
|
50
|
+
)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main nesting depth linter rule implementation
|
|
3
|
+
|
|
4
|
+
Scope: NestingDepthRule class implementing BaseLintRule interface
|
|
5
|
+
|
|
6
|
+
Overview: Implements nesting depth linter rule following BaseLintRule interface. Detects excessive
|
|
7
|
+
nesting depth in Python and TypeScript code using AST analysis. Supports configurable
|
|
8
|
+
max_nesting_depth limit (default 4). Provides helpful violation messages with refactoring
|
|
9
|
+
suggestions (early returns, guard clauses, extract method). Integrates with orchestrator for
|
|
10
|
+
automatic rule discovery. Handles both Python (using ast) and TypeScript (using typescript-estree)
|
|
11
|
+
code analysis. Gracefully handles syntax errors by reporting them as violations rather than
|
|
12
|
+
crashing. Supports configuration loading from context metadata for per-file customization.
|
|
13
|
+
|
|
14
|
+
Dependencies: BaseLintRule, BaseLintContext, Violation, PythonNestingAnalyzer, TypeScriptNestingAnalyzer
|
|
15
|
+
|
|
16
|
+
Exports: NestingDepthRule class
|
|
17
|
+
|
|
18
|
+
Interfaces: NestingDepthRule.check(context) -> list[Violation], properties for rule metadata
|
|
19
|
+
|
|
20
|
+
Implementation: AST-based analysis with configurable limits and helpful error messages
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from src.core.base import BaseLintContext, BaseLintRule
|
|
27
|
+
from src.core.types import Severity, Violation
|
|
28
|
+
from src.linter_config.ignore import IgnoreDirectiveParser
|
|
29
|
+
|
|
30
|
+
from .config import NestingConfig
|
|
31
|
+
from .python_analyzer import PythonNestingAnalyzer
|
|
32
|
+
from .typescript_analyzer import TypeScriptNestingAnalyzer
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NestingDepthRule(BaseLintRule):
|
|
36
|
+
"""Detects excessive nesting depth in functions."""
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
"""Initialize the nesting depth rule."""
|
|
40
|
+
self._ignore_parser = IgnoreDirectiveParser()
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def rule_id(self) -> str:
|
|
44
|
+
"""Unique identifier for this rule."""
|
|
45
|
+
return "nesting.excessive-depth"
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def rule_name(self) -> str:
|
|
49
|
+
"""Human-readable name for this rule."""
|
|
50
|
+
return "Excessive Nesting Depth"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def description(self) -> str:
|
|
54
|
+
"""Description of what this rule checks."""
|
|
55
|
+
return "Functions should not have excessive nesting depth for better readability"
|
|
56
|
+
|
|
57
|
+
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
58
|
+
"""Check for excessive nesting depth violations.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
context: Lint context with file information
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of violations found
|
|
65
|
+
"""
|
|
66
|
+
if context.file_content is None:
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
# Load configuration
|
|
70
|
+
config = self._load_config(context)
|
|
71
|
+
if not config.enabled:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
# Analyze based on language
|
|
75
|
+
if context.language == "python":
|
|
76
|
+
return self._check_python(context, config)
|
|
77
|
+
if context.language in ("typescript", "javascript"):
|
|
78
|
+
return self._check_typescript(context, config)
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
def _load_config(self, context: BaseLintContext) -> NestingConfig:
|
|
82
|
+
"""Load configuration from context metadata.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
context: Lint context containing metadata
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
NestingConfig instance with configuration values
|
|
89
|
+
"""
|
|
90
|
+
metadata = getattr(context, "metadata", None)
|
|
91
|
+
if metadata is None or not isinstance(metadata, dict):
|
|
92
|
+
return NestingConfig()
|
|
93
|
+
|
|
94
|
+
config_dict = metadata.get("nesting", {})
|
|
95
|
+
if not isinstance(config_dict, dict):
|
|
96
|
+
return NestingConfig()
|
|
97
|
+
|
|
98
|
+
return NestingConfig.from_dict(config_dict)
|
|
99
|
+
|
|
100
|
+
def _check_python(self, context: BaseLintContext, config: NestingConfig) -> list[Violation]:
|
|
101
|
+
"""Check Python code for nesting violations.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
context: Lint context with Python file information
|
|
105
|
+
config: Nesting configuration
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of violations found in Python code
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
tree = ast.parse(context.file_content or "")
|
|
112
|
+
except SyntaxError as e:
|
|
113
|
+
return [self._create_syntax_error_violation(e, context)]
|
|
114
|
+
|
|
115
|
+
analyzer = PythonNestingAnalyzer()
|
|
116
|
+
functions = analyzer.find_all_functions(tree)
|
|
117
|
+
return self._check_functions(functions, config, context)
|
|
118
|
+
|
|
119
|
+
def _check_functions(
|
|
120
|
+
self,
|
|
121
|
+
functions: list[ast.FunctionDef | ast.AsyncFunctionDef],
|
|
122
|
+
config: NestingConfig,
|
|
123
|
+
context: BaseLintContext,
|
|
124
|
+
) -> list[Violation]:
|
|
125
|
+
"""Check list of functions for nesting violations."""
|
|
126
|
+
violations = []
|
|
127
|
+
for func in functions:
|
|
128
|
+
violation = self._check_single_function(func, config, context)
|
|
129
|
+
if violation:
|
|
130
|
+
violations.append(violation)
|
|
131
|
+
return violations
|
|
132
|
+
|
|
133
|
+
def _check_single_function(
|
|
134
|
+
self,
|
|
135
|
+
func: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
136
|
+
config: NestingConfig,
|
|
137
|
+
context: BaseLintContext,
|
|
138
|
+
) -> Violation | None:
|
|
139
|
+
"""Check a single function for nesting violations."""
|
|
140
|
+
analyzer = PythonNestingAnalyzer()
|
|
141
|
+
max_depth, _line = analyzer.calculate_max_depth(func)
|
|
142
|
+
|
|
143
|
+
if max_depth <= config.max_nesting_depth:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
violation = self._create_nesting_violation(func, max_depth, config, context)
|
|
147
|
+
if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
return violation
|
|
151
|
+
|
|
152
|
+
def _create_syntax_error_violation(
|
|
153
|
+
self, error: SyntaxError, context: BaseLintContext
|
|
154
|
+
) -> Violation:
|
|
155
|
+
"""Create violation for syntax error."""
|
|
156
|
+
return Violation(
|
|
157
|
+
rule_id=self.rule_id,
|
|
158
|
+
file_path=str(context.file_path or ""),
|
|
159
|
+
line=error.lineno or 0,
|
|
160
|
+
column=error.offset or 0,
|
|
161
|
+
message=f"Syntax error: {error.msg}",
|
|
162
|
+
severity=Severity.ERROR,
|
|
163
|
+
suggestion="Fix syntax errors before checking nesting depth",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def _create_nesting_violation(
|
|
167
|
+
self,
|
|
168
|
+
func: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
169
|
+
max_depth: int,
|
|
170
|
+
config: NestingConfig,
|
|
171
|
+
context: BaseLintContext,
|
|
172
|
+
) -> Violation:
|
|
173
|
+
"""Create violation for excessive nesting."""
|
|
174
|
+
return Violation(
|
|
175
|
+
rule_id=self.rule_id,
|
|
176
|
+
file_path=str(context.file_path or ""),
|
|
177
|
+
line=func.lineno,
|
|
178
|
+
column=func.col_offset,
|
|
179
|
+
message=f"Function '{func.name}' has excessive nesting depth ({max_depth})",
|
|
180
|
+
severity=Severity.ERROR,
|
|
181
|
+
suggestion=(
|
|
182
|
+
f"Maximum nesting depth of {max_depth} exceeds limit of {config.max_nesting_depth}. "
|
|
183
|
+
"Consider extracting nested logic to separate functions, using early returns, "
|
|
184
|
+
"or applying guard clauses to reduce nesting."
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _check_typescript(self, context: BaseLintContext, config: NestingConfig) -> list[Violation]:
|
|
189
|
+
"""Check TypeScript code for nesting violations."""
|
|
190
|
+
analyzer = TypeScriptNestingAnalyzer()
|
|
191
|
+
root_node = analyzer.parse_typescript(context.file_content or "")
|
|
192
|
+
|
|
193
|
+
if root_node is None:
|
|
194
|
+
return []
|
|
195
|
+
|
|
196
|
+
functions = analyzer.find_all_functions(root_node)
|
|
197
|
+
return self._check_typescript_functions(functions, config, context)
|
|
198
|
+
|
|
199
|
+
def _check_typescript_functions(
|
|
200
|
+
self, functions: list, config: NestingConfig, context: BaseLintContext
|
|
201
|
+
) -> list[Violation]:
|
|
202
|
+
"""Check TypeScript functions for nesting violations."""
|
|
203
|
+
violations = []
|
|
204
|
+
|
|
205
|
+
for func_node, func_name in functions:
|
|
206
|
+
violation = self._check_single_typescript_function(
|
|
207
|
+
func_node, func_name, config, context
|
|
208
|
+
)
|
|
209
|
+
if violation:
|
|
210
|
+
violations.append(violation)
|
|
211
|
+
|
|
212
|
+
return violations
|
|
213
|
+
|
|
214
|
+
def _check_single_typescript_function(
|
|
215
|
+
self, func_node: Any, func_name: str, config: NestingConfig, context: BaseLintContext
|
|
216
|
+
) -> Violation | None:
|
|
217
|
+
"""Check a single TypeScript function for nesting violations."""
|
|
218
|
+
analyzer = TypeScriptNestingAnalyzer()
|
|
219
|
+
max_depth, _line = analyzer.calculate_max_depth(func_node)
|
|
220
|
+
|
|
221
|
+
if max_depth <= config.max_nesting_depth:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
violation = self._create_typescript_nesting_violation(
|
|
225
|
+
func_node, func_name, max_depth, config, context
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
return violation
|
|
232
|
+
|
|
233
|
+
def _create_typescript_nesting_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
234
|
+
self,
|
|
235
|
+
func_node: Any, # tree-sitter Node
|
|
236
|
+
func_name: str,
|
|
237
|
+
max_depth: int,
|
|
238
|
+
config: NestingConfig,
|
|
239
|
+
context: BaseLintContext,
|
|
240
|
+
) -> Violation:
|
|
241
|
+
"""Create violation for excessive nesting in TypeScript."""
|
|
242
|
+
line = func_node.start_point[0] + 1 # Convert to 1-indexed
|
|
243
|
+
column = func_node.start_point[1]
|
|
244
|
+
|
|
245
|
+
return Violation(
|
|
246
|
+
rule_id=self.rule_id,
|
|
247
|
+
file_path=str(context.file_path or ""),
|
|
248
|
+
line=line,
|
|
249
|
+
column=column,
|
|
250
|
+
message=f"Function '{func_name}' has excessive nesting depth ({max_depth})",
|
|
251
|
+
severity=Severity.ERROR,
|
|
252
|
+
suggestion=(
|
|
253
|
+
f"Maximum nesting depth of {max_depth} exceeds limit of {config.max_nesting_depth}. "
|
|
254
|
+
"Consider extracting nested logic to separate functions, using early returns, "
|
|
255
|
+
"or applying guard clauses to reduce nesting."
|
|
256
|
+
),
|
|
257
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Python AST-based nesting depth calculator
|
|
3
|
+
|
|
4
|
+
Scope: Python code nesting depth analysis using ast module
|
|
5
|
+
|
|
6
|
+
Overview: Analyzes Python code to calculate maximum nesting depth using AST traversal. Implements
|
|
7
|
+
visitor pattern to walk AST, tracking current depth and maximum depth found. Increments depth
|
|
8
|
+
for If, For, While, With, AsyncWith, Try, ExceptHandler, Match, and match_case nodes. Starts
|
|
9
|
+
depth counting at 1 for function body, matching reference implementation behavior. Returns
|
|
10
|
+
maximum depth found and location information for violation reporting. Provides helper method
|
|
11
|
+
to find all function definitions in an AST tree for batch processing.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for Python parsing
|
|
14
|
+
|
|
15
|
+
Exports: PythonNestingAnalyzer class with calculate_max_depth method
|
|
16
|
+
|
|
17
|
+
Interfaces: calculate_max_depth(func_node: ast.FunctionDef) -> tuple[int, int], find_all_functions
|
|
18
|
+
|
|
19
|
+
Implementation: AST visitor pattern with depth tracking, based on reference implementation algorithm
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PythonNestingAnalyzer:
|
|
26
|
+
"""Calculates maximum nesting depth in Python functions."""
|
|
27
|
+
|
|
28
|
+
def calculate_max_depth(
|
|
29
|
+
self, func_node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
30
|
+
) -> tuple[int, int]:
|
|
31
|
+
"""Calculate maximum nesting depth in a function.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
func_node: AST node for function definition
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple of (max_depth, line_number_of_max_depth)
|
|
38
|
+
"""
|
|
39
|
+
max_depth = 0
|
|
40
|
+
max_depth_line = func_node.lineno
|
|
41
|
+
|
|
42
|
+
def visit_node(node: ast.AST, current_depth: int = 0) -> None:
|
|
43
|
+
nonlocal max_depth, max_depth_line
|
|
44
|
+
|
|
45
|
+
if current_depth > max_depth:
|
|
46
|
+
max_depth = current_depth
|
|
47
|
+
max_depth_line = getattr(node, "lineno", func_node.lineno)
|
|
48
|
+
|
|
49
|
+
# Nodes that increase nesting depth
|
|
50
|
+
if isinstance(
|
|
51
|
+
node,
|
|
52
|
+
(
|
|
53
|
+
ast.If,
|
|
54
|
+
ast.For,
|
|
55
|
+
ast.While,
|
|
56
|
+
ast.With,
|
|
57
|
+
ast.AsyncWith,
|
|
58
|
+
ast.Try,
|
|
59
|
+
ast.ExceptHandler,
|
|
60
|
+
ast.Match,
|
|
61
|
+
ast.match_case,
|
|
62
|
+
),
|
|
63
|
+
):
|
|
64
|
+
current_depth += 1
|
|
65
|
+
|
|
66
|
+
# Visit children
|
|
67
|
+
for child in ast.iter_child_nodes(node):
|
|
68
|
+
visit_node(child, current_depth)
|
|
69
|
+
|
|
70
|
+
# Start at depth 1 for function body (matching reference implementation)
|
|
71
|
+
for stmt in func_node.body:
|
|
72
|
+
visit_node(stmt, 1)
|
|
73
|
+
|
|
74
|
+
return max_depth, max_depth_line
|
|
75
|
+
|
|
76
|
+
def find_all_functions(self, tree: ast.AST) -> list[ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
77
|
+
"""Find all function definitions in AST.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
tree: Python AST to search
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
List of all FunctionDef and AsyncFunctionDef nodes found
|
|
84
|
+
"""
|
|
85
|
+
functions = []
|
|
86
|
+
for node in ast.walk(tree):
|
|
87
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
88
|
+
functions.append(node)
|
|
89
|
+
return functions
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: TypeScript AST-based nesting depth calculator
|
|
3
|
+
|
|
4
|
+
Scope: TypeScript code nesting depth analysis using tree-sitter parser
|
|
5
|
+
|
|
6
|
+
Overview: Analyzes TypeScript code to calculate maximum nesting depth using AST traversal.
|
|
7
|
+
Implements visitor pattern to walk TypeScript AST from tree-sitter, tracking current depth
|
|
8
|
+
and maximum depth found. Increments depth for if_statement, for_statement, for_in_statement,
|
|
9
|
+
while_statement, do_statement, try_statement, switch_statement nodes. Starts depth counting
|
|
10
|
+
at 1 for function body. Returns maximum depth found and location.
|
|
11
|
+
|
|
12
|
+
Dependencies: tree-sitter, tree-sitter-typescript for TypeScript parsing
|
|
13
|
+
|
|
14
|
+
Exports: TypeScriptNestingAnalyzer class with calculate_max_depth and parse_typescript methods
|
|
15
|
+
|
|
16
|
+
Interfaces: calculate_max_depth(func_node) -> tuple[int, int], parse_typescript(code: str)
|
|
17
|
+
|
|
18
|
+
Implementation: tree-sitter AST visitor pattern with depth tracking for TypeScript
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import tree_sitter_typescript as tstypescript
|
|
25
|
+
from tree_sitter import Language, Node, Parser
|
|
26
|
+
|
|
27
|
+
TS_LANGUAGE = Language(tstypescript.language_typescript())
|
|
28
|
+
TS_PARSER = Parser(TS_LANGUAGE)
|
|
29
|
+
TREE_SITTER_AVAILABLE = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
TREE_SITTER_AVAILABLE = False
|
|
32
|
+
TS_PARSER = None # type: ignore
|
|
33
|
+
Node = Any # type: ignore
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TypeScriptNestingAnalyzer:
|
|
37
|
+
"""Calculates maximum nesting depth in TypeScript functions."""
|
|
38
|
+
|
|
39
|
+
# Tree-sitter node types that increase nesting depth
|
|
40
|
+
NESTING_NODE_TYPES = {
|
|
41
|
+
"if_statement",
|
|
42
|
+
"for_statement",
|
|
43
|
+
"for_in_statement",
|
|
44
|
+
"while_statement",
|
|
45
|
+
"do_statement",
|
|
46
|
+
"try_statement",
|
|
47
|
+
"switch_statement",
|
|
48
|
+
"with_statement", # Deprecated but exists
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def parse_typescript(self, code: str) -> Node | None:
|
|
52
|
+
"""Parse TypeScript code to AST using tree-sitter.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
code: TypeScript source code to parse
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Tree-sitter AST root node, or None if parsing fails
|
|
59
|
+
"""
|
|
60
|
+
if not TREE_SITTER_AVAILABLE or TS_PARSER is None:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
tree = TS_PARSER.parse(bytes(code, "utf8"))
|
|
64
|
+
return tree.root_node
|
|
65
|
+
|
|
66
|
+
def calculate_max_depth(self, func_node: Node) -> tuple[int, int]:
|
|
67
|
+
"""Calculate maximum nesting depth in a TypeScript function."""
|
|
68
|
+
if not TREE_SITTER_AVAILABLE:
|
|
69
|
+
return 0, 0
|
|
70
|
+
|
|
71
|
+
body_node = self._find_function_body(func_node)
|
|
72
|
+
if not body_node:
|
|
73
|
+
return 0, func_node.start_point[0] + 1
|
|
74
|
+
|
|
75
|
+
return self._calculate_depth_in_body(body_node)
|
|
76
|
+
|
|
77
|
+
def _find_function_body(self, func_node: Node) -> Node | None:
|
|
78
|
+
"""Find the statement_block node in a function."""
|
|
79
|
+
for child in func_node.children:
|
|
80
|
+
if child.type == "statement_block":
|
|
81
|
+
return child
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def _calculate_depth_in_body(self, body_node: Node) -> tuple[int, int]:
|
|
85
|
+
"""Calculate max depth within a function body."""
|
|
86
|
+
max_depth = 0
|
|
87
|
+
max_depth_line = body_node.start_point[0] + 1
|
|
88
|
+
|
|
89
|
+
def visit_node(node: Node, current_depth: int = 0) -> None:
|
|
90
|
+
nonlocal max_depth, max_depth_line
|
|
91
|
+
|
|
92
|
+
if current_depth > max_depth:
|
|
93
|
+
max_depth = current_depth
|
|
94
|
+
max_depth_line = node.start_point[0] + 1
|
|
95
|
+
|
|
96
|
+
new_depth = current_depth + 1 if node.type in self.NESTING_NODE_TYPES else current_depth
|
|
97
|
+
|
|
98
|
+
for child in node.children:
|
|
99
|
+
visit_node(child, new_depth)
|
|
100
|
+
|
|
101
|
+
for child in body_node.children:
|
|
102
|
+
visit_node(child, 1)
|
|
103
|
+
|
|
104
|
+
return max_depth, max_depth_line
|
|
105
|
+
|
|
106
|
+
def find_all_functions(self, root_node: Node) -> list[tuple[Node, str]]:
|
|
107
|
+
"""Find all function definitions in TypeScript AST.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
root_node: Tree-sitter root node
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of tuples: (function_node, function_name)
|
|
114
|
+
"""
|
|
115
|
+
if not TREE_SITTER_AVAILABLE:
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
functions: list[tuple[Node, str]] = []
|
|
119
|
+
self._collect_functions(root_node, functions)
|
|
120
|
+
return functions
|
|
121
|
+
|
|
122
|
+
def _collect_functions(self, node: Node, functions: list[tuple[Node, str]]) -> None:
|
|
123
|
+
"""Recursively collect function nodes from AST."""
|
|
124
|
+
function_entry = self._extract_function_if_applicable(node)
|
|
125
|
+
if function_entry:
|
|
126
|
+
functions.append(function_entry)
|
|
127
|
+
|
|
128
|
+
for child in node.children:
|
|
129
|
+
self._collect_functions(child, functions)
|
|
130
|
+
|
|
131
|
+
def _extract_function_if_applicable(self, node: Node) -> tuple[Node, str] | None:
|
|
132
|
+
"""Extract function node and name if node is a function type."""
|
|
133
|
+
if node.type == "function_declaration":
|
|
134
|
+
return self._extract_function_declaration(node)
|
|
135
|
+
if node.type == "arrow_function":
|
|
136
|
+
return self._extract_arrow_function(node)
|
|
137
|
+
if node.type == "method_definition":
|
|
138
|
+
return self._extract_method_definition(node)
|
|
139
|
+
if node.type in ("function_expression", "function"):
|
|
140
|
+
return self._extract_function_expression(node)
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def _extract_function_declaration(self, node: Node) -> tuple[Node, str]:
|
|
144
|
+
"""Extract name from function declaration node."""
|
|
145
|
+
name_node = self._find_child_by_type(node, "identifier")
|
|
146
|
+
name = name_node.text.decode("utf8") if name_node and name_node.text else "<anonymous>"
|
|
147
|
+
return (node, name)
|
|
148
|
+
|
|
149
|
+
def _extract_arrow_function(self, node: Node) -> tuple[Node, str]:
|
|
150
|
+
"""Extract name from arrow function node."""
|
|
151
|
+
name = "<arrow>"
|
|
152
|
+
parent = node.parent
|
|
153
|
+
if parent and parent.type == "variable_declarator":
|
|
154
|
+
id_node = self._find_child_by_type(parent, "identifier")
|
|
155
|
+
if id_node and id_node.text:
|
|
156
|
+
name = id_node.text.decode("utf8")
|
|
157
|
+
return (node, name)
|
|
158
|
+
|
|
159
|
+
def _extract_method_definition(self, node: Node) -> tuple[Node, str]:
|
|
160
|
+
"""Extract name from method definition node."""
|
|
161
|
+
name_node = self._find_child_by_type(node, "property_identifier")
|
|
162
|
+
name = name_node.text.decode("utf8") if name_node and name_node.text else "<method>"
|
|
163
|
+
return (node, name)
|
|
164
|
+
|
|
165
|
+
def _extract_function_expression(self, node: Node) -> tuple[Node, str]:
|
|
166
|
+
"""Extract name from function expression node."""
|
|
167
|
+
name = "<function>"
|
|
168
|
+
parent = node.parent
|
|
169
|
+
if parent and parent.type == "variable_declarator":
|
|
170
|
+
id_node = self._find_child_by_type(parent, "identifier")
|
|
171
|
+
if id_node and id_node.text:
|
|
172
|
+
name = id_node.text.decode("utf8")
|
|
173
|
+
return (node, name)
|
|
174
|
+
|
|
175
|
+
def _find_child_by_type(self, node: Node, child_type: str) -> Node | None:
|
|
176
|
+
"""Find first child node matching the given type."""
|
|
177
|
+
for child in node.children:
|
|
178
|
+
if child.type == child_type:
|
|
179
|
+
return child
|
|
180
|
+
return None
|