thailint 0.12.0__py3-none-any.whl → 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +3 -0
- src/cli/config.py +12 -12
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +3 -0
- src/cli/linters/code_patterns.py +113 -5
- src/cli/linters/code_smells.py +4 -0
- src/cli/linters/documentation.py +3 -0
- src/cli/linters/structure.py +3 -0
- src/cli/linters/structure_quality.py +3 -0
- src/cli_main.py +3 -0
- src/config.py +2 -1
- src/core/base.py +3 -2
- src/core/cli_utils.py +3 -1
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +4 -0
- src/core/rule_discovery.py +5 -1
- src/core/violation_builder.py +3 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +225 -383
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -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 +12 -0
- src/linters/collection_pipeline/continue_analyzer.py +2 -8
- src/linters/collection_pipeline/detector.py +262 -32
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +18 -35
- src/linters/collection_pipeline/suggestion_builder.py +68 -1
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +7 -4
- src/linters/dry/cache.py +7 -2
- src/linters/dry/config.py +7 -1
- src/linters/dry/constant_matcher.py +34 -25
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +48 -25
- src/linters/dry/python_analyzer.py +18 -10
- src/linters/dry/python_constant_extractor.py +51 -52
- src/linters/dry/single_statement_detector.py +14 -12
- src/linters/dry/token_hasher.py +115 -115
- src/linters/dry/typescript_analyzer.py +11 -6
- src/linters/dry/typescript_constant_extractor.py +4 -0
- src/linters/dry/typescript_statement_detector.py +208 -208
- src/linters/dry/typescript_value_extractor.py +3 -0
- src/linters/dry/violation_filter.py +1 -4
- src/linters/dry/violation_generator.py +1 -4
- src/linters/file_header/atemporal_detector.py +4 -0
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/field_validator.py +5 -8
- src/linters/file_header/linter.py +19 -12
- src/linters/file_header/markdown_parser.py +6 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/linter.py +22 -8
- src/linters/file_placement/pattern_matcher.py +21 -4
- src/linters/file_placement/pattern_validator.py +21 -7
- src/linters/file_placement/rule_checker.py +2 -2
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +66 -0
- src/linters/lazy_ignores/directive_utils.py +121 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +135 -0
- src/linters/lazy_ignores/python_analyzer.py +201 -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 +67 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +131 -0
- src/linters/lbyl/__init__.py +29 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/pattern_detectors/__init__.py +25 -0
- src/linters/lbyl/pattern_detectors/base.py +46 -0
- src/linters/magic_numbers/context_analyzer.py +227 -229
- src/linters/magic_numbers/linter.py +20 -15
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -16
- src/linters/method_property/config.py +4 -0
- src/linters/method_property/linter.py +5 -4
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- src/linters/print_statements/linter.py +6 -4
- src/linters/print_statements/python_analyzer.py +85 -81
- src/linters/print_statements/typescript_analyzer.py +6 -15
- src/linters/srp/heuristics.py +4 -4
- src/linters/srp/linter.py +12 -12
- src/linters/srp/violation_builder.py +0 -4
- src/linters/stateless_class/linter.py +30 -36
- src/linters/stateless_class/python_analyzer.py +11 -20
- src/linters/stringly_typed/config.py +4 -5
- src/linters/stringly_typed/context_filter.py +410 -410
- src/linters/stringly_typed/function_call_violation_builder.py +93 -95
- src/linters/stringly_typed/linter.py +48 -16
- src/linters/stringly_typed/python/analyzer.py +5 -1
- src/linters/stringly_typed/python/call_tracker.py +8 -5
- src/linters/stringly_typed/python/comparison_tracker.py +10 -5
- src/linters/stringly_typed/python/condition_extractor.py +3 -0
- src/linters/stringly_typed/python/conditional_detector.py +4 -1
- src/linters/stringly_typed/python/match_analyzer.py +8 -2
- src/linters/stringly_typed/python/validation_detector.py +3 -0
- src/linters/stringly_typed/storage.py +14 -14
- src/linters/stringly_typed/typescript/call_tracker.py +9 -3
- src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
- src/linters/stringly_typed/violation_generator.py +288 -259
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +166 -0
- src/utils/project_root.py +3 -0
- thailint-0.13.0.dist-info/METADATA +184 -0
- thailint-0.13.0.dist-info/RECORD +189 -0
- thailint-0.12.0.dist-info/METADATA +0 -1667
- thailint-0.12.0.dist-info/RECORD +0 -164
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,7 +12,7 @@ Overview: Implements magic numbers linter rule following BaseLintRule interface.
|
|
|
12
12
|
because refactoring for A-grade complexity requires extracting helper methods. Class maintains
|
|
13
13
|
single responsibility of magic number detection - all methods support this core purpose.
|
|
14
14
|
|
|
15
|
-
Dependencies: BaseLintRule, BaseLintContext, PythonMagicNumberAnalyzer,
|
|
15
|
+
Dependencies: BaseLintRule, BaseLintContext, PythonMagicNumberAnalyzer, is_acceptable_context,
|
|
16
16
|
ViolationBuilder, MagicNumberConfig, IgnoreDirectiveParser
|
|
17
17
|
|
|
18
18
|
Exports: MagicNumberRule class
|
|
@@ -21,10 +21,16 @@ Interfaces: MagicNumberRule.check(context) -> list[Violation], properties for ru
|
|
|
21
21
|
|
|
22
22
|
Implementation: Composition pattern with helper classes, AST-based analysis with configurable
|
|
23
23
|
allowed numbers and context detection
|
|
24
|
+
|
|
25
|
+
Suppressions:
|
|
26
|
+
- too-many-arguments,too-many-positional-arguments: TypeScript violation creation with related params
|
|
27
|
+
- srp: Rule class coordinates analyzers and violation builders. Method count exceeds limit
|
|
28
|
+
due to complexity refactoring. All methods support magic number detection.
|
|
24
29
|
"""
|
|
25
30
|
|
|
26
31
|
import ast
|
|
27
32
|
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
28
34
|
|
|
29
35
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
30
36
|
from src.core.linter_utils import load_linter_config
|
|
@@ -33,7 +39,7 @@ from src.core.violation_utils import get_violation_line, has_python_noqa
|
|
|
33
39
|
from src.linter_config.ignore import get_ignore_parser
|
|
34
40
|
|
|
35
41
|
from .config import MagicNumberConfig
|
|
36
|
-
from .context_analyzer import
|
|
42
|
+
from .context_analyzer import is_acceptable_context
|
|
37
43
|
from .python_analyzer import PythonMagicNumberAnalyzer
|
|
38
44
|
from .typescript_analyzer import TypeScriptMagicNumberAnalyzer
|
|
39
45
|
from .typescript_ignore_checker import TypeScriptIgnoreChecker
|
|
@@ -47,7 +53,6 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
47
53
|
"""Initialize the magic numbers rule."""
|
|
48
54
|
self._ignore_parser = get_ignore_parser()
|
|
49
55
|
self._violation_builder = ViolationBuilder(self.rule_id)
|
|
50
|
-
self._context_analyzer = ContextAnalyzer()
|
|
51
56
|
self._typescript_ignore_checker = TypeScriptIgnoreChecker()
|
|
52
57
|
|
|
53
58
|
@property
|
|
@@ -134,10 +139,7 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
134
139
|
return False
|
|
135
140
|
|
|
136
141
|
file_path = Path(context.file_path)
|
|
137
|
-
for pattern in config.ignore
|
|
138
|
-
if self._matches_pattern(file_path, pattern):
|
|
139
|
-
return True
|
|
140
|
-
return False
|
|
142
|
+
return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
|
|
141
143
|
|
|
142
144
|
def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
|
|
143
145
|
"""Check if file path matches a glob pattern.
|
|
@@ -251,9 +253,7 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
251
253
|
"allowed_numbers": config.allowed_numbers,
|
|
252
254
|
}
|
|
253
255
|
|
|
254
|
-
if
|
|
255
|
-
node, parent, context.file_path, config_dict
|
|
256
|
-
):
|
|
256
|
+
if is_acceptable_context(node, parent, context.file_path, config_dict):
|
|
257
257
|
return False
|
|
258
258
|
|
|
259
259
|
return True
|
|
@@ -421,12 +421,17 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
421
421
|
return value in config.allowed_numbers or self._is_test_file(context.file_path)
|
|
422
422
|
|
|
423
423
|
def _is_typescript_special_context(
|
|
424
|
-
self, node:
|
|
424
|
+
self, node: Any, analyzer: TypeScriptMagicNumberAnalyzer, context: BaseLintContext
|
|
425
425
|
) -> bool:
|
|
426
|
-
"""Check if in TypeScript-specific special context.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
"""Check if in TypeScript-specific special context.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
node: Tree-sitter Node (typed as Any due to optional dependency)
|
|
430
|
+
analyzer: TypeScript analyzer
|
|
431
|
+
context: Lint context
|
|
432
|
+
"""
|
|
433
|
+
in_enum = analyzer.is_enum_context(node)
|
|
434
|
+
in_const_def = analyzer.is_constant_definition(node, context.file_content or "")
|
|
430
435
|
return in_enum or in_const_def
|
|
431
436
|
|
|
432
437
|
def _is_test_file(self, file_path: object) -> bool:
|
|
@@ -10,7 +10,7 @@ Overview: Provides PythonMagicNumberAnalyzer class that traverses Python AST to
|
|
|
10
10
|
value, and source location. This analyzer handles Python-specific AST structure and provides
|
|
11
11
|
the foundation for magic number detection by identifying all candidates before context filtering.
|
|
12
12
|
|
|
13
|
-
Dependencies: ast module for AST parsing and node types
|
|
13
|
+
Dependencies: ast module for AST parsing and node types, analyzers.ast_utils
|
|
14
14
|
|
|
15
15
|
Exports: PythonMagicNumberAnalyzer class
|
|
16
16
|
|
|
@@ -23,6 +23,8 @@ Implementation: AST NodeVisitor pattern with parent tracking, filters for numeri
|
|
|
23
23
|
import ast
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
+
from src.analyzers.ast_utils import build_parent_map
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
class PythonMagicNumberAnalyzer(ast.NodeVisitor):
|
|
28
30
|
"""Analyzes Python AST to find numeric literals."""
|
|
@@ -44,24 +46,10 @@ class PythonMagicNumberAnalyzer(ast.NodeVisitor):
|
|
|
44
46
|
List of tuples (node, parent, value, line_number)
|
|
45
47
|
"""
|
|
46
48
|
self.numeric_literals = []
|
|
47
|
-
self.parent_map =
|
|
48
|
-
self._build_parent_map(tree)
|
|
49
|
+
self.parent_map = build_parent_map(tree)
|
|
49
50
|
self.visit(tree)
|
|
50
51
|
return self.numeric_literals
|
|
51
52
|
|
|
52
|
-
def _build_parent_map(self, node: ast.AST, parent: ast.AST | None = None) -> None:
|
|
53
|
-
"""Build a map of nodes to their parents.
|
|
54
|
-
|
|
55
|
-
Args:
|
|
56
|
-
node: Current AST node
|
|
57
|
-
parent: Parent of current node
|
|
58
|
-
"""
|
|
59
|
-
if parent is not None:
|
|
60
|
-
self.parent_map[node] = parent
|
|
61
|
-
|
|
62
|
-
for child in ast.iter_child_nodes(node):
|
|
63
|
-
self._build_parent_map(child, node)
|
|
64
|
-
|
|
65
53
|
def visit_Constant(self, node: ast.Constant) -> None:
|
|
66
54
|
"""Visit a Constant node and check if it's a numeric literal.
|
|
67
55
|
|
|
@@ -20,20 +20,17 @@ Interfaces: find_numeric_literals(root_node) -> list[tuple], is_enum_context(nod
|
|
|
20
20
|
|
|
21
21
|
Implementation: Tree-sitter node traversal with visitor pattern, context-aware filtering
|
|
22
22
|
for acceptable numeric literal locations
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
from typing import Any
|
|
26
|
-
|
|
27
|
-
from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
Suppressions:
|
|
25
|
+
- srp: Analyzer implements tree-sitter traversal with context detection methods.
|
|
26
|
+
Methods support single responsibility of magic number detection in TypeScript.
|
|
27
|
+
"""
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
from src.analyzers.typescript_base import (
|
|
30
|
+
TREE_SITTER_AVAILABLE,
|
|
31
|
+
Node,
|
|
32
|
+
TypeScriptBaseAnalyzer,
|
|
33
|
+
)
|
|
37
34
|
|
|
38
35
|
|
|
39
36
|
class TypeScriptMagicNumberAnalyzer(TypeScriptBaseAnalyzer): # thailint: ignore[srp]
|
|
@@ -44,10 +41,6 @@ class TypeScriptMagicNumberAnalyzer(TypeScriptBaseAnalyzer): # thailint: ignore
|
|
|
44
41
|
of TypeScript magic number detection - all methods support this core purpose.
|
|
45
42
|
"""
|
|
46
43
|
|
|
47
|
-
def __init__(self) -> None: # pylint: disable=useless-parent-delegation
|
|
48
|
-
"""Initialize the TypeScript magic number analyzer."""
|
|
49
|
-
super().__init__() # Sets self.tree_sitter_available from base class
|
|
50
|
-
|
|
51
44
|
def find_numeric_literals(self, root_node: Node) -> list[tuple[Node, float | int, int]]:
|
|
52
45
|
"""Find all numeric literal nodes in TypeScript/JavaScript AST.
|
|
53
46
|
|
|
@@ -18,6 +18,10 @@ Exports: MethodPropertyConfig dataclass, DEFAULT_EXCLUDE_PREFIXES, DEFAULT_EXCLU
|
|
|
18
18
|
Interfaces: from_dict(config, language) -> MethodPropertyConfig for configuration loading
|
|
19
19
|
|
|
20
20
|
Implementation: Dataclass with defaults matching Pythonic conventions and common use cases
|
|
21
|
+
|
|
22
|
+
Suppressions:
|
|
23
|
+
- dry: MethodPropertyConfig includes extensive exclusion lists that share patterns with
|
|
24
|
+
other config classes. Lists are maintained separately for clear documentation.
|
|
21
25
|
"""
|
|
22
26
|
|
|
23
27
|
from dataclasses import dataclass, field
|
|
@@ -21,6 +21,10 @@ Interfaces: check(context) -> list[Violation] for rule validation, standard rule
|
|
|
21
21
|
|
|
22
22
|
Implementation: Composition pattern with helper classes (analyzer, violation builder),
|
|
23
23
|
AST-based analysis for Python with comprehensive exclusion rules
|
|
24
|
+
|
|
25
|
+
Suppressions:
|
|
26
|
+
- srp,dry: Rule class coordinates analyzer, config, and violation building. Method count
|
|
27
|
+
exceeds limit due to comprehensive ignore directive support.
|
|
24
28
|
"""
|
|
25
29
|
|
|
26
30
|
import ast
|
|
@@ -109,10 +113,7 @@ class MethodPropertyRule(MultiLanguageLintRule): # thailint: ignore[srp,dry]
|
|
|
109
113
|
return False
|
|
110
114
|
|
|
111
115
|
file_path = Path(context.file_path)
|
|
112
|
-
for pattern in config.ignore
|
|
113
|
-
if self._matches_pattern(file_path, pattern):
|
|
114
|
-
return True
|
|
115
|
-
return False
|
|
116
|
+
return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
|
|
116
117
|
|
|
117
118
|
# dry: ignore-block
|
|
118
119
|
def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
|
|
@@ -18,6 +18,10 @@ Exports: PythonMethodAnalyzer class, PropertyCandidate dataclass
|
|
|
18
18
|
Interfaces: find_property_candidates(tree) -> list[PropertyCandidate]
|
|
19
19
|
|
|
20
20
|
Implementation: AST walk pattern with comprehensive method body analysis and exclusion checks
|
|
21
|
+
|
|
22
|
+
Suppressions:
|
|
23
|
+
- srp: Analyzer class implements comprehensive exclusion rules requiring many helper methods.
|
|
24
|
+
All methods support single responsibility of property candidate detection.
|
|
21
25
|
"""
|
|
22
26
|
|
|
23
27
|
import ast
|
|
@@ -391,10 +395,7 @@ class PythonMethodAnalyzer: # thailint: ignore[srp]
|
|
|
391
395
|
Returns:
|
|
392
396
|
True if assigning to self.*
|
|
393
397
|
"""
|
|
394
|
-
for target in targets
|
|
395
|
-
if self._is_self_target(target):
|
|
396
|
-
return True
|
|
397
|
-
return False
|
|
398
|
+
return any(self._is_self_target(target) for target in targets)
|
|
398
399
|
|
|
399
400
|
def _is_self_target(self, target: ast.expr) -> bool:
|
|
400
401
|
"""Check if target is a self attribute (self.* or self._*).
|
|
@@ -16,6 +16,9 @@ Exports: ViolationBuilder class
|
|
|
16
16
|
Interfaces: create_violation(method_name, line, column, file_path, is_get_prefix, class_name)
|
|
17
17
|
|
|
18
18
|
Implementation: Builder pattern with message templates suggesting @property decorator conversion
|
|
19
|
+
|
|
20
|
+
Suppressions:
|
|
21
|
+
- too-many-arguments,too-many-positional-arguments: Violation creation with related params
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
from pathlib import Path
|
|
@@ -16,20 +16,14 @@ Exports: TypeScriptNestingAnalyzer class with calculate_max_depth methods
|
|
|
16
16
|
Interfaces: calculate_max_depth(func_node) -> tuple[int, int], find_all_functions(root_node)
|
|
17
17
|
|
|
18
18
|
Implementation: Inherits tree-sitter parsing from base, visitor pattern with depth tracking
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
from typing import Any
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
|
|
26
|
-
try:
|
|
27
|
-
from tree_sitter import Node
|
|
20
|
+
"""
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
from src.analyzers.typescript_base import (
|
|
23
|
+
TREE_SITTER_AVAILABLE,
|
|
24
|
+
Node,
|
|
25
|
+
TypeScriptBaseAnalyzer,
|
|
26
|
+
)
|
|
33
27
|
|
|
34
28
|
from .typescript_function_extractor import TypeScriptFunctionExtractor
|
|
35
29
|
|
|
@@ -27,10 +27,6 @@ from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
|
|
|
27
27
|
class TypeScriptFunctionExtractor(TypeScriptBaseAnalyzer):
|
|
28
28
|
"""Extracts function information from TypeScript AST nodes."""
|
|
29
29
|
|
|
30
|
-
def __init__(self) -> None: # pylint: disable=useless-parent-delegation
|
|
31
|
-
"""Initialize the TypeScript function extractor."""
|
|
32
|
-
super().__init__() # Sets self.tree_sitter_available from base class
|
|
33
|
-
|
|
34
30
|
def collect_all_functions(self, root_node: Any) -> list[tuple[Any, str]]:
|
|
35
31
|
"""Collect all function nodes from TypeScript AST.
|
|
36
32
|
|
|
@@ -21,6 +21,11 @@ Interfaces: check(context) -> list[Violation] for rule validation, standard rule
|
|
|
21
21
|
|
|
22
22
|
Implementation: Composition pattern with helper classes (analyzers, violation builder),
|
|
23
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).
|
|
24
29
|
"""
|
|
25
30
|
|
|
26
31
|
import ast
|
|
@@ -121,10 +126,7 @@ class PrintStatementRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
121
126
|
return False
|
|
122
127
|
|
|
123
128
|
file_path = Path(context.file_path)
|
|
124
|
-
for pattern in config.ignore
|
|
125
|
-
if self._matches_pattern(file_path, pattern):
|
|
126
|
-
return True
|
|
127
|
-
return False
|
|
129
|
+
return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
|
|
128
130
|
|
|
129
131
|
def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
|
|
130
132
|
"""Check if file path matches a glob pattern.
|
|
@@ -10,9 +10,9 @@ Overview: Provides PythonPrintStatementAnalyzer class that traverses Python AST
|
|
|
10
10
|
Returns structured data about each print call including the AST node, parent context,
|
|
11
11
|
and line number for violation reporting. Handles both simple print() and builtins.print() calls.
|
|
12
12
|
|
|
13
|
-
Dependencies: ast module for AST parsing and node types
|
|
13
|
+
Dependencies: ast module for AST parsing and node types, analyzers.ast_utils
|
|
14
14
|
|
|
15
|
-
Exports: PythonPrintStatementAnalyzer class
|
|
15
|
+
Exports: PythonPrintStatementAnalyzer class, is_print_call function, is_main_if_block function
|
|
16
16
|
|
|
17
17
|
Interfaces: find_print_calls(tree) -> list[tuple[Call, AST | None, int]], is_in_main_block(node) -> bool
|
|
18
18
|
|
|
@@ -21,8 +21,87 @@ Implementation: AST walk pattern with parent map for context detection and __mai
|
|
|
21
21
|
|
|
22
22
|
import ast
|
|
23
23
|
|
|
24
|
+
from src.analyzers.ast_utils import build_parent_map
|
|
24
25
|
|
|
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:
|
|
26
105
|
"""Analyzes Python AST to find print() calls."""
|
|
27
106
|
|
|
28
107
|
def __init__(self) -> None:
|
|
@@ -40,24 +119,10 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
|
|
|
40
119
|
List of tuples (node, parent, line_number)
|
|
41
120
|
"""
|
|
42
121
|
self.print_calls = []
|
|
43
|
-
self.parent_map =
|
|
44
|
-
self._build_parent_map(tree)
|
|
122
|
+
self.parent_map = build_parent_map(tree)
|
|
45
123
|
self._collect_print_calls(tree)
|
|
46
124
|
return self.print_calls
|
|
47
125
|
|
|
48
|
-
def _build_parent_map(self, node: ast.AST, parent: ast.AST | None = None) -> None:
|
|
49
|
-
"""Build a map of nodes to their parents.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
node: Current AST node
|
|
53
|
-
parent: Parent of current node
|
|
54
|
-
"""
|
|
55
|
-
if parent is not None:
|
|
56
|
-
self.parent_map[node] = parent
|
|
57
|
-
|
|
58
|
-
for child in ast.iter_child_nodes(node):
|
|
59
|
-
self._build_parent_map(child, node)
|
|
60
|
-
|
|
61
126
|
def _collect_print_calls(self, tree: ast.AST) -> None:
|
|
62
127
|
"""Walk tree and collect all print() calls.
|
|
63
128
|
|
|
@@ -65,34 +130,11 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
|
|
|
65
130
|
tree: AST to traverse
|
|
66
131
|
"""
|
|
67
132
|
for node in ast.walk(tree):
|
|
68
|
-
if isinstance(node, ast.Call) and
|
|
133
|
+
if isinstance(node, ast.Call) and is_print_call(node):
|
|
69
134
|
parent = self.parent_map.get(node)
|
|
70
135
|
line_number = node.lineno if hasattr(node, "lineno") else 0
|
|
71
136
|
self.print_calls.append((node, parent, line_number))
|
|
72
137
|
|
|
73
|
-
def _is_print_call(self, node: ast.Call) -> bool:
|
|
74
|
-
"""Check if a Call node is calling print().
|
|
75
|
-
|
|
76
|
-
Args:
|
|
77
|
-
node: The Call node to check
|
|
78
|
-
|
|
79
|
-
Returns:
|
|
80
|
-
True if this is a print() call
|
|
81
|
-
"""
|
|
82
|
-
return self._is_simple_print(node) or self._is_builtins_print(node)
|
|
83
|
-
|
|
84
|
-
def _is_simple_print(self, node: ast.Call) -> bool:
|
|
85
|
-
"""Check for simple print() call."""
|
|
86
|
-
return isinstance(node.func, ast.Name) and node.func.id == "print"
|
|
87
|
-
|
|
88
|
-
def _is_builtins_print(self, node: ast.Call) -> bool:
|
|
89
|
-
"""Check for builtins.print() call."""
|
|
90
|
-
if not isinstance(node.func, ast.Attribute):
|
|
91
|
-
return False
|
|
92
|
-
if node.func.attr != "print":
|
|
93
|
-
return False
|
|
94
|
-
return isinstance(node.func.value, ast.Name) and node.func.value.id == "builtins"
|
|
95
|
-
|
|
96
138
|
def is_in_main_block(self, node: ast.AST) -> bool:
|
|
97
139
|
"""Check if node is within `if __name__ == "__main__":` block.
|
|
98
140
|
|
|
@@ -105,45 +147,7 @@ class PythonPrintStatementAnalyzer: # thailint: ignore[srp]
|
|
|
105
147
|
current = node
|
|
106
148
|
while current in self.parent_map:
|
|
107
149
|
parent = self.parent_map[current]
|
|
108
|
-
if
|
|
150
|
+
if is_main_if_block(parent):
|
|
109
151
|
return True
|
|
110
152
|
current = parent
|
|
111
153
|
return False
|
|
112
|
-
|
|
113
|
-
def _is_main_if_block(self, node: ast.AST) -> bool:
|
|
114
|
-
"""Check if node is an `if __name__ == "__main__":` statement.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
node: AST node to check
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
True if this is a __main__ if block
|
|
121
|
-
"""
|
|
122
|
-
if not isinstance(node, ast.If):
|
|
123
|
-
return False
|
|
124
|
-
if not isinstance(node.test, ast.Compare):
|
|
125
|
-
return False
|
|
126
|
-
return self._is_main_comparison(node.test)
|
|
127
|
-
|
|
128
|
-
def _is_main_comparison(self, test: ast.Compare) -> bool:
|
|
129
|
-
"""Check if comparison is __name__ == '__main__'."""
|
|
130
|
-
if not self._is_name_identifier(test.left):
|
|
131
|
-
return False
|
|
132
|
-
if not self._has_single_eq_operator(test):
|
|
133
|
-
return False
|
|
134
|
-
return self._compares_to_main(test)
|
|
135
|
-
|
|
136
|
-
def _is_name_identifier(self, node: ast.expr) -> bool:
|
|
137
|
-
"""Check if node is the __name__ identifier."""
|
|
138
|
-
return isinstance(node, ast.Name) and node.id == "__name__"
|
|
139
|
-
|
|
140
|
-
def _has_single_eq_operator(self, test: ast.Compare) -> bool:
|
|
141
|
-
"""Check if comparison has single == operator."""
|
|
142
|
-
return len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)
|
|
143
|
-
|
|
144
|
-
def _compares_to_main(self, test: ast.Compare) -> bool:
|
|
145
|
-
"""Check if comparison is to '__main__' string."""
|
|
146
|
-
if len(test.comparators) != 1:
|
|
147
|
-
return False
|
|
148
|
-
comparator = test.comparators[0]
|
|
149
|
-
return isinstance(comparator, ast.Constant) and comparator.value == "__main__"
|
|
@@ -18,32 +18,23 @@ Exports: TypeScriptPrintStatementAnalyzer class
|
|
|
18
18
|
Interfaces: find_console_calls(root_node, methods) -> list[tuple[Node, str, int]]
|
|
19
19
|
|
|
20
20
|
Implementation: Tree-sitter node traversal with call_expression and member_expression pattern matching
|
|
21
|
+
|
|
21
22
|
"""
|
|
22
23
|
|
|
23
24
|
import logging
|
|
24
|
-
from typing import Any
|
|
25
25
|
|
|
26
|
-
from src.analyzers.typescript_base import
|
|
26
|
+
from src.analyzers.typescript_base import (
|
|
27
|
+
TREE_SITTER_AVAILABLE,
|
|
28
|
+
Node,
|
|
29
|
+
TypeScriptBaseAnalyzer,
|
|
30
|
+
)
|
|
27
31
|
|
|
28
32
|
logger = logging.getLogger(__name__)
|
|
29
33
|
|
|
30
|
-
# dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
|
|
31
|
-
try:
|
|
32
|
-
from tree_sitter import Node
|
|
33
|
-
|
|
34
|
-
TREE_SITTER_AVAILABLE = True
|
|
35
|
-
except ImportError:
|
|
36
|
-
TREE_SITTER_AVAILABLE = False
|
|
37
|
-
Node = Any # type: ignore
|
|
38
|
-
|
|
39
34
|
|
|
40
35
|
class TypeScriptPrintStatementAnalyzer(TypeScriptBaseAnalyzer):
|
|
41
36
|
"""Analyzes TypeScript/JavaScript code for console.* calls using Tree-sitter."""
|
|
42
37
|
|
|
43
|
-
def __init__(self) -> None: # pylint: disable=useless-parent-delegation
|
|
44
|
-
"""Initialize the TypeScript print statement analyzer."""
|
|
45
|
-
super().__init__() # Sets self.tree_sitter_available from base class
|
|
46
|
-
|
|
47
38
|
def find_console_calls(self, root_node: Node, methods: set[str]) -> list[tuple[Node, str, int]]:
|
|
48
39
|
"""Find all console.* calls matching the specified methods.
|
|
49
40
|
|
src/linters/srp/heuristics.py
CHANGED
|
@@ -84,7 +84,7 @@ def has_property_decorator(func_node: ast.FunctionDef | ast.AsyncFunctionDef) ->
|
|
|
84
84
|
Returns:
|
|
85
85
|
True if function has @property decorator
|
|
86
86
|
"""
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
return any(
|
|
88
|
+
isinstance(decorator, ast.Name) and decorator.id == "property"
|
|
89
|
+
for decorator in func_node.decorator_list
|
|
90
|
+
)
|
src/linters/srp/linter.py
CHANGED
|
@@ -16,9 +16,13 @@ Exports: SRPRule class
|
|
|
16
16
|
Interfaces: SRPRule.check(context) -> list[Violation], properties for rule metadata
|
|
17
17
|
|
|
18
18
|
Implementation: Composition pattern with helper classes, heuristic-based SRP analysis
|
|
19
|
+
|
|
20
|
+
Suppressions:
|
|
21
|
+
- type:ignore[return-value]: Generic TypeScript analyzer return type variance
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
25
|
+
from src.core.constants import Language
|
|
22
26
|
from src.core.linter_utils import load_linter_config
|
|
23
27
|
from src.core.types import Violation
|
|
24
28
|
from src.linter_config.ignore import get_ignore_parser
|
|
@@ -100,10 +104,10 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
100
104
|
Returns:
|
|
101
105
|
List of violations found
|
|
102
106
|
"""
|
|
103
|
-
if context.language ==
|
|
107
|
+
if context.language == Language.PYTHON:
|
|
104
108
|
return self._check_python(context, config)
|
|
105
109
|
|
|
106
|
-
if context.language in (
|
|
110
|
+
if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
|
|
107
111
|
return self._check_typescript(context, config)
|
|
108
112
|
|
|
109
113
|
return []
|
|
@@ -133,10 +137,7 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
133
137
|
return False
|
|
134
138
|
|
|
135
139
|
file_path = str(context.file_path)
|
|
136
|
-
for pattern in config.ignore
|
|
137
|
-
if pattern in file_path:
|
|
138
|
-
return True
|
|
139
|
-
return False
|
|
140
|
+
return any(pattern in file_path for pattern in config.ignore)
|
|
140
141
|
|
|
141
142
|
def _check_python(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
142
143
|
"""Check Python code for SRP violations.
|
|
@@ -170,13 +171,12 @@ class SRPRule(MultiLanguageLintRule):
|
|
|
170
171
|
Returns:
|
|
171
172
|
List of violations
|
|
172
173
|
"""
|
|
173
|
-
violations = []
|
|
174
174
|
valid_metrics = (m for m in metrics_list if isinstance(m, dict))
|
|
175
|
-
|
|
176
|
-
violation
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
175
|
+
return [
|
|
176
|
+
violation
|
|
177
|
+
for metrics in valid_metrics
|
|
178
|
+
if (violation := self._create_violation_if_needed(metrics, config, context))
|
|
179
|
+
]
|
|
180
180
|
|
|
181
181
|
def _create_violation_if_needed(
|
|
182
182
|
self,
|
|
@@ -29,10 +29,6 @@ from src.core.violation_builder import BaseViolationBuilder, ViolationInfo
|
|
|
29
29
|
class ViolationBuilder(BaseViolationBuilder):
|
|
30
30
|
"""Builds SRP violations with messages and suggestions."""
|
|
31
31
|
|
|
32
|
-
def __init__(self) -> None: # pylint: disable=useless-parent-delegation
|
|
33
|
-
"""Initialize the violation builder."""
|
|
34
|
-
super().__init__() # Inherits from BaseViolationBuilder
|
|
35
|
-
|
|
36
32
|
def build_violation(
|
|
37
33
|
self,
|
|
38
34
|
metrics: dict[str, Any],
|