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
src/linter_config/loader.py
CHANGED
|
@@ -16,9 +16,9 @@ Overview: Loads linter configuration from .thailint.yaml or .thailint.json files
|
|
|
16
16
|
Dependencies: PyYAML for YAML parsing with safe_load(), json (stdlib) for JSON parsing,
|
|
17
17
|
pathlib for file path handling
|
|
18
18
|
|
|
19
|
-
Exports: LinterConfigLoader class
|
|
19
|
+
Exports: load_config function, get_defaults function, LinterConfigLoader class (compat)
|
|
20
20
|
|
|
21
|
-
Interfaces:
|
|
21
|
+
Interfaces: load_config(config_path: Path) -> dict[str, Any] for loading config files,
|
|
22
22
|
get_defaults() -> dict[str, Any] for default configuration structure
|
|
23
23
|
|
|
24
24
|
Implementation: Extension-based format detection (.yaml/.yml vs .json), yaml.safe_load()
|
|
@@ -31,13 +31,51 @@ from typing import Any
|
|
|
31
31
|
from src.core.config_parser import parse_config_file
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def get_defaults() -> dict[str, Any]:
|
|
35
|
+
"""Get default configuration.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Default configuration with empty rules and ignore lists.
|
|
39
|
+
"""
|
|
40
|
+
return {
|
|
41
|
+
"rules": {},
|
|
42
|
+
"ignore": [],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_config(config_path: Path) -> dict[str, Any]:
|
|
47
|
+
"""Load configuration from file.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config_path: Path to YAML or JSON config file.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Configuration dictionary.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ConfigParseError: If file format is unsupported or parsing fails.
|
|
57
|
+
"""
|
|
58
|
+
if not config_path.exists():
|
|
59
|
+
return get_defaults()
|
|
60
|
+
|
|
61
|
+
return parse_config_file(config_path)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Legacy class wrapper for backward compatibility
|
|
34
65
|
class LinterConfigLoader:
|
|
35
66
|
"""Load linter configuration from YAML or JSON files.
|
|
36
67
|
|
|
37
68
|
Supports loading from .thailint.yaml, .thailint.json, or custom paths.
|
|
38
69
|
Provides sensible defaults when config files don't exist.
|
|
70
|
+
|
|
71
|
+
Note: This class is a thin wrapper around module-level functions
|
|
72
|
+
for backward compatibility.
|
|
39
73
|
"""
|
|
40
74
|
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
"""Initialize the loader."""
|
|
77
|
+
pass # No state needed
|
|
78
|
+
|
|
41
79
|
def load(self, config_path: Path) -> dict[str, Any]:
|
|
42
80
|
"""Load configuration from file.
|
|
43
81
|
|
|
@@ -50,18 +88,13 @@ class LinterConfigLoader:
|
|
|
50
88
|
Raises:
|
|
51
89
|
ConfigParseError: If file format is unsupported or parsing fails.
|
|
52
90
|
"""
|
|
53
|
-
|
|
54
|
-
return self.get_defaults()
|
|
55
|
-
|
|
56
|
-
return parse_config_file(config_path)
|
|
91
|
+
return load_config(config_path)
|
|
57
92
|
|
|
58
|
-
|
|
59
|
-
|
|
93
|
+
@property
|
|
94
|
+
def defaults(self) -> dict[str, Any]:
|
|
95
|
+
"""Default configuration.
|
|
60
96
|
|
|
61
97
|
Returns:
|
|
62
98
|
Default configuration with empty rules and ignore lists.
|
|
63
99
|
"""
|
|
64
|
-
return
|
|
65
|
-
"rules": {},
|
|
66
|
-
"ignore": [],
|
|
67
|
-
}
|
|
100
|
+
return get_defaults()
|
|
@@ -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,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Collection pipeline linter package initialization
|
|
3
|
+
|
|
4
|
+
Scope: Exports for collection-pipeline linter module
|
|
5
|
+
|
|
6
|
+
Overview: Initializes the collection-pipeline linter package and exposes the main rule class
|
|
7
|
+
for external use. Exports CollectionPipelineRule as the primary interface for the linter,
|
|
8
|
+
allowing the orchestrator to discover and instantiate the rule. Also exports configuration
|
|
9
|
+
and detector classes for advanced use cases. Provides a convenience lint() function for
|
|
10
|
+
direct usage without orchestrator setup. This module serves as the entry point for
|
|
11
|
+
the collection-pipeline linter functionality within the thai-lint framework.
|
|
12
|
+
|
|
13
|
+
Dependencies: CollectionPipelineRule, CollectionPipelineConfig, PipelinePatternDetector
|
|
14
|
+
|
|
15
|
+
Exports: CollectionPipelineRule (primary), CollectionPipelineConfig, PipelinePatternDetector, lint
|
|
16
|
+
|
|
17
|
+
Interfaces: Standard Python package initialization with __all__ for explicit exports
|
|
18
|
+
|
|
19
|
+
Implementation: Simple re-export pattern for package interface, convenience lint function
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from .config import DEFAULT_MIN_CONTINUES, CollectionPipelineConfig
|
|
26
|
+
from .detector import PatternMatch, PipelinePatternDetector
|
|
27
|
+
from .linter import CollectionPipelineRule
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"CollectionPipelineRule",
|
|
31
|
+
"CollectionPipelineConfig",
|
|
32
|
+
"PipelinePatternDetector",
|
|
33
|
+
"PatternMatch",
|
|
34
|
+
"lint",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def lint(
|
|
39
|
+
path: Path | str,
|
|
40
|
+
config: dict[str, Any] | None = None,
|
|
41
|
+
min_continues: int = DEFAULT_MIN_CONTINUES,
|
|
42
|
+
) -> list:
|
|
43
|
+
"""Lint a file or directory for collection pipeline violations.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
path: Path to file or directory to lint
|
|
47
|
+
config: Configuration dict (optional, uses defaults if not provided)
|
|
48
|
+
min_continues: Minimum if/continue patterns to flag (default: 1)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of violations found
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> from src.linters.collection_pipeline import lint
|
|
55
|
+
>>> violations = lint('src/my_module.py', min_continues=2)
|
|
56
|
+
>>> for v in violations:
|
|
57
|
+
... print(f"{v.file_path}:{v.line} - {v.message}")
|
|
58
|
+
"""
|
|
59
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
60
|
+
project_root = path_obj if path_obj.is_dir() else path_obj.parent
|
|
61
|
+
|
|
62
|
+
orchestrator = _setup_pipeline_orchestrator(project_root, config, min_continues)
|
|
63
|
+
violations = _execute_pipeline_lint(orchestrator, path_obj)
|
|
64
|
+
|
|
65
|
+
return [v for v in violations if "collection-pipeline" in v.rule_id]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _setup_pipeline_orchestrator(
|
|
69
|
+
project_root: Path, config: dict[str, Any] | None, min_continues: int
|
|
70
|
+
) -> Any:
|
|
71
|
+
"""Set up orchestrator with collection-pipeline config."""
|
|
72
|
+
from src.orchestrator.core import Orchestrator
|
|
73
|
+
|
|
74
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
75
|
+
|
|
76
|
+
if config:
|
|
77
|
+
orchestrator.config["collection-pipeline"] = config
|
|
78
|
+
else:
|
|
79
|
+
orchestrator.config["collection-pipeline"] = {"min_continues": min_continues}
|
|
80
|
+
|
|
81
|
+
return orchestrator
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _execute_pipeline_lint(orchestrator: Any, path_obj: Path) -> list:
|
|
85
|
+
"""Execute linting on file or directory."""
|
|
86
|
+
if path_obj.is_file():
|
|
87
|
+
return orchestrator.lint_file(path_obj)
|
|
88
|
+
if path_obj.is_dir():
|
|
89
|
+
return orchestrator.lint_directory(path_obj)
|
|
90
|
+
return []
|
|
@@ -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
|