thailint 0.5.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.
- src/cli.py +236 -2
- src/core/cli_utils.py +16 -1
- src/core/registry.py +1 -1
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/loader.py +5 -4
- src/linters/dry/block_filter.py +11 -8
- src/linters/dry/cache.py +3 -2
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/violation_generator.py +1 -1
- src/linters/file_header/atemporal_detector.py +11 -11
- src/linters/file_header/base_parser.py +89 -0
- src/linters/file_header/bash_parser.py +58 -0
- src/linters/file_header/config.py +76 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +35 -29
- src/linters/file_header/linter.py +113 -121
- src/linters/file_header/markdown_parser.py +124 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/linter.py +9 -11
- 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/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +13 -15
- src/linters/print_statements/python_analyzer.py +8 -14
- src/linters/print_statements/typescript_analyzer.py +9 -14
- src/linters/print_statements/violation_builder.py +12 -14
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/METADATA +155 -3
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/RECORD +37 -25
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.5.0.dist-info → thailint-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,22 +6,20 @@ Scope: Validate file organization against allow/deny patterns
|
|
|
6
6
|
Overview: Implements file placement validation using regex patterns from JSON/YAML config.
|
|
7
7
|
Orchestrates configuration loading, pattern validation, path resolution, rule checking,
|
|
8
8
|
and violation creation through focused helper classes. Supports directory-specific rules,
|
|
9
|
-
global patterns, and generates helpful suggestions. Main linter class acts as coordinator
|
|
9
|
+
global patterns, and generates helpful suggestions. Main linter class acts as coordinator
|
|
10
|
+
using composition pattern with specialized helper classes for configuration loading,
|
|
11
|
+
path resolution, pattern matching, and violation creation.
|
|
10
12
|
|
|
11
|
-
Dependencies: src.core (base classes, types), pathlib, typing
|
|
13
|
+
Dependencies: src.core (base classes, types), pathlib, typing, json, yaml modules
|
|
12
14
|
|
|
13
15
|
Exports: FilePlacementLinter, FilePlacementRule
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
Interfaces: lint_path(file_path) -> list[Violation], check_file_allowed(file_path) -> bool,
|
|
18
|
+
lint_directory(dir_path) -> list[Violation]
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
multiple config formats (wrapped vs unwrapped), project root detection with fallbacks,
|
|
21
|
-
and linter caching. This complexity is inherent to adapter pattern - splitting would
|
|
22
|
-
create unnecessary indirection between framework and implementation without improving
|
|
23
|
-
maintainability. All methods are focused on the single responsibility of integrating
|
|
24
|
-
file placement validation with the linting framework.
|
|
20
|
+
Implementation: Composition pattern with helper classes for each responsibility
|
|
21
|
+
(ConfigLoader, PathResolver, PatternMatcher, PatternValidator, RuleChecker,
|
|
22
|
+
ViolationFactory)
|
|
25
23
|
"""
|
|
26
24
|
|
|
27
25
|
import json
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main method-should-be-property linter rule implementation
|
|
3
|
+
|
|
4
|
+
Scope: Method-should-be-property detection for Python files
|
|
5
|
+
|
|
6
|
+
Overview: Implements method-should-be-property linter rule following MultiLanguageLintRule
|
|
7
|
+
interface. Orchestrates configuration loading, Python AST analysis for property candidates,
|
|
8
|
+
and violation building through focused helper classes. Detects methods that should be
|
|
9
|
+
converted to @property decorators following Pythonic conventions. Supports configurable
|
|
10
|
+
max_body_statements threshold, ignore patterns for excluding files, and inline ignore
|
|
11
|
+
directives (thailint: ignore, noqa) for suppressing specific violations. Handles test file
|
|
12
|
+
detection and non-Python languages gracefully.
|
|
13
|
+
|
|
14
|
+
Dependencies: BaseLintContext and MultiLanguageLintRule from core, ast module, pathlib,
|
|
15
|
+
analyzer classes, config classes
|
|
16
|
+
|
|
17
|
+
Exports: MethodPropertyRule class implementing MultiLanguageLintRule interface
|
|
18
|
+
|
|
19
|
+
Interfaces: check(context) -> list[Violation] for rule validation, standard rule properties
|
|
20
|
+
(rule_id, rule_name, description)
|
|
21
|
+
|
|
22
|
+
Implementation: Composition pattern with helper classes (analyzer, violation builder),
|
|
23
|
+
AST-based analysis for Python with comprehensive exclusion rules
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import ast
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
30
|
+
from src.core.types import Violation
|
|
31
|
+
|
|
32
|
+
from .config import MethodPropertyConfig
|
|
33
|
+
from .python_analyzer import PropertyCandidate, PythonMethodAnalyzer
|
|
34
|
+
from .violation_builder import ViolationBuilder
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
|
|
38
|
+
"""Detects methods that should be @property decorators."""
|
|
39
|
+
|
|
40
|
+
def __init__(self) -> None:
|
|
41
|
+
"""Initialize the method property rule."""
|
|
42
|
+
self._violation_builder = ViolationBuilder(self.rule_id)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def rule_id(self) -> str:
|
|
46
|
+
"""Unique identifier for this rule."""
|
|
47
|
+
return "method-property.should-be-property"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def rule_name(self) -> str:
|
|
51
|
+
"""Human-readable name for this rule."""
|
|
52
|
+
return "method should be property"
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def description(self) -> str:
|
|
56
|
+
"""Description of what this rule checks."""
|
|
57
|
+
return "Methods should be converted to @property decorators for Pythonic attribute access"
|
|
58
|
+
|
|
59
|
+
def _load_config(self, context: BaseLintContext) -> MethodPropertyConfig:
|
|
60
|
+
"""Load configuration from context.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
context: Lint context
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
MethodPropertyConfig instance
|
|
67
|
+
"""
|
|
68
|
+
test_config = self._try_load_test_config(context)
|
|
69
|
+
if test_config is not None:
|
|
70
|
+
return test_config
|
|
71
|
+
|
|
72
|
+
return MethodPropertyConfig()
|
|
73
|
+
|
|
74
|
+
# dry: ignore-block
|
|
75
|
+
def _try_load_test_config(self, context: BaseLintContext) -> MethodPropertyConfig | None:
|
|
76
|
+
"""Try to load test-style configuration.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
context: Lint context
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Config if found, None otherwise
|
|
83
|
+
"""
|
|
84
|
+
if not hasattr(context, "config"):
|
|
85
|
+
return None
|
|
86
|
+
config_attr = context.config
|
|
87
|
+
if config_attr is None or not isinstance(config_attr, dict):
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
# Check for method-property specific config
|
|
91
|
+
linter_config = config_attr.get("method-property", config_attr)
|
|
92
|
+
return MethodPropertyConfig.from_dict(linter_config)
|
|
93
|
+
|
|
94
|
+
# dry: ignore-block
|
|
95
|
+
def _is_file_ignored(self, context: BaseLintContext, config: MethodPropertyConfig) -> bool:
|
|
96
|
+
"""Check if file matches ignore patterns.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
context: Lint context
|
|
100
|
+
config: Configuration
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if file should be ignored
|
|
104
|
+
"""
|
|
105
|
+
if not config.ignore:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
if not context.file_path:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
file_path = Path(context.file_path)
|
|
112
|
+
for pattern in config.ignore:
|
|
113
|
+
if self._matches_pattern(file_path, pattern):
|
|
114
|
+
return True
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
# dry: ignore-block
|
|
118
|
+
def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
|
|
119
|
+
"""Check if file path matches a glob pattern.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
file_path: Path to check
|
|
123
|
+
pattern: Glob pattern
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if path matches pattern
|
|
127
|
+
"""
|
|
128
|
+
if file_path.match(pattern):
|
|
129
|
+
return True
|
|
130
|
+
if pattern in str(file_path):
|
|
131
|
+
return True
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
# dry: ignore-block
|
|
135
|
+
def _is_test_file(self, file_path: object) -> bool:
|
|
136
|
+
"""Check if file is a test file.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
file_path: Path to check
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if test file
|
|
143
|
+
"""
|
|
144
|
+
path_str = str(file_path)
|
|
145
|
+
file_name = Path(path_str).name
|
|
146
|
+
|
|
147
|
+
# Check test_*.py pattern
|
|
148
|
+
if file_name.startswith("test_") and file_name.endswith(".py"):
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
# Check *_test.py pattern
|
|
152
|
+
if file_name.endswith("_test.py"):
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
def _check_python(
|
|
158
|
+
self, context: BaseLintContext, config: MethodPropertyConfig
|
|
159
|
+
) -> list[Violation]:
|
|
160
|
+
"""Check Python code for method property violations.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
context: Lint context with Python file information
|
|
164
|
+
config: Method property configuration
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
List of violations found in Python code
|
|
168
|
+
"""
|
|
169
|
+
if self._is_file_ignored(context, config):
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
if self._is_test_file(context.file_path):
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
tree = self._parse_python_code(context.file_content)
|
|
176
|
+
if tree is None:
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
analyzer = PythonMethodAnalyzer(
|
|
180
|
+
max_body_statements=config.max_body_statements,
|
|
181
|
+
exclude_prefixes=config.exclude_prefixes,
|
|
182
|
+
exclude_names=config.exclude_names,
|
|
183
|
+
)
|
|
184
|
+
candidates = analyzer.find_property_candidates(tree)
|
|
185
|
+
candidates = self._filter_ignored_methods(candidates, config)
|
|
186
|
+
return self._collect_violations(candidates, context)
|
|
187
|
+
|
|
188
|
+
def _filter_ignored_methods(
|
|
189
|
+
self,
|
|
190
|
+
candidates: list[PropertyCandidate],
|
|
191
|
+
config: MethodPropertyConfig,
|
|
192
|
+
) -> list[PropertyCandidate]:
|
|
193
|
+
"""Filter out candidates with ignored method names.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
candidates: List of property candidates
|
|
197
|
+
config: Configuration with ignore_methods list
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Filtered list of candidates
|
|
201
|
+
"""
|
|
202
|
+
if not config.ignore_methods:
|
|
203
|
+
return candidates
|
|
204
|
+
return [c for c in candidates if c.method_name not in config.ignore_methods]
|
|
205
|
+
|
|
206
|
+
# dry: ignore-block
|
|
207
|
+
def _parse_python_code(self, code: str | None) -> ast.AST | None:
|
|
208
|
+
"""Parse Python code into AST.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
code: Python source code
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
AST or None if parse fails
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
return ast.parse(code or "")
|
|
218
|
+
except SyntaxError:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
def _collect_violations(
|
|
222
|
+
self,
|
|
223
|
+
candidates: list[PropertyCandidate],
|
|
224
|
+
context: BaseLintContext,
|
|
225
|
+
) -> list[Violation]:
|
|
226
|
+
"""Collect violations from property candidates.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
candidates: List of property candidates
|
|
230
|
+
context: Lint context
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of violations
|
|
234
|
+
"""
|
|
235
|
+
violations = []
|
|
236
|
+
for candidate in candidates:
|
|
237
|
+
violation = self._create_violation(candidate, context)
|
|
238
|
+
if not self._should_ignore(violation, candidate, context):
|
|
239
|
+
violations.append(violation)
|
|
240
|
+
return violations
|
|
241
|
+
|
|
242
|
+
def _create_violation(
|
|
243
|
+
self,
|
|
244
|
+
candidate: PropertyCandidate,
|
|
245
|
+
context: BaseLintContext,
|
|
246
|
+
) -> Violation:
|
|
247
|
+
"""Create a violation for a property candidate.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
candidate: The property candidate
|
|
251
|
+
context: Lint context
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Violation object
|
|
255
|
+
"""
|
|
256
|
+
return self._violation_builder.create_violation(
|
|
257
|
+
method_name=candidate.method_name,
|
|
258
|
+
line=candidate.line,
|
|
259
|
+
column=candidate.column,
|
|
260
|
+
file_path=context.file_path,
|
|
261
|
+
is_get_prefix=candidate.is_get_prefix,
|
|
262
|
+
class_name=candidate.class_name,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def _should_ignore(
|
|
266
|
+
self,
|
|
267
|
+
violation: Violation,
|
|
268
|
+
candidate: PropertyCandidate,
|
|
269
|
+
context: BaseLintContext,
|
|
270
|
+
) -> bool:
|
|
271
|
+
"""Check if violation should be ignored based on directives.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
violation: Violation to check
|
|
275
|
+
candidate: The property candidate
|
|
276
|
+
context: Lint context
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
True if violation should be ignored
|
|
280
|
+
"""
|
|
281
|
+
if self._has_inline_ignore(violation, context):
|
|
282
|
+
return True
|
|
283
|
+
if self._has_docstring_ignore(candidate, context):
|
|
284
|
+
return True
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
# dry: ignore-block
|
|
288
|
+
def _has_inline_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
289
|
+
"""Check for inline ignore directive on method line.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
violation: Violation to check
|
|
293
|
+
context: Lint context
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
True if line has ignore directive
|
|
297
|
+
"""
|
|
298
|
+
line_text = self._get_line_text(violation.line, context)
|
|
299
|
+
if line_text is None:
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
line_lower = line_text.lower()
|
|
303
|
+
|
|
304
|
+
# Check for thailint: ignore[method-property]
|
|
305
|
+
if "thailint:" in line_lower and "ignore" in line_lower:
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
# Check for noqa
|
|
309
|
+
if "# noqa" in line_lower:
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
def _has_docstring_ignore(
|
|
315
|
+
self,
|
|
316
|
+
candidate: PropertyCandidate,
|
|
317
|
+
context: BaseLintContext,
|
|
318
|
+
) -> bool:
|
|
319
|
+
"""Check for ignore directive in method docstring.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
candidate: Property candidate
|
|
323
|
+
context: Lint context
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
True if docstring has ignore directive
|
|
327
|
+
"""
|
|
328
|
+
tree = self._parse_python_code(context.file_content)
|
|
329
|
+
if tree is None:
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
docstring = self._find_method_docstring(tree, candidate)
|
|
333
|
+
if docstring is None:
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
docstring_lower = docstring.lower()
|
|
337
|
+
return "thailint: ignore" in docstring_lower
|
|
338
|
+
|
|
339
|
+
def _find_method_docstring(
|
|
340
|
+
self,
|
|
341
|
+
tree: ast.AST,
|
|
342
|
+
candidate: PropertyCandidate,
|
|
343
|
+
) -> str | None:
|
|
344
|
+
"""Find the docstring for a method.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
tree: AST tree
|
|
348
|
+
candidate: Property candidate
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Docstring text or None
|
|
352
|
+
"""
|
|
353
|
+
target_class = self._find_class_node(tree, candidate.class_name)
|
|
354
|
+
if target_class is None:
|
|
355
|
+
return None
|
|
356
|
+
return self._find_method_in_class(target_class, candidate.method_name)
|
|
357
|
+
|
|
358
|
+
def _find_class_node(self, tree: ast.AST, class_name: str) -> ast.ClassDef | None:
|
|
359
|
+
"""Find a class node by name in the AST.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
tree: AST tree
|
|
363
|
+
class_name: Name of the class to find
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
ClassDef node or None
|
|
367
|
+
"""
|
|
368
|
+
for node in ast.walk(tree):
|
|
369
|
+
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
370
|
+
return node
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
def _find_method_in_class(self, class_node: ast.ClassDef, method_name: str) -> str | None:
|
|
374
|
+
"""Find method docstring within a class.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
class_node: Class node to search
|
|
378
|
+
method_name: Method name to find
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Docstring or None
|
|
382
|
+
"""
|
|
383
|
+
for item in class_node.body:
|
|
384
|
+
if isinstance(item, ast.FunctionDef) and item.name == method_name:
|
|
385
|
+
return ast.get_docstring(item)
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
def _get_line_text(self, line: int, context: BaseLintContext) -> str | None:
|
|
389
|
+
"""Get the text of a specific line.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
line: Line number (1-indexed)
|
|
393
|
+
context: Lint context
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Line text or None
|
|
397
|
+
"""
|
|
398
|
+
if not context.file_content:
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
lines = context.file_content.splitlines()
|
|
402
|
+
if line <= 0 or line > len(lines):
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
return lines[line - 1]
|
|
406
|
+
|
|
407
|
+
def _check_typescript(
|
|
408
|
+
self, context: BaseLintContext, config: MethodPropertyConfig
|
|
409
|
+
) -> list[Violation]:
|
|
410
|
+
"""Check TypeScript code for violations.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
context: Lint context
|
|
414
|
+
config: Configuration
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Empty list (not implemented for TypeScript)
|
|
418
|
+
"""
|
|
419
|
+
return []
|