thailint 0.1.6__py3-none-any.whl → 0.2.1__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.
Files changed (68) hide show
  1. src/__init__.py +7 -2
  2. src/analyzers/__init__.py +23 -0
  3. src/analyzers/typescript_base.py +148 -0
  4. src/api.py +1 -1
  5. src/cli.py +524 -141
  6. src/config.py +6 -31
  7. src/core/base.py +12 -0
  8. src/core/cli_utils.py +206 -0
  9. src/core/config_parser.py +99 -0
  10. src/core/linter_utils.py +168 -0
  11. src/core/registry.py +17 -92
  12. src/core/rule_discovery.py +132 -0
  13. src/core/violation_builder.py +122 -0
  14. src/linter_config/ignore.py +112 -40
  15. src/linter_config/loader.py +3 -13
  16. src/linters/dry/__init__.py +23 -0
  17. src/linters/dry/base_token_analyzer.py +76 -0
  18. src/linters/dry/block_filter.py +262 -0
  19. src/linters/dry/block_grouper.py +59 -0
  20. src/linters/dry/cache.py +218 -0
  21. src/linters/dry/cache_query.py +61 -0
  22. src/linters/dry/config.py +130 -0
  23. src/linters/dry/config_loader.py +44 -0
  24. src/linters/dry/deduplicator.py +120 -0
  25. src/linters/dry/duplicate_storage.py +126 -0
  26. src/linters/dry/file_analyzer.py +127 -0
  27. src/linters/dry/inline_ignore.py +140 -0
  28. src/linters/dry/linter.py +170 -0
  29. src/linters/dry/python_analyzer.py +517 -0
  30. src/linters/dry/storage_initializer.py +51 -0
  31. src/linters/dry/token_hasher.py +115 -0
  32. src/linters/dry/typescript_analyzer.py +590 -0
  33. src/linters/dry/violation_builder.py +74 -0
  34. src/linters/dry/violation_filter.py +91 -0
  35. src/linters/dry/violation_generator.py +174 -0
  36. src/linters/file_placement/config_loader.py +86 -0
  37. src/linters/file_placement/directory_matcher.py +80 -0
  38. src/linters/file_placement/linter.py +252 -472
  39. src/linters/file_placement/path_resolver.py +61 -0
  40. src/linters/file_placement/pattern_matcher.py +55 -0
  41. src/linters/file_placement/pattern_validator.py +106 -0
  42. src/linters/file_placement/rule_checker.py +229 -0
  43. src/linters/file_placement/violation_factory.py +177 -0
  44. src/linters/nesting/config.py +13 -3
  45. src/linters/nesting/linter.py +76 -152
  46. src/linters/nesting/typescript_analyzer.py +38 -102
  47. src/linters/nesting/typescript_function_extractor.py +130 -0
  48. src/linters/nesting/violation_builder.py +139 -0
  49. src/linters/srp/__init__.py +99 -0
  50. src/linters/srp/class_analyzer.py +113 -0
  51. src/linters/srp/config.py +76 -0
  52. src/linters/srp/heuristics.py +89 -0
  53. src/linters/srp/linter.py +225 -0
  54. src/linters/srp/metrics_evaluator.py +47 -0
  55. src/linters/srp/python_analyzer.py +72 -0
  56. src/linters/srp/typescript_analyzer.py +75 -0
  57. src/linters/srp/typescript_metrics_calculator.py +90 -0
  58. src/linters/srp/violation_builder.py +117 -0
  59. src/orchestrator/core.py +42 -7
  60. src/utils/__init__.py +4 -0
  61. src/utils/project_root.py +84 -0
  62. {thailint-0.1.6.dist-info → thailint-0.2.1.dist-info}/METADATA +414 -63
  63. thailint-0.2.1.dist-info/RECORD +75 -0
  64. src/.ai/layout.yaml +0 -48
  65. thailint-0.1.6.dist-info/RECORD +0 -28
  66. {thailint-0.1.6.dist-info → thailint-0.2.1.dist-info}/LICENSE +0 -0
  67. {thailint-0.1.6.dist-info → thailint-0.2.1.dist-info}/WHEEL +0 -0
  68. {thailint-0.1.6.dist-info → thailint-0.2.1.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 the plugin discovery system that enables the extensible architecture by
7
- automatically finding and registering linting rules from specified packages without requiring
8
- explicit registration code. The RuleRegistry maintains a collection of discovered rules indexed
9
- by rule_id, providing methods to register individual rules, retrieve rules by identifier, and
10
- list all available rules. Auto-discovery works by scanning Python packages for classes that
11
- inherit from BaseLintRule, filtering out abstract base classes, instantiating concrete rule
12
- classes, and registering them for use by the orchestrator. This enables developers to add new
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: Package scanning with pkgutil.iter_modules(), class introspection with inspect,
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
- try:
97
- package = importlib.import_module(package_path)
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
- Returns:
128
- Number of rules discovered from this module.
129
- """
88
+ def _try_register(self, rule: BaseLintRule) -> bool:
89
+ """Try to register a rule, return True if successful."""
130
90
  try:
131
- module = importlib.import_module(module_path)
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 (TypeError, AttributeError, ValueError):
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)
@@ -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 .thailintignore file with gitignore-style
9
- glob patterns for excluding files like build artifacts and dependencies. File level scans the
10
- first 10 lines for ignore-file directives (performance optimization). Method level supports
11
- ignore-next-line directives placed before functions. Line level enables inline ignore comments
12
- at the end of code lines. All levels support rule-specific ignores using bracket syntax
13
- [rule-id] and wildcard rule matching (literals.* matches literals.magic-number). The
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, first-10-lines scanning for
29
- performance, regex-based directive parsing with rule ID extraction, wildcard rule matching
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 file patterns.
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 gitignore-style patterns.
81
+ List of ignore patterns
63
82
  """
64
- ignore_file = self.project_root / ".thailintignore"
65
- if not ignore_file.exists():
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
- patterns = []
69
- for line in ignore_file.read_text(encoding="utf-8").splitlines():
70
- line = line.strip()
71
- # Skip comments and blank lines
72
- if line and not line.startswith("#"):
73
- patterns.append(line)
74
- return patterns
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
- return "# thailint: ignore-file" in line or "# design-lint: ignore-file" in line
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
- match = re.search(r"ignore-file\[([^\]]+)\]", line)
130
- if match:
131
- ignored_rules = [r.strip() for r in match.group(1).split(",")]
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 code
176
- or "# design-lint: ignore" in code
177
- or "// thailint: ignore" in code
178
- or "// design-lint: ignore" in code
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
- if pattern.endswith("*"):
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 = pattern[:-1]
237
- return rule_id.startswith(prefix)
299
+ prefix = pattern_lower[:-1]
300
+ return rule_id_lower.startswith(prefix)
238
301
 
239
302
  # Exact match
240
- if rule_id == pattern:
303
+ if rule_id_lower == pattern_lower:
241
304
  return True
242
305
 
243
306
  # Prefix match: "nesting" matches "nesting.excessive-depth"
244
- if rule_id.startswith(pattern + "."):
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()