thailint 0.7.0__py3-none-any.whl → 0.8.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.
@@ -0,0 +1,472 @@
1
+ """
2
+ Purpose: Python AST analysis for finding method-should-be-property candidates
3
+
4
+ Scope: Python method detection and property candidacy analysis
5
+
6
+ Overview: Provides PythonMethodAnalyzer class that traverses Python AST to find methods that
7
+ should be converted to @property decorators. Identifies simple accessor methods (returning
8
+ self._attribute), get_* prefixed methods (Java-style), and simple computed values. Implements
9
+ comprehensive exclusion rules to minimize false positives: methods with parameters, side
10
+ effects (assignments, loops, try/except), external function calls, decorators, complex bodies,
11
+ dunder methods, and async definitions. Returns structured data about each candidate including
12
+ method name, class name, line number, and column for violation reporting.
13
+
14
+ Dependencies: ast module for AST parsing and node types, config module for exclusion defaults
15
+
16
+ Exports: PythonMethodAnalyzer class, PropertyCandidate dataclass
17
+
18
+ Interfaces: find_property_candidates(tree) -> list[PropertyCandidate]
19
+
20
+ Implementation: AST walk pattern with comprehensive method body analysis and exclusion checks
21
+ """
22
+
23
+ import ast
24
+ from dataclasses import dataclass
25
+
26
+ from .config import DEFAULT_EXCLUDE_NAMES, DEFAULT_EXCLUDE_PREFIXES
27
+
28
+
29
+ @dataclass
30
+ class PropertyCandidate:
31
+ """Represents a method that should be a property."""
32
+
33
+ method_name: str
34
+ class_name: str
35
+ line: int
36
+ column: int
37
+ is_get_prefix: bool
38
+
39
+
40
+ class PythonMethodAnalyzer: # thailint: ignore[srp]
41
+ """Analyzes Python AST to find methods that should be properties."""
42
+
43
+ def __init__(
44
+ self,
45
+ max_body_statements: int = 3,
46
+ exclude_prefixes: tuple[str, ...] | None = None,
47
+ exclude_names: frozenset[str] | None = None,
48
+ ) -> None:
49
+ """Initialize the analyzer.
50
+
51
+ Args:
52
+ max_body_statements: Maximum statements in method body
53
+ exclude_prefixes: Action verb prefixes to exclude (uses defaults if None)
54
+ exclude_names: Action verb names to exclude (uses defaults if None)
55
+ """
56
+ self.max_body_statements = max_body_statements
57
+ self.exclude_prefixes = exclude_prefixes or DEFAULT_EXCLUDE_PREFIXES
58
+ self.exclude_names = exclude_names or DEFAULT_EXCLUDE_NAMES
59
+ self.candidates: list[PropertyCandidate] = []
60
+ self._visited_classes: set[int] = set()
61
+
62
+ def find_property_candidates(self, tree: ast.AST) -> list[PropertyCandidate]:
63
+ """Find all methods that should be properties.
64
+
65
+ Args:
66
+ tree: The AST to analyze
67
+
68
+ Returns:
69
+ List of PropertyCandidate objects
70
+ """
71
+ self.candidates = []
72
+ self._visit_classes(tree)
73
+ return self.candidates
74
+
75
+ def _visit_classes(self, tree: ast.AST) -> None:
76
+ """Visit all top-level and nested classes in the AST.
77
+
78
+ Args:
79
+ tree: AST to traverse
80
+ """
81
+ self._visited_classes.clear()
82
+ self._visit_node(tree)
83
+
84
+ def _visit_node(self, node: ast.AST) -> None:
85
+ """Visit a node and its children for classes.
86
+
87
+ Args:
88
+ node: AST node to visit
89
+ """
90
+ if isinstance(node, ast.ClassDef):
91
+ class_id = id(node)
92
+ if class_id not in self._visited_classes:
93
+ self._visited_classes.add(class_id)
94
+ self._analyze_class(node)
95
+ else:
96
+ for child in ast.iter_child_nodes(node):
97
+ self._visit_node(child)
98
+
99
+ def _analyze_class(self, class_node: ast.ClassDef) -> None:
100
+ """Analyze a class for property candidates.
101
+
102
+ Args:
103
+ class_node: The ClassDef node
104
+ """
105
+ for item in class_node.body:
106
+ self._process_class_item(item, class_node.name)
107
+
108
+ def _process_class_item(self, item: ast.stmt, class_name: str) -> None:
109
+ """Process a single item in a class body.
110
+
111
+ Args:
112
+ item: Item in the class body
113
+ class_name: Name of the containing class
114
+ """
115
+ if isinstance(item, ast.FunctionDef):
116
+ self._check_method(item, class_name)
117
+ elif isinstance(item, ast.ClassDef):
118
+ self._process_nested_class(item)
119
+
120
+ def _process_nested_class(self, class_node: ast.ClassDef) -> None:
121
+ """Process a nested class, avoiding duplicates.
122
+
123
+ Args:
124
+ class_node: The nested class node
125
+ """
126
+ class_id = id(class_node)
127
+ if class_id in self._visited_classes:
128
+ return
129
+ self._visited_classes.add(class_id)
130
+ self._analyze_class(class_node)
131
+
132
+ def _check_method(self, method: ast.FunctionDef, class_name: str) -> None:
133
+ """Check if method should be a property.
134
+
135
+ Args:
136
+ method: The FunctionDef node
137
+ class_name: Name of the containing class
138
+ """
139
+ if not self._is_property_candidate(method):
140
+ return
141
+
142
+ is_get_prefix = method.name.startswith("get_") and len(method.name) > 4
143
+ candidate = PropertyCandidate(
144
+ method_name=method.name,
145
+ class_name=class_name,
146
+ line=method.lineno,
147
+ column=method.col_offset,
148
+ is_get_prefix=is_get_prefix,
149
+ )
150
+ self.candidates.append(candidate)
151
+
152
+ def _is_property_candidate(self, method: ast.FunctionDef) -> bool:
153
+ """Check if method should be a property.
154
+
155
+ Args:
156
+ method: The FunctionDef node
157
+
158
+ Returns:
159
+ True if method is a property candidate
160
+ """
161
+ # All conditions must be met for property candidacy
162
+ checks = [
163
+ not self._is_dunder_method(method),
164
+ not self._is_action_verb_method(method),
165
+ not self._has_decorators(method),
166
+ self._takes_only_self(method),
167
+ self._has_simple_body(method),
168
+ self._returns_value(method),
169
+ not self._has_side_effects(method),
170
+ not self._has_control_flow(method),
171
+ not self._has_external_calls(method),
172
+ ]
173
+ return all(checks)
174
+
175
+ def _is_dunder_method(self, method: ast.FunctionDef) -> bool:
176
+ """Check if method is a dunder method.
177
+
178
+ Args:
179
+ method: The method node
180
+
181
+ Returns:
182
+ True if dunder method
183
+ """
184
+ name = method.name
185
+ return name.startswith("__") and name.endswith("__")
186
+
187
+ def _is_action_verb_method(self, method: ast.FunctionDef) -> bool:
188
+ """Check if method is an action verb (transformation/lifecycle method).
189
+
190
+ Methods like to_dict(), to_json(), finalize() represent actions, not
191
+ property access. These should remain as methods following Python idioms.
192
+ Also handles private method variants like _to_dict(), _generate_html().
193
+
194
+ Args:
195
+ method: The method node
196
+
197
+ Returns:
198
+ True if method is an action verb
199
+ """
200
+ name = method.name
201
+
202
+ # Strip leading underscores to handle private method variants
203
+ # e.g., _generate_legend_section should match generate_* pattern
204
+ stripped_name = name.lstrip("_")
205
+
206
+ # Check for action verb prefixes like to_*, generate_*, etc.
207
+ for prefix in self.exclude_prefixes:
208
+ if stripped_name.startswith(prefix) and len(stripped_name) > len(prefix):
209
+ return True
210
+
211
+ # Check for specific action verb names (also check stripped version)
212
+ return name in self.exclude_names or stripped_name in self.exclude_names
213
+
214
+ def _has_decorators(self, method: ast.FunctionDef) -> bool:
215
+ """Check if method has any decorators.
216
+
217
+ Args:
218
+ method: The method node
219
+
220
+ Returns:
221
+ True if method has decorators
222
+ """
223
+ return len(method.decorator_list) > 0
224
+
225
+ def _takes_only_self(self, method: ast.FunctionDef) -> bool:
226
+ """Check if method takes only self parameter.
227
+
228
+ Args:
229
+ method: The method node
230
+
231
+ Returns:
232
+ True if only self parameter
233
+ """
234
+ args = method.args
235
+ has_only_self_arg = len(args.args) == 1
236
+ has_extra_args = self._has_extra_args(args)
237
+ return has_only_self_arg and not has_extra_args
238
+
239
+ def _has_extra_args(self, args: ast.arguments) -> bool:
240
+ """Check if arguments has extra parameters beyond self.
241
+
242
+ Args:
243
+ args: Method arguments node
244
+
245
+ Returns:
246
+ True if extra arguments present
247
+ """
248
+ has_positional_only = bool(args.posonlyargs)
249
+ has_vararg = args.vararg is not None
250
+ has_keyword_only = bool(args.kwonlyargs)
251
+ has_kwarg = args.kwarg is not None
252
+ has_defaults = bool(args.defaults)
253
+ has_kw_defaults = args.kw_defaults and any(d is not None for d in args.kw_defaults)
254
+
255
+ return any(
256
+ [
257
+ has_positional_only,
258
+ has_vararg,
259
+ has_keyword_only,
260
+ has_kwarg,
261
+ has_defaults,
262
+ has_kw_defaults,
263
+ ]
264
+ )
265
+
266
+ def _has_simple_body(self, method: ast.FunctionDef) -> bool:
267
+ """Check if method body is simple (1-3 statements).
268
+
269
+ Args:
270
+ method: The method node
271
+
272
+ Returns:
273
+ True if body is simple enough
274
+ """
275
+ # Filter out docstrings
276
+ body = self._get_non_docstring_body(method)
277
+
278
+ # Check statement count
279
+ if len(body) > self.max_body_statements:
280
+ return False
281
+
282
+ if len(body) == 0:
283
+ return False
284
+
285
+ return True
286
+
287
+ def _get_non_docstring_body(self, method: ast.FunctionDef) -> list[ast.stmt]:
288
+ """Get method body excluding docstrings.
289
+
290
+ Args:
291
+ method: The method node
292
+
293
+ Returns:
294
+ List of statements excluding docstrings
295
+ """
296
+ body = method.body
297
+ if not body:
298
+ return []
299
+
300
+ # Check if first statement is a docstring
301
+ first = body[0]
302
+ if isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant):
303
+ if isinstance(first.value.value, str):
304
+ return body[1:]
305
+
306
+ return body
307
+
308
+ def _returns_value(self, method: ast.FunctionDef) -> bool:
309
+ """Check if method returns a non-None value.
310
+
311
+ Args:
312
+ method: The method node
313
+
314
+ Returns:
315
+ True if method returns a value
316
+ """
317
+ body = self._get_non_docstring_body(method)
318
+ if not body:
319
+ return False
320
+ last = body[-1]
321
+ return self._is_value_return(last)
322
+
323
+ def _is_value_return(self, node: ast.stmt) -> bool:
324
+ """Check if node is a return statement with a non-None value.
325
+
326
+ Args:
327
+ node: Statement node to check
328
+
329
+ Returns:
330
+ True if return statement with value
331
+ """
332
+ if not isinstance(node, ast.Return):
333
+ return False
334
+ if node.value is None:
335
+ return False
336
+ if isinstance(node.value, ast.Constant) and node.value.value is None:
337
+ return False
338
+ return True
339
+
340
+ def _has_side_effects(self, method: ast.FunctionDef) -> bool:
341
+ """Check if method has side effects (assignments to self.*).
342
+
343
+ Args:
344
+ method: The method node
345
+
346
+ Returns:
347
+ True if has side effects
348
+ """
349
+ return any(self._is_side_effect_node(node) for node in ast.walk(method))
350
+
351
+ def _is_side_effect_node(self, node: ast.AST) -> bool:
352
+ """Check if a node represents a side effect.
353
+
354
+ Args:
355
+ node: AST node to check
356
+
357
+ Returns:
358
+ True if node is a side effect
359
+ """
360
+ return (
361
+ self._is_self_assign(node)
362
+ or self._is_self_aug_assign(node)
363
+ or self._is_self_ann_assign(node)
364
+ or self._is_self_delete(node)
365
+ )
366
+
367
+ def _is_self_assign(self, node: ast.AST) -> bool:
368
+ """Check if node is assignment to self."""
369
+ return isinstance(node, ast.Assign) and self._assigns_to_self(node.targets)
370
+
371
+ def _is_self_aug_assign(self, node: ast.AST) -> bool:
372
+ """Check if node is augmented assignment to self."""
373
+ return isinstance(node, ast.AugAssign) and self._is_self_target(node.target)
374
+
375
+ def _is_self_ann_assign(self, node: ast.AST) -> bool:
376
+ """Check if node is annotated assignment to self."""
377
+ if not isinstance(node, ast.AnnAssign):
378
+ return False
379
+ return node.value is not None and self._is_self_target(node.target)
380
+
381
+ def _is_self_delete(self, node: ast.AST) -> bool:
382
+ """Check if node is delete of self attribute."""
383
+ return isinstance(node, ast.Delete) and self._assigns_to_self(node.targets)
384
+
385
+ def _assigns_to_self(self, targets: list[ast.expr]) -> bool:
386
+ """Check if any target is a self attribute.
387
+
388
+ Args:
389
+ targets: Assignment targets
390
+
391
+ Returns:
392
+ True if assigning to self.*
393
+ """
394
+ for target in targets:
395
+ if self._is_self_target(target):
396
+ return True
397
+ return False
398
+
399
+ def _is_self_target(self, target: ast.expr) -> bool:
400
+ """Check if target is a self attribute (self.* or self._*).
401
+
402
+ Args:
403
+ target: Assignment target
404
+
405
+ Returns:
406
+ True if target is self.*
407
+ """
408
+ if isinstance(target, ast.Attribute):
409
+ if isinstance(target.value, ast.Name) and target.value.id == "self":
410
+ return True
411
+ return False
412
+
413
+ # Node types that indicate complex control flow
414
+ _CONTROL_FLOW_TYPES: tuple[type, ...] = (
415
+ ast.For,
416
+ ast.While,
417
+ ast.Try,
418
+ ast.If,
419
+ ast.With,
420
+ ast.Raise,
421
+ ast.ListComp,
422
+ ast.DictComp,
423
+ ast.SetComp,
424
+ ast.GeneratorExp,
425
+ )
426
+
427
+ def _has_control_flow(self, method: ast.FunctionDef) -> bool:
428
+ """Check if method has complex control flow.
429
+
430
+ Args:
431
+ method: The method node
432
+
433
+ Returns:
434
+ True if has complex control flow
435
+ """
436
+ return any(isinstance(node, self._CONTROL_FLOW_TYPES) for node in ast.walk(method))
437
+
438
+ def _has_external_calls(self, method: ast.FunctionDef) -> bool:
439
+ """Check if method has external function calls.
440
+
441
+ External calls are top-level function calls like print(), format_date().
442
+ Method calls on objects like self._name.upper() or v.strip() are OK.
443
+
444
+ Args:
445
+ method: The method node
446
+
447
+ Returns:
448
+ True if has external calls
449
+ """
450
+ call_nodes = (node for node in ast.walk(method) if isinstance(node, ast.Call))
451
+ return any(self._is_external_function_call(node) for node in call_nodes)
452
+
453
+ def _is_external_function_call(self, call: ast.Call) -> bool:
454
+ """Check if call is an external function (not a method on an object).
455
+
456
+ Args:
457
+ call: The Call node
458
+
459
+ Returns:
460
+ True if external function call like print(), format_date()
461
+ """
462
+ func = call.func
463
+
464
+ # Simple name call like print(), format_date()
465
+ if isinstance(func, ast.Name):
466
+ return True
467
+
468
+ # Method call like obj.method() - these are OK
469
+ if isinstance(func, ast.Attribute):
470
+ return False
471
+
472
+ return False
@@ -0,0 +1,116 @@
1
+ """
2
+ Purpose: Builds Violation objects for method-should-be-property detection
3
+
4
+ Scope: Violation creation for methods that should be @property decorators
5
+
6
+ Overview: Provides ViolationBuilder class that creates Violation objects for method-property
7
+ detections. Generates descriptive messages indicating which methods should be converted to
8
+ @property decorators, with special handling for get_* prefix methods (Java-style) that
9
+ suggests removing the prefix. Constructs complete Violation instances with rule_id,
10
+ file_path, line number, column, message, and suggestions for Pythonic refactoring.
11
+
12
+ Dependencies: pathlib.Path for file paths, src.core.types.Violation for violation structure
13
+
14
+ Exports: ViolationBuilder class
15
+
16
+ Interfaces: create_violation(method_name, line, column, file_path, is_get_prefix, class_name)
17
+
18
+ Implementation: Builder pattern with message templates suggesting @property decorator conversion
19
+ """
20
+
21
+ from pathlib import Path
22
+
23
+ from src.core.types import Violation
24
+
25
+
26
+ class ViolationBuilder:
27
+ """Builds violations for method-should-be-property detections."""
28
+
29
+ def __init__(self, rule_id: str) -> None:
30
+ """Initialize the violation builder.
31
+
32
+ Args:
33
+ rule_id: The rule ID to use in violations
34
+ """
35
+ self.rule_id = rule_id
36
+
37
+ def create_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
38
+ self,
39
+ method_name: str,
40
+ line: int,
41
+ column: int,
42
+ file_path: Path | None,
43
+ is_get_prefix: bool = False,
44
+ class_name: str | None = None,
45
+ ) -> Violation:
46
+ """Create a violation for a method that should be a property.
47
+
48
+ Args:
49
+ method_name: Name of the method
50
+ line: Line number where the violation occurs
51
+ column: Column number where the violation occurs
52
+ file_path: Path to the file
53
+ is_get_prefix: Whether method has get_ prefix
54
+ class_name: Optional class name for context
55
+
56
+ Returns:
57
+ Violation object with details about the method
58
+ """
59
+ message = self._build_message(method_name, is_get_prefix, class_name)
60
+ suggestion = self._build_suggestion(method_name, is_get_prefix)
61
+
62
+ return Violation(
63
+ rule_id=self.rule_id,
64
+ file_path=str(file_path) if file_path else "",
65
+ line=line,
66
+ column=column,
67
+ message=message,
68
+ suggestion=suggestion,
69
+ )
70
+
71
+ def _build_message(
72
+ self,
73
+ method_name: str,
74
+ is_get_prefix: bool,
75
+ class_name: str | None,
76
+ ) -> str:
77
+ """Build the violation message.
78
+
79
+ Args:
80
+ method_name: Name of the method
81
+ is_get_prefix: Whether method has get_ prefix
82
+ class_name: Optional class name
83
+
84
+ Returns:
85
+ Human-readable message describing the violation
86
+ """
87
+ if is_get_prefix:
88
+ property_name = method_name[4:] # Remove 'get_' prefix
89
+ if class_name:
90
+ return (
91
+ f"Method '{method_name}' in class '{class_name}' should be "
92
+ f"a @property named '{property_name}'"
93
+ )
94
+ return f"Method '{method_name}' should be a @property named '{property_name}'"
95
+
96
+ if class_name:
97
+ return f"Method '{method_name}' in class '{class_name}' should be a @property"
98
+ return f"Method '{method_name}' should be a @property"
99
+
100
+ def _build_suggestion(self, method_name: str, is_get_prefix: bool) -> str:
101
+ """Build the suggestion for fixing the violation.
102
+
103
+ Args:
104
+ method_name: Name of the method
105
+ is_get_prefix: Whether method has get_ prefix
106
+
107
+ Returns:
108
+ Actionable suggestion for fixing
109
+ """
110
+ if is_get_prefix:
111
+ property_name = method_name[4:] # Remove 'get_' prefix
112
+ return (
113
+ f"Add @property decorator and rename to '{property_name}' "
114
+ f"for Pythonic attribute access"
115
+ )
116
+ return "Add @property decorator for Pythonic attribute access"
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thailint
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages
5
5
  License: MIT
6
6
  License-File: LICENSE
7
7
  Keywords: linter,ai,code-quality,static-analysis,file-placement,governance,multi-language,cli,docker,python
8
8
  Author: Steve Jackson
9
9
  Requires-Python: >=3.11,<4.0
10
- Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
37
37
 
38
38
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
39
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
40
- [![Tests](https://img.shields.io/badge/tests-571%2F571%20passing-brightgreen.svg)](tests/)
40
+ [![Tests](https://img.shields.io/badge/tests-682%2F682%20passing-brightgreen.svg)](tests/)
41
41
  [![Coverage](https://img.shields.io/badge/coverage-87%25-brightgreen.svg)](htmlcov/)
42
42
  [![Documentation Status](https://readthedocs.org/projects/thai-lint/badge/?version=latest)](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
43
43
  [![SARIF 2.1.0](https://img.shields.io/badge/SARIF-2.1.0-orange.svg)](docs/sarif-output.md)
@@ -98,6 +98,12 @@ thailint complements your existing linting stack by catching the patterns AI too
98
98
  - Configurable thresholds (lines, tokens, occurrences)
99
99
  - Language-specific detection (Python, TypeScript, JavaScript)
100
100
  - False positive filtering (keyword args, imports)
101
+ - **Method Property Linting** - Detect methods that should be @property decorators
102
+ - Python AST-based detection
103
+ - get_* prefix detection (Java-style getters)
104
+ - Simple computed value detection
105
+ - Action verb exclusion (to_*, finalize, serialize)
106
+ - Test file detection
101
107
  - **Pluggable Architecture** - Easy to extend with custom linters
102
108
  - **Multi-Language Support** - Python, TypeScript, JavaScript, and more
103
109
  - **Flexible Configuration** - YAML/JSON configs with pattern matching
@@ -2,7 +2,7 @@ src/__init__.py,sha256=f601zncODr2twrUHqTLS5wyOdZqZi9tMjAe2INhRKqU,2175
2
2
  src/analyzers/__init__.py,sha256=fFloZtjkBGwYbAhKTxS3Qy3yDr2_3i3WSfKTw1mAioo,972
3
3
  src/analyzers/typescript_base.py,sha256=4I7fAcMOAY9vY1AXh52QpohgFmguBECwOkvBRP4zCS4,5054
4
4
  src/api.py,sha256=pJ5l3qxccKBEY-BkANwzTgLAl1ZFq7OP6hx6LSxbhDw,4664
5
- src/cli.py,sha256=H8DAOCRIu4pK2fbTqryJ0arwKmJ9w3kkeRdtC-FVdAY,55579
5
+ src/cli.py,sha256=E0PEazl-fuuNu8psnUBAQ53PihtLwgKyAOL3v193Z-o,59424
6
6
  src/config.py,sha256=2ebAjIpAhw4bHbOxViEA5nCjfBlDEIrMR59DBrzcYzM,12460
7
7
  src/core/__init__.py,sha256=5FtsDvhMt4SNRx3pbcGURrxn135XRbeRrjSUxiXwkNc,381
8
8
  src/core/base.py,sha256=Eklcagi2ktfY4Kytl_ObXov2U49N9OGDpw4cu4PUzGY,7824
@@ -17,18 +17,18 @@ src/formatters/__init__.py,sha256=yE1yIL8lplTMEjsmQm7F-kOMaYq7OjmbFuiwwK0D-gM,81
17
17
  src/formatters/sarif.py,sha256=gGOwb_v7j4mx4bpvV1NNDd-JyHH8i8XX89iQ6uRSvG4,7050
18
18
  src/linter_config/__init__.py,sha256=_I2VVlZlfKyT-tKukuUA5-aVcHLOe3m6C2cev43AiEc,298
19
19
  src/linter_config/ignore.py,sha256=S2Ub0CCOOC-wpU5Y_EodMprciw18fgWcnp4z_h1MYNk,19638
20
- src/linter_config/loader.py,sha256=HB09W-uVsEcgCbvUwUHS5Jm2n0bqBXA3744vMc4GAqk,2542
20
+ src/linter_config/loader.py,sha256=BM4GJZqkEJSKxR3jIMgz0WqB_uWxXNiHsKWtwUA43AE,2545
21
21
  src/linters/__init__.py,sha256=-nnNsL8E5-2p9qlLKp_TaShHAjPH-NacOEU1sXmAR9k,77
22
22
  src/linters/dry/__init__.py,sha256=p58tN3z_VbulfTkRm1kLZJ43Bemt66T2sro1teirUY8,826
23
23
  src/linters/dry/base_token_analyzer.py,sha256=SgnsX4a4kUzOsA9hTkLkg4yB8I77ewAyiUp6cAybUrg,2684
24
- src/linters/dry/block_filter.py,sha256=FN1QDXQu6HYhdHo6fkzUDUrMX4TqaLV_myBOQRsmPfk,8242
24
+ src/linters/dry/block_filter.py,sha256=8Sc4tZqHj5GpVxJTQRMCtbMLt6zscuylF2fccEwPo2w,8248
25
25
  src/linters/dry/block_grouper.py,sha256=gBvSPgy8jumChZ53px3P7hDCJfi5PDrKhwxLTgNy7ig,1810
26
- src/linters/dry/cache.py,sha256=QbFajUNb3v2LVuYbTJXY9ExXjjIsd73DQ5Y1hB-SH1s,5879
26
+ src/linters/dry/cache.py,sha256=dedkGU5SolTjmTTsmj-dwPEnxyhWZ2aBiHGkQy1JwXo,5881
27
27
  src/linters/dry/cache_query.py,sha256=NTBh4ISy76LJb9tJ6G94fE0R2OiE0eQ-zVvB08WG2bA,1802
28
28
  src/linters/dry/config.py,sha256=XaHX5SnbH8cEBHiNKMSuYQJwCbKL-Q9XgZVRcc9UBgQ,5439
29
29
  src/linters/dry/config_loader.py,sha256=wikqnigOp6p1h9jaAATV_3bDXSiaIUFaf9xg1jQMDpo,1313
30
30
  src/linters/dry/deduplicator.py,sha256=a1TRvldxCszf5QByo1ihXF3W98dpGuyaRT74jPfQftM,3988
31
- src/linters/dry/duplicate_storage.py,sha256=3OxE2mtoWGAsNNrB8J2c-4JirLUoqZ9ptydO5beM-mg,1971
31
+ src/linters/dry/duplicate_storage.py,sha256=9pIALnwAuz5BJUYNXrPbObbP932CE9x0vgUkICryT_s,1970
32
32
  src/linters/dry/file_analyzer.py,sha256=ufSQ85ddsGTqGnBHZNTdV_5DGfTpUmJOB58sIdJNV0I,2928
33
33
  src/linters/dry/inline_ignore.py,sha256=ASfA-fp_1aPpkakN2e0T6qdTh8S7Jqj89ovxXJLmFlc,4439
34
34
  src/linters/dry/linter.py,sha256=XMLwCgGrFX0l0dVUJs1jpsXOfgxeKKDbxOtN5h5Emhk,5835
@@ -38,7 +38,7 @@ src/linters/dry/token_hasher.py,sha256=71njBzUsWvQjIWo38AKeRHQsG8K4jrjLTKuih-i6G
38
38
  src/linters/dry/typescript_analyzer.py,sha256=ShNoB2KfPe010wKEZoFxn-ZKh0MnRUwgADDQKQtfedI,21627
39
39
  src/linters/dry/violation_builder.py,sha256=EUiEQIOZjzAoHEqZiIR8WZP8m4dgqJjcveR5mdMyClI,2803
40
40
  src/linters/dry/violation_filter.py,sha256=aTOMz8kXG2sZlSVcf3cAxgxHs7f2kBXInfr1V_04fUQ,3125
41
- src/linters/dry/violation_generator.py,sha256=cc6aKvTxtHSZm0F7Y-gL1bmD3JUphRmAvcbqk9aUzGg,6128
41
+ src/linters/dry/violation_generator.py,sha256=hbMs8Fo2hS8JCXicZcZni6NEkv4fJRsyrsjzrqN6qVw,6122
42
42
  src/linters/file_header/__init__.py,sha256=S3a2xrOlxnNWD02To5K2ZwILsNEvSj1IvUAH8RjgOV4,791
43
43
  src/linters/file_header/atemporal_detector.py,sha256=bgQJPDuJj1J5gHKIIOz1TYbBwu8GHrcMafWFVqZ_zZE,3192
44
44
  src/linters/file_header/base_parser.py,sha256=HbuJpXQ4V3zTDTP_D0iFqoT7kab6gk8A1lZdlqCb6tc,3202
@@ -67,6 +67,11 @@ src/linters/magic_numbers/linter.py,sha256=maj4NgrDapv0RurKvaVgOI1BUujixZv4E7UeY
67
67
  src/linters/magic_numbers/python_analyzer.py,sha256=0u1cFaaFCqOW5yeW-YbmPoZuVIeN_KtmkFyyxup6aR0,2803
68
68
  src/linters/magic_numbers/typescript_analyzer.py,sha256=NTU1XY-Hudse5oxOtEiG6nA0Rs5Icn9HXELnyPj8OZU,7554
69
69
  src/linters/magic_numbers/violation_builder.py,sha256=SqIQv3N9lpP2GRC1TC5InrvaEdrAq24V7Ec2Xj5olb0,3308
70
+ src/linters/method_property/__init__.py,sha256=t0C6zD5WLm-McgmvVajQJg4HQfOi7_4YzNLhKNA484w,1415
71
+ src/linters/method_property/config.py,sha256=lcKfeCIeWBcD91j0AchccWhGvc68epiQeKbgL6DCEA8,5359
72
+ src/linters/method_property/linter.py,sha256=zdXUqx76A8J7Ryvg333jrAUDFWx4I4n3dzayAcN2k8w,12721
73
+ src/linters/method_property/python_analyzer.py,sha256=QQTpKrdCJzFqu7gdQfAqno9_wbzHlOQYQdCzMPriRM0,15244
74
+ src/linters/method_property/violation_builder.py,sha256=GKZC8TS9vZq_lKK5ffOVUHUlMpZN3_-Mv3inRE-v2pE,4141
70
75
  src/linters/nesting/__init__.py,sha256=tszmyCEQMpEwB5H84WcAUfRYDQl7jpsn04es5DtAHsM,3200
71
76
  src/linters/nesting/config.py,sha256=PfPA2wJn3i6HHXeM0qu6Qx-v1KJdRwlRkFOdpf7NhS8,2405
72
77
  src/linters/nesting/linter.py,sha256=-klbXIbg145beICop81CNQ5J1OInQaeycDT8u3Ff2Ww,6236
@@ -96,8 +101,8 @@ src/orchestrator/language_detector.py,sha256=rHyVMApit80NTTNyDH1ObD1usKD8LjGmH3D
96
101
  src/templates/thailint_config_template.yaml,sha256=vxyhRRi25_xOnHDRx0jzz69dgPqKU2IU5-YFGUoX5lM,4953
97
102
  src/utils/__init__.py,sha256=NiBtKeQ09Y3kuUzeN4O1JNfUIYPQDS2AP1l5ODq-Dec,125
98
103
  src/utils/project_root.py,sha256=b3YTEGTa9RPcOeHn1IByMMWyRiUabfVlpnlektL0A0o,6156
99
- thailint-0.7.0.dist-info/METADATA,sha256=DiyHLUK6NNz3o2MtQsJPJX-3PWdUX0vyMt2SsSI3ot0,41840
100
- thailint-0.7.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
101
- thailint-0.7.0.dist-info/entry_points.txt,sha256=l7DQJgU18sVLDpSaXOXY3lLhnQHQIRrSJZTQjG1cEAk,62
102
- thailint-0.7.0.dist-info/licenses/LICENSE,sha256=kxh1J0Sb62XvhNJ6MZsVNe8PqNVJ7LHRn_EWa-T3djw,1070
103
- thailint-0.7.0.dist-info/RECORD,,
104
+ thailint-0.8.0.dist-info/METADATA,sha256=JLoEBO87HaZBzNk9wlkn1MeCO_S2wG5dKnL6itrbDe0,42115
105
+ thailint-0.8.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
106
+ thailint-0.8.0.dist-info/entry_points.txt,sha256=l7DQJgU18sVLDpSaXOXY3lLhnQHQIRrSJZTQjG1cEAk,62
107
+ thailint-0.8.0.dist-info/licenses/LICENSE,sha256=kxh1J0Sb62XvhNJ6MZsVNe8PqNVJ7LHRn_EWa-T3djw,1070
108
+ thailint-0.8.0.dist-info/RECORD,,