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
@@ -10,9 +10,9 @@ Overview: Provides PythonPrintStatementAnalyzer class that traverses Python AST
10
10
  Returns structured data about each print call including the AST node, parent context,
11
11
  and line number for violation reporting. Handles both simple print() and builtins.print() calls.
12
12
 
13
- Dependencies: ast module for AST parsing and node types
13
+ Dependencies: ast module for AST parsing and node types, analyzers.ast_utils
14
14
 
15
- Exports: PythonPrintStatementAnalyzer class
15
+ Exports: PythonPrintStatementAnalyzer class, is_print_call function, is_main_if_block function
16
16
 
17
17
  Interfaces: find_print_calls(tree) -> list[tuple[Call, AST | None, int]], is_in_main_block(node) -> bool
18
18
 
@@ -21,8 +21,87 @@ Implementation: AST walk pattern with parent map for context detection and __mai
21
21
 
22
22
  import ast
23
23
 
24
+ from src.analyzers.ast_utils import build_parent_map
24
25
 
25
- class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
26
+ # --- Pure helper functions for print call detection ---
27
+
28
+
29
+ def is_print_call(node: ast.Call) -> bool:
30
+ """Check if a Call node is calling print().
31
+
32
+ Args:
33
+ node: The Call node to check
34
+
35
+ Returns:
36
+ True if this is a print() call
37
+ """
38
+ return _is_simple_print(node) or _is_builtins_print(node)
39
+
40
+
41
+ def _is_simple_print(node: ast.Call) -> bool:
42
+ """Check for simple print() call."""
43
+ return isinstance(node.func, ast.Name) and node.func.id == "print"
44
+
45
+
46
+ def _is_builtins_print(node: ast.Call) -> bool:
47
+ """Check for builtins.print() call."""
48
+ if not isinstance(node.func, ast.Attribute):
49
+ return False
50
+ if node.func.attr != "print":
51
+ return False
52
+ return isinstance(node.func.value, ast.Name) and node.func.value.id == "builtins"
53
+
54
+
55
+ # --- Pure helper functions for __main__ block detection ---
56
+
57
+
58
+ def is_main_if_block(node: ast.AST) -> bool:
59
+ """Check if node is an `if __name__ == "__main__":` statement.
60
+
61
+ Args:
62
+ node: AST node to check
63
+
64
+ Returns:
65
+ True if this is a __main__ if block
66
+ """
67
+ if not isinstance(node, ast.If):
68
+ return False
69
+ if not isinstance(node.test, ast.Compare):
70
+ return False
71
+ return _is_main_comparison(node.test)
72
+
73
+
74
+ def _is_main_comparison(test: ast.Compare) -> bool:
75
+ """Check if comparison is __name__ == '__main__'."""
76
+ if not _is_name_identifier(test.left):
77
+ return False
78
+ if not _has_single_eq_operator(test):
79
+ return False
80
+ return _compares_to_main(test)
81
+
82
+
83
+ def _is_name_identifier(node: ast.expr) -> bool:
84
+ """Check if node is the __name__ identifier."""
85
+ return isinstance(node, ast.Name) and node.id == "__name__"
86
+
87
+
88
+ def _has_single_eq_operator(test: ast.Compare) -> bool:
89
+ """Check if comparison has single == operator."""
90
+ return len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)
91
+
92
+
93
+ def _compares_to_main(test: ast.Compare) -> bool:
94
+ """Check if comparison is to '__main__' string."""
95
+ if len(test.comparators) != 1:
96
+ return False
97
+ comparator = test.comparators[0]
98
+ return isinstance(comparator, ast.Constant) and comparator.value == "__main__"
99
+
100
+
101
+ # --- Analyzer class with stateful parent tracking ---
102
+
103
+
104
+ class PythonPrintStatementAnalyzer:
26
105
  """Analyzes Python AST to find print() calls."""
27
106
 
28
107
  def __init__(self) -> None:
@@ -40,24 +119,10 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
40
119
  List of tuples (node, parent, line_number)
41
120
  """
42
121
  self.print_calls = []
43
- self.parent_map = {}
44
- self._build_parent_map(tree)
122
+ self.parent_map = build_parent_map(tree)
45
123
  self._collect_print_calls(tree)
46
124
  return self.print_calls
47
125
 
48
- def _build_parent_map(self, node: ast.AST, parent: ast.AST | None = None) -> None:
49
- """Build a map of nodes to their parents.
50
-
51
- Args:
52
- node: Current AST node
53
- parent: Parent of current node
54
- """
55
- if parent is not None:
56
- self.parent_map[node] = parent
57
-
58
- for child in ast.iter_child_nodes(node):
59
- self._build_parent_map(child, node)
60
-
61
126
  def _collect_print_calls(self, tree: ast.AST) -> None:
62
127
  """Walk tree and collect all print() calls.
63
128
 
@@ -65,34 +130,11 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
65
130
  tree: AST to traverse
66
131
  """
67
132
  for node in ast.walk(tree):
68
- if isinstance(node, ast.Call) and self._is_print_call(node):
133
+ if isinstance(node, ast.Call) and is_print_call(node):
69
134
  parent = self.parent_map.get(node)
70
135
  line_number = node.lineno if hasattr(node, "lineno") else 0
71
136
  self.print_calls.append((node, parent, line_number))
72
137
 
73
- def _is_print_call(self, node: ast.Call) -> bool:
74
- """Check if a Call node is calling print().
75
-
76
- Args:
77
- node: The Call node to check
78
-
79
- Returns:
80
- True if this is a print() call
81
- """
82
- return self._is_simple_print(node) or self._is_builtins_print(node)
83
-
84
- def _is_simple_print(self, node: ast.Call) -> bool:
85
- """Check for simple print() call."""
86
- return isinstance(node.func, ast.Name) and node.func.id == "print"
87
-
88
- def _is_builtins_print(self, node: ast.Call) -> bool:
89
- """Check for builtins.print() call."""
90
- if not isinstance(node.func, ast.Attribute):
91
- return False
92
- if node.func.attr != "print":
93
- return False
94
- return isinstance(node.func.value, ast.Name) and node.func.value.id == "builtins"
95
-
96
138
  def is_in_main_block(self, node: ast.AST) -> bool:
97
139
  """Check if node is within `if __name__ == "__main__":` block.
98
140
 
@@ -105,45 +147,7 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
105
147
  current = node
106
148
  while current in self.parent_map:
107
149
  parent = self.parent_map[current]
108
- if self._is_main_if_block(parent):
150
+ if is_main_if_block(parent):
109
151
  return True
110
152
  current = parent
111
153
  return False
112
-
113
- def _is_main_if_block(self, node: ast.AST) -> bool:
114
- """Check if node is an `if __name__ == "__main__":` statement.
115
-
116
- Args:
117
- node: AST node to check
118
-
119
- Returns:
120
- True if this is a __main__ if block
121
- """
122
- if not isinstance(node, ast.If):
123
- return False
124
- if not isinstance(node.test, ast.Compare):
125
- return False
126
- return self._is_main_comparison(node.test)
127
-
128
- def _is_main_comparison(self, test: ast.Compare) -> bool:
129
- """Check if comparison is __name__ == '__main__'."""
130
- if not self._is_name_identifier(test.left):
131
- return False
132
- if not self._has_single_eq_operator(test):
133
- return False
134
- return self._compares_to_main(test)
135
-
136
- def _is_name_identifier(self, node: ast.expr) -> bool:
137
- """Check if node is the __name__ identifier."""
138
- return isinstance(node, ast.Name) and node.id == "__name__"
139
-
140
- def _has_single_eq_operator(self, test: ast.Compare) -> bool:
141
- """Check if comparison has single == operator."""
142
- return len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)
143
-
144
- def _compares_to_main(self, test: ast.Compare) -> bool:
145
- """Check if comparison is to '__main__' string."""
146
- if len(test.comparators) != 1:
147
- return False
148
- comparator = test.comparators[0]
149
- return isinstance(comparator, ast.Constant) and comparator.value == "__main__"
@@ -18,32 +18,23 @@ Exports: TypeScriptPrintStatementAnalyzer class
18
18
  Interfaces: find_console_calls(root_node, methods) -> list[tuple[Node, str, int]]
19
19
 
20
20
  Implementation: Tree-sitter node traversal with call_expression and member_expression pattern matching
21
+
21
22
  """
22
23
 
23
24
  import logging
24
- from typing import Any
25
25
 
26
- from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
26
+ from src.analyzers.typescript_base import (
27
+ TREE_SITTER_AVAILABLE,
28
+ Node,
29
+ TypeScriptBaseAnalyzer,
30
+ )
27
31
 
28
32
  logger = logging.getLogger(__name__)
29
33
 
30
- # dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
31
- try:
32
- from tree_sitter import Node
33
-
34
- TREE_SITTER_AVAILABLE = True
35
- except ImportError:
36
- TREE_SITTER_AVAILABLE = False
37
- Node = Any # type: ignore
38
-
39
34
 
40
35
  class TypeScriptPrintStatementAnalyzer(TypeScriptBaseAnalyzer):
41
36
  """Analyzes TypeScript/JavaScript code for console.* calls using Tree-sitter."""
42
37
 
43
- def __init__(self) -> None: # pylint: disable=useless-parent-delegation
44
- """Initialize the TypeScript print statement analyzer."""
45
- super().__init__() # Sets self.tree_sitter_available from base class
46
-
47
38
  def find_console_calls(self, root_node: Node, methods: set[str]) -> list[tuple[Node, str, int]]:
48
39
  """Find all console.* calls matching the specified methods.
49
40
 
@@ -84,7 +84,7 @@ def has_property_decorator(func_node: ast.FunctionDef | ast.AsyncFunctionDef) ->
84
84
  Returns:
85
85
  True if function has @property decorator
86
86
  """
87
- for decorator in func_node.decorator_list:
88
- if isinstance(decorator, ast.Name) and decorator.id == "property":
89
- return True
90
- return False
87
+ return any(
88
+ isinstance(decorator, ast.Name) and decorator.id == "property"
89
+ for decorator in func_node.decorator_list
90
+ )
src/linters/srp/linter.py CHANGED
@@ -16,9 +16,13 @@ Exports: SRPRule class
16
16
  Interfaces: SRPRule.check(context) -> list[Violation], properties for rule metadata
17
17
 
18
18
  Implementation: Composition pattern with helper classes, heuristic-based SRP analysis
19
+
20
+ Suppressions:
21
+ - type:ignore[return-value]: Generic TypeScript analyzer return type variance
19
22
  """
20
23
 
21
24
  from src.core.base import BaseLintContext, MultiLanguageLintRule
25
+ from src.core.constants import Language
22
26
  from src.core.linter_utils import load_linter_config
23
27
  from src.core.types import Violation
24
28
  from src.linter_config.ignore import get_ignore_parser
@@ -100,10 +104,10 @@ class SRPRule(MultiLanguageLintRule):
100
104
  Returns:
101
105
  List of violations found
102
106
  """
103
- if context.language == "python":
107
+ if context.language == Language.PYTHON:
104
108
  return self._check_python(context, config)
105
109
 
106
- if context.language in ("typescript", "javascript"):
110
+ if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
107
111
  return self._check_typescript(context, config)
108
112
 
109
113
  return []
@@ -133,10 +137,7 @@ class SRPRule(MultiLanguageLintRule):
133
137
  return False
134
138
 
135
139
  file_path = str(context.file_path)
136
- for pattern in config.ignore:
137
- if pattern in file_path:
138
- return True
139
- return False
140
+ return any(pattern in file_path for pattern in config.ignore)
140
141
 
141
142
  def _check_python(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
142
143
  """Check Python code for SRP violations.
@@ -170,13 +171,12 @@ class SRPRule(MultiLanguageLintRule):
170
171
  Returns:
171
172
  List of violations
172
173
  """
173
- violations = []
174
174
  valid_metrics = (m for m in metrics_list if isinstance(m, dict))
175
- for metrics in valid_metrics:
176
- violation = self._create_violation_if_needed(metrics, config, context)
177
- if violation:
178
- violations.append(violation)
179
- return violations
175
+ return [
176
+ violation
177
+ for metrics in valid_metrics
178
+ if (violation := self._create_violation_if_needed(metrics, config, context))
179
+ ]
180
180
 
181
181
  def _create_violation_if_needed(
182
182
  self,
@@ -29,10 +29,6 @@ from src.core.violation_builder import BaseViolationBuilder, ViolationInfo
29
29
  class ViolationBuilder(BaseViolationBuilder):
30
30
  """Builds SRP violations with messages and suggestions."""
31
31
 
32
- def __init__(self) -> None: # pylint: disable=useless-parent-delegation
33
- """Initialize the violation builder."""
34
- super().__init__() # Inherits from BaseViolationBuilder
35
-
36
32
  def build_violation(
37
33
  self,
38
34
  metrics: dict[str, Any],
@@ -20,13 +20,20 @@ Interfaces: StatelessClassRule.check(context) -> list[Violation]
20
20
 
21
21
  Implementation: Composition pattern delegating analysis to specialized analyzer with
22
22
  config loading and comprehensive ignore checking
23
+
24
+ Suppressions:
25
+ - B101: Type narrowing assertion after _should_analyze guard (can't fail)
26
+ - srp,dry: Rule class coordinates analyzer, config, and ignore checking. Method count
27
+ exceeds limit due to comprehensive 5-level ignore system support.
23
28
  """
24
29
 
25
30
  from pathlib import Path
26
31
 
27
32
  from src.core.base import BaseLintContext, BaseLintRule
33
+ from src.core.constants import HEADER_SCAN_LINES, IgnoreDirective, Language
28
34
  from src.core.types import Severity, Violation
29
35
  from src.linter_config.ignore import get_ignore_parser
36
+ from src.linter_config.rule_matcher import rule_matches
30
37
 
31
38
  from .config import StatelessClassConfig
32
39
  from .python_analyzer import ClassInfo, StatelessClassAnalyzer
@@ -67,20 +74,29 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
67
74
  return []
68
75
 
69
76
  config = self._load_config(context)
70
- if not config.enabled:
71
- return []
72
-
73
- if self._is_file_ignored(context, config):
77
+ if not config.enabled or self._should_skip_file(context, config):
74
78
  return []
75
79
 
76
- if self._has_file_level_ignore(context):
77
- return []
80
+ # _should_analyze ensures file_content is set
81
+ assert context.file_content is not None # nosec B101
78
82
 
79
83
  analyzer = StatelessClassAnalyzer(min_methods=config.min_methods)
80
- stateless_classes = analyzer.analyze(context.file_content) # type: ignore[arg-type]
84
+ stateless_classes = analyzer.analyze(context.file_content)
81
85
 
82
86
  return self._filter_ignored_violations(stateless_classes, context)
83
87
 
88
+ def _should_skip_file(self, context: BaseLintContext, config: StatelessClassConfig) -> bool:
89
+ """Check if file should be skipped due to ignore patterns or directives.
90
+
91
+ Args:
92
+ context: Lint context
93
+ config: Configuration
94
+
95
+ Returns:
96
+ True if file should be skipped
97
+ """
98
+ return self._is_file_ignored(context, config) or self._has_file_level_ignore(context)
99
+
84
100
  def _should_analyze(self, context: BaseLintContext) -> bool:
85
101
  """Check if context should be analyzed.
86
102
 
@@ -90,7 +106,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
90
106
  Returns:
91
107
  True if should analyze
92
108
  """
93
- return context.language == "python" and context.file_content is not None
109
+ return context.language == Language.PYTHON and context.file_content is not None
94
110
 
95
111
  def _load_config(self, context: BaseLintContext) -> StatelessClassConfig:
96
112
  """Load configuration from context.
@@ -129,10 +145,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
129
145
  return False
130
146
 
131
147
  file_path = Path(context.file_path)
132
- for pattern in config.ignore:
133
- if self._matches_pattern(file_path, pattern):
134
- return True
135
- return False
148
+ return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
136
149
 
137
150
  def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
138
151
  """Check if file path matches a glob pattern.
@@ -162,12 +175,9 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
162
175
  if not context.file_content:
163
176
  return False
164
177
 
165
- # Check first 10 lines for ignore-file directive
166
- lines = context.file_content.splitlines()[:10]
167
- for line in lines:
168
- if self._is_file_ignore_directive(line):
169
- return True
170
- return False
178
+ # Check first lines for ignore-file directive
179
+ lines = context.file_content.splitlines()[:HEADER_SCAN_LINES]
180
+ return any(self._is_file_ignore_directive(line) for line in lines)
171
181
 
172
182
  def _is_file_ignore_directive(self, line: str) -> bool:
173
183
  """Check if line is a file-level ignore directive.
@@ -218,23 +228,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
218
228
  Returns:
219
229
  True if pattern matches this rule
220
230
  """
221
- rule_id_lower = self.rule_id.lower()
222
- pattern_lower = rule_pattern.lower()
223
-
224
- # Exact match
225
- if rule_id_lower == pattern_lower:
226
- return True
227
-
228
- # Prefix match: stateless-class matches stateless-class.violation
229
- if rule_id_lower.startswith(pattern_lower + "."):
230
- return True
231
-
232
- # Wildcard match: stateless-class.* matches stateless-class.violation
233
- if pattern_lower.endswith("*"):
234
- prefix = pattern_lower[:-1]
235
- return rule_id_lower.startswith(prefix)
236
-
237
- return False
231
+ return rule_matches(self.rule_id, rule_pattern)
238
232
 
239
233
  def _filter_ignored_violations(
240
234
  self, classes: list[ClassInfo], context: BaseLintContext
@@ -330,7 +324,7 @@ class StatelessClassRule(BaseLintRule): # thailint: ignore[srp,dry]
330
324
  return True
331
325
 
332
326
  # Rule-specific ignore
333
- return self._matches_rule_ignore(line, "ignore")
327
+ return self._matches_rule_ignore(line, IgnoreDirective.IGNORE)
334
328
 
335
329
  def _create_violation(self, info: ClassInfo, context: BaseLintContext) -> Violation:
336
330
  """Create violation from class info.
@@ -145,10 +145,10 @@ def _has_constructor(class_node: ast.ClassDef) -> bool:
145
145
  True if class has constructor
146
146
  """
147
147
  constructor_names = ("__init__", "__new__")
148
- for item in class_node.body:
149
- if isinstance(item, ast.FunctionDef) and item.name in constructor_names:
150
- return True
151
- return False
148
+ return any(
149
+ isinstance(item, ast.FunctionDef) and item.name in constructor_names
150
+ for item in class_node.body
151
+ )
152
152
 
153
153
 
154
154
  def _is_exception_case(class_node: ast.ClassDef) -> bool:
@@ -174,10 +174,7 @@ def _inherits_from_abc_or_protocol(class_node: ast.ClassDef) -> bool:
174
174
  Returns:
175
175
  True if inherits from ABC or Protocol
176
176
  """
177
- for base in class_node.bases:
178
- if _get_base_name(base) in ("ABC", "Protocol"):
179
- return True
180
- return False
177
+ return any(_get_base_name(base) in ("ABC", "Protocol") for base in class_node.bases)
181
178
 
182
179
 
183
180
  def _get_base_name(base: ast.expr) -> str:
@@ -205,10 +202,7 @@ def _has_class_attributes(class_node: ast.ClassDef) -> bool:
205
202
  Returns:
206
203
  True if class has class attributes
207
204
  """
208
- for item in class_node.body:
209
- if isinstance(item, (ast.Assign, ast.AnnAssign)):
210
- return True
211
- return False
205
+ return any(isinstance(item, (ast.Assign, ast.AnnAssign)) for item in class_node.body)
212
206
 
213
207
 
214
208
  def _has_instance_attributes(class_node: ast.ClassDef) -> bool:
@@ -220,10 +214,10 @@ def _has_instance_attributes(class_node: ast.ClassDef) -> bool:
220
214
  Returns:
221
215
  True if any method assigns to self
222
216
  """
223
- for item in class_node.body:
224
- if isinstance(item, ast.FunctionDef) and _method_has_self_assignment(item):
225
- return True
226
- return False
217
+ return any(
218
+ isinstance(item, ast.FunctionDef) and _method_has_self_assignment(item)
219
+ for item in class_node.body
220
+ )
227
221
 
228
222
 
229
223
  def _method_has_self_assignment(method: ast.FunctionDef) -> bool:
@@ -235,10 +229,7 @@ def _method_has_self_assignment(method: ast.FunctionDef) -> bool:
235
229
  Returns:
236
230
  True if method assigns to self
237
231
  """
238
- for node in ast.walk(method):
239
- if _is_self_attribute_assignment(node):
240
- return True
241
- return False
232
+ return any(_is_self_attribute_assignment(node) for node in ast.walk(method))
242
233
 
243
234
 
244
235
  def _is_self_attribute_assignment(node: ast.AST) -> bool:
@@ -18,11 +18,10 @@ Exports: StringlyTypedConfig dataclass, default constants
18
18
  Interfaces: StringlyTypedConfig.from_dict() class method for configuration loading
19
19
 
20
20
  Implementation: Dataclass with sensible defaults, validation in __post_init__, and config
21
- loading from dictionary with language-specific override support. Pylint
22
- too-many-instance-attributes suppressed because configuration dataclasses inherently
23
- require multiple cohesive fields (8 attributes for detection thresholds, filtering,
24
- cross-file settings). Splitting would reduce cohesion without benefit. This follows
25
- the established pattern in DRYConfig which has the same suppression.
21
+ loading from dictionary with language-specific override support
22
+
23
+ Suppressions:
24
+ - too-many-instance-attributes: Configuration dataclass with cohesive detection settings
26
25
  """
27
26
 
28
27
  from dataclasses import dataclass, field