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: Handles building violation objects for function calls that consistentl
10
10
 
11
11
  Dependencies: Violation, Severity, StoredFunctionCall, StringlyTypedConfig
12
12
 
13
- Exports: FunctionCallViolationBuilder class
13
+ Exports: build_function_call_violations function
14
14
 
15
- Interfaces: FunctionCallViolationBuilder.build_violations(calls, unique_values) -> list[Violation]
15
+ Interfaces: build_function_call_violations(calls, unique_values) -> list[Violation]
16
16
 
17
17
  Implementation: Builds violations with cross-file references and enum suggestions
18
18
  """
@@ -24,6 +24,21 @@ from src.core.types import Severity, Violation
24
24
  from .storage import StoredFunctionCall
25
25
 
26
26
 
27
+ def build_function_call_violations(
28
+ calls: list[StoredFunctionCall], unique_values: set[str]
29
+ ) -> list[Violation]:
30
+ """Build violations for all calls to a function with limited values.
31
+
32
+ Args:
33
+ calls: All calls to the function/param
34
+ unique_values: Set of unique string values passed
35
+
36
+ Returns:
37
+ List of violations for each call site
38
+ """
39
+ return [_build_violation(call, calls, unique_values) for call in calls]
40
+
41
+
27
42
  def _build_cross_references(call: StoredFunctionCall, all_calls: list[StoredFunctionCall]) -> str:
28
43
  """Build cross-reference string for other function call locations.
29
44
 
@@ -42,96 +57,79 @@ def _build_cross_references(call: StoredFunctionCall, all_calls: list[StoredFunc
42
57
  return ", ".join(refs[:5]) # Limit to 5 references
43
58
 
44
59
 
45
- class FunctionCallViolationBuilder:
46
- """Builds violations for function call patterns with limited string values."""
47
-
48
- def build_violations(
49
- self, calls: list[StoredFunctionCall], unique_values: set[str]
50
- ) -> list[Violation]:
51
- """Build violations for all calls to a function with limited values.
52
-
53
- Args:
54
- calls: All calls to the function/param
55
- unique_values: Set of unique string values passed
56
-
57
- Returns:
58
- List of violations for each call site
59
- """
60
- return [self._build_violation(call, calls, unique_values) for call in calls]
61
-
62
- def _build_violation(
63
- self,
64
- call: StoredFunctionCall,
65
- all_calls: list[StoredFunctionCall],
66
- unique_values: set[str],
67
- ) -> Violation:
68
- """Build a single violation for a function call.
69
-
70
- Args:
71
- call: The specific call to create violation for
72
- all_calls: All calls to the same function/param
73
- unique_values: Set of unique string values passed
74
-
75
- Returns:
76
- Violation instance
77
- """
78
- message = self._build_message(call, all_calls, unique_values)
79
- suggestion = self._build_suggestion(call, unique_values)
80
-
81
- return Violation(
82
- rule_id="stringly-typed.limited-values",
83
- file_path=str(call.file_path),
84
- line=call.line_number,
85
- column=call.column,
86
- message=message,
87
- severity=Severity.ERROR,
88
- suggestion=suggestion,
89
- )
90
-
91
- def _build_message(
92
- self,
93
- call: StoredFunctionCall,
94
- all_calls: list[StoredFunctionCall],
95
- unique_values: set[str],
96
- ) -> str:
97
- """Build violation message for function call pattern.
98
-
99
- Args:
100
- call: Current function call
101
- all_calls: All calls to the same function/param
102
- unique_values: Set of unique values passed
103
-
104
- Returns:
105
- Human-readable violation message
106
- """
107
- file_count = len({c.file_path for c in all_calls})
108
- values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
109
- param_desc = f"parameter {call.param_index}" if call.param_index > 0 else "first parameter"
110
-
111
- message = (
112
- f"Function '{call.function_name}' {param_desc} is called with "
113
- f"only {len(unique_values)} unique string values [{values_str}] "
114
- f"across {file_count} file(s)."
115
- )
116
-
117
- other_refs = _build_cross_references(call, all_calls)
118
- if other_refs:
119
- message += f" Also called in: {other_refs}."
120
-
121
- return message
122
-
123
- def _build_suggestion(self, call: StoredFunctionCall, unique_values: set[str]) -> str:
124
- """Build fix suggestion for function call pattern.
125
-
126
- Args:
127
- call: The function call
128
- unique_values: Set of unique values passed
129
-
130
- Returns:
131
- Human-readable suggestion
132
- """
133
- return (
134
- f"Consider defining an enum or type union with the "
135
- f"{len(unique_values)} possible values for '{call.function_name}' "
136
- f"parameter {call.param_index}."
137
- )
60
+ def _build_violation(
61
+ call: StoredFunctionCall,
62
+ all_calls: list[StoredFunctionCall],
63
+ unique_values: set[str],
64
+ ) -> Violation:
65
+ """Build a single violation for a function call.
66
+
67
+ Args:
68
+ call: The specific call to create violation for
69
+ all_calls: All calls to the same function/param
70
+ unique_values: Set of unique string values passed
71
+
72
+ Returns:
73
+ Violation instance
74
+ """
75
+ message = _build_message(call, all_calls, unique_values)
76
+ suggestion = _build_suggestion(call, unique_values)
77
+
78
+ return Violation(
79
+ rule_id="stringly-typed.limited-values",
80
+ file_path=str(call.file_path),
81
+ line=call.line_number,
82
+ column=call.column,
83
+ message=message,
84
+ severity=Severity.ERROR,
85
+ suggestion=suggestion,
86
+ )
87
+
88
+
89
+ def _build_message(
90
+ call: StoredFunctionCall,
91
+ all_calls: list[StoredFunctionCall],
92
+ unique_values: set[str],
93
+ ) -> str:
94
+ """Build violation message for function call pattern.
95
+
96
+ Args:
97
+ call: Current function call
98
+ all_calls: All calls to the same function/param
99
+ unique_values: Set of unique values passed
100
+
101
+ Returns:
102
+ Human-readable violation message
103
+ """
104
+ file_count = len({c.file_path for c in all_calls})
105
+ values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
106
+ param_desc = f"parameter {call.param_index}" if call.param_index > 0 else "first parameter"
107
+
108
+ message = (
109
+ f"Function '{call.function_name}' {param_desc} is called with "
110
+ f"only {len(unique_values)} unique string values [{values_str}] "
111
+ f"across {file_count} file(s)."
112
+ )
113
+
114
+ other_refs = _build_cross_references(call, all_calls)
115
+ if other_refs:
116
+ message += f" Also called in: {other_refs}."
117
+
118
+ return message
119
+
120
+
121
+ def _build_suggestion(call: StoredFunctionCall, unique_values: set[str]) -> str:
122
+ """Build fix suggestion for function call pattern.
123
+
124
+ Args:
125
+ call: The function call
126
+ unique_values: Set of unique values passed
127
+
128
+ Returns:
129
+ Human-readable suggestion
130
+ """
131
+ return (
132
+ f"Consider defining an enum or type union with the "
133
+ f"{len(unique_values)} possible values for '{call.function_name}' "
134
+ f"parameter {call.param_index}."
135
+ )
@@ -20,6 +20,11 @@ Interfaces: StringlyTypedRule.check(context) -> list[Violation],
20
20
 
21
21
  Implementation: Two-phase pattern: check() stores data, finalize() generates violations.
22
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.
23
28
  """
24
29
 
25
30
  from __future__ import annotations
@@ -131,7 +136,7 @@ class StringlyTypedComponents:
131
136
  typescript_analyzer: TypeScriptStringlyTypedAnalyzer
132
137
 
133
138
 
134
- class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
139
+ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore[srp]
135
140
  """Detects stringly-typed patterns across project files.
136
141
 
137
142
  Uses two-phase pattern:
@@ -153,6 +158,19 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
153
158
  typescript_analyzer=TypeScriptStringlyTypedAnalyzer(),
154
159
  )
155
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
+
156
174
  @property
157
175
  def rule_id(self) -> str:
158
176
  """Unique identifier for this rule."""
@@ -224,21 +242,31 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
224
242
  """
225
243
  if not self._should_analyze(context, config):
226
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
227
248
 
228
- file_path = Path(context.file_path) # type: ignore[arg-type]
229
- file_content = context.file_content or ""
230
249
  self._helpers.typescript_analyzer.config = config
231
-
232
- # Single-parse optimization: parse once, run both trackers
233
250
  call_results, comparison_results = self._helpers.typescript_analyzer.analyze_all(
234
- file_content, file_path
251
+ context.file_content, context.file_path
235
252
  )
253
+ self._store_typescript_results(call_results, comparison_results)
236
254
 
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]
255
+ def _store_typescript_results(
256
+ self,
257
+ call_results: list[FunctionCallResult],
258
+ comparison_results: list[ComparisonResult],
259
+ ) -> None:
260
+ """Store TypeScript analysis results.
239
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)
240
268
  stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
241
- self._storage.add_comparisons(stored_comparisons) # type: ignore[union-attr]
269
+ self._active_storage.add_comparisons(stored_comparisons)
242
270
 
243
271
  def _ensure_storage_initialized(
244
272
  self, context: BaseLintContext, config: StringlyTypedConfig
@@ -263,9 +291,12 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
263
291
  """
264
292
  if not self._should_analyze(context, config):
265
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
266
297
 
267
- file_path = Path(context.file_path) # type: ignore[arg-type]
268
- file_content = context.file_content or ""
298
+ file_path = context.file_path
299
+ file_content = context.file_content
269
300
  self._helpers.python_analyzer.config = config
270
301
 
271
302
  self._store_validation_patterns(file_content, file_path)
@@ -284,8 +315,9 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
284
315
  """
285
316
  if not _is_ready_for_analysis(context, self._storage):
286
317
  return False
287
- file_path = Path(context.file_path) # type: ignore[arg-type]
288
- return not is_ignored(file_path, config.ignore)
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)
289
321
 
290
322
  def _store_validation_patterns(self, file_content: str, file_path: Path) -> None:
291
323
  """Analyze and store validation patterns.
@@ -295,7 +327,7 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
295
327
  file_path: Path to file
296
328
  """
297
329
  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]
330
+ self._active_storage.add_patterns([_convert_to_stored_pattern(r) for r in results])
299
331
 
300
332
  def _store_function_calls(self, file_content: str, file_path: Path) -> None:
301
333
  """Analyze and store function call patterns.
@@ -306,7 +338,7 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
306
338
  """
307
339
  call_results = self._helpers.python_analyzer.analyze_function_calls(file_content, file_path)
308
340
  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]
341
+ self._active_storage.add_function_calls(stored_calls)
310
342
 
311
343
  def _store_comparisons(self, file_content: str, file_path: Path) -> None:
312
344
  """Analyze and store Python comparison patterns.
@@ -319,7 +351,7 @@ class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore srp
319
351
  file_content, file_path
320
352
  )
321
353
  stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
322
- self._storage.add_comparisons(stored_comparisons) # type: ignore[union-attr]
354
+ self._active_storage.add_comparisons(stored_comparisons)
323
355
 
324
356
  def finalize(self) -> list[Violation]:
325
357
  """Generate violations after all files processed.
@@ -22,6 +22,10 @@ Interfaces: PythonStringlyTypedAnalyzer.analyze(code, file_path) -> list[Analysi
22
22
  PythonStringlyTypedAnalyzer.analyze_function_calls(code, file_path) -> list[FunctionCallResult]
23
23
 
24
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.
25
29
  """
26
30
 
27
31
  import ast
@@ -120,7 +124,7 @@ class ComparisonResult:
120
124
  """Column number where the comparison starts (0-indexed)."""
121
125
 
122
126
 
123
- class PythonStringlyTypedAnalyzer: # thailint: ignore srp
127
+ class PythonStringlyTypedAnalyzer: # thailint: ignore[srp]
124
128
  """Analyzes Python code for stringly-typed patterns.
125
129
 
126
130
  Coordinates detection of various stringly-typed patterns including membership
@@ -10,18 +10,24 @@ Overview: Provides FunctionCallTracker class that traverses Python AST to find f
10
10
  suggests the parameter should be an enum. Handles both simple function calls
11
11
  (foo("value")) and method calls (obj.method("value")).
12
12
 
13
- Dependencies: ast module for AST parsing, dataclasses for pattern structure
13
+ Dependencies: ast module for AST parsing, dataclasses for pattern structure,
14
+ src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
14
15
 
15
16
  Exports: FunctionCallTracker class, FunctionCallPattern dataclass
16
17
 
17
18
  Interfaces: FunctionCallTracker.find_patterns(tree) -> list[FunctionCallPattern]
18
19
 
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
20
24
  """
21
25
 
22
26
  import ast
23
27
  from dataclasses import dataclass
24
28
 
29
+ from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
30
+
25
31
 
26
32
  @dataclass
27
33
  class FunctionCallPattern:
@@ -119,12 +125,9 @@ class FunctionCallTracker(ast.NodeVisitor):
119
125
  """
120
126
  parts: list[str] = [node.attr]
121
127
  current = node.value
122
-
123
- # Limit depth to avoid overly complex names
124
- max_depth = 3
125
128
  depth = 0
126
129
 
127
- while depth < max_depth:
130
+ while depth < MAX_ATTRIBUTE_CHAIN_DEPTH:
128
131
  if isinstance(current, ast.Name):
129
132
  parts.append(current.id)
130
133
  break
@@ -9,18 +9,26 @@ Overview: Provides ComparisonTracker class that traverses Python AST to find sca
9
9
  to multiple unique string values across files, it suggests the variable should be an enum.
10
10
  Excludes common false positives like `__name__ == "__main__"` and type name checks.
11
11
 
12
- Dependencies: ast module for AST parsing, dataclasses for pattern structure
12
+ Dependencies: ast module for AST parsing, dataclasses for pattern structure,
13
+ src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
13
14
 
14
15
  Exports: ComparisonTracker class, ComparisonPattern dataclass
15
16
 
16
17
  Interfaces: ComparisonTracker.find_patterns(tree) -> list[ComparisonPattern]
17
18
 
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.
19
25
  """
20
26
 
21
27
  import ast
22
28
  from dataclasses import dataclass
23
29
 
30
+ from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
31
+
24
32
 
25
33
  @dataclass
26
34
  class ComparisonPattern:
@@ -188,12 +196,9 @@ class ComparisonTracker(ast.NodeVisitor): # thailint: ignore[srp]
188
196
  """
189
197
  parts: list[str] = [node.attr]
190
198
  current = node.value
191
-
192
- # Limit depth to avoid overly complex names
193
- max_depth = 3
194
199
  depth = 0
195
200
 
196
- while depth < max_depth:
201
+ while depth < MAX_ATTRIBUTE_CHAIN_DEPTH:
197
202
  if isinstance(current, ast.Name):
198
203
  parts.append(current.id)
199
204
  break
@@ -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
@@ -1,7 +1,4 @@
1
1
  # pylint: disable=too-many-lines
2
- # Justification: Storage module for three related data types (patterns, function calls,
3
- # comparisons) with their dataclasses, conversion functions, SQL schemas, and CRUD methods.
4
- # Splitting into separate files would fragment cohesive SQLite storage logic.
5
2
  """
6
3
  Purpose: SQLite storage manager for stringly-typed pattern detection
7
4
 
@@ -34,6 +31,13 @@ Interfaces: StringlyTypedStorage.__init__(storage_mode), add_pattern(pattern),
34
31
 
35
32
  Implementation: SQLite with string_validations, function_calls, and string_comparisons tables,
36
33
  indexed on string_set_hash, function_name+param_index, and variable_name for performance
34
+
35
+ Suppressions:
36
+ - too-many-lines: Storage module for three related data types with dataclasses, SQL schemas, and CRUD methods
37
+ - too-many-instance-attributes: StoredPattern is a pure DTO with 8 necessary fields for SQLite storage
38
+ - consider-using-with: NamedTemporaryFile must remain open for SQLite connection lifetime (closed in close())
39
+ - srp: Storage class manages SQLite for three pattern types (validations, calls, comparisons).
40
+ Splitting would fragment related storage operations.
37
41
  """
38
42
 
39
43
  from __future__ import annotations
@@ -44,6 +48,8 @@ import tempfile
44
48
  from dataclasses import dataclass
45
49
  from pathlib import Path
46
50
 
51
+ from src.core.constants import StorageMode
52
+
47
53
  # Row index constants for SQLite query results
48
54
  _COL_FILE_PATH = 0
49
55
  _COL_LINE_NUMBER = 1
@@ -244,11 +250,8 @@ def _row_to_function_call(row: tuple) -> StoredFunctionCall:
244
250
  )
245
251
 
246
252
 
247
- # pylint: disable=too-many-instance-attributes
248
- # Justification: StoredPattern is a pure data transfer object for SQLite storage.
249
- # All 8 fields are necessary: file location (3), variable info (1), hash/values (3), pattern type (1).
250
253
  @dataclass
251
- class StoredPattern:
254
+ class StoredPattern: # pylint: disable=too-many-instance-attributes
252
255
  """Represents a stringly-typed pattern stored in SQLite.
253
256
 
254
257
  Captures all information needed to detect cross-file duplicates and generate
@@ -280,7 +283,7 @@ class StoredPattern:
280
283
  """Human-readable description of the detected pattern."""
281
284
 
282
285
 
283
- class StringlyTypedStorage: # thailint: ignore srp
286
+ class StringlyTypedStorage: # thailint: ignore[srp]
284
287
  """SQLite-backed storage for stringly-typed pattern detection.
285
288
 
286
289
  Stores patterns from analyzed files and provides queries to find patterns
@@ -297,13 +300,10 @@ class StringlyTypedStorage: # thailint: ignore srp
297
300
  self._tempfile = None
298
301
 
299
302
  # Create SQLite connection based on storage mode
300
- if storage_mode == "memory":
303
+ if storage_mode == StorageMode.MEMORY:
301
304
  self._db = sqlite3.connect(":memory:")
302
- elif storage_mode == "tempfile":
303
- # pylint: disable=consider-using-with
304
- # Justification: tempfile must remain open for SQLite connection lifetime.
305
- # It is explicitly closed in close() method when storage is finalized.
306
- self._tempfile = tempfile.NamedTemporaryFile(suffix=".db", delete=True)
305
+ elif storage_mode == StorageMode.TEMPFILE:
306
+ self._tempfile = tempfile.NamedTemporaryFile(suffix=".db", delete=True) # pylint: disable=consider-using-with
307
307
  self._db = sqlite3.connect(self._tempfile.name)
308
308
  else:
309
309
  raise ValueError(f"Invalid storage_mode: {storage_mode}")
@@ -11,19 +11,26 @@ Overview: Provides TypeScriptCallTracker class that uses tree-sitter to traverse
11
11
  function calls (foo("value")) and method calls (obj.method("value")), including
12
12
  chained method calls.
13
13
 
14
- Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, dataclasses for pattern structure
14
+ Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, dataclasses for pattern structure,
15
+ src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
15
16
 
16
17
  Exports: TypeScriptCallTracker class, TypeScriptFunctionCallPattern dataclass
17
18
 
18
19
  Interfaces: TypeScriptCallTracker.find_patterns(code) -> list[TypeScriptFunctionCallPattern]
19
20
 
20
21
  Implementation: Tree-sitter node traversal with call_expression node handling for string arguments
22
+
23
+ Suppressions:
24
+ - type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
25
+ - srp: Tracker implements tree-sitter traversal with helper methods for call extraction.
26
+ Methods support single responsibility of function call pattern detection.
21
27
  """
22
28
 
23
29
  from dataclasses import dataclass
24
30
  from typing import Any
25
31
 
26
32
  from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE, TypeScriptBaseAnalyzer
33
+ from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
27
34
 
28
35
  if TREE_SITTER_AVAILABLE:
29
36
  from tree_sitter import Node
@@ -180,10 +187,9 @@ class TypeScriptCallTracker(TypeScriptBaseAnalyzer): # thailint: ignore[srp]
180
187
  Qualified function name or None if too complex
181
188
  """
182
189
  parts: list[str] = []
183
- max_depth = 3
184
190
  current: Node | None = node
185
191
 
186
- for _ in range(max_depth):
192
+ for _ in range(MAX_ATTRIBUTE_CHAIN_DEPTH):
187
193
  if current is None:
188
194
  break
189
195
  self._add_property_name(current, parts)
@@ -10,7 +10,8 @@ Overview: Provides TypeScriptComparisonTracker class that uses tree-sitter to tr
10
10
  it suggests the variable should be an enum. Excludes common false positives like template
11
11
  literals and typeof comparisons.
12
12
 
13
- Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, dataclasses for pattern structure
13
+ Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, dataclasses for pattern structure,
14
+ src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
14
15
 
15
16
  Exports: TypeScriptComparisonTracker class, TypeScriptComparisonPattern dataclass
16
17
 
@@ -18,12 +19,18 @@ Interfaces: TypeScriptComparisonTracker.find_patterns(code) -> list[TypeScriptCo
18
19
 
19
20
  Implementation: Tree-sitter node traversal with binary_expression node handling for string
20
21
  comparisons
22
+
23
+ Suppressions:
24
+ - type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
25
+ - srp: Tracker implements tree-sitter traversal with helper methods for node extraction.
26
+ Methods support single responsibility of comparison pattern detection.
21
27
  """
22
28
 
23
29
  from dataclasses import dataclass
24
30
  from typing import Any
25
31
 
26
32
  from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE, TypeScriptBaseAnalyzer
33
+ from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
27
34
 
28
35
  if TREE_SITTER_AVAILABLE:
29
36
  from tree_sitter import Node
@@ -277,10 +284,9 @@ class TypeScriptComparisonTracker(TypeScriptBaseAnalyzer): # thailint: ignore[s
277
284
  Qualified name or None if too complex
278
285
  """
279
286
  parts: list[str] = []
280
- max_depth = 3
281
287
  current: Node | None = node
282
288
 
283
- for _ in range(max_depth):
289
+ for _ in range(MAX_ATTRIBUTE_CHAIN_DEPTH):
284
290
  if current is None:
285
291
  break
286
292
  self._add_property_name(current, parts)