thailint 0.1.5__py3-none-any.whl → 0.2.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/__init__.py +7 -2
- src/analyzers/__init__.py +23 -0
- src/analyzers/typescript_base.py +148 -0
- src/api.py +1 -1
- src/cli.py +498 -141
- src/config.py +6 -31
- src/core/base.py +12 -0
- src/core/cli_utils.py +206 -0
- src/core/config_parser.py +99 -0
- src/core/linter_utils.py +168 -0
- src/core/registry.py +17 -92
- src/core/rule_discovery.py +132 -0
- src/core/violation_builder.py +122 -0
- src/linter_config/ignore.py +112 -40
- src/linter_config/loader.py +3 -13
- src/linters/dry/__init__.py +23 -0
- src/linters/dry/base_token_analyzer.py +76 -0
- src/linters/dry/block_filter.py +262 -0
- src/linters/dry/block_grouper.py +59 -0
- src/linters/dry/cache.py +218 -0
- src/linters/dry/cache_query.py +61 -0
- src/linters/dry/config.py +130 -0
- src/linters/dry/config_loader.py +44 -0
- src/linters/dry/deduplicator.py +120 -0
- src/linters/dry/duplicate_storage.py +126 -0
- src/linters/dry/file_analyzer.py +127 -0
- src/linters/dry/inline_ignore.py +140 -0
- src/linters/dry/linter.py +170 -0
- src/linters/dry/python_analyzer.py +517 -0
- src/linters/dry/storage_initializer.py +51 -0
- src/linters/dry/token_hasher.py +115 -0
- src/linters/dry/typescript_analyzer.py +590 -0
- src/linters/dry/violation_builder.py +74 -0
- src/linters/dry/violation_filter.py +91 -0
- src/linters/dry/violation_generator.py +174 -0
- src/linters/file_placement/config_loader.py +86 -0
- src/linters/file_placement/directory_matcher.py +80 -0
- src/linters/file_placement/linter.py +252 -472
- src/linters/file_placement/path_resolver.py +61 -0
- src/linters/file_placement/pattern_matcher.py +55 -0
- src/linters/file_placement/pattern_validator.py +106 -0
- src/linters/file_placement/rule_checker.py +229 -0
- src/linters/file_placement/violation_factory.py +177 -0
- src/linters/nesting/config.py +13 -3
- src/linters/nesting/linter.py +76 -152
- src/linters/nesting/typescript_analyzer.py +38 -102
- src/linters/nesting/typescript_function_extractor.py +130 -0
- src/linters/nesting/violation_builder.py +139 -0
- src/linters/srp/__init__.py +99 -0
- src/linters/srp/class_analyzer.py +113 -0
- src/linters/srp/config.py +76 -0
- src/linters/srp/heuristics.py +89 -0
- src/linters/srp/linter.py +225 -0
- src/linters/srp/metrics_evaluator.py +47 -0
- src/linters/srp/python_analyzer.py +72 -0
- src/linters/srp/typescript_analyzer.py +75 -0
- src/linters/srp/typescript_metrics_calculator.py +90 -0
- src/linters/srp/violation_builder.py +117 -0
- src/orchestrator/core.py +42 -7
- src/utils/__init__.py +4 -0
- src/utils/project_root.py +84 -0
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/METADATA +414 -63
- thailint-0.2.0.dist-info/RECORD +75 -0
- src/.ai/layout.yaml +0 -48
- thailint-0.1.5.dist-info/RECORD +0 -28
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/LICENSE +0 -0
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/WHEEL +0 -0
- {thailint-0.1.5.dist-info → thailint-0.2.0.dist-info}/entry_points.txt +0 -0
src/core/registry.py
CHANGED
|
@@ -3,36 +3,24 @@ Purpose: Rule registry with automatic plugin discovery and registration
|
|
|
3
3
|
|
|
4
4
|
Scope: Dynamic rule management and discovery across all linter plugin packages
|
|
5
5
|
|
|
6
|
-
Overview: Implements
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
rules simply by creating a class in the appropriate package structure without modifying any
|
|
14
|
-
framework code. The registry handles import errors gracefully and supports both package-level
|
|
15
|
-
and module-level discovery patterns.
|
|
16
|
-
|
|
17
|
-
Dependencies: importlib for dynamic module loading, inspect for class introspection,
|
|
18
|
-
pkgutil for package traversal, BaseLintRule for type checking
|
|
6
|
+
Overview: Implements rule registry that maintains a collection of registered linting rules indexed
|
|
7
|
+
by rule_id. Provides methods to register individual rules, retrieve rules by identifier, list
|
|
8
|
+
all available rules, and discover rules from packages using the RuleDiscovery helper. Enables
|
|
9
|
+
the extensible plugin architecture by allowing rules to be added dynamically without framework
|
|
10
|
+
modifications. Validates rule uniqueness and handles registration errors gracefully.
|
|
11
|
+
|
|
12
|
+
Dependencies: BaseLintRule, RuleDiscovery
|
|
19
13
|
|
|
20
14
|
Exports: RuleRegistry class with register(), get(), list_all(), and discover_rules() methods
|
|
21
15
|
|
|
22
16
|
Interfaces: register(rule: BaseLintRule) -> None, get(rule_id: str) -> BaseLintRule | None,
|
|
23
17
|
list_all() -> list[BaseLintRule], discover_rules(package_path: str) -> int
|
|
24
18
|
|
|
25
|
-
Implementation:
|
|
26
|
-
subclass detection for BaseLintRule, abstract class filtering, graceful error handling for
|
|
27
|
-
failed imports, duplicate rule_id validation
|
|
19
|
+
Implementation: Dictionary-based registry with RuleDiscovery delegation, duplicate validation
|
|
28
20
|
"""
|
|
29
21
|
|
|
30
|
-
import importlib
|
|
31
|
-
import inspect
|
|
32
|
-
import pkgutil
|
|
33
|
-
from typing import Any
|
|
34
|
-
|
|
35
22
|
from .base import BaseLintRule
|
|
23
|
+
from .rule_discovery import RuleDiscovery
|
|
36
24
|
|
|
37
25
|
|
|
38
26
|
class RuleRegistry:
|
|
@@ -45,6 +33,7 @@ class RuleRegistry:
|
|
|
45
33
|
def __init__(self) -> None:
|
|
46
34
|
"""Initialize empty registry."""
|
|
47
35
|
self._rules: dict[str, BaseLintRule] = {}
|
|
36
|
+
self._discovery = RuleDiscovery()
|
|
48
37
|
|
|
49
38
|
def register(self, rule: BaseLintRule) -> None:
|
|
50
39
|
"""Register a new rule.
|
|
@@ -93,78 +82,14 @@ class RuleRegistry:
|
|
|
93
82
|
Returns:
|
|
94
83
|
Number of rules discovered and registered.
|
|
95
84
|
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
except ImportError:
|
|
99
|
-
return 0
|
|
100
|
-
|
|
101
|
-
if not hasattr(package, "__path__"):
|
|
102
|
-
return self._discover_from_module(package_path)
|
|
103
|
-
|
|
104
|
-
return self._discover_from_package_modules(package_path, package)
|
|
105
|
-
|
|
106
|
-
def _discover_from_package_modules(self, package_path: str, package: Any) -> int:
|
|
107
|
-
"""Discover rules from all modules in a package."""
|
|
108
|
-
discovered_count = 0
|
|
109
|
-
for _, module_name, _ in pkgutil.iter_modules(package.__path__):
|
|
110
|
-
full_module_name = f"{package_path}.{module_name}"
|
|
111
|
-
discovered_count += self._try_discover_from_module(full_module_name)
|
|
112
|
-
return discovered_count
|
|
113
|
-
|
|
114
|
-
def _try_discover_from_module(self, module_name: str) -> int:
|
|
115
|
-
"""Try to discover rules from a module, return 0 on error."""
|
|
116
|
-
try:
|
|
117
|
-
return self._discover_from_module(module_name)
|
|
118
|
-
except (ImportError, AttributeError):
|
|
119
|
-
return 0
|
|
120
|
-
|
|
121
|
-
def _discover_from_module(self, module_path: str) -> int:
|
|
122
|
-
"""Discover rules from a specific module.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
module_path: Full module path to search.
|
|
85
|
+
discovered_rules = self._discovery.discover_from_package(package_path)
|
|
86
|
+
return sum(1 for rule in discovered_rules if self._try_register(rule))
|
|
126
87
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
"""
|
|
88
|
+
def _try_register(self, rule: BaseLintRule) -> bool:
|
|
89
|
+
"""Try to register a rule, return True if successful."""
|
|
130
90
|
try:
|
|
131
|
-
|
|
132
|
-
except (ImportError, AttributeError):
|
|
133
|
-
return 0
|
|
134
|
-
|
|
135
|
-
return self._register_rules_from_module(module)
|
|
136
|
-
|
|
137
|
-
def _register_rules_from_module(self, module: Any) -> int:
|
|
138
|
-
"""Register all rule classes from a module."""
|
|
139
|
-
discovered_count = 0
|
|
140
|
-
for _name, obj in inspect.getmembers(module):
|
|
141
|
-
if not self._is_rule_class(obj):
|
|
142
|
-
continue
|
|
143
|
-
if self._try_register_rule_class(obj):
|
|
144
|
-
discovered_count += 1
|
|
145
|
-
return discovered_count
|
|
146
|
-
|
|
147
|
-
def _try_register_rule_class(self, rule_class: Any) -> bool:
|
|
148
|
-
"""Try to instantiate and register a rule class."""
|
|
149
|
-
try:
|
|
150
|
-
rule_instance = rule_class()
|
|
151
|
-
self.register(rule_instance)
|
|
91
|
+
self.register(rule)
|
|
152
92
|
return True
|
|
153
|
-
except
|
|
93
|
+
except ValueError:
|
|
94
|
+
# Rule already registered, skip
|
|
154
95
|
return False
|
|
155
|
-
|
|
156
|
-
def _is_rule_class(self, obj: Any) -> bool:
|
|
157
|
-
"""Check if an object is a valid rule class.
|
|
158
|
-
|
|
159
|
-
Args:
|
|
160
|
-
obj: Object to check.
|
|
161
|
-
|
|
162
|
-
Returns:
|
|
163
|
-
True if obj is a concrete BaseLintRule subclass.
|
|
164
|
-
"""
|
|
165
|
-
return (
|
|
166
|
-
inspect.isclass(obj)
|
|
167
|
-
and issubclass(obj, BaseLintRule)
|
|
168
|
-
and obj is not BaseLintRule # Don't instantiate the base class
|
|
169
|
-
and not inspect.isabstract(obj) # Don't instantiate abstract classes
|
|
170
|
-
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Automatic rule discovery for plugin-based linter architecture
|
|
3
|
+
|
|
4
|
+
Scope: Discovers and validates linting rules from Python packages
|
|
5
|
+
|
|
6
|
+
Overview: Provides automatic rule discovery functionality for the linter framework. Scans Python
|
|
7
|
+
packages for classes inheriting from BaseLintRule, filters out abstract base classes, validates
|
|
8
|
+
rule classes, and attempts instantiation. Handles import errors gracefully to support partial
|
|
9
|
+
package installations. Enables plugin architecture by discovering rules without explicit registration.
|
|
10
|
+
|
|
11
|
+
Dependencies: importlib, inspect, pkgutil, BaseLintRule
|
|
12
|
+
|
|
13
|
+
Exports: RuleDiscovery
|
|
14
|
+
|
|
15
|
+
Interfaces: discover_from_package(package_path) -> list[BaseLintRule]
|
|
16
|
+
|
|
17
|
+
Implementation: Package traversal with pkgutil, class introspection with inspect, error handling
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import importlib
|
|
21
|
+
import inspect
|
|
22
|
+
import pkgutil
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from .base import BaseLintRule
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RuleDiscovery:
|
|
29
|
+
"""Discovers linting rules from Python packages."""
|
|
30
|
+
|
|
31
|
+
def discover_from_package(self, package_path: str) -> list[BaseLintRule]:
|
|
32
|
+
"""Discover rules from a package and its modules.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
package_path: Python package path (e.g., 'src.linters')
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of discovered rule instances
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
package = importlib.import_module(package_path)
|
|
42
|
+
except ImportError:
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
if not hasattr(package, "__path__"):
|
|
46
|
+
return self._discover_from_module(package_path)
|
|
47
|
+
|
|
48
|
+
return self._discover_from_package_modules(package_path, package)
|
|
49
|
+
|
|
50
|
+
def _discover_from_package_modules(self, package_path: str, package: Any) -> list[BaseLintRule]:
|
|
51
|
+
"""Discover rules from all modules in a package.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
package_path: Package path
|
|
55
|
+
package: Imported package object
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of discovered rules
|
|
59
|
+
"""
|
|
60
|
+
rules = []
|
|
61
|
+
for _, module_name, _ in pkgutil.iter_modules(package.__path__):
|
|
62
|
+
full_module_name = f"{package_path}.{module_name}"
|
|
63
|
+
module_rules = self._try_discover_from_module(full_module_name)
|
|
64
|
+
rules.extend(module_rules)
|
|
65
|
+
return rules
|
|
66
|
+
|
|
67
|
+
def _try_discover_from_module(self, module_name: str) -> list[BaseLintRule]:
|
|
68
|
+
"""Try to discover rules from a module, return empty list on error.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
module_name: Full module name
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of discovered rules (empty on error)
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
return self._discover_from_module(module_name)
|
|
78
|
+
except (ImportError, AttributeError):
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
def _discover_from_module(self, module_path: str) -> list[BaseLintRule]:
|
|
82
|
+
"""Discover rules from a specific module.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
module_path: Full module path to search
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of discovered rule instances
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
module = importlib.import_module(module_path)
|
|
92
|
+
except (ImportError, AttributeError):
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
rules = []
|
|
96
|
+
for _name, obj in inspect.getmembers(module):
|
|
97
|
+
if not self._is_rule_class(obj):
|
|
98
|
+
continue
|
|
99
|
+
rule_instance = self._try_instantiate_rule(obj)
|
|
100
|
+
if rule_instance:
|
|
101
|
+
rules.append(rule_instance)
|
|
102
|
+
return rules
|
|
103
|
+
|
|
104
|
+
def _try_instantiate_rule(self, rule_class: type[BaseLintRule]) -> BaseLintRule | None:
|
|
105
|
+
"""Try to instantiate a rule class.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
rule_class: Rule class to instantiate
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Rule instance or None on error
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
return rule_class()
|
|
115
|
+
except (TypeError, AttributeError):
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def _is_rule_class(self, obj: Any) -> bool:
|
|
119
|
+
"""Check if an object is a valid rule class.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
obj: Object to check
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
True if obj is a concrete BaseLintRule subclass
|
|
126
|
+
"""
|
|
127
|
+
return (
|
|
128
|
+
inspect.isclass(obj)
|
|
129
|
+
and issubclass(obj, BaseLintRule)
|
|
130
|
+
and obj is not BaseLintRule
|
|
131
|
+
and not inspect.isabstract(obj)
|
|
132
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Base violation builder class for consistent violation creation across all linters
|
|
3
|
+
|
|
4
|
+
Scope: Core violation building functionality used by all linter violation builders
|
|
5
|
+
|
|
6
|
+
Overview: Provides base classes and data structures for violation creation across all linters.
|
|
7
|
+
Defines ViolationInfo dataclass containing all required and optional violation fields,
|
|
8
|
+
and BaseViolationBuilder class with common build() method. Eliminates duplicate violation
|
|
9
|
+
construction patterns across file_placement, nesting, and srp linters. Ensures consistent
|
|
10
|
+
violation creation with proper defaults for column and severity fields. Linter-specific
|
|
11
|
+
builders extend this base class to inherit common construction logic while maintaining
|
|
12
|
+
their domain-specific message generation and suggestion logic.
|
|
13
|
+
|
|
14
|
+
Dependencies: dataclasses, src.core.types (Violation, Severity)
|
|
15
|
+
|
|
16
|
+
Exports: ViolationInfo dataclass, BaseViolationBuilder class
|
|
17
|
+
|
|
18
|
+
Interfaces: ViolationInfo(rule_id, file_path, line, message, column, severity),
|
|
19
|
+
BaseViolationBuilder.build(info: ViolationInfo) -> Violation
|
|
20
|
+
|
|
21
|
+
Implementation: Uses dataclass for type-safe violation info, base class provides build()
|
|
22
|
+
method that constructs Violation objects with proper defaults
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
|
|
27
|
+
from src.core.types import Severity, Violation
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ViolationInfo:
|
|
32
|
+
"""Information needed to build a violation.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
rule_id: Identifier for the rule that was violated
|
|
36
|
+
file_path: Path to the file containing the violation
|
|
37
|
+
line: Line number where violation occurs (1-indexed)
|
|
38
|
+
message: Description of the violation
|
|
39
|
+
column: Column number where violation occurs (0-indexed, default=1)
|
|
40
|
+
severity: Severity level of the violation (default=ERROR)
|
|
41
|
+
suggestion: Optional suggestion for fixing the violation
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
rule_id: str
|
|
45
|
+
file_path: str
|
|
46
|
+
line: int
|
|
47
|
+
message: str
|
|
48
|
+
column: int = 1
|
|
49
|
+
severity: Severity = Severity.ERROR
|
|
50
|
+
suggestion: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BaseViolationBuilder:
|
|
54
|
+
"""Base class for building violations with consistent structure.
|
|
55
|
+
|
|
56
|
+
Provides common build() method for creating Violation objects from ViolationInfo.
|
|
57
|
+
Linter-specific builders extend this class to add their domain-specific violation
|
|
58
|
+
creation methods while inheriting the common construction logic.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def build(self, info: ViolationInfo) -> Violation:
|
|
62
|
+
"""Build a Violation from ViolationInfo.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
info: ViolationInfo containing all violation details
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Violation object with all fields populated
|
|
69
|
+
"""
|
|
70
|
+
return Violation(
|
|
71
|
+
rule_id=info.rule_id,
|
|
72
|
+
file_path=info.file_path,
|
|
73
|
+
line=info.line,
|
|
74
|
+
column=info.column,
|
|
75
|
+
message=info.message,
|
|
76
|
+
severity=info.severity,
|
|
77
|
+
suggestion=info.suggestion,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def build_from_params( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
81
|
+
self,
|
|
82
|
+
rule_id: str,
|
|
83
|
+
file_path: str,
|
|
84
|
+
line: int,
|
|
85
|
+
message: str,
|
|
86
|
+
column: int = 1,
|
|
87
|
+
severity: Severity = Severity.ERROR,
|
|
88
|
+
suggestion: str | None = None,
|
|
89
|
+
) -> Violation:
|
|
90
|
+
"""Build a Violation directly from parameters.
|
|
91
|
+
|
|
92
|
+
Note: Pylint too-many-arguments disabled. This convenience method mirrors the
|
|
93
|
+
ViolationInfo dataclass fields (7 parameters, 3 with defaults). The alternative
|
|
94
|
+
would require every caller to create ViolationInfo objects manually, reducing
|
|
95
|
+
readability. This is a standard builder pattern where all parameters are
|
|
96
|
+
inherently related (Violation fields).
|
|
97
|
+
|
|
98
|
+
This is a convenience method that combines ViolationInfo creation and build()
|
|
99
|
+
to reduce duplication in violation builder methods.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
rule_id: Identifier for the rule that was violated
|
|
103
|
+
file_path: Path to the file containing the violation
|
|
104
|
+
line: Line number where violation occurs (1-indexed)
|
|
105
|
+
message: Description of the violation
|
|
106
|
+
column: Column number where violation occurs (0-indexed, default=1)
|
|
107
|
+
severity: Severity level of the violation (default=ERROR)
|
|
108
|
+
suggestion: Optional suggestion for fixing the violation
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Violation object with all fields populated
|
|
112
|
+
"""
|
|
113
|
+
info = ViolationInfo(
|
|
114
|
+
rule_id=rule_id,
|
|
115
|
+
file_path=file_path,
|
|
116
|
+
line=line,
|
|
117
|
+
message=message,
|
|
118
|
+
column=column,
|
|
119
|
+
severity=severity,
|
|
120
|
+
suggestion=suggestion,
|
|
121
|
+
)
|
|
122
|
+
return self.build(info)
|
src/linter_config/ignore.py
CHANGED
|
@@ -5,18 +5,18 @@ Scope: Multi-level ignore system across repository, directory, file, method, and
|
|
|
5
5
|
|
|
6
6
|
Overview: Implements a sophisticated ignore directive system that allows developers to suppress
|
|
7
7
|
linting violations at five different granularity levels, from entire repository patterns down
|
|
8
|
-
to individual lines of code. Repository level uses
|
|
9
|
-
glob patterns for excluding files like build artifacts and dependencies.
|
|
10
|
-
first 10 lines for ignore-file directives (performance optimization).
|
|
11
|
-
ignore-next-line directives placed before functions. Line level enables
|
|
12
|
-
at the end of code lines. All levels support rule-specific ignores
|
|
13
|
-
[rule-id] and wildcard rule matching (literals.* matches literals.magic-number).
|
|
14
|
-
should_ignore_violation() method provides unified checking across all levels, integrating
|
|
8
|
+
to individual lines of code. Repository level uses global ignore patterns from .thailint.yaml
|
|
9
|
+
with gitignore-style glob patterns for excluding files like build artifacts and dependencies.
|
|
10
|
+
File level scans the first 10 lines for ignore-file directives (performance optimization).
|
|
11
|
+
Method level supports ignore-next-line directives placed before functions. Line level enables
|
|
12
|
+
inline ignore comments at the end of code lines. All levels support rule-specific ignores
|
|
13
|
+
using bracket syntax [rule-id] and wildcard rule matching (literals.* matches literals.magic-number).
|
|
14
|
+
The should_ignore_violation() method provides unified checking across all levels, integrating
|
|
15
15
|
with the violation reporting system to filter out suppressed violations before displaying
|
|
16
16
|
results to users.
|
|
17
17
|
|
|
18
18
|
Dependencies: fnmatch for gitignore-style pattern matching, re for regex-based directive parsing,
|
|
19
|
-
pathlib for file operations, Violation type for violation checking
|
|
19
|
+
pathlib for file operations, Violation type for violation checking, yaml for config loading
|
|
20
20
|
|
|
21
21
|
Exports: IgnoreDirectiveParser class
|
|
22
22
|
|
|
@@ -25,9 +25,9 @@ Interfaces: is_ignored(file_path: Path) -> bool for repo-level checking,
|
|
|
25
25
|
has_line_ignore(code: str, line_num: int, rule_id: str | None) -> bool for line-level,
|
|
26
26
|
should_ignore_violation(violation: Violation, file_content: str) -> bool for unified checking
|
|
27
27
|
|
|
28
|
-
Implementation: Gitignore-style pattern matching with fnmatch,
|
|
29
|
-
performance, regex-based directive parsing with rule ID extraction,
|
|
30
|
-
with prefix comparison, graceful error handling for malformed directives
|
|
28
|
+
Implementation: Gitignore-style pattern matching with fnmatch, YAML config loading for global patterns,
|
|
29
|
+
first-10-lines scanning for performance, regex-based directive parsing with rule ID extraction,
|
|
30
|
+
wildcard rule matching with prefix comparison, graceful error handling for malformed directives
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
import fnmatch
|
|
@@ -35,6 +35,8 @@ import re
|
|
|
35
35
|
from pathlib import Path
|
|
36
36
|
from typing import TYPE_CHECKING
|
|
37
37
|
|
|
38
|
+
import yaml
|
|
39
|
+
|
|
38
40
|
if TYPE_CHECKING:
|
|
39
41
|
from src.core.types import Violation
|
|
40
42
|
|
|
@@ -56,22 +58,58 @@ class IgnoreDirectiveParser:
|
|
|
56
58
|
self.repo_patterns = self._load_repo_ignores()
|
|
57
59
|
|
|
58
60
|
def _load_repo_ignores(self) -> list[str]:
|
|
59
|
-
"""Load .thailintignore
|
|
61
|
+
"""Load global ignore patterns from .thailintignore or .thailint.yaml."""
|
|
62
|
+
# First, try to load from .thailintignore (gitignore-style)
|
|
63
|
+
thailintignore = self.project_root / ".thailintignore"
|
|
64
|
+
if thailintignore.exists():
|
|
65
|
+
return self._parse_thailintignore_file(thailintignore)
|
|
66
|
+
|
|
67
|
+
# Fall back to .thailint.yaml
|
|
68
|
+
config_file = self.project_root / ".thailint.yaml"
|
|
69
|
+
if config_file.exists():
|
|
70
|
+
return self._parse_config_file(config_file)
|
|
71
|
+
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
def _parse_thailintignore_file(self, ignore_file: Path) -> list[str]:
|
|
75
|
+
"""Parse .thailintignore file (gitignore-style).
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
ignore_file: Path to .thailintignore file
|
|
60
79
|
|
|
61
80
|
Returns:
|
|
62
|
-
List of
|
|
81
|
+
List of ignore patterns
|
|
63
82
|
"""
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
try:
|
|
84
|
+
content = ignore_file.read_text(encoding="utf-8")
|
|
85
|
+
patterns = []
|
|
86
|
+
for line in content.splitlines():
|
|
87
|
+
line = line.strip()
|
|
88
|
+
# Skip empty lines and comments
|
|
89
|
+
if line and not line.startswith("#"):
|
|
90
|
+
patterns.append(line)
|
|
91
|
+
return patterns
|
|
92
|
+
except (OSError, UnicodeDecodeError):
|
|
66
93
|
return []
|
|
67
94
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
def _parse_config_file(self, config_file: Path) -> list[str]:
|
|
96
|
+
"""Parse YAML config file and extract ignore patterns."""
|
|
97
|
+
try:
|
|
98
|
+
config = yaml.safe_load(config_file.read_text(encoding="utf-8"))
|
|
99
|
+
return self._extract_ignore_patterns(config)
|
|
100
|
+
except (yaml.YAMLError, OSError, UnicodeDecodeError):
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _extract_ignore_patterns(config: dict | None) -> list[str]:
|
|
105
|
+
"""Extract ignore patterns from config dict."""
|
|
106
|
+
if not config or not isinstance(config, dict):
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
ignore_patterns = config.get("ignore", [])
|
|
110
|
+
if isinstance(ignore_patterns, list):
|
|
111
|
+
return [str(pattern) for pattern in ignore_patterns]
|
|
112
|
+
return []
|
|
75
113
|
|
|
76
114
|
def is_ignored(self, file_path: Path) -> bool:
|
|
77
115
|
"""Check if file matches repository-level ignore patterns.
|
|
@@ -122,13 +160,33 @@ class IgnoreDirectiveParser:
|
|
|
122
160
|
|
|
123
161
|
def _has_ignore_directive_marker(self, line: str) -> bool:
|
|
124
162
|
"""Check if line contains an ignore directive marker."""
|
|
125
|
-
|
|
163
|
+
line_lower = line.lower()
|
|
164
|
+
return "# thailint: ignore-file" in line_lower or "# design-lint: ignore-file" in line_lower
|
|
126
165
|
|
|
127
166
|
def _check_specific_rule_ignore(self, line: str, rule_id: str) -> bool:
|
|
128
167
|
"""Check if line ignores a specific rule."""
|
|
129
|
-
|
|
130
|
-
if
|
|
131
|
-
|
|
168
|
+
# Check for bracket syntax: # thailint: ignore-file[rule1, rule2]
|
|
169
|
+
if self._check_bracket_syntax_file_ignore(line, rule_id):
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
# Check for space-separated syntax: # thailint: ignore-file rule1 rule2
|
|
173
|
+
return self._check_space_syntax_file_ignore(line, rule_id)
|
|
174
|
+
|
|
175
|
+
def _check_bracket_syntax_file_ignore(self, line: str, rule_id: str) -> bool:
|
|
176
|
+
"""Check bracket syntax for file-level ignore."""
|
|
177
|
+
bracket_match = re.search(r"ignore-file\[([^\]]+)\]", line, re.IGNORECASE)
|
|
178
|
+
if bracket_match:
|
|
179
|
+
ignored_rules = [r.strip() for r in bracket_match.group(1).split(",")]
|
|
180
|
+
return any(self._rule_matches(rule_id, r) for r in ignored_rules)
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
def _check_space_syntax_file_ignore(self, line: str, rule_id: str) -> bool:
|
|
184
|
+
"""Check space-separated syntax for file-level ignore."""
|
|
185
|
+
space_match = re.search(r"ignore-file\s+([^\s#]+(?:\s+[^\s#]+)*)", line, re.IGNORECASE)
|
|
186
|
+
if space_match:
|
|
187
|
+
ignored_rules = [
|
|
188
|
+
r.strip() for r in re.split(r"[,\s]+", space_match.group(1)) if r.strip()
|
|
189
|
+
]
|
|
132
190
|
return any(self._rule_matches(rule_id, r) for r in ignored_rules)
|
|
133
191
|
return False
|
|
134
192
|
|
|
@@ -171,27 +229,28 @@ class IgnoreDirectiveParser:
|
|
|
171
229
|
|
|
172
230
|
def _has_line_ignore_marker(self, code: str) -> bool:
|
|
173
231
|
"""Check if code line has ignore marker."""
|
|
232
|
+
code_lower = code.lower()
|
|
174
233
|
return (
|
|
175
|
-
"# thailint: ignore" in
|
|
176
|
-
or "# design-lint: ignore" in
|
|
177
|
-
or "// thailint: ignore" in
|
|
178
|
-
or "// design-lint: ignore" in
|
|
234
|
+
"# thailint: ignore" in code_lower
|
|
235
|
+
or "# design-lint: ignore" in code_lower
|
|
236
|
+
or "// thailint: ignore" in code_lower
|
|
237
|
+
or "// design-lint: ignore" in code_lower
|
|
179
238
|
)
|
|
180
239
|
|
|
181
240
|
def _check_specific_rule_in_line(self, code: str, rule_id: str) -> bool:
|
|
182
241
|
"""Check if line's ignore directive matches specific rule."""
|
|
183
242
|
# Check for bracket syntax: # thailint: ignore[rule1, rule2]
|
|
184
|
-
bracket_match = re.search(r"ignore\[([^\]]+)\]", code)
|
|
243
|
+
bracket_match = re.search(r"ignore\[([^\]]+)\]", code, re.IGNORECASE)
|
|
185
244
|
if bracket_match:
|
|
186
245
|
return self._check_bracket_rules(bracket_match.group(1), rule_id)
|
|
187
246
|
|
|
188
247
|
# Check for space-separated syntax: # thailint: ignore rule1 rule2
|
|
189
|
-
space_match = re.search(r"ignore\s+([^\s#]+(?:\s+[^\s#]+)*)", code)
|
|
248
|
+
space_match = re.search(r"ignore\s+([^\s#]+(?:\s+[^\s#]+)*)", code, re.IGNORECASE)
|
|
190
249
|
if space_match:
|
|
191
250
|
return self._check_space_separated_rules(space_match.group(1), rule_id)
|
|
192
251
|
|
|
193
252
|
# No specific rules - check for "ignore-all"
|
|
194
|
-
return "ignore-all" in code
|
|
253
|
+
return "ignore-all" in code.lower()
|
|
195
254
|
|
|
196
255
|
def _check_bracket_rules(self, rules_text: str, rule_id: str) -> bool:
|
|
197
256
|
"""Check if bracketed rules match the rule ID."""
|
|
@@ -231,17 +290,21 @@ class IgnoreDirectiveParser:
|
|
|
231
290
|
Returns:
|
|
232
291
|
True if rule matches pattern.
|
|
233
292
|
"""
|
|
234
|
-
|
|
293
|
+
# Case-insensitive comparison
|
|
294
|
+
rule_id_lower = rule_id.lower()
|
|
295
|
+
pattern_lower = pattern.lower()
|
|
296
|
+
|
|
297
|
+
if pattern_lower.endswith("*"):
|
|
235
298
|
# Wildcard match: literals.* matches literals.magic-number
|
|
236
|
-
prefix =
|
|
237
|
-
return
|
|
299
|
+
prefix = pattern_lower[:-1]
|
|
300
|
+
return rule_id_lower.startswith(prefix)
|
|
238
301
|
|
|
239
302
|
# Exact match
|
|
240
|
-
if
|
|
303
|
+
if rule_id_lower == pattern_lower:
|
|
241
304
|
return True
|
|
242
305
|
|
|
243
306
|
# Prefix match: "nesting" matches "nesting.excessive-depth"
|
|
244
|
-
if
|
|
307
|
+
if rule_id_lower.startswith(pattern_lower + "."):
|
|
245
308
|
return True
|
|
246
309
|
|
|
247
310
|
return False
|
|
@@ -293,18 +356,27 @@ class IgnoreDirectiveParser:
|
|
|
293
356
|
file_path = Path(violation.file_path)
|
|
294
357
|
|
|
295
358
|
# Repository and file level checks
|
|
296
|
-
if self._is_ignored_at_file_level(file_path, violation.rule_id):
|
|
359
|
+
if self._is_ignored_at_file_level(file_path, violation.rule_id, file_content):
|
|
297
360
|
return True
|
|
298
361
|
|
|
299
362
|
# Line-based checks
|
|
300
363
|
return self._is_ignored_in_content(file_content, violation)
|
|
301
364
|
|
|
302
|
-
def _is_ignored_at_file_level(self, file_path: Path, rule_id: str) -> bool:
|
|
365
|
+
def _is_ignored_at_file_level(self, file_path: Path, rule_id: str, file_content: str) -> bool:
|
|
303
366
|
"""Check repository and file level ignores."""
|
|
304
367
|
if self.is_ignored(file_path):
|
|
305
368
|
return True
|
|
369
|
+
# Check content first (for tests with in-memory content)
|
|
370
|
+
if self._has_file_ignore_in_content(file_content, rule_id):
|
|
371
|
+
return True
|
|
372
|
+
# Fall back to reading from disk if file exists
|
|
306
373
|
return self.has_file_ignore(file_path, rule_id)
|
|
307
374
|
|
|
375
|
+
def _has_file_ignore_in_content(self, file_content: str, rule_id: str | None) -> bool:
|
|
376
|
+
"""Check if file content has ignore-file directive."""
|
|
377
|
+
lines = file_content.splitlines()[:10] # Check first 10 lines
|
|
378
|
+
return any(self._check_line_for_ignore(line, rule_id) for line in lines)
|
|
379
|
+
|
|
308
380
|
def _is_ignored_in_content(self, file_content: str, violation: "Violation") -> bool:
|
|
309
381
|
"""Check content-based ignores (block, line, method level)."""
|
|
310
382
|
lines = file_content.splitlines()
|