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.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +44 -27
- src/core/base.py +95 -5
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +36 -6
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +125 -22
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +142 -94
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +68 -21
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +20 -82
- src/linters/dry/file_analyzer.py +15 -50
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +182 -54
- src/linters/dry/python_analyzer.py +108 -336
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +129 -71
- src/linters/dry/typescript_analyzer.py +68 -380
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +9 -5
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +105 -0
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +140 -0
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +72 -0
- src/linters/file_header/linter.py +309 -0
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +42 -0
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +79 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +74 -31
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +249 -0
- src/linters/magic_numbers/linter.py +462 -0
- src/linters/magic_numbers/python_analyzer.py +64 -0
- src/linters/magic_numbers/typescript_analyzer.py +215 -0
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +31 -34
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -11
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +78 -0
- src/linters/print_statements/linter.py +413 -0
- src/linters/print_statements/python_analyzer.py +153 -0
- src/linters/print_statements/typescript_analyzer.py +125 -0
- src/linters/print_statements/violation_builder.py +96 -0
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/config.py +12 -6
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +47 -39
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +264 -16
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +354 -0
- src/utils/project_root.py +138 -16
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1055
- thailint-0.2.0.dist-info/METADATA +0 -980
- thailint-0.2.0.dist-info/RECORD +0 -75
- thailint-0.2.0.dist-info/entry_points.txt +0 -4
- {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
|