thailint 0.5.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 +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- 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 +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- 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 +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- 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 +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- 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/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -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/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- 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/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- 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 +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
"""
|
|
2
|
-
File: src/linters/print_statements/linter.py
|
|
3
|
-
|
|
4
2
|
Purpose: Main print statements linter rule implementation
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Depends: BaseLintContext, MultiLanguageLintRule, PythonPrintStatementAnalyzer,
|
|
9
|
-
TypeScriptPrintStatementAnalyzer, ViolationBuilder, PrintStatementConfig, IgnoreDirectiveParser
|
|
10
|
-
|
|
11
|
-
Implements: PrintStatementRule.check(context) -> list[Violation], properties for rule metadata
|
|
4
|
+
Scope: Print and console statement detection for Python, TypeScript, and JavaScript files
|
|
12
5
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Overview: Implements print statements linter rule following BaseLintRule interface. Orchestrates
|
|
6
|
+
Overview: Implements print statements linter rule following MultiLanguageLintRule interface. Orchestrates
|
|
16
7
|
configuration loading, Python AST analysis for print() calls, TypeScript tree-sitter analysis
|
|
17
8
|
for console.* calls, and violation building through focused helper classes. Detects print and
|
|
18
9
|
console statements that should be replaced with proper logging. Supports configurable
|
|
19
10
|
allow_in_scripts option to permit print() in __main__ blocks and configurable console_methods
|
|
20
|
-
set for TypeScript/JavaScript. Handles ignore directives for suppressing specific violations
|
|
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
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
violations = rule.check(context)
|
|
17
|
+
Exports: PrintStatementRule class implementing MultiLanguageLintRule interface
|
|
24
18
|
|
|
25
|
-
|
|
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).
|
|
26
29
|
"""
|
|
27
30
|
|
|
28
31
|
import ast
|
|
@@ -31,7 +34,8 @@ from pathlib import Path
|
|
|
31
34
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
32
35
|
from src.core.linter_utils import load_linter_config
|
|
33
36
|
from src.core.types import Violation
|
|
34
|
-
from src.
|
|
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
|
|
35
39
|
|
|
36
40
|
from .config import PrintStatementConfig
|
|
37
41
|
from .python_analyzer import PythonPrintStatementAnalyzer
|
|
@@ -44,7 +48,7 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
44
48
|
|
|
45
49
|
def __init__(self) -> None:
|
|
46
50
|
"""Initialize the print statements rule."""
|
|
47
|
-
self._ignore_parser =
|
|
51
|
+
self._ignore_parser = get_ignore_parser()
|
|
48
52
|
self._violation_builder = ViolationBuilder(self.rule_id)
|
|
49
53
|
|
|
50
54
|
@property
|
|
@@ -122,10 +126,7 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
122
126
|
return False
|
|
123
127
|
|
|
124
128
|
file_path = Path(context.file_path)
|
|
125
|
-
for pattern in config.ignore
|
|
126
|
-
if self._matches_pattern(file_path, pattern):
|
|
127
|
-
return True
|
|
128
|
-
return False
|
|
129
|
+
return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
|
|
129
130
|
|
|
130
131
|
def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
|
|
131
132
|
"""Check if file path matches a glob pattern.
|
|
@@ -257,27 +258,16 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
257
258
|
Returns:
|
|
258
259
|
True if line has generic ignore directive
|
|
259
260
|
"""
|
|
260
|
-
line_text =
|
|
261
|
+
line_text = get_violation_line(violation, context)
|
|
261
262
|
if line_text is None:
|
|
262
263
|
return False
|
|
263
264
|
return self._has_generic_ignore_directive(line_text)
|
|
264
265
|
|
|
265
|
-
def _get_violation_line(self, violation: Violation, context: BaseLintContext) -> str | None:
|
|
266
|
-
"""Get the line text for a violation."""
|
|
267
|
-
if not context.file_content:
|
|
268
|
-
return None
|
|
269
|
-
|
|
270
|
-
lines = context.file_content.splitlines()
|
|
271
|
-
if violation.line <= 0 or violation.line > len(lines):
|
|
272
|
-
return None
|
|
273
|
-
|
|
274
|
-
return lines[violation.line - 1].lower()
|
|
275
|
-
|
|
276
266
|
def _has_generic_ignore_directive(self, line_text: str) -> bool:
|
|
277
267
|
"""Check if line has generic ignore directive."""
|
|
278
268
|
if self._has_generic_thailint_ignore(line_text):
|
|
279
269
|
return True
|
|
280
|
-
return
|
|
270
|
+
return has_python_noqa(line_text)
|
|
281
271
|
|
|
282
272
|
def _has_generic_thailint_ignore(self, line_text: str) -> bool:
|
|
283
273
|
"""Check for generic thailint: ignore (no brackets)."""
|
|
@@ -286,10 +276,6 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
286
276
|
after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
|
|
287
277
|
return "[" not in after_ignore
|
|
288
278
|
|
|
289
|
-
def _has_noqa_directive(self, line_text: str) -> bool:
|
|
290
|
-
"""Check for noqa-style comments."""
|
|
291
|
-
return "# noqa" in line_text
|
|
292
|
-
|
|
293
279
|
def _check_typescript(
|
|
294
280
|
self, context: BaseLintContext, config: PrintStatementConfig
|
|
295
281
|
) -> list[Violation]:
|
|
@@ -402,7 +388,7 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
402
388
|
Returns:
|
|
403
389
|
True if line has ignore directive
|
|
404
390
|
"""
|
|
405
|
-
line_text =
|
|
391
|
+
line_text = get_violation_line(violation, context)
|
|
406
392
|
if line_text is None:
|
|
407
393
|
return False
|
|
408
394
|
return self._has_typescript_ignore_directive(line_text)
|
|
@@ -424,7 +410,4 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
424
410
|
if "[" not in after_ignore:
|
|
425
411
|
return True
|
|
426
412
|
|
|
427
|
-
|
|
428
|
-
return True
|
|
429
|
-
|
|
430
|
-
return False
|
|
413
|
+
return has_typescript_noqa(line_text)
|
|
@@ -1,34 +1,107 @@
|
|
|
1
1
|
"""
|
|
2
|
-
File: src/linters/print_statements/python_analyzer.py
|
|
3
|
-
|
|
4
2
|
Purpose: Python AST analysis for finding print() call nodes
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Depends: ast module for AST parsing and node types
|
|
9
|
-
|
|
10
|
-
Implements: PythonPrintStatementAnalyzer.find_print_calls(tree) -> list[tuple],
|
|
11
|
-
PythonPrintStatementAnalyzer.is_in_main_block(node) -> bool
|
|
12
|
-
|
|
13
|
-
Related: src/linters/magic_numbers/python_analyzer.py
|
|
4
|
+
Scope: Python print() statement detection and __main__ block context analysis
|
|
14
5
|
|
|
15
6
|
Overview: Provides PythonPrintStatementAnalyzer class that traverses Python AST to find all
|
|
16
7
|
print() function calls. Uses ast.walk() to traverse the syntax tree and collect
|
|
17
8
|
Call nodes where the function is 'print'. Tracks parent nodes to detect if print calls
|
|
18
9
|
are within __main__ blocks (if __name__ == "__main__":) for allow_in_scripts filtering.
|
|
19
10
|
Returns structured data about each print call including the AST node, parent context,
|
|
20
|
-
and line number for violation reporting.
|
|
11
|
+
and line number for violation reporting. Handles both simple print() and builtins.print() calls.
|
|
21
12
|
|
|
22
|
-
|
|
23
|
-
print_calls = analyzer.find_print_calls(ast.parse(code))
|
|
13
|
+
Dependencies: ast module for AST parsing and node types, analyzers.ast_utils
|
|
24
14
|
|
|
25
|
-
|
|
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
|
|
26
20
|
"""
|
|
27
21
|
|
|
28
22
|
import ast
|
|
29
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
|
+
|
|
30
82
|
|
|
31
|
-
|
|
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:
|
|
32
105
|
"""Analyzes Python AST to find print() calls."""
|
|
33
106
|
|
|
34
107
|
def __init__(self) -> None:
|
|
@@ -46,24 +119,10 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
|
|
|
46
119
|
List of tuples (node, parent, line_number)
|
|
47
120
|
"""
|
|
48
121
|
self.print_calls = []
|
|
49
|
-
self.parent_map =
|
|
50
|
-
self._build_parent_map(tree)
|
|
122
|
+
self.parent_map = build_parent_map(tree)
|
|
51
123
|
self._collect_print_calls(tree)
|
|
52
124
|
return self.print_calls
|
|
53
125
|
|
|
54
|
-
def _build_parent_map(self, node: ast.AST, parent: ast.AST | None = None) -> None:
|
|
55
|
-
"""Build a map of nodes to their parents.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
node: Current AST node
|
|
59
|
-
parent: Parent of current node
|
|
60
|
-
"""
|
|
61
|
-
if parent is not None:
|
|
62
|
-
self.parent_map[node] = parent
|
|
63
|
-
|
|
64
|
-
for child in ast.iter_child_nodes(node):
|
|
65
|
-
self._build_parent_map(child, node)
|
|
66
|
-
|
|
67
126
|
def _collect_print_calls(self, tree: ast.AST) -> None:
|
|
68
127
|
"""Walk tree and collect all print() calls.
|
|
69
128
|
|
|
@@ -71,34 +130,11 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
|
|
|
71
130
|
tree: AST to traverse
|
|
72
131
|
"""
|
|
73
132
|
for node in ast.walk(tree):
|
|
74
|
-
if isinstance(node, ast.Call) and
|
|
133
|
+
if isinstance(node, ast.Call) and is_print_call(node):
|
|
75
134
|
parent = self.parent_map.get(node)
|
|
76
135
|
line_number = node.lineno if hasattr(node, "lineno") else 0
|
|
77
136
|
self.print_calls.append((node, parent, line_number))
|
|
78
137
|
|
|
79
|
-
def _is_print_call(self, node: ast.Call) -> bool:
|
|
80
|
-
"""Check if a Call node is calling print().
|
|
81
|
-
|
|
82
|
-
Args:
|
|
83
|
-
node: The Call node to check
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
True if this is a print() call
|
|
87
|
-
"""
|
|
88
|
-
return self._is_simple_print(node) or self._is_builtins_print(node)
|
|
89
|
-
|
|
90
|
-
def _is_simple_print(self, node: ast.Call) -> bool:
|
|
91
|
-
"""Check for simple print() call."""
|
|
92
|
-
return isinstance(node.func, ast.Name) and node.func.id == "print"
|
|
93
|
-
|
|
94
|
-
def _is_builtins_print(self, node: ast.Call) -> bool:
|
|
95
|
-
"""Check for builtins.print() call."""
|
|
96
|
-
if not isinstance(node.func, ast.Attribute):
|
|
97
|
-
return False
|
|
98
|
-
if node.func.attr != "print":
|
|
99
|
-
return False
|
|
100
|
-
return isinstance(node.func.value, ast.Name) and node.func.value.id == "builtins"
|
|
101
|
-
|
|
102
138
|
def is_in_main_block(self, node: ast.AST) -> bool:
|
|
103
139
|
"""Check if node is within `if __name__ == "__main__":` block.
|
|
104
140
|
|
|
@@ -111,45 +147,7 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
|
|
|
111
147
|
current = node
|
|
112
148
|
while current in self.parent_map:
|
|
113
149
|
parent = self.parent_map[current]
|
|
114
|
-
if
|
|
150
|
+
if is_main_if_block(parent):
|
|
115
151
|
return True
|
|
116
152
|
current = parent
|
|
117
153
|
return False
|
|
118
|
-
|
|
119
|
-
def _is_main_if_block(self, node: ast.AST) -> bool:
|
|
120
|
-
"""Check if node is an `if __name__ == "__main__":` statement.
|
|
121
|
-
|
|
122
|
-
Args:
|
|
123
|
-
node: AST node to check
|
|
124
|
-
|
|
125
|
-
Returns:
|
|
126
|
-
True if this is a __main__ if block
|
|
127
|
-
"""
|
|
128
|
-
if not isinstance(node, ast.If):
|
|
129
|
-
return False
|
|
130
|
-
if not isinstance(node.test, ast.Compare):
|
|
131
|
-
return False
|
|
132
|
-
return self._is_main_comparison(node.test)
|
|
133
|
-
|
|
134
|
-
def _is_main_comparison(self, test: ast.Compare) -> bool:
|
|
135
|
-
"""Check if comparison is __name__ == '__main__'."""
|
|
136
|
-
if not self._is_name_identifier(test.left):
|
|
137
|
-
return False
|
|
138
|
-
if not self._has_single_eq_operator(test):
|
|
139
|
-
return False
|
|
140
|
-
return self._compares_to_main(test)
|
|
141
|
-
|
|
142
|
-
def _is_name_identifier(self, node: ast.expr) -> bool:
|
|
143
|
-
"""Check if node is the __name__ identifier."""
|
|
144
|
-
return isinstance(node, ast.Name) and node.id == "__name__"
|
|
145
|
-
|
|
146
|
-
def _has_single_eq_operator(self, test: ast.Compare) -> bool:
|
|
147
|
-
"""Check if comparison has single == operator."""
|
|
148
|
-
return len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)
|
|
149
|
-
|
|
150
|
-
def _compares_to_main(self, test: ast.Compare) -> bool:
|
|
151
|
-
"""Check if comparison is to '__main__' string."""
|
|
152
|
-
if len(test.comparators) != 1:
|
|
153
|
-
return False
|
|
154
|
-
comparator = test.comparators[0]
|
|
155
|
-
return isinstance(comparator, ast.Constant) and comparator.value == "__main__"
|
|
@@ -1,46 +1,36 @@
|
|
|
1
1
|
"""
|
|
2
|
-
File: src/linters/print_statements/typescript_analyzer.py
|
|
3
|
-
|
|
4
2
|
Purpose: TypeScript/JavaScript console.* call detection using Tree-sitter AST analysis
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Depends: TypeScriptBaseAnalyzer for tree-sitter parsing, tree-sitter Node type
|
|
9
|
-
|
|
10
|
-
Implements: find_console_calls(root_node, methods) -> list[tuple]
|
|
11
|
-
|
|
12
|
-
Related: src/linters/magic_numbers/typescript_analyzer.py, src/analyzers/typescript_base.py
|
|
4
|
+
Scope: TypeScript and JavaScript console statement detection
|
|
13
5
|
|
|
14
6
|
Overview: Analyzes TypeScript and JavaScript code to detect console.* method calls that should
|
|
15
7
|
be replaced with proper logging. Uses Tree-sitter parser to traverse TypeScript/JavaScript
|
|
16
8
|
AST and identify call expressions where the callee is console.log, console.warn, console.error,
|
|
17
9
|
console.debug, or console.info (configurable). Returns structured data with the node, method
|
|
18
10
|
name, and line number for each detected console call. Supports both TypeScript and JavaScript
|
|
19
|
-
files with shared detection logic.
|
|
11
|
+
files with shared detection logic. Handles member expression pattern matching to identify
|
|
12
|
+
console object method calls.
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
24
21
|
|
|
25
|
-
Notes: Tree-sitter node traversal with call_expression and member_expression pattern matching
|
|
26
22
|
"""
|
|
27
23
|
|
|
28
24
|
import logging
|
|
29
|
-
from typing import Any
|
|
30
25
|
|
|
31
|
-
from src.analyzers.typescript_base import
|
|
26
|
+
from src.analyzers.typescript_base import (
|
|
27
|
+
TREE_SITTER_AVAILABLE,
|
|
28
|
+
Node,
|
|
29
|
+
TypeScriptBaseAnalyzer,
|
|
30
|
+
)
|
|
32
31
|
|
|
33
32
|
logger = logging.getLogger(__name__)
|
|
34
33
|
|
|
35
|
-
# dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
|
|
36
|
-
try:
|
|
37
|
-
from tree_sitter import Node
|
|
38
|
-
|
|
39
|
-
TREE_SITTER_AVAILABLE = True
|
|
40
|
-
except ImportError:
|
|
41
|
-
TREE_SITTER_AVAILABLE = False
|
|
42
|
-
Node = Any # type: ignore
|
|
43
|
-
|
|
44
34
|
|
|
45
35
|
class TypeScriptPrintStatementAnalyzer(TypeScriptBaseAnalyzer):
|
|
46
36
|
"""Analyzes TypeScript/JavaScript code for console.* calls using Tree-sitter."""
|
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
"""
|
|
2
|
-
File: src/linters/print_statements/violation_builder.py
|
|
3
|
-
|
|
4
2
|
Purpose: Builds Violation objects for print statement detection
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Depends: ast, pathlib.Path, src.core.types.Violation
|
|
9
|
-
|
|
10
|
-
Implements: ViolationBuilder.create_python_violation(node, line, file_path) -> Violation,
|
|
11
|
-
ViolationBuilder.create_typescript_violation(method, line, file_path) -> Violation
|
|
12
|
-
|
|
13
|
-
Related: src/linters/magic_numbers/violation_builder.py, src/core/types.py
|
|
4
|
+
Scope: Violation creation for print and console statement detections
|
|
14
5
|
|
|
15
6
|
Overview: Provides ViolationBuilder class that creates Violation objects for print statement
|
|
16
7
|
detections. Generates descriptive messages suggesting the use of proper logging instead of
|
|
17
8
|
print/console statements. Constructs complete Violation instances with rule_id, file_path,
|
|
18
9
|
line number, column, message, and suggestions. Provides separate methods for Python print()
|
|
19
|
-
violations and TypeScript/JavaScript console.* violations with language-appropriate messages
|
|
10
|
+
violations and TypeScript/JavaScript console.* violations with language-appropriate messages
|
|
11
|
+
and helpful remediation guidance.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for Python AST nodes, pathlib.Path for file paths,
|
|
14
|
+
src.core.types.Violation for violation structure
|
|
15
|
+
|
|
16
|
+
Exports: ViolationBuilder class
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
Interfaces: create_python_violation(node, line, file_path) -> Violation,
|
|
19
|
+
create_typescript_violation(method, line, file_path) -> Violation
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
Implementation: Builder pattern with message templates suggesting logging as alternative
|
|
22
|
+
to print/console statements
|
|
25
23
|
"""
|
|
26
24
|
|
|
27
25
|
import ast
|
|
@@ -31,6 +31,12 @@ from .typescript_analyzer import TypeScriptSRPAnalyzer
|
|
|
31
31
|
class ClassAnalyzer:
|
|
32
32
|
"""Coordinates class analysis for Python and TypeScript."""
|
|
33
33
|
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
"""Initialize the class analyzer with singleton analyzers."""
|
|
36
|
+
# Singleton analyzers for performance (avoid recreating per-file)
|
|
37
|
+
self._python_analyzer = PythonSRPAnalyzer()
|
|
38
|
+
self._typescript_analyzer = TypeScriptSRPAnalyzer()
|
|
39
|
+
|
|
34
40
|
def analyze_python(
|
|
35
41
|
self, context: BaseLintContext, config: SRPConfig
|
|
36
42
|
) -> list[dict[str, Any]] | list[Violation]:
|
|
@@ -47,10 +53,9 @@ class ClassAnalyzer:
|
|
|
47
53
|
if isinstance(tree, list): # Syntax error violations
|
|
48
54
|
return tree
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
classes = analyzer.find_all_classes(tree)
|
|
56
|
+
classes = self._python_analyzer.find_all_classes(tree)
|
|
52
57
|
return [
|
|
53
|
-
|
|
58
|
+
self._python_analyzer.analyze_class(class_node, context.file_content or "", config)
|
|
54
59
|
for class_node in classes
|
|
55
60
|
]
|
|
56
61
|
|
|
@@ -66,14 +71,13 @@ class ClassAnalyzer:
|
|
|
66
71
|
Returns:
|
|
67
72
|
List of class metrics dicts
|
|
68
73
|
"""
|
|
69
|
-
|
|
70
|
-
root_node = analyzer.parse_typescript(context.file_content or "")
|
|
74
|
+
root_node = self._typescript_analyzer.parse_typescript(context.file_content or "")
|
|
71
75
|
if not root_node:
|
|
72
76
|
return []
|
|
73
77
|
|
|
74
|
-
classes =
|
|
78
|
+
classes = self._typescript_analyzer.find_all_classes(root_node)
|
|
75
79
|
return [
|
|
76
|
-
|
|
80
|
+
self._typescript_analyzer.analyze_class(class_node, context.file_content or "", config)
|
|
77
81
|
for class_node in classes
|
|
78
82
|
]
|
|
79
83
|
|
src/linters/srp/heuristics.py
CHANGED
|
@@ -4,12 +4,13 @@ Purpose: SRP detection heuristics for analyzing code complexity and responsibili
|
|
|
4
4
|
Scope: Helper functions for method counting, LOC calculation, and keyword detection
|
|
5
5
|
|
|
6
6
|
Overview: Provides heuristic-based analysis functions for detecting Single Responsibility
|
|
7
|
-
Principle violations. Implements method counting that excludes property decorators
|
|
8
|
-
special methods. Provides LOC calculation that filters out blank
|
|
9
|
-
Includes keyword detection for identifying generic class names that
|
|
10
|
-
violations (Manager, Handler, etc.). Supports both Python AST and
|
|
11
|
-
nodes. These heuristics enable practical SRP detection without
|
|
12
|
-
analysis, focusing on measurable code metrics that correlate
|
|
7
|
+
Principle violations. Implements method counting that excludes property decorators,
|
|
8
|
+
private methods, and special methods. Provides LOC calculation that filters out blank
|
|
9
|
+
lines and comments. Includes keyword detection for identifying generic class names that
|
|
10
|
+
often indicate SRP violations (Manager, Handler, etc.). Supports both Python AST and
|
|
11
|
+
TypeScript tree-sitter nodes. These heuristics enable practical SRP detection without
|
|
12
|
+
requiring perfect semantic analysis, focusing on measurable code metrics that correlate
|
|
13
|
+
with responsibility scope.
|
|
13
14
|
|
|
14
15
|
Dependencies: ast module for Python AST analysis, typing for type hints
|
|
15
16
|
|
|
@@ -24,22 +25,55 @@ import ast
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def count_methods(class_node: ast.ClassDef) -> int:
|
|
27
|
-
"""Count methods in a class (excludes properties and
|
|
28
|
+
"""Count public methods in a class (excludes properties and private methods).
|
|
29
|
+
|
|
30
|
+
Private methods are those starting with underscore (_), including dunder
|
|
31
|
+
methods (__init__, __str__, etc.). This focuses SRP analysis on the public
|
|
32
|
+
interface rather than implementation details.
|
|
28
33
|
|
|
29
34
|
Args:
|
|
30
35
|
class_node: AST node representing a class definition
|
|
31
36
|
|
|
32
37
|
Returns:
|
|
33
|
-
Number of methods in the class
|
|
38
|
+
Number of public methods in the class
|
|
39
|
+
"""
|
|
40
|
+
func_nodes = (
|
|
41
|
+
n for n in class_node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
42
|
+
)
|
|
43
|
+
public_methods = [n for n in func_nodes if _is_countable_method(n)]
|
|
44
|
+
return len(public_methods)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_countable_method(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
48
|
+
"""Check if a method should be counted (public and not a property).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
node: Function AST node
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
True if method should be counted
|
|
55
|
+
"""
|
|
56
|
+
if has_property_decorator(node):
|
|
57
|
+
return False
|
|
58
|
+
if _is_private_method(node.name):
|
|
59
|
+
return False
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_private_method(method_name: str) -> bool:
|
|
64
|
+
"""Check if method is private (starts with underscore).
|
|
65
|
+
|
|
66
|
+
This includes both single underscore (_helper) and dunder methods
|
|
67
|
+
(__init__, __str__). All underscore-prefixed methods are considered
|
|
68
|
+
implementation details.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
method_name: Name of the method to check
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if method is private, False otherwise
|
|
34
75
|
"""
|
|
35
|
-
|
|
36
|
-
for node in class_node.body:
|
|
37
|
-
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
38
|
-
continue
|
|
39
|
-
# Don't count @property decorators as methods
|
|
40
|
-
if not has_property_decorator(node):
|
|
41
|
-
methods += 1
|
|
42
|
-
return methods
|
|
76
|
+
return method_name.startswith("_")
|
|
43
77
|
|
|
44
78
|
|
|
45
79
|
def count_loc(class_node: ast.ClassDef, source: str) -> int:
|
|
@@ -56,8 +90,8 @@ def count_loc(class_node: ast.ClassDef, source: str) -> int:
|
|
|
56
90
|
end_line = class_node.end_lineno or start_line
|
|
57
91
|
lines = source.split("\n")[start_line - 1 : end_line]
|
|
58
92
|
|
|
59
|
-
# Filter out blank lines and comments
|
|
60
|
-
code_lines = [
|
|
93
|
+
# Filter out blank lines and comments (using walrus operator to avoid double strip)
|
|
94
|
+
code_lines = [s for line in lines if (s := line.strip()) and not s.startswith("#")]
|
|
61
95
|
return len(code_lines)
|
|
62
96
|
|
|
63
97
|
|
|
@@ -83,7 +117,7 @@ def has_property_decorator(func_node: ast.FunctionDef | ast.AsyncFunctionDef) ->
|
|
|
83
117
|
Returns:
|
|
84
118
|
True if function has @property decorator
|
|
85
119
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
120
|
+
return any(
|
|
121
|
+
isinstance(decorator, ast.Name) and decorator.id == "property"
|
|
122
|
+
for decorator in func_node.decorator_list
|
|
123
|
+
)
|