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.
Files changed (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -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: load(config_path: Path) -> dict[str, Any] for loading config files,
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
- if not config_path.exists():
54
- return self.get_defaults()
55
-
56
- return parse_config_file(config_path)
91
+ return load_config(config_path)
57
92
 
58
- def get_defaults(self) -> dict[str, Any]:
59
- """Get default configuration.
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