thailint 0.15.5__py3-none-any.whl → 0.15.6__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/core/cli_utils.py +24 -4
- src/linters/magic_numbers/config.py +38 -7
- src/linters/magic_numbers/definition_detector.py +226 -0
- src/linters/magic_numbers/linter.py +7 -0
- src/linters/nesting/python_analyzer.py +102 -39
- src/linters/stateless_class/config.py +4 -0
- src/linters/stateless_class/linter.py +116 -4
- src/linters/stateless_class/python_analyzer.py +86 -4
- {thailint-0.15.5.dist-info → thailint-0.15.6.dist-info}/METADATA +1 -1
- {thailint-0.15.5.dist-info → thailint-0.15.6.dist-info}/RECORD +13 -12
- {thailint-0.15.5.dist-info → thailint-0.15.6.dist-info}/WHEEL +0 -0
- {thailint-0.15.5.dist-info → thailint-0.15.6.dist-info}/entry_points.txt +0 -0
- {thailint-0.15.5.dist-info → thailint-0.15.6.dist-info}/licenses/LICENSE +0 -0
src/core/cli_utils.py
CHANGED
|
@@ -143,6 +143,24 @@ def _load_json_config(config_file: Path) -> dict[str, Any]:
|
|
|
143
143
|
return dict(result) if isinstance(result, dict) else {}
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
def _sanitize_string(text: str) -> str:
|
|
147
|
+
"""Remove or replace surrogate characters that can't be encoded to UTF-8.
|
|
148
|
+
|
|
149
|
+
Surrogate characters (U+D800-U+DFFF) appear when Python reads filesystem paths
|
|
150
|
+
or file content with invalid UTF-8 bytes using surrogateescape error handling.
|
|
151
|
+
These characters cannot be encoded to UTF-8 and cause UnicodeEncodeError.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
text: String that may contain surrogate characters
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
String with surrogates replaced by the Unicode replacement character
|
|
158
|
+
"""
|
|
159
|
+
# Encode with surrogateescape to handle surrogates, then decode back
|
|
160
|
+
# This effectively replaces surrogates with a replacement representation
|
|
161
|
+
return text.encode("utf-8", errors="surrogateescape").decode("utf-8", errors="replace")
|
|
162
|
+
|
|
163
|
+
|
|
146
164
|
def format_violations(violations: list, output_format: str) -> None:
|
|
147
165
|
"""Format and print violations to console.
|
|
148
166
|
|
|
@@ -168,10 +186,10 @@ def _output_json(violations: list) -> None:
|
|
|
168
186
|
"violations": [
|
|
169
187
|
{
|
|
170
188
|
"rule_id": v.rule_id,
|
|
171
|
-
"file_path": str(v.file_path),
|
|
189
|
+
"file_path": _sanitize_string(str(v.file_path)),
|
|
172
190
|
"line": v.line,
|
|
173
191
|
"column": v.column,
|
|
174
|
-
"message": v.message,
|
|
192
|
+
"message": _sanitize_string(v.message),
|
|
175
193
|
"severity": v.severity.name,
|
|
176
194
|
}
|
|
177
195
|
for v in violations
|
|
@@ -215,9 +233,11 @@ def _print_violation(v: Any) -> None:
|
|
|
215
233
|
Args:
|
|
216
234
|
v: Violation object with file_path, line, column, severity, rule_id, message
|
|
217
235
|
"""
|
|
218
|
-
|
|
236
|
+
file_path = _sanitize_string(str(v.file_path))
|
|
237
|
+
message = _sanitize_string(v.message)
|
|
238
|
+
location = f"{file_path}:{v.line}" if v.line else file_path
|
|
219
239
|
if v.column:
|
|
220
240
|
location += f":{v.column}"
|
|
221
241
|
click.echo(f" {location}")
|
|
222
|
-
click.echo(f" [{v.severity.name}] {v.rule_id}: {
|
|
242
|
+
click.echo(f" [{v.severity.name}] {v.rule_id}: {message}")
|
|
223
243
|
click.echo()
|
|
@@ -4,8 +4,9 @@ Purpose: Configuration schema for magic numbers linter
|
|
|
4
4
|
Scope: MagicNumberConfig dataclass with allowed_numbers and max_small_integer settings
|
|
5
5
|
|
|
6
6
|
Overview: Defines configuration schema for magic numbers linter. Provides MagicNumberConfig dataclass
|
|
7
|
-
with allowed_numbers set (default includes common acceptable numbers like -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000
|
|
8
|
-
and
|
|
7
|
+
with allowed_numbers set (default includes common acceptable numbers like -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000
|
|
8
|
+
and standard ports like 80, 443, 22, 21, 8080, 8443, 3000, 5000) and max_small_integer threshold (default 10)
|
|
9
|
+
for range() contexts. Supports per-file and per-directory
|
|
9
10
|
config overrides through from_dict class method. Validates that configuration values are appropriate
|
|
10
11
|
types. Integrates with orchestrator's configuration system to allow users to customize allowed numbers
|
|
11
12
|
via .thailint.yaml configuration files.
|
|
@@ -18,11 +19,41 @@ Interfaces: MagicNumberConfig(allowed_numbers: set, max_small_integer: int, enab
|
|
|
18
19
|
from_dict class method for loading configuration from dictionary
|
|
19
20
|
|
|
20
21
|
Implementation: Dataclass with validation and defaults, matches reference implementation patterns
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- unnecessary-lambda: The lambda is required here because dataclass field default_factory
|
|
25
|
+
needs a callable, and DEFAULT_ALLOWED_NUMBERS.copy() is a method call, not a callable.
|
|
26
|
+
Using `default_factory=DEFAULT_ALLOWED_NUMBERS.copy` would call the method at class
|
|
27
|
+
definition time, not at instance creation time.
|
|
21
28
|
"""
|
|
22
29
|
|
|
23
30
|
from dataclasses import dataclass, field
|
|
24
31
|
from typing import Any
|
|
25
32
|
|
|
33
|
+
# Default allowed numbers including common small integers and standard ports
|
|
34
|
+
DEFAULT_ALLOWED_NUMBERS: set[int | float] = {
|
|
35
|
+
# Common small integers
|
|
36
|
+
-1,
|
|
37
|
+
0,
|
|
38
|
+
1,
|
|
39
|
+
2,
|
|
40
|
+
3,
|
|
41
|
+
4,
|
|
42
|
+
5,
|
|
43
|
+
10,
|
|
44
|
+
100,
|
|
45
|
+
1000,
|
|
46
|
+
# Standard ports
|
|
47
|
+
21, # FTP
|
|
48
|
+
22, # SSH
|
|
49
|
+
80, # HTTP
|
|
50
|
+
443, # HTTPS
|
|
51
|
+
3000, # Common dev server (Node.js, Rails)
|
|
52
|
+
5000, # Flask default
|
|
53
|
+
8080, # Alternate HTTP
|
|
54
|
+
8443, # Alternate HTTPS
|
|
55
|
+
}
|
|
56
|
+
|
|
26
57
|
|
|
27
58
|
@dataclass
|
|
28
59
|
class MagicNumberConfig:
|
|
@@ -30,10 +61,11 @@ class MagicNumberConfig:
|
|
|
30
61
|
|
|
31
62
|
enabled: bool = True
|
|
32
63
|
allowed_numbers: set[int | float] = field(
|
|
33
|
-
default_factory=lambda:
|
|
64
|
+
default_factory=lambda: DEFAULT_ALLOWED_NUMBERS.copy() # pylint: disable=unnecessary-lambda
|
|
34
65
|
)
|
|
35
66
|
max_small_integer: int = 10
|
|
36
67
|
ignore: list[str] = field(default_factory=list)
|
|
68
|
+
exempt_definition_files: bool = True
|
|
37
69
|
|
|
38
70
|
def __post_init__(self) -> None:
|
|
39
71
|
"""Validate configuration values."""
|
|
@@ -58,16 +90,14 @@ class MagicNumberConfig:
|
|
|
58
90
|
allowed_numbers = set(
|
|
59
91
|
lang_config.get(
|
|
60
92
|
"allowed_numbers",
|
|
61
|
-
config.get("allowed_numbers",
|
|
93
|
+
config.get("allowed_numbers", DEFAULT_ALLOWED_NUMBERS),
|
|
62
94
|
)
|
|
63
95
|
)
|
|
64
96
|
max_small_integer = lang_config.get(
|
|
65
97
|
"max_small_integer", config.get("max_small_integer", 10)
|
|
66
98
|
)
|
|
67
99
|
else:
|
|
68
|
-
allowed_numbers = set(
|
|
69
|
-
config.get("allowed_numbers", {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000})
|
|
70
|
-
)
|
|
100
|
+
allowed_numbers = set(config.get("allowed_numbers", DEFAULT_ALLOWED_NUMBERS))
|
|
71
101
|
max_small_integer = config.get("max_small_integer", 10)
|
|
72
102
|
|
|
73
103
|
ignore_patterns = config.get("ignore", [])
|
|
@@ -79,4 +109,5 @@ class MagicNumberConfig:
|
|
|
79
109
|
allowed_numbers=allowed_numbers,
|
|
80
110
|
max_small_integer=max_small_integer,
|
|
81
111
|
ignore=ignore_patterns,
|
|
112
|
+
exempt_definition_files=config.get("exempt_definition_files", True),
|
|
82
113
|
)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Detect constant definition files that should be exempt from magic number checking
|
|
3
|
+
|
|
4
|
+
Scope: File-level detection of definition patterns (status codes, constants files)
|
|
5
|
+
|
|
6
|
+
Overview: Provides functions to detect if a file is a constant definition file that should
|
|
7
|
+
be exempt from magic number violations. Definition files exist specifically to define
|
|
8
|
+
named constants and shouldn't be flagged. Detection is based on:
|
|
9
|
+
1. Filename patterns (*_codes.py, *_constants.py, constants.py)
|
|
10
|
+
2. Content patterns (dicts with 5+ int keys, 10+ UPPERCASE constant assignments)
|
|
11
|
+
Files matching these patterns contain legitimate constant definitions.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for parsing, pathlib for Path handling, re for pattern matching
|
|
14
|
+
|
|
15
|
+
Exports: is_definition_file function
|
|
16
|
+
|
|
17
|
+
Interfaces: is_definition_file(file_path, content) -> bool
|
|
18
|
+
|
|
19
|
+
Implementation: Filename pattern matching and AST-based content analysis
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
import re
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
# Threshold for number of UPPERCASE constants to consider a file as definition file
|
|
27
|
+
MIN_UPPERCASE_CONSTANTS = 10
|
|
28
|
+
|
|
29
|
+
# Threshold for number of int keys in a dict to consider it a definition pattern
|
|
30
|
+
MIN_DICT_INT_KEYS = 5
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_definition_file(file_path: Path | str | None, content: str | None) -> bool:
|
|
34
|
+
"""Check if file is a constant definition file that should be exempt.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
file_path: Path to the file
|
|
38
|
+
content: File content
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if file is a definition file that should be exempt
|
|
42
|
+
"""
|
|
43
|
+
if _matches_definition_filename(file_path):
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
if content and _has_definition_content_patterns(content):
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _matches_definition_filename(file_path: Path | str | None) -> bool:
|
|
53
|
+
"""Check if filename matches definition file patterns.
|
|
54
|
+
|
|
55
|
+
Patterns:
|
|
56
|
+
- *_codes.py (status_codes.py, error_codes.py, etc.)
|
|
57
|
+
- *_constants.py (app_constants.py, etc.)
|
|
58
|
+
- constants.py
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
file_path: Path to the file
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if filename matches definition patterns
|
|
65
|
+
"""
|
|
66
|
+
if not file_path:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
file_name = Path(file_path).name.lower()
|
|
70
|
+
|
|
71
|
+
# Check for *_codes.py pattern
|
|
72
|
+
if file_name.endswith("_codes.py"):
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
# Check for constants.py or *_constants.py
|
|
76
|
+
if file_name == "constants.py" or file_name.endswith("_constants.py"):
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _has_definition_content_patterns(content: str) -> bool:
|
|
83
|
+
"""Check if content has definition file patterns.
|
|
84
|
+
|
|
85
|
+
Patterns:
|
|
86
|
+
- 10+ UPPERCASE constant assignments
|
|
87
|
+
- Dict with 5+ integer keys
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
content: File content
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if content matches definition patterns
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
tree = ast.parse(content)
|
|
97
|
+
except SyntaxError:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# Check for many UPPERCASE constants
|
|
101
|
+
if _count_uppercase_constants(tree) >= MIN_UPPERCASE_CONSTANTS:
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
# Check for dicts with many int keys
|
|
105
|
+
if _has_dict_with_int_keys(tree):
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _count_uppercase_constants(tree: ast.Module) -> int:
|
|
112
|
+
"""Count UPPERCASE constant assignments at module level.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
tree: Parsed AST module
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Number of UPPERCASE constant assignments
|
|
119
|
+
"""
|
|
120
|
+
count = 0
|
|
121
|
+
for node in tree.body:
|
|
122
|
+
if isinstance(node, ast.Assign):
|
|
123
|
+
count += _count_numeric_constant_targets(node)
|
|
124
|
+
return count
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _count_numeric_constant_targets(assign_node: ast.Assign) -> int:
|
|
128
|
+
"""Count UPPERCASE constant targets with numeric values in an assignment.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
assign_node: AST Assign node
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Number of uppercase constant targets with numeric values
|
|
135
|
+
"""
|
|
136
|
+
if not _is_numeric_constant(assign_node.value):
|
|
137
|
+
return 0
|
|
138
|
+
return sum(1 for t in assign_node.targets if _is_uppercase_name_target(t))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _is_numeric_constant(value: ast.expr) -> bool:
|
|
142
|
+
"""Check if value is a numeric constant.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
value: AST expression node
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
True if value is a numeric constant
|
|
149
|
+
"""
|
|
150
|
+
return isinstance(value, ast.Constant) and isinstance(value.value, (int, float))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _is_uppercase_name_target(target: ast.expr) -> bool:
|
|
154
|
+
"""Check if target is an uppercase name.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
target: AST expression node
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if target is an uppercase Name node
|
|
161
|
+
"""
|
|
162
|
+
return isinstance(target, ast.Name) and _is_constant_name(target.id)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _is_constant_name(name: str) -> bool:
|
|
166
|
+
"""Check if name follows UPPERCASE constant convention.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
name: Variable name
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if name is UPPERCASE (with underscores allowed)
|
|
173
|
+
"""
|
|
174
|
+
# Must be uppercase and contain at least 2 characters
|
|
175
|
+
if len(name) < 2:
|
|
176
|
+
return False
|
|
177
|
+
# Allow underscores but must have uppercase letters
|
|
178
|
+
return re.match(r"^[A-Z][A-Z0-9_]*$", name) is not None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _has_dict_with_int_keys(tree: ast.Module) -> bool:
|
|
182
|
+
"""Check if module has a dict with many integer keys.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
tree: Parsed AST module
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True if there's a dict with MIN_DICT_INT_KEYS+ int keys
|
|
189
|
+
"""
|
|
190
|
+
return any(_has_enough_int_keys(node) for node in ast.walk(tree) if isinstance(node, ast.Dict))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _has_enough_int_keys(dict_node: ast.Dict) -> bool:
|
|
194
|
+
"""Check if dict has enough integer keys to be a definition pattern.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
dict_node: AST Dict node
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
True if dict has MIN_DICT_INT_KEYS or more integer keys
|
|
201
|
+
"""
|
|
202
|
+
return _count_int_keys(dict_node) >= MIN_DICT_INT_KEYS
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _count_int_keys(dict_node: ast.Dict) -> int:
|
|
206
|
+
"""Count integer keys in a dict.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
dict_node: AST Dict node
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Number of integer constant keys
|
|
213
|
+
"""
|
|
214
|
+
return sum(1 for key in dict_node.keys if _is_int_key(key))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _is_int_key(key: ast.expr | None) -> bool:
|
|
218
|
+
"""Check if key is an integer constant.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
key: AST expression node (or None for **dict unpacking)
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
True if key is an integer constant
|
|
225
|
+
"""
|
|
226
|
+
return isinstance(key, ast.Constant) and isinstance(key.value, int)
|
|
@@ -40,6 +40,7 @@ from src.linter_config.ignore import get_ignore_parser
|
|
|
40
40
|
|
|
41
41
|
from .config import MagicNumberConfig
|
|
42
42
|
from .context_analyzer import is_acceptable_context
|
|
43
|
+
from .definition_detector import is_definition_file
|
|
43
44
|
from .python_analyzer import PythonMagicNumberAnalyzer
|
|
44
45
|
from .typescript_analyzer import TypeScriptMagicNumberAnalyzer
|
|
45
46
|
from .typescript_ignore_checker import TypeScriptIgnoreChecker
|
|
@@ -174,6 +175,12 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
174
175
|
if self._is_file_ignored(context, config):
|
|
175
176
|
return []
|
|
176
177
|
|
|
178
|
+
# Check if file is a definition file (status_codes.py, constants.py, etc.)
|
|
179
|
+
if config.exempt_definition_files and is_definition_file(
|
|
180
|
+
context.file_path, context.file_content
|
|
181
|
+
):
|
|
182
|
+
return []
|
|
183
|
+
|
|
177
184
|
tree = self._parse_python_code(context.file_content)
|
|
178
185
|
if tree is None:
|
|
179
186
|
return []
|
|
@@ -5,10 +5,12 @@ Scope: Python code nesting depth analysis using ast module
|
|
|
5
5
|
|
|
6
6
|
Overview: Analyzes Python code to calculate maximum nesting depth using AST traversal. Implements
|
|
7
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.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
for If, For, While, With, AsyncWith, Try, ExceptHandler, Match, and match_case nodes. Correctly
|
|
9
|
+
handles elif chains by detecting when an If node is in elif position (sole child in parent's
|
|
10
|
+
orelse list) and not incrementing depth. Starts depth counting at 1 for function body, matching
|
|
11
|
+
reference implementation behavior. Returns maximum depth found and location information for
|
|
12
|
+
violation reporting. Provides helper method to find all function definitions in an AST tree
|
|
13
|
+
for batch processing.
|
|
12
14
|
|
|
13
15
|
Dependencies: ast module for Python parsing
|
|
14
16
|
|
|
@@ -16,11 +18,37 @@ Exports: PythonNestingAnalyzer class with calculate_max_depth method
|
|
|
16
18
|
|
|
17
19
|
Interfaces: calculate_max_depth(func_node: ast.FunctionDef) -> tuple[int, int], find_all_functions
|
|
18
20
|
|
|
19
|
-
Implementation: AST visitor pattern with depth tracking,
|
|
21
|
+
Implementation: AST visitor pattern with depth tracking, elif detection via parent orelse inspection
|
|
20
22
|
"""
|
|
21
23
|
|
|
22
24
|
import ast
|
|
23
25
|
|
|
26
|
+
# Control structure types that increase nesting depth
|
|
27
|
+
_CONTROL_STRUCTURES = (
|
|
28
|
+
ast.For,
|
|
29
|
+
ast.While,
|
|
30
|
+
ast.With,
|
|
31
|
+
ast.AsyncWith,
|
|
32
|
+
ast.Try,
|
|
33
|
+
ast.Match,
|
|
34
|
+
ast.match_case,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _DepthTracker:
|
|
39
|
+
"""Tracks maximum nesting depth during AST traversal."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, default_line: int) -> None:
|
|
42
|
+
"""Initialize tracker with default line number."""
|
|
43
|
+
self.max_depth = 0
|
|
44
|
+
self.max_depth_line = default_line
|
|
45
|
+
|
|
46
|
+
def record(self, node: ast.AST, depth: int, default_line: int) -> None:
|
|
47
|
+
"""Record depth if it's the new maximum."""
|
|
48
|
+
if depth > self.max_depth:
|
|
49
|
+
self.max_depth = depth
|
|
50
|
+
self.max_depth_line = getattr(node, "lineno", default_line)
|
|
51
|
+
|
|
24
52
|
|
|
25
53
|
class PythonNestingAnalyzer:
|
|
26
54
|
"""Calculates maximum nesting depth in Python functions."""
|
|
@@ -40,42 +68,12 @@ class PythonNestingAnalyzer:
|
|
|
40
68
|
Returns:
|
|
41
69
|
Tuple of (max_depth, line_number_of_max_depth)
|
|
42
70
|
"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def visit_node(node: ast.AST, current_depth: int = 0) -> None:
|
|
47
|
-
nonlocal max_depth, max_depth_line
|
|
48
|
-
|
|
49
|
-
if current_depth > max_depth:
|
|
50
|
-
max_depth = current_depth
|
|
51
|
-
max_depth_line = getattr(node, "lineno", func_node.lineno)
|
|
52
|
-
|
|
53
|
-
# Nodes that increase nesting depth
|
|
54
|
-
if isinstance(
|
|
55
|
-
node,
|
|
56
|
-
(
|
|
57
|
-
ast.If,
|
|
58
|
-
ast.For,
|
|
59
|
-
ast.While,
|
|
60
|
-
ast.With,
|
|
61
|
-
ast.AsyncWith,
|
|
62
|
-
ast.Try,
|
|
63
|
-
ast.ExceptHandler,
|
|
64
|
-
ast.Match,
|
|
65
|
-
ast.match_case,
|
|
66
|
-
),
|
|
67
|
-
):
|
|
68
|
-
current_depth += 1
|
|
69
|
-
|
|
70
|
-
# Visit children
|
|
71
|
-
for child in ast.iter_child_nodes(node):
|
|
72
|
-
visit_node(child, current_depth)
|
|
73
|
-
|
|
74
|
-
# Start at depth 1 for function body (matching reference implementation)
|
|
71
|
+
tracker = _DepthTracker(func_node.lineno)
|
|
72
|
+
|
|
75
73
|
for stmt in func_node.body:
|
|
76
|
-
|
|
74
|
+
_visit_node(stmt, 0, tracker, func_node.lineno)
|
|
77
75
|
|
|
78
|
-
return max_depth, max_depth_line
|
|
76
|
+
return tracker.max_depth, tracker.max_depth_line
|
|
79
77
|
|
|
80
78
|
def find_all_functions(self, tree: ast.AST) -> list[ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
81
79
|
"""Find all function definitions in AST.
|
|
@@ -91,3 +89,68 @@ class PythonNestingAnalyzer:
|
|
|
91
89
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
92
90
|
functions.append(node)
|
|
93
91
|
return functions
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _visit_node(
|
|
95
|
+
node: ast.AST,
|
|
96
|
+
current_depth: int,
|
|
97
|
+
tracker: _DepthTracker,
|
|
98
|
+
default_line: int,
|
|
99
|
+
is_elif: bool = False,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Visit AST node, tracking nesting depth for control structures only."""
|
|
102
|
+
if isinstance(node, ast.If):
|
|
103
|
+
_visit_if_node(node, current_depth, tracker, default_line, is_elif)
|
|
104
|
+
elif isinstance(node, _CONTROL_STRUCTURES):
|
|
105
|
+
_visit_control_structure(node, current_depth, tracker, default_line)
|
|
106
|
+
else:
|
|
107
|
+
_visit_children(node, current_depth, tracker, default_line)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _visit_if_node(
|
|
111
|
+
node: ast.If, current_depth: int, tracker: _DepthTracker, default_line: int, is_elif: bool
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Visit If node with special elif handling."""
|
|
114
|
+
if not is_elif:
|
|
115
|
+
current_depth += 1
|
|
116
|
+
tracker.record(node, current_depth, default_line)
|
|
117
|
+
|
|
118
|
+
# Visit body
|
|
119
|
+
for child in node.body:
|
|
120
|
+
_visit_node(child, current_depth, tracker, default_line)
|
|
121
|
+
|
|
122
|
+
# Handle orelse - check for elif chain
|
|
123
|
+
if _is_elif_chain(node.orelse):
|
|
124
|
+
_visit_node(node.orelse[0], current_depth, tracker, default_line, is_elif=True)
|
|
125
|
+
else:
|
|
126
|
+
for child in node.orelse:
|
|
127
|
+
_visit_node(child, current_depth, tracker, default_line)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _visit_control_structure(
|
|
131
|
+
node: ast.AST, current_depth: int, tracker: _DepthTracker, default_line: int
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Visit a control structure node that increases depth."""
|
|
134
|
+
current_depth += 1
|
|
135
|
+
tracker.record(node, current_depth, default_line)
|
|
136
|
+
_visit_children(node, current_depth, tracker, default_line)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _visit_children(
|
|
140
|
+
node: ast.AST, current_depth: int, tracker: _DepthTracker, default_line: int
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Visit all children of a node without incrementing depth."""
|
|
143
|
+
for child in ast.iter_child_nodes(node):
|
|
144
|
+
_visit_node(child, current_depth, tracker, default_line)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _is_elif_chain(orelse: list[ast.stmt]) -> bool:
|
|
148
|
+
"""Check if orelse list represents an elif (single If node).
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
orelse: The orelse list from an If node
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True if this is an elif (single If in orelse), False otherwise
|
|
155
|
+
"""
|
|
156
|
+
return len(orelse) == 1 and isinstance(orelse[0], ast.If)
|
|
@@ -30,6 +30,8 @@ class StatelessClassConfig:
|
|
|
30
30
|
enabled: bool = True
|
|
31
31
|
min_methods: int = 2
|
|
32
32
|
ignore: list[str] = field(default_factory=list)
|
|
33
|
+
exempt_test_classes: bool = True
|
|
34
|
+
exempt_mixins: bool = True
|
|
33
35
|
|
|
34
36
|
@classmethod
|
|
35
37
|
def from_dict(
|
|
@@ -55,4 +57,6 @@ class StatelessClassConfig:
|
|
|
55
57
|
enabled=config.get("enabled", True),
|
|
56
58
|
min_methods=config.get("min_methods", 2),
|
|
57
59
|
ignore=ignore_patterns,
|
|
60
|
+
exempt_test_classes=config.get("exempt_test_classes", True),
|
|
61
|
+
exempt_mixins=config.get("exempt_mixins", True),
|
|
58
62
|
)
|
|
@@ -27,6 +27,8 @@ Suppressions:
|
|
|
27
27
|
exceeds limit due to comprehensive 5-level ignore system support.
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
+
import ast
|
|
31
|
+
from collections.abc import Callable
|
|
30
32
|
from pathlib import Path
|
|
31
33
|
|
|
32
34
|
from src.core.base import BaseLintContext, BaseLintRule
|
|
@@ -36,7 +38,13 @@ from src.linter_config.ignore import get_ignore_parser
|
|
|
36
38
|
from src.linter_config.rule_matcher import rule_matches
|
|
37
39
|
|
|
38
40
|
from .config import StatelessClassConfig
|
|
39
|
-
from .python_analyzer import
|
|
41
|
+
from .python_analyzer import (
|
|
42
|
+
ClassInfo,
|
|
43
|
+
StatelessClassAnalyzer,
|
|
44
|
+
is_mixin_class,
|
|
45
|
+
is_test_class,
|
|
46
|
+
is_test_file,
|
|
47
|
+
)
|
|
40
48
|
|
|
41
49
|
|
|
42
50
|
class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
@@ -74,16 +82,41 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
74
82
|
return []
|
|
75
83
|
|
|
76
84
|
config = self._load_config(context)
|
|
77
|
-
if not config.enabled
|
|
85
|
+
if not config.enabled:
|
|
86
|
+
return []
|
|
87
|
+
if self._should_skip_file(context, config):
|
|
78
88
|
return []
|
|
79
89
|
|
|
80
90
|
# _should_analyze ensures file_content is set
|
|
81
91
|
assert context.file_content is not None # nosec B101
|
|
82
92
|
|
|
93
|
+
stateless_classes = self._find_stateless_classes(context, config)
|
|
94
|
+
return self._filter_ignored_violations(stateless_classes, context)
|
|
95
|
+
|
|
96
|
+
def _find_stateless_classes(
|
|
97
|
+
self, context: BaseLintContext, config: StatelessClassConfig
|
|
98
|
+
) -> list[ClassInfo]:
|
|
99
|
+
"""Find stateless classes and apply exemptions.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
context: Lint context
|
|
103
|
+
config: Configuration
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
List of stateless classes after applying exemptions
|
|
107
|
+
"""
|
|
108
|
+
assert context.file_content is not None # nosec B101
|
|
109
|
+
|
|
83
110
|
analyzer = StatelessClassAnalyzer(min_methods=config.min_methods)
|
|
84
|
-
|
|
111
|
+
classes = analyzer.analyze(context.file_content)
|
|
85
112
|
|
|
86
|
-
|
|
113
|
+
if config.exempt_test_classes:
|
|
114
|
+
classes = self._filter_test_classes(classes, context)
|
|
115
|
+
|
|
116
|
+
if config.exempt_mixins:
|
|
117
|
+
classes = self._filter_mixin_classes(classes, context)
|
|
118
|
+
|
|
119
|
+
return classes
|
|
87
120
|
|
|
88
121
|
def _should_skip_file(self, context: BaseLintContext, config: StatelessClassConfig) -> bool:
|
|
89
122
|
"""Check if file should be skipped due to ignore patterns or directives.
|
|
@@ -230,6 +263,85 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
|
230
263
|
"""
|
|
231
264
|
return rule_matches(self.rule_id, rule_pattern)
|
|
232
265
|
|
|
266
|
+
def _parse_class_nodes(self, context: BaseLintContext) -> dict[str, ast.ClassDef] | None:
|
|
267
|
+
"""Parse code and build map of class names to AST nodes.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
context: Lint context
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Dict mapping class names to ClassDef nodes, or None if parsing fails
|
|
274
|
+
"""
|
|
275
|
+
if not context.file_content:
|
|
276
|
+
return None
|
|
277
|
+
try:
|
|
278
|
+
tree = ast.parse(context.file_content)
|
|
279
|
+
except SyntaxError:
|
|
280
|
+
return None
|
|
281
|
+
return {node.name: node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)}
|
|
282
|
+
|
|
283
|
+
def _filter_test_classes(
|
|
284
|
+
self, classes: list[ClassInfo], context: BaseLintContext
|
|
285
|
+
) -> list[ClassInfo]:
|
|
286
|
+
"""Filter out test classes from stateless class list.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
classes: List of stateless classes found
|
|
290
|
+
context: Lint context
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
List of classes with test classes removed
|
|
294
|
+
"""
|
|
295
|
+
# If file is a test file, exempt all classes
|
|
296
|
+
if is_test_file(str(context.file_path) if context.file_path else None):
|
|
297
|
+
return []
|
|
298
|
+
|
|
299
|
+
class_nodes = self._parse_class_nodes(context)
|
|
300
|
+
if class_nodes is None:
|
|
301
|
+
return classes
|
|
302
|
+
|
|
303
|
+
return self._filter_by_predicate(classes, class_nodes, is_test_class)
|
|
304
|
+
|
|
305
|
+
def _filter_by_predicate(
|
|
306
|
+
self,
|
|
307
|
+
classes: list[ClassInfo],
|
|
308
|
+
class_nodes: dict[str, ast.ClassDef],
|
|
309
|
+
predicate: Callable[[ast.ClassDef], bool],
|
|
310
|
+
) -> list[ClassInfo]:
|
|
311
|
+
"""Filter classes based on a predicate function.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
classes: List of class info to filter
|
|
315
|
+
class_nodes: Map of class names to AST nodes
|
|
316
|
+
predicate: Function that returns True for classes to exclude
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
List of classes not matching the predicate
|
|
320
|
+
"""
|
|
321
|
+
return [
|
|
322
|
+
info
|
|
323
|
+
for info in classes
|
|
324
|
+
if info.name not in class_nodes or not predicate(class_nodes[info.name])
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
def _filter_mixin_classes(
|
|
328
|
+
self, classes: list[ClassInfo], context: BaseLintContext
|
|
329
|
+
) -> list[ClassInfo]:
|
|
330
|
+
"""Filter out mixin classes from stateless class list.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
classes: List of stateless classes found
|
|
334
|
+
context: Lint context
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
List of classes with mixin classes removed
|
|
338
|
+
"""
|
|
339
|
+
class_nodes = self._parse_class_nodes(context)
|
|
340
|
+
if class_nodes is None:
|
|
341
|
+
return classes
|
|
342
|
+
|
|
343
|
+
return self._filter_by_predicate(classes, class_nodes, is_mixin_class)
|
|
344
|
+
|
|
233
345
|
def _filter_ignored_violations(
|
|
234
346
|
self, classes: list[ClassInfo], context: BaseLintContext
|
|
235
347
|
) -> list[Violation]:
|
|
@@ -6,14 +6,16 @@ Scope: AST-based analysis of Python class definitions for stateless patterns
|
|
|
6
6
|
Overview: Analyzes Python source code using AST to detect classes that have no
|
|
7
7
|
constructor (__init__ or __new__), no instance state (self.attr assignments),
|
|
8
8
|
and 2+ methods - indicating they should be refactored to module-level functions.
|
|
9
|
-
Excludes legitimate patterns like ABC, Protocol, decorated classes,
|
|
10
|
-
with class-level attributes
|
|
9
|
+
Excludes legitimate patterns like ABC, Protocol, decorated classes, classes
|
|
10
|
+
with class-level attributes, test classes (Test* prefix or TestCase inheritance),
|
|
11
|
+
and mixin classes (name contains "Mixin").
|
|
11
12
|
|
|
12
13
|
Dependencies: Python AST module
|
|
13
14
|
|
|
14
|
-
Exports: analyze_code function, ClassInfo dataclass
|
|
15
|
+
Exports: analyze_code function, ClassInfo dataclass, is_test_class function
|
|
15
16
|
|
|
16
|
-
Interfaces: analyze_code(code) -> list[ClassInfo] returning detected stateless classes
|
|
17
|
+
Interfaces: analyze_code(code) -> list[ClassInfo] returning detected stateless classes,
|
|
18
|
+
is_test_class(class_node) -> bool for test class detection
|
|
17
19
|
|
|
18
20
|
Implementation: AST visitor pattern with focused helper functions for different checks
|
|
19
21
|
"""
|
|
@@ -262,6 +264,86 @@ def _is_self_attribute(node: ast.expr) -> bool:
|
|
|
262
264
|
return node.value.id == "self"
|
|
263
265
|
|
|
264
266
|
|
|
267
|
+
def is_test_class(class_node: ast.ClassDef) -> bool:
|
|
268
|
+
"""Check if class is a test class that should be exempt.
|
|
269
|
+
|
|
270
|
+
Test classes are exempt because they commonly have multiple methods
|
|
271
|
+
without instance state (setup/teardown patterns, assertion methods).
|
|
272
|
+
|
|
273
|
+
Criteria:
|
|
274
|
+
- Class name starts with "Test"
|
|
275
|
+
- Class inherits from unittest.TestCase or TestCase
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
class_node: AST ClassDef node
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
True if class is a test class
|
|
282
|
+
"""
|
|
283
|
+
# Check class name
|
|
284
|
+
if class_node.name.startswith("Test"):
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
# Check base classes for TestCase
|
|
288
|
+
for base in class_node.bases:
|
|
289
|
+
base_name = _get_base_name(base)
|
|
290
|
+
if base_name in ("TestCase", "unittest.TestCase"):
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def is_test_file(file_path: str | None) -> bool:
|
|
297
|
+
"""Check if file is a test file based on path.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
file_path: Path to the file
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
True if file is in tests/ directory or named test_*.py
|
|
304
|
+
"""
|
|
305
|
+
if not file_path:
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
path_str = str(file_path)
|
|
309
|
+
return _is_in_tests_directory(path_str) or _has_test_filename(path_str)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _is_in_tests_directory(path_str: str) -> bool:
|
|
313
|
+
"""Check if path is in a tests/ directory."""
|
|
314
|
+
return (
|
|
315
|
+
"/tests/" in path_str
|
|
316
|
+
or "\\tests\\" in path_str
|
|
317
|
+
or path_str.startswith("tests/")
|
|
318
|
+
or path_str.startswith("tests\\")
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _has_test_filename(path_str: str) -> bool:
|
|
323
|
+
"""Check if path has a test_*.py filename."""
|
|
324
|
+
file_name = path_str.rsplit("/", maxsplit=1)[-1].rsplit("\\", maxsplit=1)[-1]
|
|
325
|
+
return file_name.startswith("test_")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def is_mixin_class(class_node: ast.ClassDef) -> bool:
|
|
329
|
+
"""Check if class is a mixin class that should be exempt.
|
|
330
|
+
|
|
331
|
+
Mixin classes provide reusable methods intended to be combined with other
|
|
332
|
+
classes via multiple inheritance. They commonly have multiple methods without
|
|
333
|
+
instance state, which is an intentional pattern.
|
|
334
|
+
|
|
335
|
+
Criteria:
|
|
336
|
+
- Class name contains "Mixin" (case-insensitive)
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
class_node: AST ClassDef node
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
True if class is a mixin class
|
|
343
|
+
"""
|
|
344
|
+
return "mixin" in class_node.name.lower()
|
|
345
|
+
|
|
346
|
+
|
|
265
347
|
# Legacy class wrapper for backward compatibility with linter.py
|
|
266
348
|
class StatelessClassAnalyzer:
|
|
267
349
|
"""Analyzes Python code for stateless classes.
|
|
@@ -23,7 +23,7 @@ src/cli_main.py,sha256=C0Ey7YNlG3ipqb3KsJZ8rL8PJ4ueVp_45IUirGidvHI,1618
|
|
|
23
23
|
src/config.py,sha256=O3ixzsYekGjlggmIsawCU1bctOa0MyG2IczHpg3mGyw,12753
|
|
24
24
|
src/core/__init__.py,sha256=5FtsDvhMt4SNRx3pbcGURrxn135XRbeRrjSUxiXwkNc,381
|
|
25
25
|
src/core/base.py,sha256=u5A8geprlKnsJk4ShiLHTKXRekZUB4I6rPQWxgiFeto,8019
|
|
26
|
-
src/core/cli_utils.py,sha256=
|
|
26
|
+
src/core/cli_utils.py,sha256=o7lWPSlic96lRyfcsmBf8S1ej25M1-wlclx8_Eup20g,7446
|
|
27
27
|
src/core/config_parser.py,sha256=CRHV2-csxag6yQzx_4IYYz57QSUYjPkeSb0XvOyshRI,4272
|
|
28
28
|
src/core/constants.py,sha256=PKtPDqk6k9VuOSgjq1FAdi2CTvlnhdXvLj91dNaMDTA,1584
|
|
29
29
|
src/core/linter_utils.py,sha256=StnKFzJgSvLyao1S0LpTKhsXo8nOwpdKpxo7mXl5PIg,8594
|
|
@@ -141,9 +141,10 @@ src/linters/lbyl/pattern_detectors/string_validator_detector.py,sha256=GXXcsCfmp
|
|
|
141
141
|
src/linters/lbyl/python_analyzer.py,sha256=auPrWFUEmFxtv1GB3exJzz7uX71gXqEUCHKO64UDR8w,8041
|
|
142
142
|
src/linters/lbyl/violation_builder.py,sha256=6wVX9U7Jq1ONWcGuasvIwJE9mXHcT778p0OcPC0Wx7w,10296
|
|
143
143
|
src/linters/magic_numbers/__init__.py,sha256=17dkCUf0uiYLvpOZF01VDojj92NzxXZMtRhrSBUzsdc,1689
|
|
144
|
-
src/linters/magic_numbers/config.py,sha256=
|
|
144
|
+
src/linters/magic_numbers/config.py,sha256=l18AO6XY6zkrX6_aumn3CZLpCsw185sfy5B_v2nY-OA,4233
|
|
145
145
|
src/linters/magic_numbers/context_analyzer.py,sha256=EgDyxxjvEqyD3FX0Fnxj5RcOPyvyVs_rYFxj2HOxYdg,7309
|
|
146
|
-
src/linters/magic_numbers/
|
|
146
|
+
src/linters/magic_numbers/definition_detector.py,sha256=brENrT17ofYzZUpFjAq05DeG4DS2pKdWAWm4DyGTrDY,6156
|
|
147
|
+
src/linters/magic_numbers/linter.py,sha256=JoguQjpWNt1Xp818HuzzCdTv9RqVKQoZXy6fFK8zk0o,17023
|
|
147
148
|
src/linters/magic_numbers/python_analyzer.py,sha256=Ba-EODvAkUIOhqMFv86MxMlXqF20ngvgubiWN_U_IUk,2446
|
|
148
149
|
src/linters/magic_numbers/typescript_analyzer.py,sha256=-2YPmNWXHJN8R2siV3pJk_3Baj-A9nnvQRpU35YBKgs,7519
|
|
149
150
|
src/linters/magic_numbers/typescript_ignore_checker.py,sha256=9JWqtXd8KU_GCc_66KSZT2X7uQhNGpxE2ikOyjcLyao,2847
|
|
@@ -156,7 +157,7 @@ src/linters/method_property/violation_builder.py,sha256=A7SwZWlVG_7W5pJiHOvIroI2
|
|
|
156
157
|
src/linters/nesting/__init__.py,sha256=tszmyCEQMpEwB5H84WcAUfRYDQl7jpsn04es5DtAHsM,3200
|
|
157
158
|
src/linters/nesting/config.py,sha256=PfPA2wJn3i6HHXeM0qu6Qx-v1KJdRwlRkFOdpf7NhS8,2405
|
|
158
159
|
src/linters/nesting/linter.py,sha256=bn5aPlxKZNw3T2LsOSfZUK_shkxcsdUS_LtFVsGJexk,6622
|
|
159
|
-
src/linters/nesting/python_analyzer.py,sha256=
|
|
160
|
+
src/linters/nesting/python_analyzer.py,sha256=ZaZuFErwpyEG3G0O5LqYwWn7Kmae9mqB8GCSRHSkjmU,5344
|
|
160
161
|
src/linters/nesting/typescript_analyzer.py,sha256=70TsjP3EJWiHJ1ncMaveFE0e9_HdukWZr9LM0_MDXr8,3639
|
|
161
162
|
src/linters/nesting/typescript_function_extractor.py,sha256=dDB1otJnFMCo-Pj4mTr4gekKe7V4ArOAtX6gV0dBDc4,4494
|
|
162
163
|
src/linters/nesting/violation_builder.py,sha256=WwgR_Q9pfPJOoVuNZQL4MU3-Wc6RX_GGL5Rc2-RVlbI,4829
|
|
@@ -186,9 +187,9 @@ src/linters/srp/typescript_analyzer.py,sha256=Wi0P_G1v5AnZYtMN3sNm1iHva84-8Kep2L
|
|
|
186
187
|
src/linters/srp/typescript_metrics_calculator.py,sha256=cDaHlnzMgFSTd2Sn5-tldR2HS6P8GMv4Qptep6PJozw,4093
|
|
187
188
|
src/linters/srp/violation_builder.py,sha256=jaIjVtRYWUTs1SVJVwd0FxCojo0DxhPzfhyfMKmAroM,3881
|
|
188
189
|
src/linters/stateless_class/__init__.py,sha256=8ePpinmCD27PCz7ukwUWcNwo-ZgyvhOquns-U51MyiQ,1063
|
|
189
|
-
src/linters/stateless_class/config.py,sha256=
|
|
190
|
-
src/linters/stateless_class/linter.py,sha256=
|
|
191
|
-
src/linters/stateless_class/python_analyzer.py,sha256=
|
|
190
|
+
src/linters/stateless_class/config.py,sha256=nLowY3nGjvku-GSfPwzclCmVieRulyhaoTjTyWpElk4,2195
|
|
191
|
+
src/linters/stateless_class/linter.py,sha256=G6ftfGqocCbAg24IWJgi2wk8Rld-cJu7OVRPdToZmqY,14783
|
|
192
|
+
src/linters/stateless_class/python_analyzer.py,sha256=P7PJAoCw_1mJdyyqe7EN5ybkDB4FguuTvgTeyy2qgJs,10018
|
|
192
193
|
src/linters/stringly_typed/__init__.py,sha256=6r4IIykZ6mm551KQpRTSDp418EFqJQbuzjSfLHcwyBc,1511
|
|
193
194
|
src/linters/stringly_typed/config.py,sha256=-M7fwwr9axQsQcGtowVINC9Bh1cS1b2-KPxFb2GtL3M,7500
|
|
194
195
|
src/linters/stringly_typed/context_filter.py,sha256=JohTFvXiHKfVzUowRbsDrY37QngJDmhFfoxyoTzKriY,11422
|
|
@@ -219,8 +220,8 @@ src/orchestrator/language_detector.py,sha256=ALt2BEZKXQM2dWr1ChF9lZVj83YF4Bl9xwr
|
|
|
219
220
|
src/templates/thailint_config_template.yaml,sha256=57ZtLxnIoOHtR5Ejq3clb4nhY9J4n6h36XFb79ZZPlc,12020
|
|
220
221
|
src/utils/__init__.py,sha256=NiBtKeQ09Y3kuUzeN4O1JNfUIYPQDS2AP1l5ODq-Dec,125
|
|
221
222
|
src/utils/project_root.py,sha256=aaxUM-LQ1okrPClmZWPFd_D09W3V1ArgJiidEEp_eU8,6262
|
|
222
|
-
thailint-0.15.
|
|
223
|
-
thailint-0.15.
|
|
224
|
-
thailint-0.15.
|
|
225
|
-
thailint-0.15.
|
|
226
|
-
thailint-0.15.
|
|
223
|
+
thailint-0.15.6.dist-info/METADATA,sha256=0ZW5TC2eyix3Bil_XocUFO4fcOCK037NB3d0CLvCdkI,7202
|
|
224
|
+
thailint-0.15.6.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
225
|
+
thailint-0.15.6.dist-info/entry_points.txt,sha256=DNoGUlxpaMFqxQDgHp1yeGqohOjdFR-kH19uHYi3OUY,72
|
|
226
|
+
thailint-0.15.6.dist-info/licenses/LICENSE,sha256=kxh1J0Sb62XvhNJ6MZsVNe8PqNVJ7LHRn_EWa-T3djw,1070
|
|
227
|
+
thailint-0.15.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|