thailint 0.11.0__py3-none-any.whl → 0.13.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 (129) 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 +3 -0
  8. src/cli/linters/code_patterns.py +113 -5
  9. src/cli/linters/code_smells.py +118 -7
  10. src/cli/linters/documentation.py +3 -0
  11. src/cli/linters/structure.py +3 -0
  12. src/cli/linters/structure_quality.py +3 -0
  13. src/cli/utils.py +29 -9
  14. src/cli_main.py +3 -0
  15. src/config.py +2 -1
  16. src/core/base.py +3 -2
  17. src/core/cli_utils.py +3 -1
  18. src/core/config_parser.py +5 -2
  19. src/core/constants.py +54 -0
  20. src/core/linter_utils.py +4 -0
  21. src/core/rule_discovery.py +5 -1
  22. src/core/violation_builder.py +3 -0
  23. src/linter_config/directive_markers.py +109 -0
  24. src/linter_config/ignore.py +225 -383
  25. src/linter_config/pattern_utils.py +65 -0
  26. src/linter_config/rule_matcher.py +89 -0
  27. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  28. src/linters/collection_pipeline/ast_utils.py +40 -0
  29. src/linters/collection_pipeline/config.py +12 -0
  30. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  31. src/linters/collection_pipeline/detector.py +262 -32
  32. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  33. src/linters/collection_pipeline/linter.py +18 -35
  34. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  35. src/linters/dry/base_token_analyzer.py +16 -9
  36. src/linters/dry/block_filter.py +7 -4
  37. src/linters/dry/cache.py +7 -2
  38. src/linters/dry/config.py +7 -1
  39. src/linters/dry/constant_matcher.py +34 -25
  40. src/linters/dry/file_analyzer.py +4 -2
  41. src/linters/dry/inline_ignore.py +7 -16
  42. src/linters/dry/linter.py +48 -25
  43. src/linters/dry/python_analyzer.py +18 -10
  44. src/linters/dry/python_constant_extractor.py +51 -52
  45. src/linters/dry/single_statement_detector.py +14 -12
  46. src/linters/dry/token_hasher.py +115 -115
  47. src/linters/dry/typescript_analyzer.py +11 -6
  48. src/linters/dry/typescript_constant_extractor.py +4 -0
  49. src/linters/dry/typescript_statement_detector.py +208 -208
  50. src/linters/dry/typescript_value_extractor.py +3 -0
  51. src/linters/dry/violation_filter.py +1 -4
  52. src/linters/dry/violation_generator.py +1 -4
  53. src/linters/file_header/atemporal_detector.py +4 -0
  54. src/linters/file_header/base_parser.py +4 -0
  55. src/linters/file_header/bash_parser.py +4 -0
  56. src/linters/file_header/field_validator.py +5 -8
  57. src/linters/file_header/linter.py +19 -12
  58. src/linters/file_header/markdown_parser.py +6 -0
  59. src/linters/file_placement/config_loader.py +3 -1
  60. src/linters/file_placement/linter.py +22 -8
  61. src/linters/file_placement/pattern_matcher.py +21 -4
  62. src/linters/file_placement/pattern_validator.py +21 -7
  63. src/linters/file_placement/rule_checker.py +2 -2
  64. src/linters/lazy_ignores/__init__.py +43 -0
  65. src/linters/lazy_ignores/config.py +66 -0
  66. src/linters/lazy_ignores/directive_utils.py +121 -0
  67. src/linters/lazy_ignores/header_parser.py +177 -0
  68. src/linters/lazy_ignores/linter.py +158 -0
  69. src/linters/lazy_ignores/matcher.py +135 -0
  70. src/linters/lazy_ignores/python_analyzer.py +201 -0
  71. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  72. src/linters/lazy_ignores/skip_detector.py +298 -0
  73. src/linters/lazy_ignores/types.py +67 -0
  74. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  75. src/linters/lazy_ignores/violation_builder.py +131 -0
  76. src/linters/lbyl/__init__.py +29 -0
  77. src/linters/lbyl/config.py +63 -0
  78. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  79. src/linters/lbyl/pattern_detectors/base.py +46 -0
  80. src/linters/magic_numbers/context_analyzer.py +227 -229
  81. src/linters/magic_numbers/linter.py +20 -15
  82. src/linters/magic_numbers/python_analyzer.py +4 -16
  83. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  84. src/linters/method_property/config.py +4 -0
  85. src/linters/method_property/linter.py +5 -4
  86. src/linters/method_property/python_analyzer.py +5 -4
  87. src/linters/method_property/violation_builder.py +3 -0
  88. src/linters/nesting/typescript_analyzer.py +6 -12
  89. src/linters/nesting/typescript_function_extractor.py +0 -4
  90. src/linters/print_statements/linter.py +6 -4
  91. src/linters/print_statements/python_analyzer.py +85 -81
  92. src/linters/print_statements/typescript_analyzer.py +6 -15
  93. src/linters/srp/heuristics.py +4 -4
  94. src/linters/srp/linter.py +12 -12
  95. src/linters/srp/violation_builder.py +0 -4
  96. src/linters/stateless_class/linter.py +30 -36
  97. src/linters/stateless_class/python_analyzer.py +11 -20
  98. src/linters/stringly_typed/__init__.py +22 -9
  99. src/linters/stringly_typed/config.py +32 -8
  100. src/linters/stringly_typed/context_filter.py +451 -0
  101. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  102. src/linters/stringly_typed/ignore_checker.py +102 -0
  103. src/linters/stringly_typed/ignore_utils.py +51 -0
  104. src/linters/stringly_typed/linter.py +376 -0
  105. src/linters/stringly_typed/python/__init__.py +9 -5
  106. src/linters/stringly_typed/python/analyzer.py +159 -9
  107. src/linters/stringly_typed/python/call_tracker.py +175 -0
  108. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  109. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  110. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  111. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  112. src/linters/stringly_typed/python/validation_detector.py +3 -0
  113. src/linters/stringly_typed/storage.py +630 -0
  114. src/linters/stringly_typed/storage_initializer.py +45 -0
  115. src/linters/stringly_typed/typescript/__init__.py +28 -0
  116. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  117. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  118. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  119. src/linters/stringly_typed/violation_generator.py +405 -0
  120. src/orchestrator/core.py +13 -4
  121. src/templates/thailint_config_template.yaml +166 -0
  122. src/utils/project_root.py +3 -0
  123. thailint-0.13.0.dist-info/METADATA +184 -0
  124. thailint-0.13.0.dist-info/RECORD +189 -0
  125. thailint-0.11.0.dist-info/METADATA +0 -1661
  126. thailint-0.11.0.dist-info/RECORD +0 -150
  127. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
  128. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
  129. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,175 @@
1
+ """
2
+ Purpose: Detect function calls with string literal arguments in Python AST
3
+
4
+ Scope: Find function and method calls that consistently receive string arguments
5
+
6
+ Overview: Provides FunctionCallTracker class that traverses Python AST to find function
7
+ and method calls where string literals are passed as arguments. Tracks the function
8
+ name, parameter index, and string value to enable cross-file aggregation. When a
9
+ function is called with the same set of limited string values across files, it
10
+ suggests the parameter should be an enum. Handles both simple function calls
11
+ (foo("value")) and method calls (obj.method("value")).
12
+
13
+ Dependencies: ast module for AST parsing, dataclasses for pattern structure,
14
+ src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
15
+
16
+ Exports: FunctionCallTracker class, FunctionCallPattern dataclass
17
+
18
+ Interfaces: FunctionCallTracker.find_patterns(tree) -> list[FunctionCallPattern]
19
+
20
+ Implementation: AST NodeVisitor pattern with Call node handling for string arguments
21
+
22
+ Suppressions:
23
+ - invalid-name: visit_Call follows AST NodeVisitor method naming convention
24
+ """
25
+
26
+ import ast
27
+ from dataclasses import dataclass
28
+
29
+ from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
30
+
31
+
32
+ @dataclass
33
+ class FunctionCallPattern:
34
+ """Represents a function call with a string literal argument.
35
+
36
+ Captures information about a function or method call where a string literal
37
+ is passed as an argument, enabling cross-file analysis to detect limited
38
+ value sets that should be enums.
39
+ """
40
+
41
+ function_name: str
42
+ """Fully qualified function name (e.g., 'process' or 'obj.method')."""
43
+
44
+ param_index: int
45
+ """Index of the parameter receiving the string value (0-indexed)."""
46
+
47
+ string_value: str
48
+ """The string literal value passed to the function."""
49
+
50
+ line_number: int
51
+ """Line number where the call occurs (1-indexed)."""
52
+
53
+ column: int
54
+ """Column number where the call starts (0-indexed)."""
55
+
56
+
57
+ class FunctionCallTracker(ast.NodeVisitor):
58
+ """Tracks function calls with string literal arguments.
59
+
60
+ Finds patterns like 'process("active")' and 'obj.set_status("pending")' where
61
+ string literals are used for arguments that could be enums.
62
+ """
63
+
64
+ def __init__(self) -> None:
65
+ """Initialize the tracker."""
66
+ self.patterns: list[FunctionCallPattern] = []
67
+
68
+ def find_patterns(self, tree: ast.AST) -> list[FunctionCallPattern]:
69
+ """Find all function calls with string arguments in the AST.
70
+
71
+ Args:
72
+ tree: The AST to analyze
73
+
74
+ Returns:
75
+ List of FunctionCallPattern instances for each detected call
76
+ """
77
+ self.patterns = []
78
+ self.visit(tree)
79
+ return self.patterns
80
+
81
+ def visit_Call(self, node: ast.Call) -> None: # pylint: disable=invalid-name
82
+ """Visit a Call node to check for string arguments.
83
+
84
+ Handles both simple function calls and method calls, extracting
85
+ the function name and any string literal arguments.
86
+
87
+ Args:
88
+ node: The Call node to analyze
89
+ """
90
+ function_name = self._extract_function_name(node.func)
91
+ if function_name is None:
92
+ self.generic_visit(node)
93
+ return
94
+
95
+ self._check_positional_args(node, function_name)
96
+ self.generic_visit(node)
97
+
98
+ def _extract_function_name(self, func_node: ast.expr) -> str | None:
99
+ """Extract the function name from a call expression.
100
+
101
+ Handles simple names (foo) and attribute access (obj.method).
102
+
103
+ Args:
104
+ func_node: The function expression node
105
+
106
+ Returns:
107
+ Function name string or None if not extractable
108
+ """
109
+ if isinstance(func_node, ast.Name):
110
+ return func_node.id
111
+ if isinstance(func_node, ast.Attribute):
112
+ return self._extract_attribute_name(func_node)
113
+ return None
114
+
115
+ def _extract_attribute_name(self, node: ast.Attribute) -> str | None:
116
+ """Extract function name from an attribute access.
117
+
118
+ Builds qualified names like 'obj.method' or 'a.b.method'.
119
+
120
+ Args:
121
+ node: The Attribute node
122
+
123
+ Returns:
124
+ Qualified function name or None if too complex
125
+ """
126
+ parts: list[str] = [node.attr]
127
+ current = node.value
128
+ depth = 0
129
+
130
+ while depth < MAX_ATTRIBUTE_CHAIN_DEPTH:
131
+ if isinstance(current, ast.Name):
132
+ parts.append(current.id)
133
+ break
134
+ if isinstance(current, ast.Attribute):
135
+ parts.append(current.attr)
136
+ current = current.value
137
+ depth += 1
138
+ else:
139
+ # Complex expression (call result, subscript, etc.)
140
+ # Use placeholder to maintain function identity
141
+ parts.append("_")
142
+ break
143
+
144
+ return ".".join(reversed(parts))
145
+
146
+ def _check_positional_args(self, node: ast.Call, function_name: str) -> None:
147
+ """Check positional arguments for string literals.
148
+
149
+ Args:
150
+ node: The Call node
151
+ function_name: Extracted function name
152
+ """
153
+ for param_index, arg in enumerate(node.args):
154
+ if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
155
+ self._add_pattern(node, function_name, param_index, arg.value)
156
+
157
+ def _add_pattern(
158
+ self, node: ast.Call, function_name: str, param_index: int, string_value: str
159
+ ) -> None:
160
+ """Create and add a function call pattern to results.
161
+
162
+ Args:
163
+ node: The Call node containing the pattern
164
+ function_name: Name of the function being called
165
+ param_index: Index of the string argument
166
+ string_value: The string literal value
167
+ """
168
+ pattern = FunctionCallPattern(
169
+ function_name=function_name,
170
+ param_index=param_index,
171
+ string_value=string_value,
172
+ line_number=node.lineno,
173
+ column=node.col_offset,
174
+ )
175
+ self.patterns.append(pattern)
@@ -0,0 +1,257 @@
1
+ """
2
+ Purpose: Detect scattered string comparisons in Python AST
3
+
4
+ Scope: Find equality/inequality comparisons with string literals across Python files
5
+
6
+ Overview: Provides ComparisonTracker class that traverses Python AST to find scattered
7
+ string comparisons like `if env == "production"`. Tracks the variable name, compared
8
+ string value, and operator to enable cross-file aggregation. When a variable is compared
9
+ to multiple unique string values across files, it suggests the variable should be an enum.
10
+ Excludes common false positives like `__name__ == "__main__"` and type name checks.
11
+
12
+ Dependencies: ast module for AST parsing, dataclasses for pattern structure,
13
+ src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
14
+
15
+ Exports: ComparisonTracker class, ComparisonPattern dataclass
16
+
17
+ Interfaces: ComparisonTracker.find_patterns(tree) -> list[ComparisonPattern]
18
+
19
+ Implementation: AST NodeVisitor pattern with Compare node handling for string comparisons
20
+
21
+ Suppressions:
22
+ - invalid-name: visit_Compare follows AST NodeVisitor method naming convention
23
+ - srp: Tracker implements AST visitor pattern with multiple visit methods.
24
+ Methods support single responsibility of comparison pattern detection.
25
+ """
26
+
27
+ import ast
28
+ from dataclasses import dataclass
29
+
30
+ from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
31
+
32
+
33
+ @dataclass
34
+ class ComparisonPattern:
35
+ """Represents a string comparison found in Python code.
36
+
37
+ Captures information about a comparison like `if (env == "production")` to
38
+ enable cross-file analysis for detecting scattered string comparisons that
39
+ suggest missing enums.
40
+ """
41
+
42
+ variable_name: str
43
+ """Variable name being compared (e.g., 'env' or 'self.status')."""
44
+
45
+ compared_value: str
46
+ """The string literal value being compared to."""
47
+
48
+ operator: str
49
+ """The comparison operator ('==' or '!=')."""
50
+
51
+ line_number: int
52
+ """Line number where the comparison occurs (1-indexed)."""
53
+
54
+ column: int
55
+ """Column number where the comparison starts (0-indexed)."""
56
+
57
+
58
+ # Excluded variable names that are common false positives
59
+ _EXCLUDED_VARIABLES = frozenset(
60
+ {
61
+ "__name__",
62
+ "__class__.__name__",
63
+ }
64
+ )
65
+
66
+ # Excluded values that are common in legitimate comparisons
67
+ _EXCLUDED_VALUES = frozenset(
68
+ {
69
+ "__main__",
70
+ }
71
+ )
72
+
73
+
74
+ class ComparisonTracker(ast.NodeVisitor): # thailint: ignore[srp]
75
+ """Tracks scattered string comparisons in Python AST.
76
+
77
+ Finds patterns like `if env == "production"` and `if status != "deleted"` where
78
+ string literals are used for comparisons that could use enums instead.
79
+
80
+ Note: Method count exceeds SRP limit because AST traversal requires multiple helper
81
+ methods for extracting variable names, attribute names, and pattern filtering. All
82
+ methods support the single responsibility of tracking string comparisons.
83
+ """
84
+
85
+ def __init__(self) -> None:
86
+ """Initialize the tracker."""
87
+ self.patterns: list[ComparisonPattern] = []
88
+
89
+ def find_patterns(self, tree: ast.AST) -> list[ComparisonPattern]:
90
+ """Find all string comparisons in the AST.
91
+
92
+ Args:
93
+ tree: The AST to analyze
94
+
95
+ Returns:
96
+ List of ComparisonPattern instances for each detected comparison
97
+ """
98
+ self.patterns = []
99
+ self.visit(tree)
100
+ return self.patterns
101
+
102
+ def visit_Compare(self, node: ast.Compare) -> None: # pylint: disable=invalid-name
103
+ """Visit a Compare node to check for string comparisons.
104
+
105
+ Handles both `var == "string"` and `"string" == var` patterns.
106
+
107
+ Args:
108
+ node: The Compare node to analyze
109
+ """
110
+ self._check_comparison(node)
111
+ self.generic_visit(node)
112
+
113
+ def _check_comparison(self, node: ast.Compare) -> None:
114
+ """Check if comparison is a string comparison to track.
115
+
116
+ Args:
117
+ node: The Compare node to check
118
+ """
119
+ # Only handle simple binary comparisons
120
+ if len(node.ops) != 1 or len(node.comparators) != 1:
121
+ return
122
+
123
+ operator = node.ops[0]
124
+ if not isinstance(operator, (ast.Eq, ast.NotEq)):
125
+ return
126
+
127
+ op_str = "==" if isinstance(operator, ast.Eq) else "!="
128
+ left = node.left
129
+ right = node.comparators[0]
130
+
131
+ # Try both orientations: var == "string" and "string" == var
132
+ self._try_extract_pattern(left, right, op_str, node)
133
+ self._try_extract_pattern(right, left, op_str, node)
134
+
135
+ def _try_extract_pattern(
136
+ self,
137
+ var_side: ast.expr,
138
+ string_side: ast.expr,
139
+ operator: str,
140
+ node: ast.Compare,
141
+ ) -> None:
142
+ """Try to extract a pattern from a comparison.
143
+
144
+ Args:
145
+ var_side: The expression that might be a variable
146
+ string_side: The expression that might be a string literal
147
+ operator: The comparison operator
148
+ node: The original Compare node for location info
149
+ """
150
+ # Check if string_side is a string literal
151
+ if not isinstance(string_side, ast.Constant):
152
+ return
153
+ if not isinstance(string_side.value, str):
154
+ return
155
+
156
+ string_value = string_side.value
157
+
158
+ # Extract variable name
159
+ var_name = self._extract_variable_name(var_side)
160
+ if var_name is None:
161
+ return
162
+
163
+ # Check for excluded patterns
164
+ if self._should_exclude(var_name, string_value):
165
+ return
166
+
167
+ self._add_pattern(var_name, string_value, operator, node)
168
+
169
+ def _extract_variable_name(self, node: ast.expr) -> str | None:
170
+ """Extract variable name from an expression.
171
+
172
+ Handles simple names (var) and attribute access (obj.attr).
173
+
174
+ Args:
175
+ node: The expression to extract from
176
+
177
+ Returns:
178
+ Variable name string or None if not extractable
179
+ """
180
+ if isinstance(node, ast.Name):
181
+ return node.id
182
+ if isinstance(node, ast.Attribute):
183
+ return self._extract_attribute_name(node)
184
+ return None
185
+
186
+ def _extract_attribute_name(self, node: ast.Attribute) -> str | None:
187
+ """Extract attribute name from an attribute access.
188
+
189
+ Builds qualified names like 'obj.attr' or 'a.b.attr'.
190
+
191
+ Args:
192
+ node: The Attribute node
193
+
194
+ Returns:
195
+ Qualified attribute name or None if too complex
196
+ """
197
+ parts: list[str] = [node.attr]
198
+ current = node.value
199
+ depth = 0
200
+
201
+ while depth < MAX_ATTRIBUTE_CHAIN_DEPTH:
202
+ if isinstance(current, ast.Name):
203
+ parts.append(current.id)
204
+ break
205
+ if isinstance(current, ast.Attribute):
206
+ parts.append(current.attr)
207
+ current = current.value
208
+ depth += 1
209
+ else:
210
+ # Complex expression, still return what we have
211
+ parts.append("_")
212
+ break
213
+
214
+ return ".".join(reversed(parts))
215
+
216
+ def _should_exclude(self, var_name: str, string_value: str) -> bool:
217
+ """Check if this comparison should be excluded.
218
+
219
+ Filters out common patterns that are not stringly-typed code:
220
+ - __name__ == "__main__"
221
+ - __class__.__name__ checks
222
+
223
+ Args:
224
+ var_name: The variable name
225
+ string_value: The string value
226
+
227
+ Returns:
228
+ True if the comparison should be excluded
229
+ """
230
+ if var_name in _EXCLUDED_VARIABLES:
231
+ return True
232
+ if string_value in _EXCLUDED_VALUES:
233
+ return True
234
+ # Also exclude if the full qualified name ends with __name__
235
+ if var_name.endswith("__name__"):
236
+ return True
237
+ return False
238
+
239
+ def _add_pattern(
240
+ self, var_name: str, string_value: str, operator: str, node: ast.Compare
241
+ ) -> None:
242
+ """Create and add a comparison pattern to results.
243
+
244
+ Args:
245
+ var_name: The variable name
246
+ string_value: The string value being compared
247
+ operator: The comparison operator
248
+ node: The Compare node for location info
249
+ """
250
+ pattern = ComparisonPattern(
251
+ variable_name=var_name,
252
+ compared_value=string_value,
253
+ operator=operator,
254
+ line_number=node.lineno,
255
+ column=node.col_offset,
256
+ )
257
+ self.patterns.append(pattern)
@@ -15,6 +15,9 @@ Exports: extract_from_condition, is_simple_string_equality, get_string_constant
15
15
  Interfaces: Functions for extracting string comparisons from AST nodes
16
16
 
17
17
  Implementation: Recursive traversal of BoolOp nodes with Compare extraction
18
+
19
+ Suppressions:
20
+ - type:ignore[attr-defined]: AST node attribute access varies by node type (value.value)
18
21
  """
19
22
 
20
23
  import ast
@@ -18,6 +18,9 @@ Exports: ConditionalPatternDetector class, EqualityChainPattern dataclass
18
18
  Interfaces: ConditionalPatternDetector.find_patterns(tree) -> list[EqualityChainPattern]
19
19
 
20
20
  Implementation: AST NodeVisitor pattern with If node chain traversal and Match statement handling
21
+
22
+ Suppressions:
23
+ - invalid-name: visit_If, visit_Match follow AST NodeVisitor method naming convention
21
24
  """
22
25
 
23
26
  import ast
@@ -110,7 +113,7 @@ class ConditionalPatternDetector(ast.NodeVisitor):
110
113
  """
111
114
  pattern = analyze_match_statement(node, EqualityChainPattern)
112
115
  if pattern is not None:
113
- self.patterns.append(pattern) # type: ignore[arg-type]
116
+ self.patterns.append(pattern)
114
117
  self.generic_visit(node)
115
118
 
116
119
  def _analyze_if_chain(self, node: ast.If) -> None:
@@ -17,16 +17,22 @@ Interfaces: MatchStatementAnalyzer.analyze(node) -> EqualityChainPattern | None
17
17
  Implementation: AST pattern matching for MatchValue nodes with string constants
18
18
  """
19
19
 
20
+ from __future__ import annotations
21
+
20
22
  import ast
23
+ from typing import TYPE_CHECKING
21
24
 
22
25
  from .constants import MIN_VALUES_FOR_PATTERN
23
26
  from .variable_extractor import extract_variable_name
24
27
 
28
+ if TYPE_CHECKING:
29
+ from .conditional_detector import EqualityChainPattern
30
+
25
31
 
26
32
  def analyze_match_statement(
27
33
  node: ast.Match,
28
- pattern_class: type,
29
- ) -> object | None:
34
+ pattern_class: type[EqualityChainPattern],
35
+ ) -> EqualityChainPattern | None:
30
36
  """Analyze a match statement for string case patterns.
31
37
 
32
38
  Args:
@@ -18,6 +18,9 @@ Exports: MembershipValidationDetector class, MembershipPattern dataclass
18
18
  Interfaces: MembershipValidationDetector.find_patterns(tree) -> list[MembershipPattern]
19
19
 
20
20
  Implementation: AST NodeVisitor pattern with Compare node handling for In/NotIn operators
21
+
22
+ Suppressions:
23
+ - invalid-name: visit_Compare follows AST NodeVisitor method naming convention
21
24
  """
22
25
 
23
26
  import ast