thailint 0.11.0__py3-none-any.whl → 0.12.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.
@@ -0,0 +1,51 @@
1
+ """
2
+ Purpose: Shared ignore pattern matching utilities
3
+
4
+ Scope: Common ignore pattern checking for stringly-typed linter components
5
+
6
+ Overview: Provides shared utility functions for checking if file paths match ignore patterns.
7
+ Used by both the main linter and violation generator to avoid duplicating ignore pattern
8
+ matching logic. Centralizes the ignore pattern matching algorithm.
9
+
10
+ Dependencies: pathlib.Path, fnmatch
11
+
12
+ Exports: is_ignored function
13
+
14
+ Interfaces: is_ignored(file_path, ignore_patterns) -> bool
15
+
16
+ Implementation: Glob pattern matching with fnmatch for flexible ignore patterns
17
+ """
18
+
19
+ import fnmatch
20
+ from pathlib import Path
21
+
22
+
23
+ def is_ignored(file_path: str | Path, ignore_patterns: list[str]) -> bool:
24
+ """Check if file path matches any ignore pattern.
25
+
26
+ Supports glob patterns like:
27
+ - **/tests/** - matches any file in tests directories
28
+ - **/*_test.py - matches any file ending in _test.py
29
+ - tests/ - simple substring match
30
+
31
+ Args:
32
+ file_path: Path to check (string or Path object)
33
+ ignore_patterns: List of patterns to match against
34
+
35
+ Returns:
36
+ True if file should be ignored
37
+ """
38
+ if not ignore_patterns:
39
+ return False
40
+
41
+ path_str = str(file_path)
42
+
43
+ for pattern in ignore_patterns:
44
+ # Use fnmatch for glob-style patterns
45
+ if fnmatch.fnmatch(path_str, pattern):
46
+ return True
47
+ # Also check if pattern appears as substring (for simple patterns)
48
+ if pattern in path_str:
49
+ return True
50
+
51
+ return False
@@ -0,0 +1,344 @@
1
+ """
2
+ Purpose: Main stringly-typed linter rule with cross-file detection
3
+
4
+ Scope: StringlyTypedRule implementing MultiLanguageLintRule for cross-file pattern detection
5
+
6
+ Overview: Implements stringly-typed linter rule following MultiLanguageLintRule interface with
7
+ cross-file detection using SQLite storage. Orchestrates pattern detection by delegating to
8
+ language-specific analyzers (Python, TypeScript). During check() phase, patterns are collected
9
+ into storage. During finalize() phase, storage is queried for patterns appearing across
10
+ multiple files and violations are generated. Maintains minimal orchestration logic to comply
11
+ with SRP.
12
+
13
+ Dependencies: MultiLanguageLintRule, BaseLintContext, PythonStringlyTypedAnalyzer,
14
+ StringlyTypedStorage, StorageInitializer, ViolationGenerator, StringlyTypedConfig
15
+
16
+ Exports: StringlyTypedRule class
17
+
18
+ Interfaces: StringlyTypedRule.check(context) -> list[Violation],
19
+ StringlyTypedRule.finalize() -> list[Violation]
20
+
21
+ Implementation: Two-phase pattern: check() stores data, finalize() generates violations.
22
+ Delegates all logic to helper classes, maintains only orchestration and state.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+
30
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
31
+ from src.core.linter_utils import load_linter_config
32
+ from src.core.types import Violation
33
+
34
+ from .config import StringlyTypedConfig
35
+ from .ignore_utils import is_ignored
36
+ from .python.analyzer import (
37
+ AnalysisResult,
38
+ ComparisonResult,
39
+ FunctionCallResult,
40
+ PythonStringlyTypedAnalyzer,
41
+ )
42
+ from .storage import StoredComparison, StoredFunctionCall, StoredPattern, StringlyTypedStorage
43
+ from .storage_initializer import StorageInitializer
44
+ from .typescript.analyzer import TypeScriptStringlyTypedAnalyzer
45
+ from .violation_generator import ViolationGenerator
46
+
47
+
48
+ def compute_string_set_hash(values: set[str]) -> int:
49
+ """Compute consistent hash for a set of strings.
50
+
51
+ Args:
52
+ values: Set of string values to hash
53
+
54
+ Returns:
55
+ Hash value based on sorted, lowercased strings
56
+ """
57
+ return hash(tuple(sorted(s.lower() for s in values)))
58
+
59
+
60
+ def _is_ready_for_analysis(context: BaseLintContext, storage: StringlyTypedStorage | None) -> bool:
61
+ """Check if context and storage are ready for analysis."""
62
+ return bool(context.file_path and context.file_content and storage)
63
+
64
+
65
+ def _convert_to_stored_pattern(result: AnalysisResult) -> StoredPattern:
66
+ """Convert AnalysisResult to StoredPattern.
67
+
68
+ Args:
69
+ result: Analysis result from language analyzer
70
+
71
+ Returns:
72
+ StoredPattern for storage
73
+ """
74
+ return StoredPattern(
75
+ file_path=result.file_path,
76
+ line_number=result.line_number,
77
+ column=result.column,
78
+ variable_name=result.variable_name,
79
+ string_set_hash=compute_string_set_hash(result.string_values),
80
+ string_values=sorted(result.string_values),
81
+ pattern_type=result.pattern_type,
82
+ details=result.details,
83
+ )
84
+
85
+
86
+ def _convert_to_stored_function_call(result: FunctionCallResult) -> StoredFunctionCall:
87
+ """Convert FunctionCallResult to StoredFunctionCall.
88
+
89
+ Args:
90
+ result: Function call result from language analyzer
91
+
92
+ Returns:
93
+ StoredFunctionCall for storage
94
+ """
95
+ return StoredFunctionCall(
96
+ file_path=result.file_path,
97
+ line_number=result.line_number,
98
+ column=result.column,
99
+ function_name=result.function_name,
100
+ param_index=result.param_index,
101
+ string_value=result.string_value,
102
+ )
103
+
104
+
105
+ def _convert_to_stored_comparison(result: ComparisonResult) -> StoredComparison:
106
+ """Convert ComparisonResult to StoredComparison.
107
+
108
+ Args:
109
+ result: Comparison result from language analyzer
110
+
111
+ Returns:
112
+ StoredComparison for storage
113
+ """
114
+ return StoredComparison(
115
+ file_path=result.file_path,
116
+ line_number=result.line_number,
117
+ column=result.column,
118
+ variable_name=result.variable_name,
119
+ compared_value=result.compared_value,
120
+ operator=result.operator,
121
+ )
122
+
123
+
124
+ @dataclass
125
+ class StringlyTypedComponents:
126
+ """Component dependencies for stringly-typed linter."""
127
+
128
+ storage_initializer: StorageInitializer
129
+ violation_generator: ViolationGenerator
130
+ python_analyzer: PythonStringlyTypedAnalyzer
131
+ typescript_analyzer: TypeScriptStringlyTypedAnalyzer
132
+
133
+
134
+ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
135
+ """Detects stringly-typed patterns across project files.
136
+
137
+ Uses two-phase pattern:
138
+ 1. check() - Collects patterns into SQLite storage (returns empty list)
139
+ 2. finalize() - Queries storage and generates violations for cross-file patterns
140
+ """
141
+
142
+ def __init__(self) -> None:
143
+ """Initialize the stringly-typed rule with helper components."""
144
+ self._storage: StringlyTypedStorage | None = None
145
+ self._initialized = False
146
+ self._config: StringlyTypedConfig | None = None
147
+
148
+ # Helper components grouped to reduce instance attributes
149
+ self._helpers = StringlyTypedComponents(
150
+ storage_initializer=StorageInitializer(),
151
+ violation_generator=ViolationGenerator(),
152
+ python_analyzer=PythonStringlyTypedAnalyzer(),
153
+ typescript_analyzer=TypeScriptStringlyTypedAnalyzer(),
154
+ )
155
+
156
+ @property
157
+ def rule_id(self) -> str:
158
+ """Unique identifier for this rule."""
159
+ return "stringly-typed.repeated-validation"
160
+
161
+ @property
162
+ def rule_name(self) -> str:
163
+ """Human-readable name for this rule."""
164
+ return "Stringly-Typed Pattern"
165
+
166
+ @property
167
+ def description(self) -> str:
168
+ """Description of what this rule checks."""
169
+ return "Detects stringly-typed code patterns that should use enums"
170
+
171
+ def _load_config(self, context: BaseLintContext) -> StringlyTypedConfig:
172
+ """Load configuration from context.
173
+
174
+ Args:
175
+ context: Lint context with metadata
176
+
177
+ Returns:
178
+ StringlyTypedConfig instance
179
+ """
180
+ return load_linter_config(context, "stringly_typed", StringlyTypedConfig)
181
+
182
+ def _check_python(
183
+ self, context: BaseLintContext, config: StringlyTypedConfig
184
+ ) -> list[Violation]:
185
+ """Analyze Python code and store patterns.
186
+
187
+ Args:
188
+ context: Lint context with file content
189
+ config: Stringly-typed configuration
190
+
191
+ Returns:
192
+ Empty list (violations generated in finalize)
193
+ """
194
+ self._ensure_storage_initialized(context, config)
195
+ self._analyze_python_file(context, config)
196
+ return []
197
+
198
+ def _check_typescript(
199
+ self, context: BaseLintContext, config: StringlyTypedConfig
200
+ ) -> list[Violation]:
201
+ """Analyze TypeScript code and store patterns.
202
+
203
+ Args:
204
+ context: Lint context with file content
205
+ config: Stringly-typed configuration
206
+
207
+ Returns:
208
+ Empty list (violations generated in finalize)
209
+ """
210
+ self._ensure_storage_initialized(context, config)
211
+ self._analyze_typescript_file(context, config)
212
+ return []
213
+
214
+ def _analyze_typescript_file(
215
+ self, context: BaseLintContext, config: StringlyTypedConfig
216
+ ) -> None:
217
+ """Analyze TypeScript file and store patterns.
218
+
219
+ Uses single-parse optimization to avoid duplicate parsing overhead.
220
+
221
+ Args:
222
+ context: Lint context with file content
223
+ config: Stringly-typed configuration
224
+ """
225
+ if not self._should_analyze(context, config):
226
+ return
227
+
228
+ file_path = Path(context.file_path) # type: ignore[arg-type]
229
+ file_content = context.file_content or ""
230
+ self._helpers.typescript_analyzer.config = config
231
+
232
+ # Single-parse optimization: parse once, run both trackers
233
+ call_results, comparison_results = self._helpers.typescript_analyzer.analyze_all(
234
+ file_content, file_path
235
+ )
236
+
237
+ stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
238
+ self._storage.add_function_calls(stored_calls) # type: ignore[union-attr]
239
+
240
+ stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
241
+ self._storage.add_comparisons(stored_comparisons) # type: ignore[union-attr]
242
+
243
+ def _ensure_storage_initialized(
244
+ self, context: BaseLintContext, config: StringlyTypedConfig
245
+ ) -> None:
246
+ """Initialize storage and analyzers on first call.
247
+
248
+ Args:
249
+ context: Lint context
250
+ config: Stringly-typed configuration
251
+ """
252
+ if not self._initialized:
253
+ self._storage = self._helpers.storage_initializer.initialize(context, config)
254
+ self._config = config
255
+ self._initialized = True
256
+
257
+ def _analyze_python_file(self, context: BaseLintContext, config: StringlyTypedConfig) -> None:
258
+ """Analyze Python file and store patterns.
259
+
260
+ Args:
261
+ context: Lint context with file content
262
+ config: Stringly-typed configuration
263
+ """
264
+ if not self._should_analyze(context, config):
265
+ return
266
+
267
+ file_path = Path(context.file_path) # type: ignore[arg-type]
268
+ file_content = context.file_content or ""
269
+ self._helpers.python_analyzer.config = config
270
+
271
+ self._store_validation_patterns(file_content, file_path)
272
+ self._store_function_calls(file_content, file_path)
273
+ self._store_comparisons(file_content, file_path)
274
+
275
+ def _should_analyze(self, context: BaseLintContext, config: StringlyTypedConfig) -> bool:
276
+ """Check if file should be analyzed.
277
+
278
+ Args:
279
+ context: Lint context
280
+ config: Configuration
281
+
282
+ Returns:
283
+ True if file should be analyzed
284
+ """
285
+ if not _is_ready_for_analysis(context, self._storage):
286
+ return False
287
+ file_path = Path(context.file_path) # type: ignore[arg-type]
288
+ return not is_ignored(file_path, config.ignore)
289
+
290
+ def _store_validation_patterns(self, file_content: str, file_path: Path) -> None:
291
+ """Analyze and store validation patterns.
292
+
293
+ Args:
294
+ file_content: Python source code
295
+ file_path: Path to file
296
+ """
297
+ results = self._helpers.python_analyzer.analyze(file_content, file_path)
298
+ self._storage.add_patterns([_convert_to_stored_pattern(r) for r in results]) # type: ignore[union-attr]
299
+
300
+ def _store_function_calls(self, file_content: str, file_path: Path) -> None:
301
+ """Analyze and store function call patterns.
302
+
303
+ Args:
304
+ file_content: Python source code
305
+ file_path: Path to file
306
+ """
307
+ call_results = self._helpers.python_analyzer.analyze_function_calls(file_content, file_path)
308
+ stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
309
+ self._storage.add_function_calls(stored_calls) # type: ignore[union-attr]
310
+
311
+ def _store_comparisons(self, file_content: str, file_path: Path) -> None:
312
+ """Analyze and store Python comparison patterns.
313
+
314
+ Args:
315
+ file_content: Python source code
316
+ file_path: Path to file
317
+ """
318
+ comparison_results = self._helpers.python_analyzer.analyze_comparisons(
319
+ file_content, file_path
320
+ )
321
+ stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
322
+ self._storage.add_comparisons(stored_comparisons) # type: ignore[union-attr]
323
+
324
+ def finalize(self) -> list[Violation]:
325
+ """Generate violations after all files processed.
326
+
327
+ Returns:
328
+ List of violations for patterns appearing in multiple files
329
+ """
330
+ if not self._storage or not self._config:
331
+ return []
332
+
333
+ # Generate violations from cross-file patterns
334
+ violations = self._helpers.violation_generator.generate_violations(
335
+ self._storage, self.rule_id, self._config
336
+ )
337
+
338
+ # Cleanup and reset state for next run
339
+ self._storage.close()
340
+ self._storage = None
341
+ self._config = None
342
+ self._initialized = False
343
+
344
+ return violations
@@ -1,17 +1,19 @@
1
1
  """
2
2
  Purpose: Python-specific detection for stringly-typed patterns
3
3
 
4
- Scope: Python AST analysis for membership validation and equality chain detection
4
+ Scope: Python AST analysis for membership validation, equality chains, and function calls
5
5
 
6
6
  Overview: Exposes Python analysis components for detecting stringly-typed patterns in Python
7
7
  source code. Includes validation_detector for finding 'x in ("a", "b")' patterns,
8
- conditional_detector for finding if/elif chains and match statements, and analyzer
9
- for coordinating detection across Python files. Uses AST traversal to identify where
10
- plain strings are used instead of proper enums or typed alternatives.
8
+ conditional_detector for finding if/elif chains and match statements, call_tracker for
9
+ finding function calls with string arguments, and analyzer for coordinating detection
10
+ across Python files. Uses AST traversal to identify where plain strings are used instead
11
+ of proper enums or typed alternatives.
11
12
 
12
13
  Dependencies: ast module for Python AST parsing
13
14
 
14
- Exports: MembershipValidationDetector, ConditionalPatternDetector, PythonStringlyTypedAnalyzer
15
+ Exports: MembershipValidationDetector, ConditionalPatternDetector, FunctionCallTracker,
16
+ PythonStringlyTypedAnalyzer
15
17
 
16
18
  Interfaces: Detector and analyzer classes for Python stringly-typed pattern detection
17
19
 
@@ -19,11 +21,13 @@ Implementation: AST NodeVisitor pattern for traversing Python syntax trees
19
21
  """
20
22
 
21
23
  from .analyzer import PythonStringlyTypedAnalyzer
24
+ from .call_tracker import FunctionCallTracker
22
25
  from .conditional_detector import ConditionalPatternDetector
23
26
  from .validation_detector import MembershipValidationDetector
24
27
 
25
28
  __all__ = [
26
29
  "ConditionalPatternDetector",
30
+ "FunctionCallTracker",
27
31
  "MembershipValidationDetector",
28
32
  "PythonStringlyTypedAnalyzer",
29
33
  ]
@@ -5,17 +5,21 @@ Scope: Orchestrate detection of all stringly-typed patterns in Python files
5
5
 
6
6
  Overview: Provides PythonStringlyTypedAnalyzer class that coordinates detection of
7
7
  stringly-typed patterns across Python source files. Uses MembershipValidationDetector
8
- to find 'x in ("a", "b")' patterns and ConditionalPatternDetector to find if/elif
9
- chains and match statements. Returns unified AnalysisResult objects. Handles AST
10
- parsing errors gracefully and provides a single entry point for Python analysis.
11
- Supports configuration options for filtering and thresholds.
8
+ to find 'x in ("a", "b")' patterns, ConditionalPatternDetector to find if/elif chains
9
+ and match statements, and FunctionCallTracker to find function calls with string
10
+ arguments. Returns unified AnalysisResult objects for validation patterns and
11
+ FunctionCallResult objects for function calls. Handles AST parsing errors gracefully
12
+ and provides a single entry point for Python analysis. Supports configuration options
13
+ for filtering and thresholds.
12
14
 
13
15
  Dependencies: ast module, MembershipValidationDetector, ConditionalPatternDetector,
14
- StringlyTypedConfig
16
+ FunctionCallTracker, StringlyTypedConfig
15
17
 
16
- Exports: PythonStringlyTypedAnalyzer class, AnalysisResult dataclass
18
+ Exports: PythonStringlyTypedAnalyzer class, AnalysisResult dataclass, FunctionCallResult dataclass,
19
+ ComparisonResult dataclass
17
20
 
18
- Interfaces: PythonStringlyTypedAnalyzer.analyze(code, file_path) -> list[AnalysisResult]
21
+ Interfaces: PythonStringlyTypedAnalyzer.analyze(code, file_path) -> list[AnalysisResult],
22
+ PythonStringlyTypedAnalyzer.analyze_function_calls(code, file_path) -> list[FunctionCallResult]
19
23
 
20
24
  Implementation: Facade pattern coordinating multiple detectors with unified result format
21
25
  """
@@ -25,6 +29,8 @@ from dataclasses import dataclass
25
29
  from pathlib import Path
26
30
 
27
31
  from ..config import StringlyTypedConfig
32
+ from .call_tracker import FunctionCallPattern, FunctionCallTracker
33
+ from .comparison_tracker import ComparisonPattern, ComparisonTracker
28
34
  from .conditional_detector import ConditionalPatternDetector, EqualityChainPattern
29
35
  from .validation_detector import MembershipPattern, MembershipValidationDetector
30
36
 
@@ -59,12 +65,72 @@ class AnalysisResult:
59
65
  """Human-readable description of the detected pattern."""
60
66
 
61
67
 
62
- class PythonStringlyTypedAnalyzer:
68
+ @dataclass
69
+ class FunctionCallResult:
70
+ """Represents a function call with a string argument.
71
+
72
+ Provides information about a single function call with a string literal
73
+ argument, enabling aggregation across files to detect limited value sets.
74
+ """
75
+
76
+ function_name: str
77
+ """Fully qualified function name (e.g., 'process' or 'obj.method')."""
78
+
79
+ param_index: int
80
+ """Index of the parameter receiving the string value (0-indexed)."""
81
+
82
+ string_value: str
83
+ """The string literal value passed to the function."""
84
+
85
+ file_path: Path
86
+ """Path to the file containing the call."""
87
+
88
+ line_number: int
89
+ """Line number where the call occurs (1-indexed)."""
90
+
91
+ column: int
92
+ """Column number where the call starts (0-indexed)."""
93
+
94
+
95
+ @dataclass
96
+ class ComparisonResult:
97
+ """Represents a string comparison found in Python code.
98
+
99
+ Provides information about a comparison like `if env == "production"` to
100
+ enable cross-file aggregation for detecting scattered comparisons that
101
+ suggest missing enums.
102
+ """
103
+
104
+ variable_name: str
105
+ """Variable name being compared (e.g., 'env' or 'self.status')."""
106
+
107
+ compared_value: str
108
+ """The string literal value being compared to."""
109
+
110
+ operator: str
111
+ """The comparison operator ('==' or '!=')."""
112
+
113
+ file_path: Path
114
+ """Path to the file containing the comparison."""
115
+
116
+ line_number: int
117
+ """Line number where the comparison occurs (1-indexed)."""
118
+
119
+ column: int
120
+ """Column number where the comparison starts (0-indexed)."""
121
+
122
+
123
+ class PythonStringlyTypedAnalyzer: # thailint: ignore srp
63
124
  """Analyzes Python code for stringly-typed patterns.
64
125
 
65
126
  Coordinates detection of various stringly-typed patterns including membership
66
- validation ('x in ("a", "b")') and equality chains ('if x == "a" elif x == "b"').
127
+ validation ('x in ("a", "b")'), equality chains ('if x == "a" elif x == "b"'),
128
+ and function calls with string arguments ('process("active")').
67
129
  Provides configuration-aware analysis with filtering support.
130
+
131
+ Note: Method count exceeds SRP limit due to analyzer coordination role. Multiple
132
+ analysis methods are required for different pattern types (membership, conditional,
133
+ function calls, comparisons) and their converters.
68
134
  """
69
135
 
70
136
  def __init__(self, config: StringlyTypedConfig | None = None) -> None:
@@ -76,6 +142,8 @@ class PythonStringlyTypedAnalyzer:
76
142
  self.config = config or StringlyTypedConfig()
77
143
  self._membership_detector = MembershipValidationDetector()
78
144
  self._conditional_detector = ConditionalPatternDetector()
145
+ self._call_tracker = FunctionCallTracker()
146
+ self._comparison_tracker = ComparisonTracker()
79
147
 
80
148
  def analyze(self, code: str, file_path: Path) -> list[AnalysisResult]:
81
149
  """Analyze Python code for stringly-typed patterns.
@@ -196,3 +264,81 @@ class PythonStringlyTypedAnalyzer:
196
264
  "match_statement": "Match statement",
197
265
  }
198
266
  return labels.get(pattern_type, "Conditional pattern")
267
+
268
+ def analyze_function_calls(self, code: str, file_path: Path) -> list[FunctionCallResult]:
269
+ """Analyze Python code for function calls with string arguments.
270
+
271
+ Args:
272
+ code: Python source code to analyze
273
+ file_path: Path to the file being analyzed
274
+
275
+ Returns:
276
+ List of FunctionCallResult instances for each detected call
277
+ """
278
+ tree = self._parse_code(code)
279
+ if tree is None:
280
+ return []
281
+
282
+ call_patterns = self._call_tracker.find_patterns(tree)
283
+ return [self._convert_call_pattern(pattern, file_path) for pattern in call_patterns]
284
+
285
+ def _convert_call_pattern(
286
+ self, pattern: FunctionCallPattern, file_path: Path
287
+ ) -> FunctionCallResult:
288
+ """Convert a FunctionCallPattern to FunctionCallResult.
289
+
290
+ Args:
291
+ pattern: Detected function call pattern
292
+ file_path: Path to the file containing the call
293
+
294
+ Returns:
295
+ FunctionCallResult representing the call
296
+ """
297
+ return FunctionCallResult(
298
+ function_name=pattern.function_name,
299
+ param_index=pattern.param_index,
300
+ string_value=pattern.string_value,
301
+ file_path=file_path,
302
+ line_number=pattern.line_number,
303
+ column=pattern.column,
304
+ )
305
+
306
+ def analyze_comparisons(self, code: str, file_path: Path) -> list[ComparisonResult]:
307
+ """Analyze Python code for string comparisons.
308
+
309
+ Args:
310
+ code: Python source code to analyze
311
+ file_path: Path to the file being analyzed
312
+
313
+ Returns:
314
+ List of ComparisonResult instances for each detected comparison
315
+ """
316
+ tree = self._parse_code(code)
317
+ if tree is None:
318
+ return []
319
+
320
+ comparison_patterns = self._comparison_tracker.find_patterns(tree)
321
+ return [
322
+ self._convert_comparison_pattern(pattern, file_path) for pattern in comparison_patterns
323
+ ]
324
+
325
+ def _convert_comparison_pattern(
326
+ self, pattern: ComparisonPattern, file_path: Path
327
+ ) -> ComparisonResult:
328
+ """Convert a ComparisonPattern to ComparisonResult.
329
+
330
+ Args:
331
+ pattern: Detected comparison pattern
332
+ file_path: Path to the file containing the comparison
333
+
334
+ Returns:
335
+ ComparisonResult representing the comparison
336
+ """
337
+ return ComparisonResult(
338
+ variable_name=pattern.variable_name,
339
+ compared_value=pattern.compared_value,
340
+ operator=pattern.operator,
341
+ file_path=file_path,
342
+ line_number=pattern.line_number,
343
+ column=pattern.column,
344
+ )