thailint 0.11.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 +118 -7
- src/cli/linters/documentation.py +3 -0
- src/cli/linters/structure.py +3 -0
- src/cli/linters/structure_quality.py +3 -0
- src/cli/utils.py +29 -9
- 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/__init__.py +22 -9
- src/linters/stringly_typed/config.py +32 -8
- 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 +102 -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 +9 -5
- src/linters/stringly_typed/python/analyzer.py +159 -9
- 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 +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 +630 -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 +405 -0
- 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.11.0.dist-info/METADATA +0 -1661
- thailint-0.11.0.dist-info/RECORD +0 -150
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Pattern matching utilities for file paths and content parsing
|
|
3
|
+
|
|
4
|
+
Scope: Gitignore-style pattern matching and content parsing
|
|
5
|
+
|
|
6
|
+
Overview: Provides utility functions for matching file paths against gitignore-style
|
|
7
|
+
patterns and extracting patterns from configuration files. Supports directory
|
|
8
|
+
patterns (trailing /), standard glob patterns via fnmatch, and comment filtering.
|
|
9
|
+
|
|
10
|
+
Dependencies: fnmatch for glob pattern matching, pathlib for path operations
|
|
11
|
+
|
|
12
|
+
Exports: matches_pattern, extract_patterns_from_content
|
|
13
|
+
|
|
14
|
+
Interfaces: matches_pattern(path, pattern) -> bool, extract_patterns_from_content(content) -> list
|
|
15
|
+
|
|
16
|
+
Implementation: fnmatch-based pattern matching with directory-aware logic
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import fnmatch
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def matches_pattern(path: str, pattern: str) -> bool:
|
|
24
|
+
"""Check if path matches gitignore-style pattern.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
path: File path to check.
|
|
28
|
+
pattern: Gitignore-style pattern.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if path matches pattern.
|
|
32
|
+
"""
|
|
33
|
+
if pattern.endswith("/"):
|
|
34
|
+
return _matches_directory_pattern(path, pattern)
|
|
35
|
+
return fnmatch.fnmatch(path, pattern) or fnmatch.fnmatch(str(Path(path)), pattern)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _matches_directory_pattern(path: str, pattern: str) -> bool:
|
|
39
|
+
"""Check if path matches a directory pattern (trailing /).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
path: File path to check
|
|
43
|
+
pattern: Directory pattern ending with /
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if path is within the directory
|
|
47
|
+
"""
|
|
48
|
+
dir_pattern = pattern.rstrip("/")
|
|
49
|
+
path_parts = Path(path).parts
|
|
50
|
+
if dir_pattern in path_parts:
|
|
51
|
+
return True
|
|
52
|
+
return fnmatch.fnmatch(path, dir_pattern + "*")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def extract_patterns_from_content(content: str) -> list[str]:
|
|
56
|
+
"""Extract non-empty, non-comment patterns from content.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
content: File content with patterns (one per line)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
List of valid patterns (non-empty, non-comment lines)
|
|
63
|
+
"""
|
|
64
|
+
lines = [line.strip() for line in content.splitlines()]
|
|
65
|
+
return [line for line in lines if line and not line.startswith("#")]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Rule ID matching utilities for ignore directive processing
|
|
3
|
+
|
|
4
|
+
Scope: Pattern matching between rule IDs and ignore patterns
|
|
5
|
+
|
|
6
|
+
Overview: Provides functions for matching rule IDs against ignore patterns. Supports
|
|
7
|
+
exact matching, wildcard matching (*.suffix), and prefix matching (category matches
|
|
8
|
+
category.specific). All comparisons are case-insensitive to handle variations in
|
|
9
|
+
rule ID formatting.
|
|
10
|
+
|
|
11
|
+
Dependencies: re for regex operations
|
|
12
|
+
|
|
13
|
+
Exports: rule_matches, check_bracket_rules, check_space_separated_rules
|
|
14
|
+
|
|
15
|
+
Interfaces: rule_matches(rule_id, pattern) -> bool for checking if rule matches pattern
|
|
16
|
+
|
|
17
|
+
Implementation: String-based pattern matching with wildcard and prefix support
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def rule_matches(rule_id: str, pattern: str) -> bool:
|
|
24
|
+
"""Check if rule ID matches pattern (supports wildcards and prefixes).
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
rule_id: Rule ID to check (e.g., "nesting.excessive-depth").
|
|
28
|
+
pattern: Pattern with optional wildcard (e.g., "nesting.*" or "nesting").
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if rule matches pattern.
|
|
32
|
+
"""
|
|
33
|
+
rule_id_lower = rule_id.lower()
|
|
34
|
+
pattern_lower = pattern.lower()
|
|
35
|
+
|
|
36
|
+
if pattern_lower.endswith("*"):
|
|
37
|
+
prefix = pattern_lower[:-1]
|
|
38
|
+
return rule_id_lower.startswith(prefix)
|
|
39
|
+
|
|
40
|
+
if rule_id_lower == pattern_lower:
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
if rule_id_lower.startswith(pattern_lower + "."):
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_bracket_rules(rules_text: str, rule_id: str) -> bool:
|
|
50
|
+
"""Check if bracketed rules match the rule ID.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
rules_text: Comma-separated rule patterns from bracket syntax
|
|
54
|
+
rule_id: Rule ID to match against
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if any pattern matches the rule ID
|
|
58
|
+
"""
|
|
59
|
+
ignored_rules = [r.strip() for r in rules_text.split(",")]
|
|
60
|
+
return any(rule_matches(rule_id, r) for r in ignored_rules)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_space_separated_rules(rules_text: str, rule_id: str) -> bool:
|
|
64
|
+
"""Check if space-separated rules match the rule ID.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
rules_text: Space or comma-separated rule patterns
|
|
68
|
+
rule_id: Rule ID to match against
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if any pattern matches the rule ID
|
|
72
|
+
"""
|
|
73
|
+
ignored_rules = [r.strip() for r in re.split(r"[,\s]+", rules_text) if r.strip()]
|
|
74
|
+
return any(rule_matches(rule_id, r) for r in ignored_rules)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def rules_match_violation(ignored_rules: set[str], rule_id: str) -> bool:
|
|
78
|
+
"""Check if any of the ignored rules match the violation rule ID.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
ignored_rules: Set of rule patterns to check
|
|
82
|
+
rule_id: Rule ID of the violation
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if any pattern matches (or if wildcard "*" is present)
|
|
86
|
+
"""
|
|
87
|
+
if "*" in ignored_rules:
|
|
88
|
+
return True
|
|
89
|
+
return any(rule_matches(rule_id, pattern) for pattern in ignored_rules)
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Analyze any()/all() anti-patterns in for loops
|
|
3
|
+
|
|
4
|
+
Scope: Extract and validate loops that return True/False and could use any()/all()
|
|
5
|
+
|
|
6
|
+
Overview: Provides helper functions for analyzing loops that iterate and return boolean
|
|
7
|
+
values based on conditions. Detects patterns like 'for x in iter: if cond: return True;
|
|
8
|
+
return False' which can be refactored to 'return any(cond for x in iter)'. Also handles
|
|
9
|
+
the inverse all() pattern. Requires function context to analyze return statements.
|
|
10
|
+
|
|
11
|
+
Dependencies: ast module for Python AST processing
|
|
12
|
+
|
|
13
|
+
Exports: extract_any_pattern, extract_all_pattern, AnyAllMatch dataclass
|
|
14
|
+
|
|
15
|
+
Interfaces: Functions for analyzing any/all patterns in AST function bodies
|
|
16
|
+
|
|
17
|
+
Implementation: AST-based pattern matching for any/all pattern identification
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import ast
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import cast
|
|
23
|
+
|
|
24
|
+
from . import ast_utils
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class AnyAllMatch:
|
|
29
|
+
"""Information about a detected any()/all() pattern."""
|
|
30
|
+
|
|
31
|
+
for_node: ast.For
|
|
32
|
+
"""The for loop AST node."""
|
|
33
|
+
|
|
34
|
+
condition: ast.expr
|
|
35
|
+
"""The condition expression inside the if statement."""
|
|
36
|
+
|
|
37
|
+
is_any: bool
|
|
38
|
+
"""True for any() pattern, False for all() pattern."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extract_any_pattern(func_body: list[ast.stmt], for_node: ast.For) -> AnyAllMatch | None:
|
|
42
|
+
"""Extract any() pattern from a for loop in a function body.
|
|
43
|
+
|
|
44
|
+
Pattern: for x in iter: if cond: return True; return False
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
func_body: List of statements in the function body
|
|
48
|
+
for_node: The for loop AST node to analyze
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
AnyAllMatch if pattern detected, None otherwise
|
|
52
|
+
"""
|
|
53
|
+
# Check for/else - different semantics, skip
|
|
54
|
+
if for_node.orelse:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
# Check loop body has exactly one if statement with return True
|
|
58
|
+
if_return_true = _extract_if_return_true(for_node.body)
|
|
59
|
+
if if_return_true is None:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
# Find position of for loop in function body
|
|
63
|
+
for_index = _get_stmt_index(func_body, for_node)
|
|
64
|
+
if for_index is None:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
# Check next statement is return False
|
|
68
|
+
if not _is_next_stmt_return_false(func_body, for_index):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
return AnyAllMatch(
|
|
72
|
+
for_node=for_node,
|
|
73
|
+
condition=if_return_true.test,
|
|
74
|
+
is_any=True,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def extract_all_pattern(func_body: list[ast.stmt], for_node: ast.For) -> AnyAllMatch | None:
|
|
79
|
+
"""Extract all() pattern from a for loop in a function body.
|
|
80
|
+
|
|
81
|
+
Pattern: for x in iter: if not cond: return False; return True
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
func_body: List of statements in the function body
|
|
85
|
+
for_node: The for loop AST node to analyze
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
AnyAllMatch if pattern detected, None otherwise
|
|
89
|
+
"""
|
|
90
|
+
# Check for/else - different semantics, skip
|
|
91
|
+
if for_node.orelse:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
# Check loop body has exactly one if statement with return False
|
|
95
|
+
if_return_false = _extract_if_return_false(for_node.body)
|
|
96
|
+
if if_return_false is None:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
# Find position of for loop in function body
|
|
100
|
+
for_index = _get_stmt_index(func_body, for_node)
|
|
101
|
+
if for_index is None:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
# Check next statement is return True
|
|
105
|
+
if not _is_next_stmt_return_true(func_body, for_index):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
# Invert the condition for all() suggestion
|
|
109
|
+
condition = _invert_condition(if_return_false.test)
|
|
110
|
+
|
|
111
|
+
return AnyAllMatch(
|
|
112
|
+
for_node=for_node,
|
|
113
|
+
condition=condition,
|
|
114
|
+
is_any=False,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _is_simple_if_return(stmt: ast.stmt) -> ast.Return | None:
|
|
119
|
+
"""Check if statement is simple if with single return and no else.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
stmt: Statement to check
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
The return statement if pattern matches, None otherwise
|
|
126
|
+
"""
|
|
127
|
+
if not isinstance(stmt, ast.If):
|
|
128
|
+
return None
|
|
129
|
+
if stmt.orelse:
|
|
130
|
+
return None
|
|
131
|
+
if len(stmt.body) != 1:
|
|
132
|
+
return None
|
|
133
|
+
if not isinstance(stmt.body[0], ast.Return):
|
|
134
|
+
return None
|
|
135
|
+
return stmt.body[0]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _extract_if_return_true(body: list[ast.stmt]) -> ast.If | None:
|
|
139
|
+
"""Extract if statement that only contains return True.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
body: List of statements in for loop body
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
The if statement if pattern matches, None otherwise
|
|
146
|
+
"""
|
|
147
|
+
if len(body) != 1:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
stmt = body[0]
|
|
151
|
+
return_stmt = _is_simple_if_return(stmt)
|
|
152
|
+
if return_stmt is None:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
if not _is_literal_true(return_stmt.value):
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
return cast(ast.If, stmt)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _extract_if_return_false(body: list[ast.stmt]) -> ast.If | None:
|
|
162
|
+
"""Extract if statement that only contains return False.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
body: List of statements in for loop body
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The if statement if pattern matches, None otherwise
|
|
169
|
+
"""
|
|
170
|
+
if len(body) != 1:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
stmt = body[0]
|
|
174
|
+
return_stmt = _is_simple_if_return(stmt)
|
|
175
|
+
if return_stmt is None:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
if not _is_literal_false(return_stmt.value):
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
return cast(ast.If, stmt)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _get_stmt_index(func_body: list[ast.stmt], target: ast.stmt) -> int | None:
|
|
185
|
+
"""Find index of a statement in a function body.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
func_body: List of statements in function body
|
|
189
|
+
target: Statement to find
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Index if found, None otherwise
|
|
193
|
+
"""
|
|
194
|
+
for i, stmt in enumerate(func_body):
|
|
195
|
+
if stmt is target:
|
|
196
|
+
return i
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _is_next_stmt_return_false(func_body: list[ast.stmt], for_index: int) -> bool:
|
|
201
|
+
"""Check if the next statement after for loop is return False.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
func_body: List of statements in function body
|
|
205
|
+
for_index: Index of the for loop
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if next statement is return False
|
|
209
|
+
"""
|
|
210
|
+
stmt = ast_utils.get_next_return_stmt(func_body, for_index)
|
|
211
|
+
if stmt is None:
|
|
212
|
+
return False
|
|
213
|
+
return _is_literal_false(stmt.value)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _is_next_stmt_return_true(func_body: list[ast.stmt], for_index: int) -> bool:
|
|
217
|
+
"""Check if the next statement after for loop is return True.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
func_body: List of statements in function body
|
|
221
|
+
for_index: Index of the for loop
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
True if next statement is return True
|
|
225
|
+
"""
|
|
226
|
+
stmt = ast_utils.get_next_return_stmt(func_body, for_index)
|
|
227
|
+
if stmt is None:
|
|
228
|
+
return False
|
|
229
|
+
return _is_literal_true(stmt.value)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_literal_true(node: ast.expr | None) -> bool:
|
|
233
|
+
"""Check if expression is literal True.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
node: AST expression node
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
True if node is literal True
|
|
240
|
+
"""
|
|
241
|
+
if node is None:
|
|
242
|
+
return False
|
|
243
|
+
if isinstance(node, ast.Constant):
|
|
244
|
+
return node.value is True
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _is_literal_false(node: ast.expr | None) -> bool:
|
|
249
|
+
"""Check if expression is literal False.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
node: AST expression node
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
True if node is literal False
|
|
256
|
+
"""
|
|
257
|
+
if node is None:
|
|
258
|
+
return False
|
|
259
|
+
if isinstance(node, ast.Constant):
|
|
260
|
+
return node.value is False
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _invert_condition(node: ast.expr) -> ast.expr:
|
|
265
|
+
"""Invert a boolean condition.
|
|
266
|
+
|
|
267
|
+
If condition is 'not x', returns 'x'.
|
|
268
|
+
Otherwise wraps in 'not (...)'.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
node: AST expression to invert
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Inverted expression
|
|
275
|
+
"""
|
|
276
|
+
# If already negated with 'not', unwrap it
|
|
277
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
|
|
278
|
+
return node.operand
|
|
279
|
+
|
|
280
|
+
# Otherwise wrap in 'not'
|
|
281
|
+
return ast.UnaryOp(op=ast.Not(), operand=node)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Shared AST utility functions for collection pipeline analyzers
|
|
3
|
+
|
|
4
|
+
Scope: Common patterns for analyzing function bodies and return statements
|
|
5
|
+
|
|
6
|
+
Overview: Provides reusable AST analysis helpers to reduce duplication across
|
|
7
|
+
any_all_analyzer, filter_map_analyzer, and other pattern detection modules.
|
|
8
|
+
Centralizes common patterns like finding return statements after for loops.
|
|
9
|
+
|
|
10
|
+
Dependencies: ast module
|
|
11
|
+
|
|
12
|
+
Exports: get_next_return_stmt
|
|
13
|
+
|
|
14
|
+
Interfaces: get_next_return_stmt(func_body, index) -> ast.Return | None
|
|
15
|
+
|
|
16
|
+
Implementation: Pure functions using Python ast module for AST node inspection
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import ast
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_next_return_stmt(func_body: list[ast.stmt], current_index: int) -> ast.Return | None:
|
|
23
|
+
"""Get the next return statement after a given index, if it exists.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
func_body: List of statements in function body
|
|
27
|
+
current_index: Index of the current statement (e.g., for loop)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The Return statement if the next statement is a return, None otherwise
|
|
31
|
+
"""
|
|
32
|
+
next_index = current_index + 1
|
|
33
|
+
if next_index >= len(func_body):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
stmt = func_body[next_index]
|
|
37
|
+
if not isinstance(stmt, ast.Return):
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
return stmt
|
|
@@ -38,6 +38,15 @@ class CollectionPipelineConfig:
|
|
|
38
38
|
ignore: list[str] = field(default_factory=list)
|
|
39
39
|
"""File patterns to ignore."""
|
|
40
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
|
+
|
|
41
50
|
def __post_init__(self) -> None:
|
|
42
51
|
"""Validate configuration values."""
|
|
43
52
|
if self.min_continues < 1:
|
|
@@ -60,4 +69,7 @@ class CollectionPipelineConfig:
|
|
|
60
69
|
enabled=config.get("enabled", True),
|
|
61
70
|
min_continues=config.get("min_continues", DEFAULT_MIN_CONTINUES),
|
|
62
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),
|
|
63
75
|
)
|
|
@@ -66,10 +66,7 @@ def has_side_effects(continues: list[ast.If]) -> bool:
|
|
|
66
66
|
Returns:
|
|
67
67
|
True if any condition has side effects (e.g., walrus operator)
|
|
68
68
|
"""
|
|
69
|
-
for if_node in continues
|
|
70
|
-
if _condition_has_side_effects(if_node.test):
|
|
71
|
-
return True
|
|
72
|
-
return False
|
|
69
|
+
return any(_condition_has_side_effects(if_node.test) for if_node in continues)
|
|
73
70
|
|
|
74
71
|
|
|
75
72
|
def _condition_has_side_effects(node: ast.expr) -> bool:
|
|
@@ -81,10 +78,7 @@ def _condition_has_side_effects(node: ast.expr) -> bool:
|
|
|
81
78
|
Returns:
|
|
82
79
|
True if expression has side effects
|
|
83
80
|
"""
|
|
84
|
-
for child in ast.walk(node)
|
|
85
|
-
if isinstance(child, ast.NamedExpr):
|
|
86
|
-
return True
|
|
87
|
-
return False
|
|
81
|
+
return any(isinstance(child, ast.NamedExpr) for child in ast.walk(node))
|
|
88
82
|
|
|
89
83
|
|
|
90
84
|
def has_body_after_continues(body: list[ast.stmt], num_continues: int) -> bool:
|