thailint 0.5.0__py3-none-any.whl → 0.15.3__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 (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,21 +1,22 @@
1
1
  """
2
- File: src/linters/file_header/violation_builder.py
3
2
  Purpose: Builds violation messages for file header linter
4
- Exports: ViolationBuilder class
5
- Depends: Violation type from core
6
- Implements: Message templates with context-specific details
7
- Related: linter.py for builder usage, atemporal_detector.py for temporal violations
8
3
 
9
- Overview:
10
- Creates formatted violation messages for file header validation failures.
4
+ Scope: Violation message creation for file header validation failures
5
+
6
+ Overview: Creates formatted violation messages for file header validation failures.
11
7
  Handles missing fields, atemporal language, and other header issues with clear,
12
- actionable messages. Provides consistent violation format across all validation types.
8
+ actionable messages. Provides consistent violation format across all validation types
9
+ including rule_id, message, location, severity, and helpful suggestions. Supports
10
+ multiple violation types with appropriate error messages and remediation guidance.
11
+
12
+ Dependencies: Violation and Severity types from core.types module
13
+
14
+ Exports: ViolationBuilder class
13
15
 
14
- Usage:
15
- builder = ViolationBuilder("file-header.validation")
16
- violation = builder.build_missing_field("Purpose", "test.py", 1)
16
+ Interfaces: build_missing_field(field_name, file_path, line) -> Violation,
17
+ build_atemporal_violation(pattern, description, file_path, line) -> Violation
17
18
 
18
- Notes: Follows standard violation format with rule_id, message, location, severity, suggestion
19
+ Implementation: Builder pattern with message templates for different violation types
19
20
  """
20
21
 
21
22
  from src.core.types import Severity, Violation
@@ -23,6 +23,8 @@ from typing import Any
23
23
 
24
24
  import yaml
25
25
 
26
+ from src.core.constants import CONFIG_EXTENSIONS
27
+
26
28
 
27
29
  class ConfigLoader:
28
30
  """Loads configuration files for file placement linter."""
@@ -79,7 +81,7 @@ class ConfigLoader:
79
81
  ValueError: If file format is unsupported
80
82
  """
81
83
  with config_path.open(encoding="utf-8") as f:
82
- if config_path.suffix in [".yaml", ".yml"]:
84
+ if config_path.suffix in CONFIG_EXTENSIONS:
83
85
  return yaml.safe_load(f) or {}
84
86
  if config_path.suffix == ".json":
85
87
  return json.load(f)
@@ -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]:
@@ -6,25 +6,28 @@ 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
 
17
+ Interfaces: lint_path(file_path) -> list[Violation], check_file_allowed(file_path) -> bool,
18
+ lint_directory(dir_path) -> list[Violation]
19
+
15
20
  Implementation: Composition pattern with helper classes for each responsibility
21
+ (ConfigLoader, PathResolver, PatternMatcher, PatternValidator, RuleChecker,
22
+ ViolationFactory)
16
23
 
17
- SRP Exception: FilePlacementRule has 13 methods (exceeds max 8)
18
- Justification: Framework adapter class that bridges BaseLintRule interface with
19
- FilePlacementLinter implementation. Must handle multiple config sources (metadata vs file),
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.
24
+ Suppressions:
25
+ - srp.violation: Rule class coordinates multiple helper classes for comprehensive
26
+ file placement validation. Method count reflects composition orchestration.
25
27
  """
26
28
 
27
29
  import json
30
+ from contextlib import suppress
28
31
  from pathlib import Path
29
32
  from typing import Any
30
33
 
@@ -74,21 +77,31 @@ class FilePlacementLinter:
74
77
 
75
78
  # Load and validate config
76
79
  if config_obj:
77
- # Handle both wrapped and unwrapped config formats
78
- # Wrapped: {"file-placement": {...}} or {"file_placement": {...}}
79
- # Unwrapped: {"directories": {...}, "global_deny": [...], ...}
80
- # Try both hyphenated and underscored keys for backward compatibility
81
- self.config = config_obj.get(
82
- "file-placement", config_obj.get("file_placement", config_obj)
83
- )
80
+ self.config = self._unwrap_config(config_obj)
84
81
  elif config_file:
85
- self.config = self._components.config_loader.load_config_file(config_file)
82
+ raw_config = self._components.config_loader.load_config_file(config_file)
83
+ self.config = self._unwrap_config(raw_config)
86
84
  else:
87
85
  self.config = {}
88
86
 
89
87
  # Validate regex patterns in config
90
88
  self._components.pattern_validator.validate_config(self.config)
91
89
 
90
+ def _unwrap_config(self, config: dict[str, Any]) -> dict[str, Any]:
91
+ """Unwrap file-placement config from wrapper if present.
92
+
93
+ Args:
94
+ config: Raw config dict (may be wrapped or unwrapped)
95
+
96
+ Returns:
97
+ Unwrapped file-placement config dict
98
+ """
99
+ # Handle both wrapped and unwrapped config formats
100
+ # Wrapped: {"file-placement": {...}} or {"file_placement": {...}}
101
+ # Unwrapped: {"directories": {...}, "global_deny": [...], ...}
102
+ # Try both hyphenated and underscored keys for backward compatibility
103
+ return config.get("file-placement", config.get("file_placement", config))
104
+
92
105
  def lint_path(self, file_path: Path) -> list[Violation]:
93
106
  """Lint a single file path.
94
107
 
@@ -126,20 +139,40 @@ class FilePlacementLinter:
126
139
  Returns:
127
140
  List of all violations found
128
141
  """
129
- from src.linter_config.ignore import IgnoreDirectiveParser
142
+ valid_files = self._get_valid_files(dir_path, recursive)
143
+ return self._lint_files(valid_files)
144
+
145
+ def _get_valid_files(self, dir_path: Path, recursive: bool) -> list[Path]:
146
+ """Get list of valid files to lint from directory.
147
+
148
+ Args:
149
+ dir_path: Directory to scan
150
+ recursive: Scan recursively
151
+
152
+ Returns:
153
+ List of file paths to lint
154
+ """
155
+ from src.linter_config.ignore import get_ignore_parser
130
156
 
131
- ignore_parser = IgnoreDirectiveParser(self.project_root)
157
+ ignore_parser = get_ignore_parser(self.project_root)
132
158
  pattern = "**/*" if recursive else "*"
133
159
 
134
- violations = []
135
- for file_path in dir_path.glob(pattern):
136
- if not file_path.is_file():
137
- continue
138
- if ignore_parser.is_ignored(file_path):
139
- continue
140
- file_violations = self.lint_path(file_path)
141
- violations.extend(file_violations)
160
+ return [
161
+ f for f in dir_path.glob(pattern) if f.is_file() and not ignore_parser.is_ignored(f)
162
+ ]
163
+
164
+ def _lint_files(self, file_paths: list[Path]) -> list[Violation]:
165
+ """Lint multiple files and collect violations.
166
+
167
+ Args:
168
+ file_paths: List of file paths to lint
142
169
 
170
+ Returns:
171
+ List of all violations found
172
+ """
173
+ violations = []
174
+ for file_path in file_paths:
175
+ violations.extend(self.lint_path(file_path))
143
176
  return violations
144
177
 
145
178
 
@@ -295,10 +328,10 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
295
328
  if not hasattr(context, "metadata"):
296
329
  return None
297
330
  # Try hyphenated format first (original format)
298
- if "file-placement" in context.metadata:
331
+ with suppress(KeyError):
299
332
  return context.metadata["file-placement"]
300
333
  # Try underscored format (normalized format)
301
- if "file_placement" in context.metadata:
334
+ with suppress(KeyError):
302
335
  return context.metadata["file_placement"]
303
336
  return None
304
337
 
@@ -332,7 +365,7 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
332
365
  FilePlacementLinter instance
333
366
  """
334
367
  # Check if cached linter exists for this project root
335
- if project_root in self._linter_cache:
368
+ with suppress(KeyError):
336
369
  return self._linter_cache[project_root]
337
370
 
338
371
  # Try to get config from context metadata (orchestrator passes config here)
@@ -388,11 +421,10 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
388
421
  config = self._parse_layout_file(layout_path)
389
422
 
390
423
  # Unwrap file-placement key if present (try both formats for backward compatibility)
391
- if "file-placement" in config:
424
+ with suppress(KeyError):
392
425
  return config["file-placement"]
393
- if "file_placement" in config:
426
+ with suppress(KeyError):
394
427
  return config["file_placement"]
395
-
396
428
  return config
397
429
  except Exception:
398
430
  return {}
@@ -18,30 +18,65 @@ Implementation: Uses re.search() for pattern matching with IGNORECASE flag
18
18
  """
19
19
 
20
20
  import re
21
+ from collections.abc import Sequence
22
+ from re import Pattern
21
23
 
22
24
 
23
25
  class PatternMatcher:
24
26
  """Handles regex pattern matching for file paths."""
25
27
 
28
+ def __init__(self) -> None:
29
+ """Initialize the pattern matcher with compiled regex cache."""
30
+ self._compiled_patterns: dict[str, Pattern[str]] = {}
31
+
32
+ def _get_compiled(self, pattern: str) -> Pattern[str]:
33
+ """Get compiled regex pattern, caching for reuse.
34
+
35
+ Args:
36
+ pattern: Regex pattern string
37
+
38
+ Returns:
39
+ Compiled regex Pattern object
40
+ """
41
+ if pattern not in self._compiled_patterns:
42
+ self._compiled_patterns[pattern] = re.compile(pattern, re.IGNORECASE)
43
+ return self._compiled_patterns[pattern]
44
+
26
45
  def match_deny_patterns(
27
- self, path_str: str, deny_patterns: list[dict[str, str]]
46
+ self, path_str: str, deny_patterns: Sequence[dict[str, str] | str]
28
47
  ) -> tuple[bool, str | None]:
29
48
  """Check if path matches any deny patterns.
30
49
 
31
50
  Args:
32
51
  path_str: File path to check
33
- deny_patterns: List of deny pattern dicts with 'pattern' and 'reason'
52
+ deny_patterns: List of deny patterns (either dicts with 'pattern'/'reason'
53
+ or plain regex strings for backward compatibility)
34
54
 
35
55
  Returns:
36
56
  Tuple of (is_denied, reason)
37
57
  """
38
58
  for deny_item in deny_patterns:
39
- pattern = deny_item["pattern"]
40
- if re.search(pattern, path_str, re.IGNORECASE):
41
- reason = deny_item.get("reason", "File not allowed in this location")
59
+ pattern, reason = self._extract_pattern_and_reason(deny_item)
60
+ compiled = self._get_compiled(pattern)
61
+ if compiled.search(path_str):
42
62
  return True, reason
43
63
  return False, None
44
64
 
65
+ def _extract_pattern_and_reason(self, deny_item: dict[str, str] | str) -> tuple[str, str]:
66
+ """Extract pattern and reason from a deny item.
67
+
68
+ Args:
69
+ deny_item: Either a dict with 'pattern' key or a plain string pattern
70
+
71
+ Returns:
72
+ Tuple of (pattern, reason)
73
+ """
74
+ if isinstance(deny_item, str):
75
+ return deny_item, "File not allowed in this location"
76
+ return deny_item["pattern"], deny_item.get(
77
+ "reason", deny_item.get("message", "File not allowed in this location")
78
+ )
79
+
45
80
  def match_allow_patterns(self, path_str: str, allow_patterns: list[str]) -> bool:
46
81
  """Check if path matches any allow patterns.
47
82
 
@@ -52,4 +87,4 @@ class PatternMatcher:
52
87
  Returns:
53
88
  True if path matches any pattern
54
89
  """
55
- return any(re.search(pattern, path_str, re.IGNORECASE) for pattern in allow_patterns)
90
+ return any(self._get_compiled(pattern).search(path_str) for pattern in allow_patterns)
@@ -18,25 +18,44 @@ Implementation: Uses re.compile() to test pattern validity, provides detailed er
18
18
  """
19
19
 
20
20
  import re
21
+ from contextlib import suppress
21
22
  from typing import Any
22
23
 
23
24
 
25
+ def _extract_pattern(deny_item: dict[str, str] | str) -> str:
26
+ """Extract pattern from a deny item.
27
+
28
+ Args:
29
+ deny_item: Either a dict with 'pattern' key or a plain string pattern
30
+
31
+ Returns:
32
+ The pattern string
33
+ """
34
+ if isinstance(deny_item, str):
35
+ return deny_item
36
+ return deny_item.get("pattern", "")
37
+
38
+
24
39
  class PatternValidator:
25
40
  """Validates regex patterns in file placement configuration."""
26
41
 
42
+ def __init__(self) -> None:
43
+ """Initialize the pattern validator."""
44
+ pass # Stateless validator for regex patterns
45
+
27
46
  def validate_config(self, config: dict[str, Any]) -> None:
28
47
  """Validate all regex patterns in configuration.
29
48
 
30
49
  Args:
31
- config: Full configuration dict
50
+ config: File placement configuration dict (already unwrapped)
32
51
 
33
52
  Raises:
34
53
  ValueError: If any regex pattern is invalid
35
54
  """
36
- fp_config = config.get("file-placement", {})
37
- self._validate_directory_patterns(fp_config)
38
- self._validate_global_patterns(fp_config)
39
- self._validate_global_deny_patterns(fp_config)
55
+ # Config is already unwrapped from file-placement key by FilePlacementLinter
56
+ self._validate_directory_patterns(config)
57
+ self._validate_global_patterns(config)
58
+ self._validate_global_deny_patterns(config)
40
59
 
41
60
  def _validate_pattern(self, pattern: str) -> None:
42
61
  """Validate a single regex pattern.
@@ -58,7 +77,7 @@ class PatternValidator:
58
77
  Args:
59
78
  rules: Rules dictionary containing allow patterns
60
79
  """
61
- if "allow" in rules:
80
+ with suppress(KeyError):
62
81
  for pattern in rules["allow"]:
63
82
  self._validate_pattern(pattern)
64
83
 
@@ -68,9 +87,9 @@ class PatternValidator:
68
87
  Args:
69
88
  rules: Rules dictionary containing deny patterns
70
89
  """
71
- if "deny" in rules:
90
+ with suppress(KeyError):
72
91
  for deny_item in rules["deny"]:
73
- pattern = deny_item.get("pattern", "")
92
+ pattern = _extract_pattern(deny_item)
74
93
  self._validate_pattern(pattern)
75
94
 
76
95
  def _validate_directory_patterns(self, fp_config: dict[str, Any]) -> None:
@@ -79,7 +98,7 @@ class PatternValidator:
79
98
  Args:
80
99
  fp_config: File placement configuration section
81
100
  """
82
- if "directories" in fp_config:
101
+ with suppress(KeyError):
83
102
  for _dir_path, rules in fp_config["directories"].items():
84
103
  self._validate_allow_patterns(rules)
85
104
  self._validate_deny_patterns(rules)
@@ -90,7 +109,7 @@ class PatternValidator:
90
109
  Args:
91
110
  fp_config: File placement configuration section
92
111
  """
93
- if "global_patterns" in fp_config:
112
+ with suppress(KeyError):
94
113
  self._validate_allow_patterns(fp_config["global_patterns"])
95
114
  self._validate_deny_patterns(fp_config["global_patterns"])
96
115
 
@@ -100,7 +119,7 @@ class PatternValidator:
100
119
  Args:
101
120
  fp_config: File placement configuration section
102
121
  """
103
- if "global_deny" in fp_config:
122
+ with suppress(KeyError):
104
123
  for deny_item in fp_config["global_deny"]:
105
- pattern = deny_item.get("pattern", "")
124
+ pattern = _extract_pattern(deny_item)
106
125
  self._validate_pattern(pattern)
@@ -19,6 +19,7 @@ Implementation: Checks deny before allow, delegates directory matching to Direct
19
19
  uses RuleCheckContext dataclass to reduce parameter duplication
20
20
  """
21
21
 
22
+ from contextlib import suppress
22
23
  from dataclasses import dataclass
23
24
  from pathlib import Path
24
25
  from typing import Any
@@ -76,17 +77,17 @@ class RuleChecker:
76
77
  """
77
78
  violations: list[Violation] = []
78
79
 
79
- if "directories" in fp_config:
80
+ with suppress(KeyError):
80
81
  dir_violations = self._check_directory_rules(
81
82
  path_str, rel_path, fp_config["directories"]
82
83
  )
83
84
  violations.extend(dir_violations)
84
85
 
85
- if "global_deny" in fp_config:
86
+ with suppress(KeyError):
86
87
  deny_violations = self._check_global_deny(path_str, rel_path, fp_config["global_deny"])
87
88
  violations.extend(deny_violations)
88
89
 
89
- if "global_patterns" in fp_config:
90
+ with suppress(KeyError):
90
91
  global_violations = self._check_global_patterns(
91
92
  path_str, rel_path, fp_config["global_patterns"]
92
93
  )
@@ -177,14 +178,14 @@ class RuleChecker:
177
178
  return [violation] if violation else []
178
179
 
179
180
  def _check_global_deny(
180
- self, path_str: str, rel_path: Path, global_deny: list[dict[str, str]]
181
+ self, path_str: str, rel_path: Path, global_deny: list[dict[str, str] | str]
181
182
  ) -> list[Violation]:
182
183
  """Check file against global deny patterns.
183
184
 
184
185
  Args:
185
186
  path_str: Normalized path string
186
187
  rel_path: Relative path object
187
- global_deny: Global deny patterns
188
+ global_deny: Global deny patterns (dicts with pattern/reason or plain strings)
188
189
 
189
190
  Returns:
190
191
  List of violations
@@ -209,21 +210,25 @@ class RuleChecker:
209
210
  List of violations
210
211
  """
211
212
  # Check deny patterns first
212
- if "deny" in global_patterns:
213
+ try:
213
214
  is_denied, reason = self.pattern_matcher.match_deny_patterns(
214
215
  path_str, global_patterns["deny"]
215
216
  )
216
217
  if is_denied:
217
218
  violation = self.violation_factory.create_global_deny_violation(rel_path, reason)
218
219
  return self._wrap_violation(violation)
220
+ except KeyError:
221
+ pass # No deny patterns
219
222
 
220
223
  # Check allow patterns
221
- if "allow" in global_patterns:
224
+ try:
222
225
  is_allowed = self.pattern_matcher.match_allow_patterns(
223
226
  path_str, global_patterns["allow"]
224
227
  )
225
228
  if not is_allowed:
226
229
  violation = self.violation_factory.create_global_allow_violation(rel_path)
227
230
  return self._wrap_violation(violation)
231
+ except KeyError:
232
+ pass # No allow patterns
228
233
 
229
234
  return []
@@ -0,0 +1,43 @@
1
+ """
2
+ Purpose: Lazy-ignores linter package exports
3
+
4
+ Scope: Detect unjustified linting suppressions in code files
5
+
6
+ Overview: Package providing lazy-ignores linter functionality. Detects when AI agents add
7
+ linting suppressions (noqa, type:ignore, pylint:disable, nosec, thailint:ignore, etc.)
8
+ or test skips (pytest.mark.skip, it.skip, describe.skip) without proper justification
9
+ in the file header's Suppressions section. Enforces header-based declaration model
10
+ where all suppressions must be documented with human approval.
11
+
12
+ Dependencies: src.core for base types, re for pattern matching
13
+
14
+ Exports: IgnoreType, IgnoreDirective, SuppressionEntry, LazyIgnoresConfig,
15
+ PythonIgnoreDetector, TypeScriptIgnoreDetector, TestSkipDetector, SuppressionsParser
16
+
17
+ Interfaces: LazyIgnoresConfig.from_dict() for YAML configuration loading
18
+
19
+ Implementation: Enum and dataclass definitions for ignore directive representation
20
+ """
21
+
22
+ from .config import LazyIgnoresConfig
23
+ from .header_parser import SuppressionsParser
24
+ from .linter import LazyIgnoresRule
25
+ from .python_analyzer import PythonIgnoreDetector
26
+ from .skip_detector import TestSkipDetector
27
+ from .types import IgnoreDirective, IgnoreType, SuppressionEntry
28
+ from .typescript_analyzer import TypeScriptIgnoreDetector
29
+ from .violation_builder import build_orphaned_violation, build_unjustified_violation
30
+
31
+ __all__ = [
32
+ "IgnoreType",
33
+ "IgnoreDirective",
34
+ "SuppressionEntry",
35
+ "LazyIgnoresConfig",
36
+ "PythonIgnoreDetector",
37
+ "TypeScriptIgnoreDetector",
38
+ "TestSkipDetector",
39
+ "SuppressionsParser",
40
+ "LazyIgnoresRule",
41
+ "build_unjustified_violation",
42
+ "build_orphaned_violation",
43
+ ]
@@ -0,0 +1,74 @@
1
+ """
2
+ Purpose: Configuration for lazy-ignores linter
3
+
4
+ Scope: All configurable options for ignore detection
5
+
6
+ Overview: Provides LazyIgnoresConfig dataclass with pattern-specific toggles for each
7
+ ignore type (noqa, type:ignore, pylint, nosec, pyright, typescript, eslint, thailint).
8
+ Includes orphaned detection toggle and file pattern ignores. Configuration can be
9
+ loaded from dictionary (YAML) with sensible defaults for all options.
10
+
11
+ Dependencies: dataclasses, typing
12
+
13
+ Exports: LazyIgnoresConfig
14
+
15
+ Interfaces: LazyIgnoresConfig.from_dict() for YAML configuration loading
16
+
17
+ Implementation: Dataclass with factory defaults and validation in from_dict
18
+
19
+ Suppressions:
20
+ too-many-instance-attributes: Configuration dataclass requires many toggles
21
+ """
22
+
23
+ from dataclasses import dataclass, field
24
+ from typing import Any
25
+
26
+
27
+ @dataclass
28
+ class LazyIgnoresConfig: # pylint: disable=too-many-instance-attributes
29
+ """Configuration for the lazy-ignores linter."""
30
+
31
+ # Pattern detection toggles
32
+ check_noqa: bool = True
33
+ check_type_ignore: bool = True
34
+ check_pylint_disable: bool = True
35
+ check_nosec: bool = True
36
+ check_pyright_ignore: bool = True
37
+ check_ts_ignore: bool = True
38
+ check_eslint_disable: bool = True
39
+ check_thailint_ignore: bool = True
40
+ check_test_skips: bool = True
41
+
42
+ # Orphaned detection
43
+ check_orphaned: bool = True # Header entries without matching ignores
44
+
45
+ # Inline justification options
46
+ allow_inline_justifications: bool = True # Allow " - reason" syntax
47
+ min_justification_length: int = 10 # Minimum chars for valid justification
48
+
49
+ # File patterns to ignore
50
+ ignore_patterns: list[str] = field(
51
+ default_factory=lambda: [
52
+ "tests/**", # Don't enforce in test files by default
53
+ "**/__pycache__/**",
54
+ ]
55
+ )
56
+
57
+ @classmethod
58
+ def from_dict(cls, config_dict: dict[str, Any]) -> "LazyIgnoresConfig":
59
+ """Create config from dictionary."""
60
+ return cls(
61
+ check_noqa=config_dict.get("check_noqa", True),
62
+ check_type_ignore=config_dict.get("check_type_ignore", True),
63
+ check_pylint_disable=config_dict.get("check_pylint_disable", True),
64
+ check_nosec=config_dict.get("check_nosec", True),
65
+ check_pyright_ignore=config_dict.get("check_pyright_ignore", True),
66
+ check_ts_ignore=config_dict.get("check_ts_ignore", True),
67
+ check_eslint_disable=config_dict.get("check_eslint_disable", True),
68
+ check_thailint_ignore=config_dict.get("check_thailint_ignore", True),
69
+ check_test_skips=config_dict.get("check_test_skips", True),
70
+ check_orphaned=config_dict.get("check_orphaned", True),
71
+ allow_inline_justifications=config_dict.get("allow_inline_justifications", True),
72
+ min_justification_length=config_dict.get("min_justification_length", 10),
73
+ ignore_patterns=config_dict.get("ignore_patterns", []),
74
+ )