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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration dataclass for collection-pipeline linter
|
|
3
|
+
|
|
4
|
+
Scope: Define configurable options for embedded filtering pattern detection
|
|
5
|
+
|
|
6
|
+
Overview: Provides CollectionPipelineConfig for customizing linter behavior including
|
|
7
|
+
minimum number of continue patterns to flag, enable/disable toggle, and ignore
|
|
8
|
+
patterns. Integrates with the orchestrator's configuration system to allow users
|
|
9
|
+
to customize collection-pipeline detection via .thailint.yaml configuration files.
|
|
10
|
+
Follows the same configuration pattern as other thai-lint linters.
|
|
11
|
+
|
|
12
|
+
Dependencies: dataclasses, typing
|
|
13
|
+
|
|
14
|
+
Exports: CollectionPipelineConfig dataclass, DEFAULT_MIN_CONTINUES constant
|
|
15
|
+
|
|
16
|
+
Interfaces: CollectionPipelineConfig.from_dict() class method for configuration loading
|
|
17
|
+
|
|
18
|
+
Implementation: Dataclass with sensible defaults and config loading from dictionary
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
# Default threshold for minimum continue guards to flag
|
|
25
|
+
DEFAULT_MIN_CONTINUES = 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class CollectionPipelineConfig:
|
|
30
|
+
"""Configuration for collection-pipeline linter."""
|
|
31
|
+
|
|
32
|
+
enabled: bool = True
|
|
33
|
+
"""Whether the linter is enabled."""
|
|
34
|
+
|
|
35
|
+
min_continues: int = DEFAULT_MIN_CONTINUES
|
|
36
|
+
"""Minimum number of if/continue patterns required to flag a violation."""
|
|
37
|
+
|
|
38
|
+
ignore: list[str] = field(default_factory=list)
|
|
39
|
+
"""File patterns to ignore."""
|
|
40
|
+
|
|
41
|
+
detect_any_all: bool = True
|
|
42
|
+
"""Whether to detect any()/all() pattern anti-patterns."""
|
|
43
|
+
|
|
44
|
+
detect_filter_map: bool = True
|
|
45
|
+
"""Whether to detect filter-map and takewhile pattern anti-patterns."""
|
|
46
|
+
|
|
47
|
+
use_walrus_operator: bool = True
|
|
48
|
+
"""Whether to suggest walrus operator (:=) in filter-map suggestions (Python 3.8+)."""
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
"""Validate configuration values."""
|
|
52
|
+
if self.min_continues < 1:
|
|
53
|
+
raise ValueError(f"min_continues must be at least 1, got {self.min_continues}")
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_dict(
|
|
57
|
+
cls, config: dict[str, Any], language: str | None = None
|
|
58
|
+
) -> "CollectionPipelineConfig":
|
|
59
|
+
"""Load configuration from dictionary.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
config: Dictionary containing configuration values
|
|
63
|
+
language: Programming language (unused, for interface compatibility)
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
CollectionPipelineConfig instance with values from dictionary
|
|
67
|
+
"""
|
|
68
|
+
return cls(
|
|
69
|
+
enabled=config.get("enabled", True),
|
|
70
|
+
min_continues=config.get("min_continues", DEFAULT_MIN_CONTINUES),
|
|
71
|
+
ignore=config.get("ignore", []),
|
|
72
|
+
detect_any_all=config.get("detect_any_all", True),
|
|
73
|
+
detect_filter_map=config.get("detect_filter_map", True),
|
|
74
|
+
use_walrus_operator=config.get("use_walrus_operator", True),
|
|
75
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Analyze continue guard patterns in for loops
|
|
3
|
+
|
|
4
|
+
Scope: Extract and validate if/continue patterns from loop bodies
|
|
5
|
+
|
|
6
|
+
Overview: Provides helper functions for analyzing continue guard patterns in for loop
|
|
7
|
+
bodies. Handles extraction of sequential if/continue statements, validation of
|
|
8
|
+
simple continue-only patterns, and detection of side effects in conditions.
|
|
9
|
+
Separates pattern analysis logic from main detection for better maintainability.
|
|
10
|
+
|
|
11
|
+
Dependencies: ast module for Python AST processing
|
|
12
|
+
|
|
13
|
+
Exports: extract_continue_patterns, is_continue_only, has_side_effects, has_body_after_continues
|
|
14
|
+
|
|
15
|
+
Interfaces: Functions for analyzing continue patterns in AST structures
|
|
16
|
+
|
|
17
|
+
Implementation: AST-based pattern matching for continue guard identification
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import ast
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def extract_continue_patterns(body: list[ast.stmt]) -> list[ast.If]:
|
|
24
|
+
"""Extract leading if statements that only contain continue.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
body: List of statements in for loop body
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of ast.If nodes that are continue guards
|
|
31
|
+
"""
|
|
32
|
+
continues: list[ast.If] = []
|
|
33
|
+
for stmt in body:
|
|
34
|
+
if not isinstance(stmt, ast.If):
|
|
35
|
+
break
|
|
36
|
+
if not is_continue_only(stmt):
|
|
37
|
+
break
|
|
38
|
+
continues.append(stmt)
|
|
39
|
+
return continues
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_continue_only(if_node: ast.If) -> bool:
|
|
43
|
+
"""Check if an if statement only contains continue.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
if_node: AST If node to check
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True if the if statement is a simple continue guard
|
|
50
|
+
"""
|
|
51
|
+
if len(if_node.body) != 1:
|
|
52
|
+
return False
|
|
53
|
+
if not isinstance(if_node.body[0], ast.Continue):
|
|
54
|
+
return False
|
|
55
|
+
if if_node.orelse:
|
|
56
|
+
return False
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def has_side_effects(continues: list[ast.If]) -> bool:
|
|
61
|
+
"""Check if any condition has side effects.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
continues: List of continue guard if statements
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if any condition has side effects (e.g., walrus operator)
|
|
68
|
+
"""
|
|
69
|
+
return any(_condition_has_side_effects(if_node.test) for if_node in continues)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _condition_has_side_effects(node: ast.expr) -> bool:
|
|
73
|
+
"""Check if expression has side effects.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
node: AST expression node to check
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True if expression has side effects
|
|
80
|
+
"""
|
|
81
|
+
return any(isinstance(child, ast.NamedExpr) for child in ast.walk(node))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def has_body_after_continues(body: list[ast.stmt], num_continues: int) -> bool:
|
|
85
|
+
"""Check if there are statements after continue guards.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
body: List of statements in for loop body
|
|
89
|
+
num_continues: Number of continue guards detected
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True if there are statements after the continue guards
|
|
93
|
+
"""
|
|
94
|
+
return len(body) > num_continues
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: AST-based detection of collection pipeline anti-patterns
|
|
3
|
+
|
|
4
|
+
Scope: Pattern matching for for loops with embedded filtering via if/continue
|
|
5
|
+
|
|
6
|
+
Overview: Implements the core detection logic for identifying imperative loop patterns
|
|
7
|
+
that use if/continue for filtering instead of collection pipelines. Uses Python's
|
|
8
|
+
AST module to analyze code structure and identify refactoring opportunities. Detects
|
|
9
|
+
patterns like 'for x in iter: if not cond: continue; action(x)' and suggests
|
|
10
|
+
refactoring to generator expressions or filter(). Handles edge cases like walrus
|
|
11
|
+
operators (side effects), else branches, and empty loop bodies.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module, continue_analyzer, suggestion_builder
|
|
14
|
+
|
|
15
|
+
Exports: PipelinePatternDetector class, PatternMatch dataclass, PatternType enum
|
|
16
|
+
|
|
17
|
+
Interfaces: PipelinePatternDetector.detect_patterns() -> list[PatternMatch]
|
|
18
|
+
|
|
19
|
+
Implementation: AST visitor pattern with delegated pattern matching and suggestion generation
|
|
20
|
+
|
|
21
|
+
Suppressions:
|
|
22
|
+
- invalid-name: AST NodeVisitor visit_* methods follow convention, not PEP8
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import ast
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from enum import Enum
|
|
28
|
+
|
|
29
|
+
from . import any_all_analyzer, continue_analyzer, filter_map_analyzer, suggestion_builder
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PatternType(Enum):
|
|
33
|
+
"""Type of collection pipeline anti-pattern detected."""
|
|
34
|
+
|
|
35
|
+
EMBEDDED_FILTER = "embedded-filter"
|
|
36
|
+
"""for x: if not cond: continue; action(x) -> generator expression"""
|
|
37
|
+
|
|
38
|
+
ANY_PATTERN = "any-pattern"
|
|
39
|
+
"""for x: if cond: return True; return False -> any()"""
|
|
40
|
+
|
|
41
|
+
ALL_PATTERN = "all-pattern"
|
|
42
|
+
"""for x: if not cond: return False; return True -> all()"""
|
|
43
|
+
|
|
44
|
+
FILTER_MAP = "filter-map"
|
|
45
|
+
"""result=[]; for x: y=f(x); if y: result.append(y) -> list comprehension"""
|
|
46
|
+
|
|
47
|
+
TAKEWHILE = "takewhile"
|
|
48
|
+
"""result=[]; for x: if not cond: break; result.append(x) -> takewhile()"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class PatternMatch:
|
|
53
|
+
"""Represents a detected anti-pattern."""
|
|
54
|
+
|
|
55
|
+
line_number: int
|
|
56
|
+
"""Line number where the for loop starts (1-indexed)."""
|
|
57
|
+
|
|
58
|
+
loop_var: str
|
|
59
|
+
"""Name of the loop variable."""
|
|
60
|
+
|
|
61
|
+
iterable: str
|
|
62
|
+
"""Source representation of the iterable."""
|
|
63
|
+
|
|
64
|
+
conditions: list[str]
|
|
65
|
+
"""List of filter conditions (inverted from continue guards)."""
|
|
66
|
+
|
|
67
|
+
has_side_effects: bool
|
|
68
|
+
"""Whether any condition has side effects."""
|
|
69
|
+
|
|
70
|
+
suggestion: str
|
|
71
|
+
"""Refactoring suggestion as a code snippet."""
|
|
72
|
+
|
|
73
|
+
pattern_type: PatternType = field(default=PatternType.EMBEDDED_FILTER)
|
|
74
|
+
"""Type of anti-pattern detected (default: EMBEDDED_FILTER for backward compat)."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Module-level pattern match factory functions (extracted from class to reduce SRP violations)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_any_match(match: any_all_analyzer.AnyAllMatch) -> PatternMatch:
|
|
81
|
+
"""Create PatternMatch for any() pattern.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
match: AnyAllMatch from analyzer
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
PatternMatch for the any() pattern
|
|
88
|
+
"""
|
|
89
|
+
loop_var = suggestion_builder.get_target_name(match.for_node.target)
|
|
90
|
+
iterable = ast.unparse(match.for_node.iter)
|
|
91
|
+
condition = ast.unparse(match.condition)
|
|
92
|
+
suggestion = suggestion_builder.build_any_suggestion(loop_var, iterable, condition)
|
|
93
|
+
|
|
94
|
+
return PatternMatch(
|
|
95
|
+
line_number=match.for_node.lineno,
|
|
96
|
+
loop_var=loop_var,
|
|
97
|
+
iterable=iterable,
|
|
98
|
+
conditions=[condition],
|
|
99
|
+
has_side_effects=False,
|
|
100
|
+
suggestion=suggestion,
|
|
101
|
+
pattern_type=PatternType.ANY_PATTERN,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def create_all_match(match: any_all_analyzer.AnyAllMatch) -> PatternMatch:
|
|
106
|
+
"""Create PatternMatch for all() pattern.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
match: AnyAllMatch from analyzer
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
PatternMatch for the all() pattern
|
|
113
|
+
"""
|
|
114
|
+
loop_var = suggestion_builder.get_target_name(match.for_node.target)
|
|
115
|
+
iterable = ast.unparse(match.for_node.iter)
|
|
116
|
+
condition = ast.unparse(match.condition)
|
|
117
|
+
suggestion = suggestion_builder.build_all_suggestion(loop_var, iterable, condition)
|
|
118
|
+
|
|
119
|
+
return PatternMatch(
|
|
120
|
+
line_number=match.for_node.lineno,
|
|
121
|
+
loop_var=loop_var,
|
|
122
|
+
iterable=iterable,
|
|
123
|
+
conditions=[condition],
|
|
124
|
+
has_side_effects=False,
|
|
125
|
+
suggestion=suggestion,
|
|
126
|
+
pattern_type=PatternType.ALL_PATTERN,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def create_filter_map_match(match: filter_map_analyzer.FilterMapMatch) -> PatternMatch:
|
|
131
|
+
"""Create PatternMatch for filter-map pattern.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
match: FilterMapMatch from analyzer
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
PatternMatch for the filter-map pattern
|
|
138
|
+
"""
|
|
139
|
+
loop_var = suggestion_builder.get_target_name(match.for_node.target)
|
|
140
|
+
iterable = ast.unparse(match.for_node.iter)
|
|
141
|
+
suggestion = suggestion_builder.build_filter_map_suggestion(
|
|
142
|
+
loop_var, iterable, match.transform_var, match.transform_expr
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return PatternMatch(
|
|
146
|
+
line_number=match.for_node.lineno,
|
|
147
|
+
loop_var=loop_var,
|
|
148
|
+
iterable=iterable,
|
|
149
|
+
conditions=[match.transform_expr],
|
|
150
|
+
has_side_effects=False,
|
|
151
|
+
suggestion=suggestion,
|
|
152
|
+
pattern_type=PatternType.FILTER_MAP,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def create_takewhile_match(match: filter_map_analyzer.TakewhileMatch) -> PatternMatch:
|
|
157
|
+
"""Create PatternMatch for takewhile pattern.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
match: TakewhileMatch from analyzer
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
PatternMatch for the takewhile pattern
|
|
164
|
+
"""
|
|
165
|
+
loop_var = suggestion_builder.get_target_name(match.for_node.target)
|
|
166
|
+
iterable = ast.unparse(match.for_node.iter)
|
|
167
|
+
condition = ast.unparse(match.condition)
|
|
168
|
+
suggestion = suggestion_builder.build_takewhile_suggestion(loop_var, iterable, condition)
|
|
169
|
+
|
|
170
|
+
return PatternMatch(
|
|
171
|
+
line_number=match.for_node.lineno,
|
|
172
|
+
loop_var=loop_var,
|
|
173
|
+
iterable=iterable,
|
|
174
|
+
conditions=[condition],
|
|
175
|
+
has_side_effects=False,
|
|
176
|
+
suggestion=suggestion,
|
|
177
|
+
pattern_type=PatternType.TAKEWHILE,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def create_embedded_filter_match(for_node: ast.For, continues: list[ast.If]) -> PatternMatch:
|
|
182
|
+
"""Create a PatternMatch for embedded filter pattern.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
for_node: AST For node
|
|
186
|
+
continues: List of continue guard if statements
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
PatternMatch object with detection information
|
|
190
|
+
"""
|
|
191
|
+
loop_var = suggestion_builder.get_target_name(for_node.target)
|
|
192
|
+
iterable = ast.unparse(for_node.iter)
|
|
193
|
+
conditions = [suggestion_builder.invert_condition(c.test) for c in continues]
|
|
194
|
+
suggestion = suggestion_builder.build_suggestion(loop_var, iterable, conditions)
|
|
195
|
+
|
|
196
|
+
return PatternMatch(
|
|
197
|
+
line_number=for_node.lineno,
|
|
198
|
+
loop_var=loop_var,
|
|
199
|
+
iterable=iterable,
|
|
200
|
+
conditions=conditions,
|
|
201
|
+
has_side_effects=False,
|
|
202
|
+
suggestion=suggestion,
|
|
203
|
+
pattern_type=PatternType.EMBEDDED_FILTER,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def analyze_for_loop(node: ast.For) -> PatternMatch | None:
|
|
208
|
+
"""Analyze a for loop for embedded filtering patterns.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
node: AST For node to analyze
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
PatternMatch if pattern detected, None otherwise
|
|
215
|
+
"""
|
|
216
|
+
continues = continue_analyzer.extract_continue_patterns(node.body)
|
|
217
|
+
if not continues:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
if continue_analyzer.has_side_effects(continues):
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
if not continue_analyzer.has_body_after_continues(node.body, len(continues)):
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
return create_embedded_filter_match(node, continues)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class PipelinePatternDetector(ast.NodeVisitor):
|
|
230
|
+
"""Detects for loops with embedded filtering via if/continue patterns."""
|
|
231
|
+
|
|
232
|
+
def __init__(self, source_code: str) -> None:
|
|
233
|
+
"""Initialize detector with source code.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
source_code: Python source code to analyze
|
|
237
|
+
"""
|
|
238
|
+
self.source_code = source_code
|
|
239
|
+
self.matches: list[PatternMatch] = []
|
|
240
|
+
self._func_body_stack: list[list[ast.stmt]] = []
|
|
241
|
+
|
|
242
|
+
def detect_patterns(self) -> list[PatternMatch]:
|
|
243
|
+
"""Analyze source code and return detected patterns.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of PatternMatch objects for each detected anti-pattern
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
tree = ast.parse(self.source_code)
|
|
250
|
+
self.visit(tree)
|
|
251
|
+
except SyntaxError:
|
|
252
|
+
pass # Invalid Python, return empty list
|
|
253
|
+
return self.matches
|
|
254
|
+
|
|
255
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # pylint: disable=invalid-name
|
|
256
|
+
"""Visit function and track body for any/all pattern detection.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
node: AST FunctionDef node
|
|
260
|
+
"""
|
|
261
|
+
self._func_body_stack.append(node.body)
|
|
262
|
+
self.generic_visit(node)
|
|
263
|
+
self._func_body_stack.pop()
|
|
264
|
+
|
|
265
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: # pylint: disable=invalid-name
|
|
266
|
+
"""Visit async function and track body for any/all pattern detection.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
node: AST AsyncFunctionDef node
|
|
270
|
+
"""
|
|
271
|
+
self._func_body_stack.append(node.body)
|
|
272
|
+
self.generic_visit(node)
|
|
273
|
+
self._func_body_stack.pop()
|
|
274
|
+
|
|
275
|
+
def visit_For(self, node: ast.For) -> None: # pylint: disable=invalid-name
|
|
276
|
+
"""Visit for loop and check for filtering patterns.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
node: AST For node to analyze
|
|
280
|
+
"""
|
|
281
|
+
match = self._find_pattern_match(node)
|
|
282
|
+
if match is not None:
|
|
283
|
+
self.matches.append(match)
|
|
284
|
+
self.generic_visit(node)
|
|
285
|
+
|
|
286
|
+
def _find_pattern_match(self, node: ast.For) -> PatternMatch | None:
|
|
287
|
+
"""Find the first matching anti-pattern for a for loop.
|
|
288
|
+
|
|
289
|
+
Checks patterns in priority order: any/all, filter-map/takewhile, embedded filter.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
node: AST For node to analyze
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
PatternMatch if any pattern detected, None otherwise
|
|
296
|
+
"""
|
|
297
|
+
# Check for any/all patterns (requires function context)
|
|
298
|
+
any_all_match = self._analyze_any_all_pattern(node)
|
|
299
|
+
if any_all_match is not None:
|
|
300
|
+
return any_all_match
|
|
301
|
+
|
|
302
|
+
# Check for filter-map/takewhile patterns
|
|
303
|
+
filter_map_match = self._analyze_filter_map_pattern(node)
|
|
304
|
+
if filter_map_match is not None:
|
|
305
|
+
return filter_map_match
|
|
306
|
+
|
|
307
|
+
# Check for embedded filter patterns
|
|
308
|
+
return analyze_for_loop(node)
|
|
309
|
+
|
|
310
|
+
def _analyze_any_all_pattern(self, node: ast.For) -> PatternMatch | None:
|
|
311
|
+
"""Analyze a for loop for any()/all() patterns.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
node: AST For node to analyze
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
PatternMatch if any/all pattern detected, None otherwise
|
|
318
|
+
"""
|
|
319
|
+
if not self._func_body_stack:
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
func_body = self._func_body_stack[-1]
|
|
323
|
+
|
|
324
|
+
# Try any() pattern first
|
|
325
|
+
any_match = any_all_analyzer.extract_any_pattern(func_body, node)
|
|
326
|
+
if any_match is not None:
|
|
327
|
+
return create_any_match(any_match)
|
|
328
|
+
|
|
329
|
+
# Try all() pattern
|
|
330
|
+
all_match = any_all_analyzer.extract_all_pattern(func_body, node)
|
|
331
|
+
if all_match is not None:
|
|
332
|
+
return create_all_match(all_match)
|
|
333
|
+
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
def _analyze_filter_map_pattern(self, node: ast.For) -> PatternMatch | None:
|
|
337
|
+
"""Analyze a for loop for filter-map/takewhile patterns.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
node: AST For node to analyze
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
PatternMatch if filter-map/takewhile pattern detected, None otherwise
|
|
344
|
+
"""
|
|
345
|
+
if not self._func_body_stack:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
func_body = self._func_body_stack[-1]
|
|
349
|
+
|
|
350
|
+
# Try filter-map pattern first
|
|
351
|
+
fm_match = filter_map_analyzer.extract_filter_map_pattern(func_body, node)
|
|
352
|
+
if fm_match is not None:
|
|
353
|
+
return create_filter_map_match(fm_match)
|
|
354
|
+
|
|
355
|
+
# Try takewhile pattern
|
|
356
|
+
tw_match = filter_map_analyzer.extract_takewhile_pattern(func_body, node)
|
|
357
|
+
if tw_match is not None:
|
|
358
|
+
return create_takewhile_match(tw_match)
|
|
359
|
+
|
|
360
|
+
return None
|