thailint 0.7.0__py3-none-any.whl → 0.9.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/cli.py +233 -1
- src/core/base.py +4 -0
- src/core/rule_discovery.py +110 -84
- src/core/violation_builder.py +75 -15
- src/linter_config/loader.py +45 -12
- src/linters/dry/block_filter.py +15 -8
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +3 -2
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/token_hasher.py +5 -1
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +4 -0
- src/linters/dry/violation_generator.py +1 -1
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/pattern_matcher.py +4 -0
- src/linters/file_placement/pattern_validator.py +4 -0
- src/linters/magic_numbers/context_analyzer.py +4 -0
- src/linters/magic_numbers/typescript_analyzer.py +4 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +135 -0
- src/linters/method_property/linter.py +419 -0
- src/linters/method_property/python_analyzer.py +472 -0
- src/linters/method_property/violation_builder.py +116 -0
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_function_extractor.py +4 -0
- src/linters/print_statements/typescript_analyzer.py +4 -0
- src/linters/srp/class_analyzer.py +4 -0
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +83 -47
- src/linters/srp/violation_builder.py +4 -0
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +355 -0
- src/linters/stateless_class/python_analyzer.py +299 -0
- {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/METADATA +119 -3
- {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/RECORD +41 -32
- {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/WHEEL +0 -0
- {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/licenses/LICENSE +0 -0
src/linter_config/loader.py
CHANGED
|
@@ -16,9 +16,9 @@ Overview: Loads linter configuration from .thailint.yaml or .thailint.json files
|
|
|
16
16
|
Dependencies: PyYAML for YAML parsing with safe_load(), json (stdlib) for JSON parsing,
|
|
17
17
|
pathlib for file path handling
|
|
18
18
|
|
|
19
|
-
Exports: LinterConfigLoader class
|
|
19
|
+
Exports: load_config function, get_defaults function, LinterConfigLoader class (compat)
|
|
20
20
|
|
|
21
|
-
Interfaces:
|
|
21
|
+
Interfaces: load_config(config_path: Path) -> dict[str, Any] for loading config files,
|
|
22
22
|
get_defaults() -> dict[str, Any] for default configuration structure
|
|
23
23
|
|
|
24
24
|
Implementation: Extension-based format detection (.yaml/.yml vs .json), yaml.safe_load()
|
|
@@ -31,13 +31,51 @@ from typing import Any
|
|
|
31
31
|
from src.core.config_parser import parse_config_file
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def get_defaults() -> dict[str, Any]:
|
|
35
|
+
"""Get default configuration.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Default configuration with empty rules and ignore lists.
|
|
39
|
+
"""
|
|
40
|
+
return {
|
|
41
|
+
"rules": {},
|
|
42
|
+
"ignore": [],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_config(config_path: Path) -> dict[str, Any]:
|
|
47
|
+
"""Load configuration from file.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config_path: Path to YAML or JSON config file.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Configuration dictionary.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ConfigParseError: If file format is unsupported or parsing fails.
|
|
57
|
+
"""
|
|
58
|
+
if not config_path.exists():
|
|
59
|
+
return get_defaults()
|
|
60
|
+
|
|
61
|
+
return parse_config_file(config_path)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Legacy class wrapper for backward compatibility
|
|
34
65
|
class LinterConfigLoader:
|
|
35
66
|
"""Load linter configuration from YAML or JSON files.
|
|
36
67
|
|
|
37
68
|
Supports loading from .thailint.yaml, .thailint.json, or custom paths.
|
|
38
69
|
Provides sensible defaults when config files don't exist.
|
|
70
|
+
|
|
71
|
+
Note: This class is a thin wrapper around module-level functions
|
|
72
|
+
for backward compatibility.
|
|
39
73
|
"""
|
|
40
74
|
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
"""Initialize the loader."""
|
|
77
|
+
pass # No state needed
|
|
78
|
+
|
|
41
79
|
def load(self, config_path: Path) -> dict[str, Any]:
|
|
42
80
|
"""Load configuration from file.
|
|
43
81
|
|
|
@@ -50,18 +88,13 @@ class LinterConfigLoader:
|
|
|
50
88
|
Raises:
|
|
51
89
|
ConfigParseError: If file format is unsupported or parsing fails.
|
|
52
90
|
"""
|
|
53
|
-
|
|
54
|
-
return self.get_defaults()
|
|
55
|
-
|
|
56
|
-
return parse_config_file(config_path)
|
|
91
|
+
return load_config(config_path)
|
|
57
92
|
|
|
58
|
-
|
|
59
|
-
|
|
93
|
+
@property
|
|
94
|
+
def defaults(self) -> dict[str, Any]:
|
|
95
|
+
"""Default configuration.
|
|
60
96
|
|
|
61
97
|
Returns:
|
|
62
98
|
Default configuration with empty rules and ignore lists.
|
|
63
99
|
"""
|
|
64
|
-
return
|
|
65
|
-
"rules": {},
|
|
66
|
-
"ignore": [],
|
|
67
|
-
}
|
|
100
|
+
return get_defaults()
|
src/linters/dry/block_filter.py
CHANGED
|
@@ -53,9 +53,10 @@ class BaseBlockFilter(ABC):
|
|
|
53
53
|
"""
|
|
54
54
|
pass
|
|
55
55
|
|
|
56
|
+
@property
|
|
56
57
|
@abstractmethod
|
|
57
|
-
def
|
|
58
|
-
"""
|
|
58
|
+
def name(self) -> str:
|
|
59
|
+
"""Filter name for configuration and logging."""
|
|
59
60
|
pass
|
|
60
61
|
|
|
61
62
|
|
|
@@ -152,8 +153,9 @@ class KeywordArgumentFilter(BaseBlockFilter):
|
|
|
152
153
|
return False
|
|
153
154
|
return True
|
|
154
155
|
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
@property
|
|
157
|
+
def name(self) -> str:
|
|
158
|
+
"""Filter name."""
|
|
157
159
|
return "keyword_argument_filter"
|
|
158
160
|
|
|
159
161
|
|
|
@@ -163,6 +165,10 @@ class ImportGroupFilter(BaseBlockFilter):
|
|
|
163
165
|
Import organization often creates similar patterns that aren't meaningful duplication.
|
|
164
166
|
"""
|
|
165
167
|
|
|
168
|
+
def __init__(self) -> None:
|
|
169
|
+
"""Initialize the import group filter."""
|
|
170
|
+
pass # Stateless filter for import blocks
|
|
171
|
+
|
|
166
172
|
def should_filter(self, block: CodeBlock, file_content: str) -> bool:
|
|
167
173
|
"""Check if block is only import statements.
|
|
168
174
|
|
|
@@ -184,8 +190,9 @@ class ImportGroupFilter(BaseBlockFilter):
|
|
|
184
190
|
|
|
185
191
|
return True
|
|
186
192
|
|
|
187
|
-
|
|
188
|
-
|
|
193
|
+
@property
|
|
194
|
+
def name(self) -> str:
|
|
195
|
+
"""Filter name."""
|
|
189
196
|
return "import_group_filter"
|
|
190
197
|
|
|
191
198
|
|
|
@@ -204,7 +211,7 @@ class BlockFilterRegistry:
|
|
|
204
211
|
filter_instance: Filter to register
|
|
205
212
|
"""
|
|
206
213
|
self._filters.append(filter_instance)
|
|
207
|
-
self._enabled_filters.add(filter_instance.
|
|
214
|
+
self._enabled_filters.add(filter_instance.name)
|
|
208
215
|
|
|
209
216
|
def enable_filter(self, filter_name: str) -> None:
|
|
210
217
|
"""Enable a specific filter by name.
|
|
@@ -233,7 +240,7 @@ class BlockFilterRegistry:
|
|
|
233
240
|
True if block should be filtered out
|
|
234
241
|
"""
|
|
235
242
|
for filter_instance in self._filters:
|
|
236
|
-
if filter_instance.
|
|
243
|
+
if filter_instance.name not in self._enabled_filters:
|
|
237
244
|
continue
|
|
238
245
|
|
|
239
246
|
if filter_instance.should_filter(block, file_content):
|
src/linters/dry/block_grouper.py
CHANGED
|
@@ -26,6 +26,10 @@ from .cache import CodeBlock
|
|
|
26
26
|
class BlockGrouper:
|
|
27
27
|
"""Groups blocks and violations by file path."""
|
|
28
28
|
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
"""Initialize the block grouper."""
|
|
31
|
+
pass # Stateless grouper for code blocks
|
|
32
|
+
|
|
29
33
|
def group_blocks_by_file(self, blocks: list[CodeBlock]) -> dict[Path, list[CodeBlock]]:
|
|
30
34
|
"""Group blocks by file path.
|
|
31
35
|
|
src/linters/dry/cache.py
CHANGED
|
@@ -157,8 +157,9 @@ class DRYCache:
|
|
|
157
157
|
|
|
158
158
|
return blocks
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
@property
|
|
161
|
+
def duplicate_hashes(self) -> list[int]:
|
|
162
|
+
"""Hash values that appear 2+ times.
|
|
162
163
|
|
|
163
164
|
Returns:
|
|
164
165
|
List of hash values with 2 or more occurrences
|
src/linters/dry/cache_query.py
CHANGED
|
@@ -22,6 +22,10 @@ import sqlite3
|
|
|
22
22
|
class CacheQueryService:
|
|
23
23
|
"""Handles cache database queries."""
|
|
24
24
|
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
"""Initialize the cache query service."""
|
|
27
|
+
pass # Stateless query service for database operations
|
|
28
|
+
|
|
25
29
|
def get_duplicate_hashes(self, db: sqlite3.Connection) -> list[int]:
|
|
26
30
|
"""Get all hash values that appear 2+ times.
|
|
27
31
|
|
|
@@ -11,7 +11,7 @@ Dependencies: DRYCache, CodeBlock, Path
|
|
|
11
11
|
|
|
12
12
|
Exports: DuplicateStorage class
|
|
13
13
|
|
|
14
|
-
Interfaces: DuplicateStorage.add_blocks(file_path, blocks),
|
|
14
|
+
Interfaces: DuplicateStorage.add_blocks(file_path, blocks), duplicate_hashes property,
|
|
15
15
|
get_blocks_for_hash(hash_value)
|
|
16
16
|
|
|
17
17
|
Implementation: Delegates to SQLite cache for all storage operations
|
|
@@ -43,13 +43,14 @@ class DuplicateStorage:
|
|
|
43
43
|
if blocks:
|
|
44
44
|
self._cache.add_blocks(file_path, blocks)
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
@property
|
|
47
|
+
def duplicate_hashes(self) -> list[int]:
|
|
48
|
+
"""Hash values with 2+ occurrences from SQLite.
|
|
48
49
|
|
|
49
50
|
Returns:
|
|
50
51
|
List of hash values that appear in multiple blocks
|
|
51
52
|
"""
|
|
52
|
-
return self._cache.
|
|
53
|
+
return self._cache.duplicate_hashes
|
|
53
54
|
|
|
54
55
|
def get_blocks_for_hash(self, hash_value: int) -> list[CodeBlock]:
|
|
55
56
|
"""Get all blocks with given hash value from SQLite.
|
src/linters/dry/token_hasher.py
CHANGED
|
@@ -20,9 +20,13 @@ Implementation: Token-based normalization with rolling window algorithm, languag
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
class TokenHasher:
|
|
23
|
+
class TokenHasher: # thailint: ignore[srp] - Methods support single responsibility of code tokenization
|
|
24
24
|
"""Tokenize code and create rolling hashes for duplicate detection."""
|
|
25
25
|
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
"""Initialize the token hasher."""
|
|
28
|
+
pass # Stateless hasher for code tokenization
|
|
29
|
+
|
|
26
30
|
def tokenize(self, code: str) -> list[str]:
|
|
27
31
|
"""Tokenize code by stripping comments and normalizing whitespace.
|
|
28
32
|
|
|
@@ -27,6 +27,10 @@ from .cache import CodeBlock
|
|
|
27
27
|
class DRYViolationBuilder:
|
|
28
28
|
"""Builds violation messages for duplicate code."""
|
|
29
29
|
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
"""Initialize the DRY violation builder."""
|
|
32
|
+
pass # Stateless builder for duplicate code violations
|
|
33
|
+
|
|
30
34
|
def build_violation(
|
|
31
35
|
self, block: CodeBlock, all_duplicates: list[CodeBlock], rule_id: str
|
|
32
36
|
) -> Violation:
|
|
@@ -25,6 +25,10 @@ DEFAULT_FALLBACK_LINE_COUNT = 5
|
|
|
25
25
|
class ViolationFilter:
|
|
26
26
|
"""Filters overlapping violations."""
|
|
27
27
|
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
"""Initialize the violation filter."""
|
|
30
|
+
pass # Stateless filter for overlapping violations
|
|
31
|
+
|
|
28
32
|
def filter_overlapping(self, sorted_violations: list[Violation]) -> list[Violation]:
|
|
29
33
|
"""Filter overlapping violations, keeping first occurrence.
|
|
30
34
|
|
|
@@ -55,7 +55,7 @@ class ViolationGenerator:
|
|
|
55
55
|
Returns:
|
|
56
56
|
List of violations filtered by ignore patterns and inline directives
|
|
57
57
|
"""
|
|
58
|
-
duplicate_hashes = storage.
|
|
58
|
+
duplicate_hashes = storage.duplicate_hashes
|
|
59
59
|
violations = []
|
|
60
60
|
|
|
61
61
|
for hash_value in duplicate_hashes:
|
|
@@ -24,6 +24,10 @@ from src.linters.file_header.base_parser import BaseHeaderParser
|
|
|
24
24
|
class BashHeaderParser(BaseHeaderParser):
|
|
25
25
|
"""Extracts and parses Bash file headers from comment blocks."""
|
|
26
26
|
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
"""Initialize the Bash header parser."""
|
|
29
|
+
pass # BaseHeaderParser has no __init__, but we need this for stateless-class
|
|
30
|
+
|
|
27
31
|
def extract_header(self, code: str) -> str | None:
|
|
28
32
|
"""Extract comment header from Bash script."""
|
|
29
33
|
if not code or not code.strip():
|
|
@@ -23,6 +23,10 @@ from typing import Any
|
|
|
23
23
|
class DirectoryMatcher:
|
|
24
24
|
"""Finds matching directory rules based on path prefixes."""
|
|
25
25
|
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
"""Initialize the directory matcher."""
|
|
28
|
+
pass # Stateless matcher for directory rules
|
|
29
|
+
|
|
26
30
|
def find_matching_rule(
|
|
27
31
|
self, path_str: str, directories: dict[str, Any]
|
|
28
32
|
) -> tuple[dict[str, Any] | None, str | None]:
|
|
@@ -23,6 +23,10 @@ import re
|
|
|
23
23
|
class PatternMatcher:
|
|
24
24
|
"""Handles regex pattern matching for file paths."""
|
|
25
25
|
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
"""Initialize the pattern matcher."""
|
|
28
|
+
pass # Stateless matcher for regex patterns
|
|
29
|
+
|
|
26
30
|
def match_deny_patterns(
|
|
27
31
|
self, path_str: str, deny_patterns: list[dict[str, str]]
|
|
28
32
|
) -> tuple[bool, str | None]:
|
|
@@ -24,6 +24,10 @@ from typing import Any
|
|
|
24
24
|
class PatternValidator:
|
|
25
25
|
"""Validates regex patterns in file placement configuration."""
|
|
26
26
|
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
"""Initialize the pattern validator."""
|
|
29
|
+
pass # Stateless validator for regex patterns
|
|
30
|
+
|
|
27
31
|
def validate_config(self, config: dict[str, Any]) -> None:
|
|
28
32
|
"""Validate all regex patterns in configuration.
|
|
29
33
|
|
|
@@ -30,6 +30,10 @@ from pathlib import Path
|
|
|
30
30
|
class ContextAnalyzer: # thailint: ignore[srp]
|
|
31
31
|
"""Analyzes contexts to determine if numeric literals are acceptable."""
|
|
32
32
|
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
"""Initialize the context analyzer."""
|
|
35
|
+
pass # Stateless analyzer for context checking
|
|
36
|
+
|
|
33
37
|
def is_acceptable_context(
|
|
34
38
|
self,
|
|
35
39
|
node: ast.Constant,
|
|
@@ -44,6 +44,10 @@ class TypeScriptMagicNumberAnalyzer(TypeScriptBaseAnalyzer): # thailint: ignore
|
|
|
44
44
|
of TypeScript magic number detection - all methods support this core purpose.
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
|
+
def __init__(self) -> None: # pylint: disable=useless-parent-delegation
|
|
48
|
+
"""Initialize the TypeScript magic number analyzer."""
|
|
49
|
+
super().__init__() # Sets self.tree_sitter_available from base class
|
|
50
|
+
|
|
47
51
|
def find_numeric_literals(self, root_node: Node) -> list[tuple[Node, float | int, int]]:
|
|
48
52
|
"""Find all numeric literal nodes in TypeScript/JavaScript AST.
|
|
49
53
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Package exports for method-should-be-property linter
|
|
3
|
+
|
|
4
|
+
Scope: Method property linter public API
|
|
5
|
+
|
|
6
|
+
Overview: Exports the MethodPropertyRule class and MethodPropertyConfig dataclass for use by
|
|
7
|
+
the orchestrator and external consumers. Provides a convenience lint() function for
|
|
8
|
+
standalone usage of the linter.
|
|
9
|
+
|
|
10
|
+
Dependencies: MethodPropertyRule from linter module, MethodPropertyConfig from config module
|
|
11
|
+
|
|
12
|
+
Exports: MethodPropertyRule, MethodPropertyConfig, lint function
|
|
13
|
+
|
|
14
|
+
Interfaces: lint(file_path, content, config) -> list[Violation] convenience function
|
|
15
|
+
|
|
16
|
+
Implementation: Simple re-exports from submodules with optional convenience wrapper
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .config import MethodPropertyConfig
|
|
20
|
+
from .linter import MethodPropertyRule
|
|
21
|
+
|
|
22
|
+
__all__ = ["MethodPropertyRule", "MethodPropertyConfig", "lint"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def lint(
|
|
26
|
+
file_path: str,
|
|
27
|
+
content: str,
|
|
28
|
+
config: dict | None = None,
|
|
29
|
+
) -> list:
|
|
30
|
+
"""Lint a file for method-should-be-property violations.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
file_path: Path to the file being linted
|
|
34
|
+
content: Content of the file
|
|
35
|
+
config: Optional configuration dictionary
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of Violation objects
|
|
39
|
+
"""
|
|
40
|
+
from unittest.mock import Mock
|
|
41
|
+
|
|
42
|
+
rule = MethodPropertyRule()
|
|
43
|
+
context = Mock()
|
|
44
|
+
context.file_path = file_path
|
|
45
|
+
context.file_content = content
|
|
46
|
+
context.language = "python"
|
|
47
|
+
context.config = config
|
|
48
|
+
|
|
49
|
+
return rule.check(context)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration schema for method-should-be-property linter
|
|
3
|
+
|
|
4
|
+
Scope: Method property linter configuration for Python files
|
|
5
|
+
|
|
6
|
+
Overview: Defines configuration schema for method-should-be-property linter. Provides
|
|
7
|
+
MethodPropertyConfig dataclass with enabled flag, max_body_statements threshold (default 3)
|
|
8
|
+
for determining when a method body is too complex to be a property candidate, and ignore
|
|
9
|
+
patterns list for excluding specific files or directories. Includes configurable action verb
|
|
10
|
+
exclusions (prefixes and names) with sensible defaults that can be extended or overridden.
|
|
11
|
+
Supports per-file and per-directory config overrides through from_dict class method.
|
|
12
|
+
Integrates with orchestrator's configuration system via .thailint.yaml.
|
|
13
|
+
|
|
14
|
+
Dependencies: dataclasses module for configuration structure, typing module for type hints
|
|
15
|
+
|
|
16
|
+
Exports: MethodPropertyConfig dataclass, DEFAULT_EXCLUDE_PREFIXES, DEFAULT_EXCLUDE_NAMES
|
|
17
|
+
|
|
18
|
+
Interfaces: from_dict(config, language) -> MethodPropertyConfig for configuration loading
|
|
19
|
+
|
|
20
|
+
Implementation: Dataclass with defaults matching Pythonic conventions and common use cases
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
# Default action verb prefixes - methods starting with these are excluded
|
|
27
|
+
# These represent actions/transformations, not property access
|
|
28
|
+
DEFAULT_EXCLUDE_PREFIXES: tuple[str, ...] = (
|
|
29
|
+
"to_", # Transformation: to_dict, to_json, to_string
|
|
30
|
+
"dump_", # Serialization: dump_to_json, dump_to_apigw
|
|
31
|
+
"generate_", # Factory: generate_report, generate_html
|
|
32
|
+
"create_", # Factory: create_instance, create_config
|
|
33
|
+
"build_", # Construction: build_query, build_html
|
|
34
|
+
"make_", # Factory: make_request, make_connection
|
|
35
|
+
"render_", # Output: render_template, render_html
|
|
36
|
+
"compute_", # Calculation: compute_hash, compute_total
|
|
37
|
+
"calculate_", # Calculation: calculate_sum, calculate_average
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Default action verb names - exact method names that are excluded
|
|
41
|
+
# These are lifecycle hooks, display actions, and resource operations
|
|
42
|
+
DEFAULT_EXCLUDE_NAMES: frozenset[str] = frozenset(
|
|
43
|
+
{
|
|
44
|
+
"finalize", # Lifecycle hook
|
|
45
|
+
"serialize", # Transformation
|
|
46
|
+
"dump", # Serialization
|
|
47
|
+
"validate", # Validation action
|
|
48
|
+
"show", # Display action
|
|
49
|
+
"display", # Display action
|
|
50
|
+
"print", # Output action
|
|
51
|
+
"refresh", # Update action
|
|
52
|
+
"reset", # State action
|
|
53
|
+
"clear", # State action
|
|
54
|
+
"close", # Resource action
|
|
55
|
+
"open", # Resource action
|
|
56
|
+
"save", # Persistence action
|
|
57
|
+
"load", # Persistence action
|
|
58
|
+
"execute", # Action
|
|
59
|
+
"run", # Action
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _load_list_config(
|
|
65
|
+
config: dict[str, Any], key: str, override_key: str, default: tuple[str, ...]
|
|
66
|
+
) -> tuple[str, ...]:
|
|
67
|
+
"""Load a list config with extend/override semantics."""
|
|
68
|
+
if override_key in config and isinstance(config[override_key], list):
|
|
69
|
+
return tuple(config[override_key])
|
|
70
|
+
if key in config and isinstance(config[key], list):
|
|
71
|
+
return default + tuple(config[key])
|
|
72
|
+
return default
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _load_set_config(
|
|
76
|
+
config: dict[str, Any], key: str, override_key: str, default: frozenset[str]
|
|
77
|
+
) -> frozenset[str]:
|
|
78
|
+
"""Load a set config with extend/override semantics."""
|
|
79
|
+
if override_key in config and isinstance(config[override_key], list):
|
|
80
|
+
return frozenset(config[override_key])
|
|
81
|
+
if key in config and isinstance(config[key], list):
|
|
82
|
+
return default | frozenset(config[key])
|
|
83
|
+
return default
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class MethodPropertyConfig: # thailint: ignore[dry]
|
|
88
|
+
"""Configuration for method-should-be-property linter."""
|
|
89
|
+
|
|
90
|
+
enabled: bool = True
|
|
91
|
+
max_body_statements: int = 3
|
|
92
|
+
ignore: list[str] = field(default_factory=list)
|
|
93
|
+
ignore_methods: list[str] = field(default_factory=list)
|
|
94
|
+
|
|
95
|
+
# Action verb exclusions (extend defaults or override)
|
|
96
|
+
exclude_prefixes: tuple[str, ...] = DEFAULT_EXCLUDE_PREFIXES
|
|
97
|
+
exclude_names: frozenset[str] = DEFAULT_EXCLUDE_NAMES
|
|
98
|
+
|
|
99
|
+
# dry: ignore-block
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_dict(
|
|
102
|
+
cls, config: dict[str, Any] | None, language: str | None = None
|
|
103
|
+
) -> "MethodPropertyConfig":
|
|
104
|
+
"""Load configuration from dictionary.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
config: Dictionary containing configuration values, or None
|
|
108
|
+
language: Programming language (unused, for interface compatibility)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
MethodPropertyConfig instance with values from dictionary
|
|
112
|
+
"""
|
|
113
|
+
if config is None:
|
|
114
|
+
return cls()
|
|
115
|
+
|
|
116
|
+
ignore_patterns = config.get("ignore", [])
|
|
117
|
+
if not isinstance(ignore_patterns, list):
|
|
118
|
+
ignore_patterns = []
|
|
119
|
+
|
|
120
|
+
ignore_methods = config.get("ignore_methods", [])
|
|
121
|
+
if not isinstance(ignore_methods, list):
|
|
122
|
+
ignore_methods = []
|
|
123
|
+
|
|
124
|
+
return cls(
|
|
125
|
+
enabled=config.get("enabled", True),
|
|
126
|
+
max_body_statements=config.get("max_body_statements", 3),
|
|
127
|
+
ignore=ignore_patterns,
|
|
128
|
+
ignore_methods=ignore_methods,
|
|
129
|
+
exclude_prefixes=_load_list_config(
|
|
130
|
+
config, "exclude_prefixes", "exclude_prefixes_override", DEFAULT_EXCLUDE_PREFIXES
|
|
131
|
+
),
|
|
132
|
+
exclude_names=_load_set_config(
|
|
133
|
+
config, "exclude_names", "exclude_names_override", DEFAULT_EXCLUDE_NAMES
|
|
134
|
+
),
|
|
135
|
+
)
|