thailint 0.12.0__py3-none-any.whl → 0.14.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.
Files changed (135) hide show
  1. src/analyzers/__init__.py +4 -3
  2. src/analyzers/ast_utils.py +54 -0
  3. src/analyzers/typescript_base.py +4 -0
  4. src/cli/__init__.py +3 -0
  5. src/cli/config.py +12 -12
  6. src/cli/config_merge.py +241 -0
  7. src/cli/linters/__init__.py +9 -0
  8. src/cli/linters/code_patterns.py +107 -257
  9. src/cli/linters/code_smells.py +48 -165
  10. src/cli/linters/documentation.py +21 -95
  11. src/cli/linters/performance.py +274 -0
  12. src/cli/linters/shared.py +232 -6
  13. src/cli/linters/structure.py +26 -21
  14. src/cli/linters/structure_quality.py +28 -21
  15. src/cli_main.py +3 -0
  16. src/config.py +2 -1
  17. src/core/base.py +3 -2
  18. src/core/cli_utils.py +3 -1
  19. src/core/config_parser.py +5 -2
  20. src/core/constants.py +54 -0
  21. src/core/linter_utils.py +95 -6
  22. src/core/rule_discovery.py +5 -1
  23. src/core/violation_builder.py +3 -0
  24. src/linter_config/directive_markers.py +109 -0
  25. src/linter_config/ignore.py +225 -383
  26. src/linter_config/pattern_utils.py +65 -0
  27. src/linter_config/rule_matcher.py +89 -0
  28. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  29. src/linters/collection_pipeline/ast_utils.py +40 -0
  30. src/linters/collection_pipeline/config.py +12 -0
  31. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  32. src/linters/collection_pipeline/detector.py +262 -32
  33. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  34. src/linters/collection_pipeline/linter.py +18 -35
  35. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  36. src/linters/dry/base_token_analyzer.py +16 -9
  37. src/linters/dry/block_filter.py +7 -4
  38. src/linters/dry/cache.py +7 -2
  39. src/linters/dry/config.py +7 -1
  40. src/linters/dry/constant_matcher.py +34 -25
  41. src/linters/dry/file_analyzer.py +4 -2
  42. src/linters/dry/inline_ignore.py +7 -16
  43. src/linters/dry/linter.py +48 -25
  44. src/linters/dry/python_analyzer.py +18 -10
  45. src/linters/dry/python_constant_extractor.py +51 -52
  46. src/linters/dry/single_statement_detector.py +14 -12
  47. src/linters/dry/token_hasher.py +115 -115
  48. src/linters/dry/typescript_analyzer.py +11 -6
  49. src/linters/dry/typescript_constant_extractor.py +4 -0
  50. src/linters/dry/typescript_statement_detector.py +208 -208
  51. src/linters/dry/typescript_value_extractor.py +3 -0
  52. src/linters/dry/violation_filter.py +1 -4
  53. src/linters/dry/violation_generator.py +1 -4
  54. src/linters/file_header/atemporal_detector.py +58 -40
  55. src/linters/file_header/base_parser.py +4 -0
  56. src/linters/file_header/bash_parser.py +4 -0
  57. src/linters/file_header/config.py +14 -0
  58. src/linters/file_header/field_validator.py +5 -8
  59. src/linters/file_header/linter.py +19 -12
  60. src/linters/file_header/markdown_parser.py +6 -0
  61. src/linters/file_placement/config_loader.py +3 -1
  62. src/linters/file_placement/linter.py +22 -8
  63. src/linters/file_placement/pattern_matcher.py +21 -4
  64. src/linters/file_placement/pattern_validator.py +21 -7
  65. src/linters/file_placement/rule_checker.py +2 -2
  66. src/linters/lazy_ignores/__init__.py +43 -0
  67. src/linters/lazy_ignores/config.py +66 -0
  68. src/linters/lazy_ignores/directive_utils.py +121 -0
  69. src/linters/lazy_ignores/header_parser.py +177 -0
  70. src/linters/lazy_ignores/linter.py +158 -0
  71. src/linters/lazy_ignores/matcher.py +135 -0
  72. src/linters/lazy_ignores/python_analyzer.py +205 -0
  73. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  74. src/linters/lazy_ignores/skip_detector.py +298 -0
  75. src/linters/lazy_ignores/types.py +69 -0
  76. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  77. src/linters/lazy_ignores/violation_builder.py +131 -0
  78. src/linters/lbyl/__init__.py +29 -0
  79. src/linters/lbyl/config.py +63 -0
  80. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  81. src/linters/lbyl/pattern_detectors/base.py +46 -0
  82. src/linters/magic_numbers/context_analyzer.py +227 -229
  83. src/linters/magic_numbers/linter.py +20 -15
  84. src/linters/magic_numbers/python_analyzer.py +4 -16
  85. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  86. src/linters/method_property/config.py +4 -1
  87. src/linters/method_property/linter.py +5 -10
  88. src/linters/method_property/python_analyzer.py +5 -4
  89. src/linters/method_property/violation_builder.py +3 -0
  90. src/linters/nesting/linter.py +11 -6
  91. src/linters/nesting/typescript_analyzer.py +6 -12
  92. src/linters/nesting/typescript_function_extractor.py +0 -4
  93. src/linters/nesting/violation_builder.py +1 -0
  94. src/linters/performance/__init__.py +91 -0
  95. src/linters/performance/config.py +43 -0
  96. src/linters/performance/constants.py +49 -0
  97. src/linters/performance/linter.py +149 -0
  98. src/linters/performance/python_analyzer.py +365 -0
  99. src/linters/performance/regex_analyzer.py +312 -0
  100. src/linters/performance/regex_linter.py +139 -0
  101. src/linters/performance/typescript_analyzer.py +236 -0
  102. src/linters/performance/violation_builder.py +160 -0
  103. src/linters/print_statements/linter.py +6 -4
  104. src/linters/print_statements/python_analyzer.py +85 -81
  105. src/linters/print_statements/typescript_analyzer.py +6 -15
  106. src/linters/srp/heuristics.py +4 -4
  107. src/linters/srp/linter.py +12 -12
  108. src/linters/srp/violation_builder.py +0 -4
  109. src/linters/stateless_class/linter.py +30 -36
  110. src/linters/stateless_class/python_analyzer.py +11 -20
  111. src/linters/stringly_typed/config.py +4 -5
  112. src/linters/stringly_typed/context_filter.py +410 -410
  113. src/linters/stringly_typed/function_call_violation_builder.py +93 -95
  114. src/linters/stringly_typed/linter.py +48 -16
  115. src/linters/stringly_typed/python/analyzer.py +5 -1
  116. src/linters/stringly_typed/python/call_tracker.py +8 -5
  117. src/linters/stringly_typed/python/comparison_tracker.py +10 -5
  118. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  119. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  120. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  121. src/linters/stringly_typed/python/validation_detector.py +3 -0
  122. src/linters/stringly_typed/storage.py +14 -14
  123. src/linters/stringly_typed/typescript/call_tracker.py +9 -3
  124. src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
  125. src/linters/stringly_typed/violation_generator.py +288 -259
  126. src/orchestrator/core.py +13 -4
  127. src/templates/thailint_config_template.yaml +196 -0
  128. src/utils/project_root.py +3 -0
  129. thailint-0.14.0.dist-info/METADATA +185 -0
  130. thailint-0.14.0.dist-info/RECORD +199 -0
  131. thailint-0.12.0.dist-info/METADATA +0 -1667
  132. thailint-0.12.0.dist-info/RECORD +0 -164
  133. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
  134. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
  135. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -42,15 +42,12 @@ class FieldValidator:
42
42
  Returns:
43
43
  List of (field_name, error_message) tuples for missing/invalid fields
44
44
  """
45
- violations = []
46
45
  required_fields = self._get_required_fields(language)
47
-
48
- for field_name in required_fields:
49
- error = self._check_field(fields, field_name)
50
- if error:
51
- violations.append(error)
52
-
53
- return violations
46
+ return [
47
+ error
48
+ for field_name in required_fields
49
+ if (error := self._check_field(fields, field_name))
50
+ ]
54
51
 
55
52
  def _check_field(self, fields: dict[str, str], field_name: str) -> tuple[str, str] | None:
56
53
  """Check a single field for presence and content."""
@@ -20,15 +20,22 @@ Interfaces: check(context) -> list[Violation] for rule validation, standard rule
20
20
 
21
21
  Implementation: Composition pattern with helper classes for parsing, validation,
22
22
  and violation building
23
+
24
+ Suppressions:
25
+ - type:ignore[type-var]: Protocol pattern with generic type matching
26
+ - srp: Rule class coordinates parsing, validation, and violation building for multiple
27
+ languages. Methods support single responsibility of file header validation.
23
28
  """
24
29
 
25
30
  from pathlib import Path
26
31
  from typing import Protocol
27
32
 
28
33
  from src.core.base import BaseLintContext, BaseLintRule
34
+ from src.core.constants import HEADER_SCAN_LINES, Language
29
35
  from src.core.linter_utils import load_linter_config
30
36
  from src.core.types import Violation
31
- from src.linter_config.ignore import get_ignore_parser
37
+ from src.linter_config.directive_markers import check_general_ignore, has_ignore_directive_marker
38
+ from src.linter_config.ignore import _check_specific_rule_ignore, get_ignore_parser
32
39
 
33
40
  from .atemporal_detector import AtemporalDetector
34
41
  from .bash_parser import BashHeaderParser
@@ -111,7 +118,7 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
111
118
  return []
112
119
 
113
120
  # Markdown has special atemporal handling
114
- if context.language == "markdown":
121
+ if context.language == Language.MARKDOWN:
115
122
  return self._check_markdown_header(parser, context, config)
116
123
 
117
124
  return self._check_header_with_parser(parser, context, config)
@@ -158,20 +165,20 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
158
165
 
159
166
  return self._has_custom_ignore_syntax(file_content)
160
167
 
161
- def _has_standard_ignore(self, file_content: str) -> bool: # thailint: ignore[nesting]
168
+ def _has_standard_ignore(self, file_content: str) -> bool:
162
169
  """Check standard ignore parser for file-level ignores."""
163
- first_lines = file_content.splitlines()[:10]
164
- for line in first_lines:
165
- if self._ignore_parser._has_ignore_directive_marker(line): # pylint: disable=protected-access
166
- if self._ignore_parser._check_specific_rule_ignore(line, self.rule_id): # pylint: disable=protected-access
167
- return True
168
- if self._ignore_parser._check_general_ignore(line): # pylint: disable=protected-access
169
- return True
170
- return False
170
+ first_lines = file_content.splitlines()[:HEADER_SCAN_LINES]
171
+ return any(self._line_has_matching_ignore(line) for line in first_lines)
172
+
173
+ def _line_has_matching_ignore(self, line: str) -> bool:
174
+ """Check if line has matching ignore directive for this rule."""
175
+ if not has_ignore_directive_marker(line):
176
+ return False
177
+ return _check_specific_rule_ignore(line, self.rule_id) or check_general_ignore(line)
171
178
 
172
179
  def _has_custom_ignore_syntax(self, file_content: str) -> bool:
173
180
  """Check custom file-level ignore syntax."""
174
- first_lines = file_content.splitlines()[:10]
181
+ first_lines = file_content.splitlines()[:HEADER_SCAN_LINES]
175
182
  return any(self._is_ignore_line(line) for line in first_lines)
176
183
 
177
184
  def _is_ignore_line(self, line: str) -> bool:
@@ -17,6 +17,12 @@ Interfaces: extract_header(code) -> str | None for frontmatter extraction,
17
17
  parse_fields(header) -> dict[str, str] for field parsing
18
18
 
19
19
  Implementation: YAML frontmatter extraction with PyYAML parsing and regex fallback for robustness
20
+
21
+ Suppressions:
22
+ - BLE001: Broad exception catch for YAML parsing fallback (any exception triggers regex fallback)
23
+ - srp: Class coordinates YAML extraction, parsing, and field validation for Markdown.
24
+ Method count exceeds limit due to complexity refactoring.
25
+ - nesting,dry: _parse_simple_yaml uses nested loops for YAML structure traversal.
20
26
  """
21
27
 
22
28
  import logging
@@ -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)
@@ -20,6 +20,10 @@ Interfaces: lint_path(file_path) -> list[Violation], check_file_allowed(file_pat
20
20
  Implementation: Composition pattern with helper classes for each responsibility
21
21
  (ConfigLoader, PathResolver, PatternMatcher, PatternValidator, RuleChecker,
22
22
  ViolationFactory)
23
+
24
+ Suppressions:
25
+ - srp.violation: Rule class coordinates multiple helper classes for comprehensive
26
+ file placement validation. Method count reflects composition orchestration.
23
27
  """
24
28
 
25
29
  import json
@@ -72,21 +76,31 @@ class FilePlacementLinter:
72
76
 
73
77
  # Load and validate config
74
78
  if config_obj:
75
- # Handle both wrapped and unwrapped config formats
76
- # Wrapped: {"file-placement": {...}} or {"file_placement": {...}}
77
- # Unwrapped: {"directories": {...}, "global_deny": [...], ...}
78
- # Try both hyphenated and underscored keys for backward compatibility
79
- self.config = config_obj.get(
80
- "file-placement", config_obj.get("file_placement", config_obj)
81
- )
79
+ self.config = self._unwrap_config(config_obj)
82
80
  elif config_file:
83
- self.config = self._components.config_loader.load_config_file(config_file)
81
+ raw_config = self._components.config_loader.load_config_file(config_file)
82
+ self.config = self._unwrap_config(raw_config)
84
83
  else:
85
84
  self.config = {}
86
85
 
87
86
  # Validate regex patterns in config
88
87
  self._components.pattern_validator.validate_config(self.config)
89
88
 
89
+ def _unwrap_config(self, config: dict[str, Any]) -> dict[str, Any]:
90
+ """Unwrap file-placement config from wrapper if present.
91
+
92
+ Args:
93
+ config: Raw config dict (may be wrapped or unwrapped)
94
+
95
+ Returns:
96
+ Unwrapped file-placement config dict
97
+ """
98
+ # Handle both wrapped and unwrapped config formats
99
+ # Wrapped: {"file-placement": {...}} or {"file_placement": {...}}
100
+ # Unwrapped: {"directories": {...}, "global_deny": [...], ...}
101
+ # Try both hyphenated and underscored keys for backward compatibility
102
+ return config.get("file-placement", config.get("file_placement", config))
103
+
90
104
  def lint_path(self, file_path: Path) -> list[Violation]:
91
105
  """Lint a single file path.
92
106
 
@@ -18,6 +18,7 @@ Implementation: Uses re.search() for pattern matching with IGNORECASE flag
18
18
  """
19
19
 
20
20
  import re
21
+ from collections.abc import Sequence
21
22
  from re import Pattern
22
23
 
23
24
 
@@ -42,24 +43,40 @@ class PatternMatcher:
42
43
  return self._compiled_patterns[pattern]
43
44
 
44
45
  def match_deny_patterns(
45
- self, path_str: str, deny_patterns: list[dict[str, str]]
46
+ self, path_str: str, deny_patterns: Sequence[dict[str, str] | str]
46
47
  ) -> tuple[bool, str | None]:
47
48
  """Check if path matches any deny patterns.
48
49
 
49
50
  Args:
50
51
  path_str: File path to check
51
- 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)
52
54
 
53
55
  Returns:
54
56
  Tuple of (is_denied, reason)
55
57
  """
56
58
  for deny_item in deny_patterns:
57
- compiled = self._get_compiled(deny_item["pattern"])
59
+ pattern, reason = self._extract_pattern_and_reason(deny_item)
60
+ compiled = self._get_compiled(pattern)
58
61
  if compiled.search(path_str):
59
- reason = deny_item.get("reason", "File not allowed in this location")
60
62
  return True, reason
61
63
  return False, None
62
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
+
63
80
  def match_allow_patterns(self, path_str: str, allow_patterns: list[str]) -> bool:
64
81
  """Check if path matches any allow patterns.
65
82
 
@@ -21,6 +21,20 @@ import re
21
21
  from typing import Any
22
22
 
23
23
 
24
+ def _extract_pattern(deny_item: dict[str, str] | str) -> str:
25
+ """Extract pattern from a deny item.
26
+
27
+ Args:
28
+ deny_item: Either a dict with 'pattern' key or a plain string pattern
29
+
30
+ Returns:
31
+ The pattern string
32
+ """
33
+ if isinstance(deny_item, str):
34
+ return deny_item
35
+ return deny_item.get("pattern", "")
36
+
37
+
24
38
  class PatternValidator:
25
39
  """Validates regex patterns in file placement configuration."""
26
40
 
@@ -32,15 +46,15 @@ class PatternValidator:
32
46
  """Validate all regex patterns in configuration.
33
47
 
34
48
  Args:
35
- config: Full configuration dict
49
+ config: File placement configuration dict (already unwrapped)
36
50
 
37
51
  Raises:
38
52
  ValueError: If any regex pattern is invalid
39
53
  """
40
- fp_config = config.get("file-placement", {})
41
- self._validate_directory_patterns(fp_config)
42
- self._validate_global_patterns(fp_config)
43
- self._validate_global_deny_patterns(fp_config)
54
+ # Config is already unwrapped from file-placement key by FilePlacementLinter
55
+ self._validate_directory_patterns(config)
56
+ self._validate_global_patterns(config)
57
+ self._validate_global_deny_patterns(config)
44
58
 
45
59
  def _validate_pattern(self, pattern: str) -> None:
46
60
  """Validate a single regex pattern.
@@ -74,7 +88,7 @@ class PatternValidator:
74
88
  """
75
89
  if "deny" in rules:
76
90
  for deny_item in rules["deny"]:
77
- pattern = deny_item.get("pattern", "")
91
+ pattern = _extract_pattern(deny_item)
78
92
  self._validate_pattern(pattern)
79
93
 
80
94
  def _validate_directory_patterns(self, fp_config: dict[str, Any]) -> None:
@@ -106,5 +120,5 @@ class PatternValidator:
106
120
  """
107
121
  if "global_deny" in fp_config:
108
122
  for deny_item in fp_config["global_deny"]:
109
- pattern = deny_item.get("pattern", "")
123
+ pattern = _extract_pattern(deny_item)
110
124
  self._validate_pattern(pattern)
@@ -177,14 +177,14 @@ class RuleChecker:
177
177
  return [violation] if violation else []
178
178
 
179
179
  def _check_global_deny(
180
- self, path_str: str, rel_path: Path, global_deny: list[dict[str, str]]
180
+ self, path_str: str, rel_path: Path, global_deny: list[dict[str, str] | str]
181
181
  ) -> list[Violation]:
182
182
  """Check file against global deny patterns.
183
183
 
184
184
  Args:
185
185
  path_str: Normalized path string
186
186
  rel_path: Relative path object
187
- global_deny: Global deny patterns
187
+ global_deny: Global deny patterns (dicts with pattern/reason or plain strings)
188
188
 
189
189
  Returns:
190
190
  List of violations
@@ -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,66 @@
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, typescript, eslint, thailint). Includes
8
+ orphaned detection toggle and file pattern ignores. Configuration can be loaded from
9
+ 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_ts_ignore: bool = True
37
+ check_eslint_disable: bool = True
38
+ check_thailint_ignore: bool = True
39
+ check_test_skips: bool = True
40
+
41
+ # Orphaned detection
42
+ check_orphaned: bool = True # Header entries without matching ignores
43
+
44
+ # File patterns to ignore
45
+ ignore_patterns: list[str] = field(
46
+ default_factory=lambda: [
47
+ "tests/**", # Don't enforce in test files by default
48
+ "**/__pycache__/**",
49
+ ]
50
+ )
51
+
52
+ @classmethod
53
+ def from_dict(cls, config_dict: dict[str, Any]) -> "LazyIgnoresConfig":
54
+ """Create config from dictionary."""
55
+ return cls(
56
+ check_noqa=config_dict.get("check_noqa", True),
57
+ check_type_ignore=config_dict.get("check_type_ignore", True),
58
+ check_pylint_disable=config_dict.get("check_pylint_disable", True),
59
+ check_nosec=config_dict.get("check_nosec", True),
60
+ check_ts_ignore=config_dict.get("check_ts_ignore", True),
61
+ check_eslint_disable=config_dict.get("check_eslint_disable", True),
62
+ check_thailint_ignore=config_dict.get("check_thailint_ignore", True),
63
+ check_test_skips=config_dict.get("check_test_skips", True),
64
+ check_orphaned=config_dict.get("check_orphaned", True),
65
+ ignore_patterns=config_dict.get("ignore_patterns", []),
66
+ )
@@ -0,0 +1,121 @@
1
+ """
2
+ Purpose: Shared utility functions for creating IgnoreDirective objects
3
+
4
+ Scope: Common directive creation and path normalization for ignore detectors
5
+
6
+ Overview: Provides shared utility functions used across Python, TypeScript, and test skip
7
+ detectors. Centralizes logic for normalizing file paths, extracting rule IDs from
8
+ regex matches, and creating IgnoreDirective objects to avoid code duplication.
9
+
10
+ Dependencies: re for match handling, pathlib for file paths, types module for dataclasses
11
+
12
+ Exports: normalize_path, extract_rule_ids, create_directive, create_directive_no_rules
13
+
14
+ Interfaces: Pure utility functions with no state
15
+
16
+ Implementation: Simple helper functions for directive creation
17
+ """
18
+
19
+ import re
20
+ from pathlib import Path
21
+
22
+ from src.linters.lazy_ignores.types import IgnoreDirective, IgnoreType
23
+
24
+
25
+ def normalize_path(file_path: Path | str | None) -> Path:
26
+ """Normalize file path to Path object.
27
+
28
+ Args:
29
+ file_path: Path object, string path, or None
30
+
31
+ Returns:
32
+ Path object, defaults to Path("unknown") if None
33
+ """
34
+ if file_path is None:
35
+ return Path("unknown")
36
+ if isinstance(file_path, str):
37
+ return Path(file_path)
38
+ return file_path
39
+
40
+
41
+ def _get_captured_group(match: re.Match[str]) -> str | None:
42
+ """Get the first captured group from a regex match if it exists.
43
+
44
+ Args:
45
+ match: Regex match object
46
+
47
+ Returns:
48
+ Captured group text or None if no capture groups
49
+ """
50
+ if match.lastindex is None or match.lastindex < 1:
51
+ return None
52
+ return match.group(1)
53
+
54
+
55
+ def extract_rule_ids(match: re.Match[str]) -> list[str]:
56
+ """Extract rule IDs from regex match group 1.
57
+
58
+ Args:
59
+ match: Regex match object with optional group 1 containing rule IDs
60
+
61
+ Returns:
62
+ List of rule ID strings, empty if no specific rules
63
+ """
64
+ group = _get_captured_group(match)
65
+ if not group:
66
+ return []
67
+
68
+ ids = [rule_id.strip() for rule_id in group.split(",")]
69
+ return [rule_id for rule_id in ids if rule_id]
70
+
71
+
72
+ def create_directive(
73
+ match: re.Match[str],
74
+ ignore_type: IgnoreType,
75
+ line_num: int,
76
+ file_path: Path,
77
+ rule_ids: tuple[str, ...] | None = None,
78
+ ) -> IgnoreDirective:
79
+ """Create an IgnoreDirective from a regex match.
80
+
81
+ Args:
82
+ match: Regex match object
83
+ ignore_type: Type of ignore pattern
84
+ line_num: 1-indexed line number
85
+ file_path: Path to source file
86
+ rule_ids: Optional tuple of rule IDs; if None, extracts from match group 1
87
+
88
+ Returns:
89
+ IgnoreDirective for this match
90
+ """
91
+ if rule_ids is None:
92
+ rule_ids = tuple(extract_rule_ids(match))
93
+
94
+ return IgnoreDirective(
95
+ ignore_type=ignore_type,
96
+ rule_ids=rule_ids,
97
+ line=line_num,
98
+ column=match.start() + 1,
99
+ raw_text=match.group(0).strip(),
100
+ file_path=file_path,
101
+ )
102
+
103
+
104
+ def create_directive_no_rules(
105
+ match: re.Match[str],
106
+ ignore_type: IgnoreType,
107
+ line_num: int,
108
+ file_path: Path,
109
+ ) -> IgnoreDirective:
110
+ """Create an IgnoreDirective without rule IDs (for patterns like test skips).
111
+
112
+ Args:
113
+ match: Regex match object
114
+ ignore_type: Type of ignore pattern
115
+ line_num: 1-indexed line number
116
+ file_path: Path to source file
117
+
118
+ Returns:
119
+ IgnoreDirective with empty rule_ids tuple
120
+ """
121
+ return create_directive(match, ignore_type, line_num, file_path, rule_ids=())