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,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,376 @@
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
+ Suppressions:
25
+ - B101: Type narrowing assertions after guards (storage initialized, file_path/content set)
26
+ - srp: Rule class orchestrates cross-file detection with storage, analyzers, and generators.
27
+ Splitting would fragment the two-phase detection workflow.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from dataclasses import dataclass
33
+ from pathlib import Path
34
+
35
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
36
+ from src.core.linter_utils import load_linter_config
37
+ from src.core.types import Violation
38
+
39
+ from .config import StringlyTypedConfig
40
+ from .ignore_utils import is_ignored
41
+ from .python.analyzer import (
42
+ AnalysisResult,
43
+ ComparisonResult,
44
+ FunctionCallResult,
45
+ PythonStringlyTypedAnalyzer,
46
+ )
47
+ from .storage import StoredComparison, StoredFunctionCall, StoredPattern, StringlyTypedStorage
48
+ from .storage_initializer import StorageInitializer
49
+ from .typescript.analyzer import TypeScriptStringlyTypedAnalyzer
50
+ from .violation_generator import ViolationGenerator
51
+
52
+
53
+ def compute_string_set_hash(values: set[str]) -> int:
54
+ """Compute consistent hash for a set of strings.
55
+
56
+ Args:
57
+ values: Set of string values to hash
58
+
59
+ Returns:
60
+ Hash value based on sorted, lowercased strings
61
+ """
62
+ return hash(tuple(sorted(s.lower() for s in values)))
63
+
64
+
65
+ def _is_ready_for_analysis(context: BaseLintContext, storage: StringlyTypedStorage | None) -> bool:
66
+ """Check if context and storage are ready for analysis."""
67
+ return bool(context.file_path and context.file_content and storage)
68
+
69
+
70
+ def _convert_to_stored_pattern(result: AnalysisResult) -> StoredPattern:
71
+ """Convert AnalysisResult to StoredPattern.
72
+
73
+ Args:
74
+ result: Analysis result from language analyzer
75
+
76
+ Returns:
77
+ StoredPattern for storage
78
+ """
79
+ return StoredPattern(
80
+ file_path=result.file_path,
81
+ line_number=result.line_number,
82
+ column=result.column,
83
+ variable_name=result.variable_name,
84
+ string_set_hash=compute_string_set_hash(result.string_values),
85
+ string_values=sorted(result.string_values),
86
+ pattern_type=result.pattern_type,
87
+ details=result.details,
88
+ )
89
+
90
+
91
+ def _convert_to_stored_function_call(result: FunctionCallResult) -> StoredFunctionCall:
92
+ """Convert FunctionCallResult to StoredFunctionCall.
93
+
94
+ Args:
95
+ result: Function call result from language analyzer
96
+
97
+ Returns:
98
+ StoredFunctionCall for storage
99
+ """
100
+ return StoredFunctionCall(
101
+ file_path=result.file_path,
102
+ line_number=result.line_number,
103
+ column=result.column,
104
+ function_name=result.function_name,
105
+ param_index=result.param_index,
106
+ string_value=result.string_value,
107
+ )
108
+
109
+
110
+ def _convert_to_stored_comparison(result: ComparisonResult) -> StoredComparison:
111
+ """Convert ComparisonResult to StoredComparison.
112
+
113
+ Args:
114
+ result: Comparison result from language analyzer
115
+
116
+ Returns:
117
+ StoredComparison for storage
118
+ """
119
+ return StoredComparison(
120
+ file_path=result.file_path,
121
+ line_number=result.line_number,
122
+ column=result.column,
123
+ variable_name=result.variable_name,
124
+ compared_value=result.compared_value,
125
+ operator=result.operator,
126
+ )
127
+
128
+
129
+ @dataclass
130
+ class StringlyTypedComponents:
131
+ """Component dependencies for stringly-typed linter."""
132
+
133
+ storage_initializer: StorageInitializer
134
+ violation_generator: ViolationGenerator
135
+ python_analyzer: PythonStringlyTypedAnalyzer
136
+ typescript_analyzer: TypeScriptStringlyTypedAnalyzer
137
+
138
+
139
+ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore[srp]
140
+ """Detects stringly-typed patterns across project files.
141
+
142
+ Uses two-phase pattern:
143
+ 1. check() - Collects patterns into SQLite storage (returns empty list)
144
+ 2. finalize() - Queries storage and generates violations for cross-file patterns
145
+ """
146
+
147
+ def __init__(self) -> None:
148
+ """Initialize the stringly-typed rule with helper components."""
149
+ self._storage: StringlyTypedStorage | None = None
150
+ self._initialized = False
151
+ self._config: StringlyTypedConfig | None = None
152
+
153
+ # Helper components grouped to reduce instance attributes
154
+ self._helpers = StringlyTypedComponents(
155
+ storage_initializer=StorageInitializer(),
156
+ violation_generator=ViolationGenerator(),
157
+ python_analyzer=PythonStringlyTypedAnalyzer(),
158
+ typescript_analyzer=TypeScriptStringlyTypedAnalyzer(),
159
+ )
160
+
161
+ @property
162
+ def _active_storage(self) -> StringlyTypedStorage:
163
+ """Get storage, asserting it has been initialized.
164
+
165
+ Returns:
166
+ The initialized storage instance.
167
+
168
+ Raises:
169
+ AssertionError: If storage has not been initialized.
170
+ """
171
+ assert self._storage is not None, "Storage not initialized" # nosec B101
172
+ return self._storage
173
+
174
+ @property
175
+ def rule_id(self) -> str:
176
+ """Unique identifier for this rule."""
177
+ return "stringly-typed.repeated-validation"
178
+
179
+ @property
180
+ def rule_name(self) -> str:
181
+ """Human-readable name for this rule."""
182
+ return "Stringly-Typed Pattern"
183
+
184
+ @property
185
+ def description(self) -> str:
186
+ """Description of what this rule checks."""
187
+ return "Detects stringly-typed code patterns that should use enums"
188
+
189
+ def _load_config(self, context: BaseLintContext) -> StringlyTypedConfig:
190
+ """Load configuration from context.
191
+
192
+ Args:
193
+ context: Lint context with metadata
194
+
195
+ Returns:
196
+ StringlyTypedConfig instance
197
+ """
198
+ return load_linter_config(context, "stringly_typed", StringlyTypedConfig)
199
+
200
+ def _check_python(
201
+ self, context: BaseLintContext, config: StringlyTypedConfig
202
+ ) -> list[Violation]:
203
+ """Analyze Python code and store patterns.
204
+
205
+ Args:
206
+ context: Lint context with file content
207
+ config: Stringly-typed configuration
208
+
209
+ Returns:
210
+ Empty list (violations generated in finalize)
211
+ """
212
+ self._ensure_storage_initialized(context, config)
213
+ self._analyze_python_file(context, config)
214
+ return []
215
+
216
+ def _check_typescript(
217
+ self, context: BaseLintContext, config: StringlyTypedConfig
218
+ ) -> list[Violation]:
219
+ """Analyze TypeScript code and store patterns.
220
+
221
+ Args:
222
+ context: Lint context with file content
223
+ config: Stringly-typed configuration
224
+
225
+ Returns:
226
+ Empty list (violations generated in finalize)
227
+ """
228
+ self._ensure_storage_initialized(context, config)
229
+ self._analyze_typescript_file(context, config)
230
+ return []
231
+
232
+ def _analyze_typescript_file(
233
+ self, context: BaseLintContext, config: StringlyTypedConfig
234
+ ) -> None:
235
+ """Analyze TypeScript file and store patterns.
236
+
237
+ Uses single-parse optimization to avoid duplicate parsing overhead.
238
+
239
+ Args:
240
+ context: Lint context with file content
241
+ config: Stringly-typed configuration
242
+ """
243
+ if not self._should_analyze(context, config):
244
+ return
245
+ # _should_analyze ensures file_path and file_content are set
246
+ assert context.file_path is not None # nosec B101
247
+ assert context.file_content is not None # nosec B101
248
+
249
+ self._helpers.typescript_analyzer.config = config
250
+ call_results, comparison_results = self._helpers.typescript_analyzer.analyze_all(
251
+ context.file_content, context.file_path
252
+ )
253
+ self._store_typescript_results(call_results, comparison_results)
254
+
255
+ def _store_typescript_results(
256
+ self,
257
+ call_results: list[FunctionCallResult],
258
+ comparison_results: list[ComparisonResult],
259
+ ) -> None:
260
+ """Store TypeScript analysis results.
261
+
262
+ Args:
263
+ call_results: Function call patterns found
264
+ comparison_results: Comparison patterns found
265
+ """
266
+ stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
267
+ self._active_storage.add_function_calls(stored_calls)
268
+ stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
269
+ self._active_storage.add_comparisons(stored_comparisons)
270
+
271
+ def _ensure_storage_initialized(
272
+ self, context: BaseLintContext, config: StringlyTypedConfig
273
+ ) -> None:
274
+ """Initialize storage and analyzers on first call.
275
+
276
+ Args:
277
+ context: Lint context
278
+ config: Stringly-typed configuration
279
+ """
280
+ if not self._initialized:
281
+ self._storage = self._helpers.storage_initializer.initialize(context, config)
282
+ self._config = config
283
+ self._initialized = True
284
+
285
+ def _analyze_python_file(self, context: BaseLintContext, config: StringlyTypedConfig) -> None:
286
+ """Analyze Python file and store patterns.
287
+
288
+ Args:
289
+ context: Lint context with file content
290
+ config: Stringly-typed configuration
291
+ """
292
+ if not self._should_analyze(context, config):
293
+ return
294
+ # _should_analyze ensures file_path and file_content are set
295
+ assert context.file_path is not None # nosec B101
296
+ assert context.file_content is not None # nosec B101
297
+
298
+ file_path = context.file_path
299
+ file_content = context.file_content
300
+ self._helpers.python_analyzer.config = config
301
+
302
+ self._store_validation_patterns(file_content, file_path)
303
+ self._store_function_calls(file_content, file_path)
304
+ self._store_comparisons(file_content, file_path)
305
+
306
+ def _should_analyze(self, context: BaseLintContext, config: StringlyTypedConfig) -> bool:
307
+ """Check if file should be analyzed.
308
+
309
+ Args:
310
+ context: Lint context
311
+ config: Configuration
312
+
313
+ Returns:
314
+ True if file should be analyzed
315
+ """
316
+ if not _is_ready_for_analysis(context, self._storage):
317
+ return False
318
+ # _is_ready_for_analysis ensures file_path is set
319
+ assert context.file_path is not None # nosec B101
320
+ return not is_ignored(context.file_path, config.ignore)
321
+
322
+ def _store_validation_patterns(self, file_content: str, file_path: Path) -> None:
323
+ """Analyze and store validation patterns.
324
+
325
+ Args:
326
+ file_content: Python source code
327
+ file_path: Path to file
328
+ """
329
+ results = self._helpers.python_analyzer.analyze(file_content, file_path)
330
+ self._active_storage.add_patterns([_convert_to_stored_pattern(r) for r in results])
331
+
332
+ def _store_function_calls(self, file_content: str, file_path: Path) -> None:
333
+ """Analyze and store function call patterns.
334
+
335
+ Args:
336
+ file_content: Python source code
337
+ file_path: Path to file
338
+ """
339
+ call_results = self._helpers.python_analyzer.analyze_function_calls(file_content, file_path)
340
+ stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
341
+ self._active_storage.add_function_calls(stored_calls)
342
+
343
+ def _store_comparisons(self, file_content: str, file_path: Path) -> None:
344
+ """Analyze and store Python comparison patterns.
345
+
346
+ Args:
347
+ file_content: Python source code
348
+ file_path: Path to file
349
+ """
350
+ comparison_results = self._helpers.python_analyzer.analyze_comparisons(
351
+ file_content, file_path
352
+ )
353
+ stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
354
+ self._active_storage.add_comparisons(stored_comparisons)
355
+
356
+ def finalize(self) -> list[Violation]:
357
+ """Generate violations after all files processed.
358
+
359
+ Returns:
360
+ List of violations for patterns appearing in multiple files
361
+ """
362
+ if not self._storage or not self._config:
363
+ return []
364
+
365
+ # Generate violations from cross-file patterns
366
+ violations = self._helpers.violation_generator.generate_violations(
367
+ self._storage, self.rule_id, self._config
368
+ )
369
+
370
+ # Cleanup and reset state for next run
371
+ self._storage.close()
372
+ self._storage = None
373
+ self._config = None
374
+ self._initialized = False
375
+
376
+ 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,19 +5,27 @@ 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
25
+
26
+ Suppressions:
27
+ - srp: Analyzer coordinates multiple detectors (membership, conditional, call tracker).
28
+ Facade pattern justifies combining orchestration methods.
21
29
  """
22
30
 
23
31
  import ast
@@ -25,6 +33,8 @@ from dataclasses import dataclass
25
33
  from pathlib import Path
26
34
 
27
35
  from ..config import StringlyTypedConfig
36
+ from .call_tracker import FunctionCallPattern, FunctionCallTracker
37
+ from .comparison_tracker import ComparisonPattern, ComparisonTracker
28
38
  from .conditional_detector import ConditionalPatternDetector, EqualityChainPattern
29
39
  from .validation_detector import MembershipPattern, MembershipValidationDetector
30
40
 
@@ -59,12 +69,72 @@ class AnalysisResult:
59
69
  """Human-readable description of the detected pattern."""
60
70
 
61
71
 
62
- class PythonStringlyTypedAnalyzer:
72
+ @dataclass
73
+ class FunctionCallResult:
74
+ """Represents a function call with a string argument.
75
+
76
+ Provides information about a single function call with a string literal
77
+ argument, enabling aggregation across files to detect limited value sets.
78
+ """
79
+
80
+ function_name: str
81
+ """Fully qualified function name (e.g., 'process' or 'obj.method')."""
82
+
83
+ param_index: int
84
+ """Index of the parameter receiving the string value (0-indexed)."""
85
+
86
+ string_value: str
87
+ """The string literal value passed to the function."""
88
+
89
+ file_path: Path
90
+ """Path to the file containing the call."""
91
+
92
+ line_number: int
93
+ """Line number where the call occurs (1-indexed)."""
94
+
95
+ column: int
96
+ """Column number where the call starts (0-indexed)."""
97
+
98
+
99
+ @dataclass
100
+ class ComparisonResult:
101
+ """Represents a string comparison found in Python code.
102
+
103
+ Provides information about a comparison like `if env == "production"` to
104
+ enable cross-file aggregation for detecting scattered comparisons that
105
+ suggest missing enums.
106
+ """
107
+
108
+ variable_name: str
109
+ """Variable name being compared (e.g., 'env' or 'self.status')."""
110
+
111
+ compared_value: str
112
+ """The string literal value being compared to."""
113
+
114
+ operator: str
115
+ """The comparison operator ('==' or '!=')."""
116
+
117
+ file_path: Path
118
+ """Path to the file containing the comparison."""
119
+
120
+ line_number: int
121
+ """Line number where the comparison occurs (1-indexed)."""
122
+
123
+ column: int
124
+ """Column number where the comparison starts (0-indexed)."""
125
+
126
+
127
+ class PythonStringlyTypedAnalyzer: # thailint: ignore[srp]
63
128
  """Analyzes Python code for stringly-typed patterns.
64
129
 
65
130
  Coordinates detection of various stringly-typed patterns including membership
66
- validation ('x in ("a", "b")') and equality chains ('if x == "a" elif x == "b"').
131
+ validation ('x in ("a", "b")'), equality chains ('if x == "a" elif x == "b"'),
132
+ and function calls with string arguments ('process("active")').
67
133
  Provides configuration-aware analysis with filtering support.
134
+
135
+ Note: Method count exceeds SRP limit due to analyzer coordination role. Multiple
136
+ analysis methods are required for different pattern types (membership, conditional,
137
+ function calls, comparisons) and their converters.
68
138
  """
69
139
 
70
140
  def __init__(self, config: StringlyTypedConfig | None = None) -> None:
@@ -76,6 +146,8 @@ class PythonStringlyTypedAnalyzer:
76
146
  self.config = config or StringlyTypedConfig()
77
147
  self._membership_detector = MembershipValidationDetector()
78
148
  self._conditional_detector = ConditionalPatternDetector()
149
+ self._call_tracker = FunctionCallTracker()
150
+ self._comparison_tracker = ComparisonTracker()
79
151
 
80
152
  def analyze(self, code: str, file_path: Path) -> list[AnalysisResult]:
81
153
  """Analyze Python code for stringly-typed patterns.
@@ -196,3 +268,81 @@ class PythonStringlyTypedAnalyzer:
196
268
  "match_statement": "Match statement",
197
269
  }
198
270
  return labels.get(pattern_type, "Conditional pattern")
271
+
272
+ def analyze_function_calls(self, code: str, file_path: Path) -> list[FunctionCallResult]:
273
+ """Analyze Python code for function calls with string arguments.
274
+
275
+ Args:
276
+ code: Python source code to analyze
277
+ file_path: Path to the file being analyzed
278
+
279
+ Returns:
280
+ List of FunctionCallResult instances for each detected call
281
+ """
282
+ tree = self._parse_code(code)
283
+ if tree is None:
284
+ return []
285
+
286
+ call_patterns = self._call_tracker.find_patterns(tree)
287
+ return [self._convert_call_pattern(pattern, file_path) for pattern in call_patterns]
288
+
289
+ def _convert_call_pattern(
290
+ self, pattern: FunctionCallPattern, file_path: Path
291
+ ) -> FunctionCallResult:
292
+ """Convert a FunctionCallPattern to FunctionCallResult.
293
+
294
+ Args:
295
+ pattern: Detected function call pattern
296
+ file_path: Path to the file containing the call
297
+
298
+ Returns:
299
+ FunctionCallResult representing the call
300
+ """
301
+ return FunctionCallResult(
302
+ function_name=pattern.function_name,
303
+ param_index=pattern.param_index,
304
+ string_value=pattern.string_value,
305
+ file_path=file_path,
306
+ line_number=pattern.line_number,
307
+ column=pattern.column,
308
+ )
309
+
310
+ def analyze_comparisons(self, code: str, file_path: Path) -> list[ComparisonResult]:
311
+ """Analyze Python code for string comparisons.
312
+
313
+ Args:
314
+ code: Python source code to analyze
315
+ file_path: Path to the file being analyzed
316
+
317
+ Returns:
318
+ List of ComparisonResult instances for each detected comparison
319
+ """
320
+ tree = self._parse_code(code)
321
+ if tree is None:
322
+ return []
323
+
324
+ comparison_patterns = self._comparison_tracker.find_patterns(tree)
325
+ return [
326
+ self._convert_comparison_pattern(pattern, file_path) for pattern in comparison_patterns
327
+ ]
328
+
329
+ def _convert_comparison_pattern(
330
+ self, pattern: ComparisonPattern, file_path: Path
331
+ ) -> ComparisonResult:
332
+ """Convert a ComparisonPattern to ComparisonResult.
333
+
334
+ Args:
335
+ pattern: Detected comparison pattern
336
+ file_path: Path to the file containing the comparison
337
+
338
+ Returns:
339
+ ComparisonResult representing the comparison
340
+ """
341
+ return ComparisonResult(
342
+ variable_name=pattern.variable_name,
343
+ compared_value=pattern.compared_value,
344
+ operator=pattern.operator,
345
+ file_path=file_path,
346
+ line_number=pattern.line_number,
347
+ column=pattern.column,
348
+ )