thailint 0.2.0__py3-none-any.whl → 0.15.3__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 (214) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,413 @@
1
+ """
2
+ Purpose: Main print statements linter rule implementation
3
+
4
+ Scope: Print and console statement detection for Python, TypeScript, and JavaScript files
5
+
6
+ Overview: Implements print statements linter rule following MultiLanguageLintRule interface. Orchestrates
7
+ configuration loading, Python AST analysis for print() calls, TypeScript tree-sitter analysis
8
+ for console.* calls, and violation building through focused helper classes. Detects print and
9
+ console statements that should be replaced with proper logging. Supports configurable
10
+ allow_in_scripts option to permit print() in __main__ blocks and configurable console_methods
11
+ set for TypeScript/JavaScript. Handles ignore directives for suppressing specific violations
12
+ through inline comments and configuration patterns.
13
+
14
+ Dependencies: BaseLintContext and MultiLanguageLintRule from core, ast module, pathlib,
15
+ analyzer classes, config classes
16
+
17
+ Exports: PrintStatementRule class implementing MultiLanguageLintRule interface
18
+
19
+ Interfaces: check(context) -> list[Violation] for rule validation, standard rule properties
20
+ (rule_id, rule_name, description)
21
+
22
+ Implementation: Composition pattern with helper classes (analyzers, violation builder),
23
+ AST-based analysis for Python, tree-sitter for TypeScript/JavaScript
24
+
25
+ Suppressions:
26
+ - too-many-arguments,too-many-positional-arguments: Violation creation with related fields
27
+ - srp: Rule class coordinates multiple language analyzers and violation building.
28
+ Method count exceeds limit due to dual-language support (Python + TypeScript).
29
+ """
30
+
31
+ import ast
32
+ from pathlib import Path
33
+
34
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
35
+ from src.core.linter_utils import load_linter_config
36
+ from src.core.types import Violation
37
+ from src.core.violation_utils import get_violation_line, has_python_noqa, has_typescript_noqa
38
+ from src.linter_config.ignore import get_ignore_parser
39
+
40
+ from .config import PrintStatementConfig
41
+ from .python_analyzer import PythonPrintStatementAnalyzer
42
+ from .typescript_analyzer import TypeScriptPrintStatementAnalyzer
43
+ from .violation_builder import ViolationBuilder
44
+
45
+
46
+ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
47
+ """Detects print/console statements that should be replaced with proper logging."""
48
+
49
+ def __init__(self) -> None:
50
+ """Initialize the print statements rule."""
51
+ self._ignore_parser = get_ignore_parser()
52
+ self._violation_builder = ViolationBuilder(self.rule_id)
53
+
54
+ @property
55
+ def rule_id(self) -> str:
56
+ """Unique identifier for this rule."""
57
+ return "print-statements.detected"
58
+
59
+ @property
60
+ def rule_name(self) -> str:
61
+ """Human-readable name for this rule."""
62
+ return "Print Statements"
63
+
64
+ @property
65
+ def description(self) -> str:
66
+ """Description of what this rule checks."""
67
+ return "Print/console statements should be replaced with proper logging"
68
+
69
+ def _load_config(self, context: BaseLintContext) -> PrintStatementConfig:
70
+ """Load configuration from context.
71
+
72
+ Args:
73
+ context: Lint context
74
+
75
+ Returns:
76
+ PrintStatementConfig instance
77
+ """
78
+ test_config = self._try_load_test_config(context)
79
+ if test_config is not None:
80
+ return test_config
81
+
82
+ prod_config = self._try_load_production_config(context)
83
+ if prod_config is not None:
84
+ return prod_config
85
+
86
+ return PrintStatementConfig()
87
+
88
+ def _try_load_test_config(self, context: BaseLintContext) -> PrintStatementConfig | None:
89
+ """Try to load test-style configuration."""
90
+ if not hasattr(context, "config"):
91
+ return None
92
+ config_attr = context.config
93
+ if config_attr is None or not isinstance(config_attr, dict):
94
+ return None
95
+ return PrintStatementConfig.from_dict(config_attr, context.language)
96
+
97
+ def _try_load_production_config(self, context: BaseLintContext) -> PrintStatementConfig | None:
98
+ """Try to load production configuration."""
99
+ if not hasattr(context, "metadata") or not isinstance(context.metadata, dict):
100
+ return None
101
+
102
+ metadata = context.metadata
103
+
104
+ if "print_statements" in metadata:
105
+ return load_linter_config(context, "print_statements", PrintStatementConfig)
106
+
107
+ if "print-statements" in metadata:
108
+ return load_linter_config(context, "print-statements", PrintStatementConfig)
109
+
110
+ return None
111
+
112
+ def _is_file_ignored(self, context: BaseLintContext, config: PrintStatementConfig) -> bool:
113
+ """Check if file matches ignore patterns.
114
+
115
+ Args:
116
+ context: Lint context
117
+ config: Print statements configuration
118
+
119
+ Returns:
120
+ True if file should be ignored
121
+ """
122
+ if not config.ignore:
123
+ return False
124
+
125
+ if not context.file_path:
126
+ return False
127
+
128
+ file_path = Path(context.file_path)
129
+ return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
130
+
131
+ def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
132
+ """Check if file path matches a glob pattern.
133
+
134
+ Args:
135
+ file_path: Path to check
136
+ pattern: Glob pattern
137
+
138
+ Returns:
139
+ True if path matches pattern
140
+ """
141
+ if file_path.match(pattern):
142
+ return True
143
+ if pattern in str(file_path):
144
+ return True
145
+ return False
146
+
147
+ def _check_python(
148
+ self, context: BaseLintContext, config: PrintStatementConfig
149
+ ) -> list[Violation]:
150
+ """Check Python code for print() violations.
151
+
152
+ Args:
153
+ context: Lint context with Python file information
154
+ config: Print statements configuration
155
+
156
+ Returns:
157
+ List of violations found in Python code
158
+ """
159
+ if self._is_file_ignored(context, config):
160
+ return []
161
+
162
+ tree = self._parse_python_code(context.file_content)
163
+ if tree is None:
164
+ return []
165
+
166
+ analyzer = PythonPrintStatementAnalyzer()
167
+ print_calls = analyzer.find_print_calls(tree)
168
+ return self._collect_python_violations(print_calls, context, config, analyzer)
169
+
170
+ def _parse_python_code(self, code: str | None) -> ast.AST | None:
171
+ """Parse Python code into AST."""
172
+ try:
173
+ return ast.parse(code or "")
174
+ except SyntaxError:
175
+ return None
176
+
177
+ def _collect_python_violations(
178
+ self,
179
+ print_calls: list,
180
+ context: BaseLintContext,
181
+ config: PrintStatementConfig,
182
+ analyzer: PythonPrintStatementAnalyzer,
183
+ ) -> list[Violation]:
184
+ """Collect violations from Python print() calls.
185
+
186
+ Args:
187
+ print_calls: List of (node, parent, line_number) tuples
188
+ context: Lint context
189
+ config: Configuration
190
+ analyzer: Python analyzer instance
191
+
192
+ Returns:
193
+ List of violations
194
+ """
195
+ violations = []
196
+ for node, _parent, line_number in print_calls:
197
+ violation = self._try_create_python_violation(
198
+ node, line_number, context, config, analyzer
199
+ )
200
+ if violation is not None:
201
+ violations.append(violation)
202
+ return violations
203
+
204
+ def _try_create_python_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
205
+ self,
206
+ node: ast.Call,
207
+ line_number: int,
208
+ context: BaseLintContext,
209
+ config: PrintStatementConfig,
210
+ analyzer: PythonPrintStatementAnalyzer,
211
+ ) -> Violation | None:
212
+ """Try to create a violation for a Python print() call.
213
+
214
+ Args:
215
+ node: AST Call node
216
+ line_number: Line number
217
+ context: Lint context
218
+ config: Configuration
219
+ analyzer: Python analyzer
220
+
221
+ Returns:
222
+ Violation or None if should not flag
223
+ """
224
+ # Check if in __main__ block and allow_in_scripts is enabled
225
+ if config.allow_in_scripts and analyzer.is_in_main_block(node):
226
+ return None
227
+
228
+ violation = self._violation_builder.create_python_violation(
229
+ node, line_number, context.file_path
230
+ )
231
+
232
+ if self._should_ignore(violation, context):
233
+ return None
234
+
235
+ return violation
236
+
237
+ def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
238
+ """Check if violation should be ignored based on inline directives.
239
+
240
+ Args:
241
+ violation: Violation to check
242
+ context: Lint context with file content
243
+
244
+ Returns:
245
+ True if violation should be ignored
246
+ """
247
+ if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
248
+ return True
249
+ return self._check_generic_ignore(violation, context)
250
+
251
+ def _check_generic_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
252
+ """Check for generic ignore directives.
253
+
254
+ Args:
255
+ violation: Violation to check
256
+ context: Lint context
257
+
258
+ Returns:
259
+ True if line has generic ignore directive
260
+ """
261
+ line_text = get_violation_line(violation, context)
262
+ if line_text is None:
263
+ return False
264
+ return self._has_generic_ignore_directive(line_text)
265
+
266
+ def _has_generic_ignore_directive(self, line_text: str) -> bool:
267
+ """Check if line has generic ignore directive."""
268
+ if self._has_generic_thailint_ignore(line_text):
269
+ return True
270
+ return has_python_noqa(line_text)
271
+
272
+ def _has_generic_thailint_ignore(self, line_text: str) -> bool:
273
+ """Check for generic thailint: ignore (no brackets)."""
274
+ if "# thailint: ignore" not in line_text:
275
+ return False
276
+ after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
277
+ return "[" not in after_ignore
278
+
279
+ def _check_typescript(
280
+ self, context: BaseLintContext, config: PrintStatementConfig
281
+ ) -> list[Violation]:
282
+ """Check TypeScript/JavaScript code for console.* violations.
283
+
284
+ Args:
285
+ context: Lint context with TypeScript/JavaScript file information
286
+ config: Print statements configuration
287
+
288
+ Returns:
289
+ List of violations found in TypeScript/JavaScript code
290
+ """
291
+ if self._is_file_ignored(context, config):
292
+ return []
293
+
294
+ analyzer = TypeScriptPrintStatementAnalyzer()
295
+ root_node = analyzer.parse_typescript(context.file_content or "")
296
+ if root_node is None:
297
+ return []
298
+
299
+ console_calls = analyzer.find_console_calls(root_node, config.console_methods)
300
+ return self._collect_typescript_violations(console_calls, context)
301
+
302
+ def _collect_typescript_violations(
303
+ self,
304
+ console_calls: list,
305
+ context: BaseLintContext,
306
+ ) -> list[Violation]:
307
+ """Collect violations from TypeScript console.* calls.
308
+
309
+ Args:
310
+ console_calls: List of (node, method_name, line_number) tuples
311
+ context: Lint context
312
+
313
+ Returns:
314
+ List of violations
315
+ """
316
+ violations = []
317
+ for _node, method_name, line_number in console_calls:
318
+ violation = self._try_create_typescript_violation(method_name, line_number, context)
319
+ if violation is not None:
320
+ violations.append(violation)
321
+ return violations
322
+
323
+ def _try_create_typescript_violation(
324
+ self,
325
+ method_name: str,
326
+ line_number: int,
327
+ context: BaseLintContext,
328
+ ) -> Violation | None:
329
+ """Try to create a violation for a TypeScript console.* call.
330
+
331
+ Args:
332
+ method_name: Console method name (log, warn, etc.)
333
+ line_number: Line number
334
+ context: Lint context
335
+
336
+ Returns:
337
+ Violation or None if should not flag
338
+ """
339
+ # Check if test file (skip test files)
340
+ if self._is_test_file(context.file_path):
341
+ return None
342
+
343
+ violation = self._violation_builder.create_typescript_violation(
344
+ method_name, line_number, context.file_path
345
+ )
346
+
347
+ if self._should_ignore_typescript(violation, context):
348
+ return None
349
+
350
+ return violation
351
+
352
+ def _is_test_file(self, file_path: object) -> bool:
353
+ """Check if file is a test file.
354
+
355
+ Args:
356
+ file_path: Path to check
357
+
358
+ Returns:
359
+ True if test file
360
+ """
361
+ path_str = str(file_path)
362
+ return any(
363
+ pattern in path_str
364
+ for pattern in [".test.", ".spec.", "test_", "_test.", "/tests/", "/test/"]
365
+ )
366
+
367
+ def _should_ignore_typescript(self, violation: Violation, context: BaseLintContext) -> bool:
368
+ """Check if TypeScript violation should be ignored.
369
+
370
+ Args:
371
+ violation: Violation to check
372
+ context: Lint context
373
+
374
+ Returns:
375
+ True if should ignore
376
+ """
377
+ if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
378
+ return True
379
+ return self._check_typescript_ignore(violation, context)
380
+
381
+ def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
382
+ """Check for TypeScript-style ignore directives.
383
+
384
+ Args:
385
+ violation: Violation to check
386
+ context: Lint context
387
+
388
+ Returns:
389
+ True if line has ignore directive
390
+ """
391
+ line_text = get_violation_line(violation, context)
392
+ if line_text is None:
393
+ return False
394
+ return self._has_typescript_ignore_directive(line_text)
395
+
396
+ def _has_typescript_ignore_directive(self, line_text: str) -> bool:
397
+ """Check if line has TypeScript-style ignore directive.
398
+
399
+ Args:
400
+ line_text: Line text to check
401
+
402
+ Returns:
403
+ True if has ignore directive
404
+ """
405
+ if "// thailint: ignore[print-statements]" in line_text:
406
+ return True
407
+
408
+ if "// thailint: ignore" in line_text:
409
+ after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
410
+ if "[" not in after_ignore:
411
+ return True
412
+
413
+ return has_typescript_noqa(line_text)
@@ -0,0 +1,153 @@
1
+ """
2
+ Purpose: Python AST analysis for finding print() call nodes
3
+
4
+ Scope: Python print() statement detection and __main__ block context analysis
5
+
6
+ Overview: Provides PythonPrintStatementAnalyzer class that traverses Python AST to find all
7
+ print() function calls. Uses ast.walk() to traverse the syntax tree and collect
8
+ Call nodes where the function is 'print'. Tracks parent nodes to detect if print calls
9
+ are within __main__ blocks (if __name__ == "__main__":) for allow_in_scripts filtering.
10
+ Returns structured data about each print call including the AST node, parent context,
11
+ and line number for violation reporting. Handles both simple print() and builtins.print() calls.
12
+
13
+ Dependencies: ast module for AST parsing and node types, analyzers.ast_utils
14
+
15
+ Exports: PythonPrintStatementAnalyzer class, is_print_call function, is_main_if_block function
16
+
17
+ Interfaces: find_print_calls(tree) -> list[tuple[Call, AST | None, int]], is_in_main_block(node) -> bool
18
+
19
+ Implementation: AST walk pattern with parent map for context detection and __main__ block identification
20
+ """
21
+
22
+ import ast
23
+
24
+ from src.analyzers.ast_utils import build_parent_map
25
+
26
+ # --- Pure helper functions for print call detection ---
27
+
28
+
29
+ def is_print_call(node: ast.Call) -> bool:
30
+ """Check if a Call node is calling print().
31
+
32
+ Args:
33
+ node: The Call node to check
34
+
35
+ Returns:
36
+ True if this is a print() call
37
+ """
38
+ return _is_simple_print(node) or _is_builtins_print(node)
39
+
40
+
41
+ def _is_simple_print(node: ast.Call) -> bool:
42
+ """Check for simple print() call."""
43
+ return isinstance(node.func, ast.Name) and node.func.id == "print"
44
+
45
+
46
+ def _is_builtins_print(node: ast.Call) -> bool:
47
+ """Check for builtins.print() call."""
48
+ if not isinstance(node.func, ast.Attribute):
49
+ return False
50
+ if node.func.attr != "print":
51
+ return False
52
+ return isinstance(node.func.value, ast.Name) and node.func.value.id == "builtins"
53
+
54
+
55
+ # --- Pure helper functions for __main__ block detection ---
56
+
57
+
58
+ def is_main_if_block(node: ast.AST) -> bool:
59
+ """Check if node is an `if __name__ == "__main__":` statement.
60
+
61
+ Args:
62
+ node: AST node to check
63
+
64
+ Returns:
65
+ True if this is a __main__ if block
66
+ """
67
+ if not isinstance(node, ast.If):
68
+ return False
69
+ if not isinstance(node.test, ast.Compare):
70
+ return False
71
+ return _is_main_comparison(node.test)
72
+
73
+
74
+ def _is_main_comparison(test: ast.Compare) -> bool:
75
+ """Check if comparison is __name__ == '__main__'."""
76
+ if not _is_name_identifier(test.left):
77
+ return False
78
+ if not _has_single_eq_operator(test):
79
+ return False
80
+ return _compares_to_main(test)
81
+
82
+
83
+ def _is_name_identifier(node: ast.expr) -> bool:
84
+ """Check if node is the __name__ identifier."""
85
+ return isinstance(node, ast.Name) and node.id == "__name__"
86
+
87
+
88
+ def _has_single_eq_operator(test: ast.Compare) -> bool:
89
+ """Check if comparison has single == operator."""
90
+ return len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)
91
+
92
+
93
+ def _compares_to_main(test: ast.Compare) -> bool:
94
+ """Check if comparison is to '__main__' string."""
95
+ if len(test.comparators) != 1:
96
+ return False
97
+ comparator = test.comparators[0]
98
+ return isinstance(comparator, ast.Constant) and comparator.value == "__main__"
99
+
100
+
101
+ # --- Analyzer class with stateful parent tracking ---
102
+
103
+
104
+ class PythonPrintStatementAnalyzer:
105
+ """Analyzes Python AST to find print() calls."""
106
+
107
+ def __init__(self) -> None:
108
+ """Initialize the analyzer."""
109
+ self.print_calls: list[tuple[ast.Call, ast.AST | None, int]] = []
110
+ self.parent_map: dict[ast.AST, ast.AST] = {}
111
+
112
+ def find_print_calls(self, tree: ast.AST) -> list[tuple[ast.Call, ast.AST | None, int]]:
113
+ """Find all print() calls in the AST.
114
+
115
+ Args:
116
+ tree: The AST to analyze
117
+
118
+ Returns:
119
+ List of tuples (node, parent, line_number)
120
+ """
121
+ self.print_calls = []
122
+ self.parent_map = build_parent_map(tree)
123
+ self._collect_print_calls(tree)
124
+ return self.print_calls
125
+
126
+ def _collect_print_calls(self, tree: ast.AST) -> None:
127
+ """Walk tree and collect all print() calls.
128
+
129
+ Args:
130
+ tree: AST to traverse
131
+ """
132
+ for node in ast.walk(tree):
133
+ if isinstance(node, ast.Call) and is_print_call(node):
134
+ parent = self.parent_map.get(node)
135
+ line_number = node.lineno if hasattr(node, "lineno") else 0
136
+ self.print_calls.append((node, parent, line_number))
137
+
138
+ def is_in_main_block(self, node: ast.AST) -> bool:
139
+ """Check if node is within `if __name__ == "__main__":` block.
140
+
141
+ Args:
142
+ node: AST node to check
143
+
144
+ Returns:
145
+ True if node is inside a __main__ block
146
+ """
147
+ current = node
148
+ while current in self.parent_map:
149
+ parent = self.parent_map[current]
150
+ if is_main_if_block(parent):
151
+ return True
152
+ current = parent
153
+ return False
@@ -0,0 +1,125 @@
1
+ """
2
+ Purpose: TypeScript/JavaScript console.* call detection using Tree-sitter AST analysis
3
+
4
+ Scope: TypeScript and JavaScript console statement detection
5
+
6
+ Overview: Analyzes TypeScript and JavaScript code to detect console.* method calls that should
7
+ be replaced with proper logging. Uses Tree-sitter parser to traverse TypeScript/JavaScript
8
+ AST and identify call expressions where the callee is console.log, console.warn, console.error,
9
+ console.debug, or console.info (configurable). Returns structured data with the node, method
10
+ name, and line number for each detected console call. Supports both TypeScript and JavaScript
11
+ files with shared detection logic. Handles member expression pattern matching to identify
12
+ console object method calls.
13
+
14
+ Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing infrastructure, tree-sitter Node type, logging module
15
+
16
+ Exports: TypeScriptPrintStatementAnalyzer class
17
+
18
+ Interfaces: find_console_calls(root_node, methods) -> list[tuple[Node, str, int]]
19
+
20
+ Implementation: Tree-sitter node traversal with call_expression and member_expression pattern matching
21
+
22
+ """
23
+
24
+ import logging
25
+
26
+ from src.analyzers.typescript_base import (
27
+ TREE_SITTER_AVAILABLE,
28
+ Node,
29
+ TypeScriptBaseAnalyzer,
30
+ )
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class TypeScriptPrintStatementAnalyzer(TypeScriptBaseAnalyzer):
36
+ """Analyzes TypeScript/JavaScript code for console.* calls using Tree-sitter."""
37
+
38
+ def find_console_calls(self, root_node: Node, methods: set[str]) -> list[tuple[Node, str, int]]:
39
+ """Find all console.* calls matching the specified methods.
40
+
41
+ Args:
42
+ root_node: Root tree-sitter node to search from
43
+ methods: Set of console method names to detect (e.g., {"log", "warn"})
44
+
45
+ Returns:
46
+ List of (node, method_name, line_number) tuples for each console call
47
+ """
48
+ logger.debug(
49
+ "find_console_calls: TREE_SITTER_AVAILABLE=%s, root_node=%s",
50
+ TREE_SITTER_AVAILABLE,
51
+ root_node is not None,
52
+ )
53
+ if not TREE_SITTER_AVAILABLE or root_node is None:
54
+ logger.debug("Early return: tree-sitter not available or root_node is None")
55
+ return []
56
+
57
+ calls: list[tuple[Node, str, int]] = []
58
+ self._collect_console_calls(root_node, methods, calls)
59
+ logger.debug("find_console_calls: found %d calls", len(calls))
60
+ return calls
61
+
62
+ def _collect_console_calls(
63
+ self, node: Node, methods: set[str], calls: list[tuple[Node, str, int]]
64
+ ) -> None:
65
+ """Recursively collect console.* calls from AST.
66
+
67
+ Args:
68
+ node: Current tree-sitter node
69
+ methods: Set of console method names to detect
70
+ calls: List to accumulate found calls
71
+ """
72
+ if node.type == "call_expression":
73
+ method_name = self._extract_console_method(node, methods)
74
+ if method_name is not None:
75
+ line_number = node.start_point[0] + 1
76
+ calls.append((node, method_name, line_number))
77
+
78
+ for child in node.children:
79
+ self._collect_console_calls(child, methods, calls)
80
+
81
+ def _extract_console_method(self, node: Node, methods: set[str]) -> str | None:
82
+ """Extract console method name if this is a console.* call.
83
+
84
+ Args:
85
+ node: Tree-sitter call_expression node
86
+ methods: Set of console method names to detect
87
+
88
+ Returns:
89
+ Method name if this is a matching console call, None otherwise
90
+ """
91
+ func_node = self.find_child_by_type(node, "member_expression")
92
+ if func_node is None:
93
+ return None
94
+ if not self._is_console_object(func_node):
95
+ return None
96
+ return self._get_matching_method(func_node, methods)
97
+
98
+ def _is_console_object(self, func_node: Node) -> bool:
99
+ """Check if the member expression is on 'console' object."""
100
+ object_node = self._find_object_node(func_node)
101
+ if object_node is None:
102
+ return False
103
+ return self.extract_node_text(object_node) == "console"
104
+
105
+ def _get_matching_method(self, func_node: Node, methods: set[str]) -> str | None:
106
+ """Get method name if it matches the configured methods."""
107
+ method_node = self.find_child_by_type(func_node, "property_identifier")
108
+ if method_node is None:
109
+ return None
110
+ method_name = self.extract_node_text(method_node)
111
+ return method_name if method_name in methods else None
112
+
113
+ def _find_object_node(self, member_expr: Node) -> Node | None:
114
+ """Find the object node in a member expression.
115
+
116
+ Args:
117
+ member_expr: Tree-sitter member_expression node
118
+
119
+ Returns:
120
+ Object node (identifier) or None
121
+ """
122
+ for child in member_expr.children:
123
+ if child.type == "identifier":
124
+ return child
125
+ return None