thailint 0.5.0__py3-none-any.whl → 0.15.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI commands for project structure linters (file-placement, pipeline)
|
|
3
|
+
|
|
4
|
+
Scope: File placement validation and collection pipeline anti-pattern detection commands
|
|
5
|
+
|
|
6
|
+
Overview: Provides CLI commands for project structure linting: file-placement checks that files are
|
|
7
|
+
in appropriate directories according to configured rules, and pipeline detects for loops with
|
|
8
|
+
embedded if/continue filtering that could use collection pipelines. Both commands support
|
|
9
|
+
standard options (config, format, recursive) and integrate with the orchestrator for execution.
|
|
10
|
+
|
|
11
|
+
Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities,
|
|
12
|
+
src.cli.linters.shared for linter-specific helpers
|
|
13
|
+
|
|
14
|
+
Exports: file_placement command, pipeline command
|
|
15
|
+
|
|
16
|
+
Interfaces: Click CLI commands registered to main CLI group
|
|
17
|
+
|
|
18
|
+
Implementation: Click decorators for command definition, orchestrator-based linting execution
|
|
19
|
+
|
|
20
|
+
SRP Exception: CLI command modules follow Click framework patterns requiring similar command
|
|
21
|
+
structure across all linter commands. This is intentional design for consistency.
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import TYPE_CHECKING, Any, NoReturn
|
|
32
|
+
|
|
33
|
+
import click
|
|
34
|
+
|
|
35
|
+
from src.cli.linters.shared import (
|
|
36
|
+
ensure_config_section,
|
|
37
|
+
extract_command_context,
|
|
38
|
+
set_config_value,
|
|
39
|
+
)
|
|
40
|
+
from src.cli.main import cli
|
|
41
|
+
from src.cli.utils import (
|
|
42
|
+
execute_linting_on_paths,
|
|
43
|
+
format_option,
|
|
44
|
+
get_or_detect_project_root,
|
|
45
|
+
handle_linting_error,
|
|
46
|
+
load_config_file,
|
|
47
|
+
parallel_option,
|
|
48
|
+
setup_base_orchestrator,
|
|
49
|
+
validate_paths_exist,
|
|
50
|
+
)
|
|
51
|
+
from src.core.cli_utils import format_violations
|
|
52
|
+
from src.core.types import Violation
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from src.orchestrator.core import Orchestrator
|
|
56
|
+
|
|
57
|
+
# Configure module logger
|
|
58
|
+
logger = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# =============================================================================
|
|
62
|
+
# File Placement Command
|
|
63
|
+
# =============================================================================
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _setup_orchestrator(
|
|
67
|
+
path_objs: list[Path],
|
|
68
|
+
config_file: str | None,
|
|
69
|
+
rules: str | None,
|
|
70
|
+
verbose: bool,
|
|
71
|
+
project_root: Path | None = None,
|
|
72
|
+
) -> "Orchestrator":
|
|
73
|
+
"""Set up and configure the orchestrator for file-placement."""
|
|
74
|
+
from src.orchestrator.core import Orchestrator
|
|
75
|
+
|
|
76
|
+
project_root = get_or_detect_project_root(path_objs, project_root)
|
|
77
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
78
|
+
_apply_orchestrator_config(orchestrator, config_file, rules, verbose)
|
|
79
|
+
return orchestrator
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _apply_orchestrator_config(
|
|
83
|
+
orchestrator: "Orchestrator", config_file: str | None, rules: str | None, verbose: bool
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Apply configuration to orchestrator."""
|
|
86
|
+
if rules:
|
|
87
|
+
_apply_inline_rules(orchestrator, rules, verbose)
|
|
88
|
+
elif config_file:
|
|
89
|
+
load_config_file(orchestrator, config_file, verbose)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _apply_inline_rules(orchestrator: "Orchestrator", rules: str, verbose: bool) -> None:
|
|
93
|
+
"""Parse and apply inline JSON rules."""
|
|
94
|
+
rules_config = _parse_json_rules(rules)
|
|
95
|
+
orchestrator.config.update(rules_config)
|
|
96
|
+
if verbose:
|
|
97
|
+
logger.debug(f"Applied inline rules: {rules_config}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _parse_json_rules(rules: str) -> dict[str, Any]:
|
|
101
|
+
"""Parse JSON rules string, exit on error."""
|
|
102
|
+
try:
|
|
103
|
+
result: dict[str, Any] = json.loads(rules)
|
|
104
|
+
return result
|
|
105
|
+
except json.JSONDecodeError as e:
|
|
106
|
+
click.echo(f"Error: Invalid JSON in --rules: {e}", err=True)
|
|
107
|
+
sys.exit(2)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@cli.command("file-placement")
|
|
111
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
112
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
113
|
+
@click.option("--rules", "-r", help="Inline JSON rules configuration")
|
|
114
|
+
@format_option
|
|
115
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
116
|
+
@parallel_option
|
|
117
|
+
@click.pass_context
|
|
118
|
+
def file_placement( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
119
|
+
ctx: click.Context,
|
|
120
|
+
paths: tuple[str, ...],
|
|
121
|
+
config_file: str | None,
|
|
122
|
+
rules: str | None,
|
|
123
|
+
format: str,
|
|
124
|
+
recursive: bool,
|
|
125
|
+
parallel: bool,
|
|
126
|
+
) -> None:
|
|
127
|
+
# Justification for Pylint disables:
|
|
128
|
+
# - too-many-arguments/positional: CLI requires 1 ctx + 1 arg + 4 options = 6 params
|
|
129
|
+
"""
|
|
130
|
+
Lint files for proper file placement.
|
|
131
|
+
|
|
132
|
+
Checks that files are placed in appropriate directories according to
|
|
133
|
+
configured rules and patterns.
|
|
134
|
+
|
|
135
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
|
|
139
|
+
\b
|
|
140
|
+
# Lint current directory (all files recursively)
|
|
141
|
+
thai-lint file-placement
|
|
142
|
+
|
|
143
|
+
\b
|
|
144
|
+
# Lint specific directory
|
|
145
|
+
thai-lint file-placement src/
|
|
146
|
+
|
|
147
|
+
\b
|
|
148
|
+
# Lint single file
|
|
149
|
+
thai-lint file-placement src/app.py
|
|
150
|
+
|
|
151
|
+
\b
|
|
152
|
+
# Lint multiple files
|
|
153
|
+
thai-lint file-placement src/app.py src/utils.py tests/test_app.py
|
|
154
|
+
|
|
155
|
+
\b
|
|
156
|
+
# Use custom config
|
|
157
|
+
thai-lint file-placement --config rules.json .
|
|
158
|
+
|
|
159
|
+
\b
|
|
160
|
+
# Inline JSON rules
|
|
161
|
+
thai-lint file-placement --rules '{"allow": [".*\\.py$"]}' .
|
|
162
|
+
"""
|
|
163
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
_execute_file_placement_lint(
|
|
167
|
+
cmd_ctx.path_objs,
|
|
168
|
+
config_file,
|
|
169
|
+
rules,
|
|
170
|
+
format,
|
|
171
|
+
recursive,
|
|
172
|
+
parallel,
|
|
173
|
+
cmd_ctx.verbose,
|
|
174
|
+
cmd_ctx.project_root,
|
|
175
|
+
)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _execute_file_placement_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
181
|
+
path_objs: list[Path],
|
|
182
|
+
config_file: str | None,
|
|
183
|
+
rules: str | None,
|
|
184
|
+
format: str,
|
|
185
|
+
recursive: bool,
|
|
186
|
+
parallel: bool,
|
|
187
|
+
verbose: bool,
|
|
188
|
+
project_root: Path | None = None,
|
|
189
|
+
) -> NoReturn:
|
|
190
|
+
"""Execute file placement linting."""
|
|
191
|
+
validate_paths_exist(path_objs)
|
|
192
|
+
orchestrator = _setup_orchestrator(path_objs, config_file, rules, verbose, project_root)
|
|
193
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
194
|
+
|
|
195
|
+
# Filter to only file-placement violations
|
|
196
|
+
violations = [v for v in all_violations if v.rule_id.startswith("file-placement")]
|
|
197
|
+
|
|
198
|
+
if verbose:
|
|
199
|
+
logger.info(f"Found {len(violations)} violation(s)")
|
|
200
|
+
|
|
201
|
+
format_violations(violations, format)
|
|
202
|
+
sys.exit(1 if violations else 0)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# =============================================================================
|
|
206
|
+
# Collection Pipeline Command
|
|
207
|
+
# =============================================================================
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _setup_pipeline_orchestrator(
|
|
211
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
212
|
+
) -> "Orchestrator":
|
|
213
|
+
"""Set up orchestrator for pipeline command."""
|
|
214
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _apply_pipeline_config_override(
|
|
218
|
+
orchestrator: "Orchestrator", min_continues: int | None, verbose: bool
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Apply min_continues override to orchestrator config."""
|
|
221
|
+
if min_continues is None:
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
pipeline_config = ensure_config_section(orchestrator, "collection_pipeline")
|
|
225
|
+
set_config_value(pipeline_config, "min_continues", min_continues, verbose)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _run_pipeline_lint(
|
|
229
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
230
|
+
) -> list[Violation]:
|
|
231
|
+
"""Execute collection-pipeline lint on files or directories."""
|
|
232
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
233
|
+
return [v for v in all_violations if "collection-pipeline" in v.rule_id]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@cli.command("pipeline")
|
|
237
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
238
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
239
|
+
@format_option
|
|
240
|
+
@click.option("--min-continues", type=int, help="Override min continue guards to flag (default: 1)")
|
|
241
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
242
|
+
@parallel_option
|
|
243
|
+
@click.pass_context
|
|
244
|
+
def pipeline( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
245
|
+
ctx: click.Context,
|
|
246
|
+
paths: tuple[str, ...],
|
|
247
|
+
config_file: str | None,
|
|
248
|
+
format: str,
|
|
249
|
+
min_continues: int | None,
|
|
250
|
+
recursive: bool,
|
|
251
|
+
parallel: bool,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Check for collection pipeline anti-patterns in code.
|
|
254
|
+
|
|
255
|
+
Detects for loops with embedded if/continue filtering patterns that could
|
|
256
|
+
be refactored to use collection pipelines (generator expressions, filter()).
|
|
257
|
+
|
|
258
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
259
|
+
|
|
260
|
+
Examples:
|
|
261
|
+
|
|
262
|
+
\b
|
|
263
|
+
# Check current directory (all Python files recursively)
|
|
264
|
+
thai-lint pipeline
|
|
265
|
+
|
|
266
|
+
\b
|
|
267
|
+
# Check specific directory
|
|
268
|
+
thai-lint pipeline src/
|
|
269
|
+
|
|
270
|
+
\b
|
|
271
|
+
# Check single file
|
|
272
|
+
thai-lint pipeline src/app.py
|
|
273
|
+
|
|
274
|
+
\b
|
|
275
|
+
# Only flag loops with 2+ continue guards
|
|
276
|
+
thai-lint pipeline --min-continues 2 src/
|
|
277
|
+
|
|
278
|
+
\b
|
|
279
|
+
# Get JSON output
|
|
280
|
+
thai-lint pipeline --format json .
|
|
281
|
+
|
|
282
|
+
\b
|
|
283
|
+
# Get SARIF output for CI/CD integration
|
|
284
|
+
thai-lint pipeline --format sarif src/
|
|
285
|
+
|
|
286
|
+
\b
|
|
287
|
+
# Use custom config file
|
|
288
|
+
thai-lint pipeline --config .thailint.yaml src/
|
|
289
|
+
"""
|
|
290
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
_execute_pipeline_lint(
|
|
294
|
+
cmd_ctx.path_objs,
|
|
295
|
+
config_file,
|
|
296
|
+
format,
|
|
297
|
+
min_continues,
|
|
298
|
+
recursive,
|
|
299
|
+
parallel,
|
|
300
|
+
cmd_ctx.verbose,
|
|
301
|
+
cmd_ctx.project_root,
|
|
302
|
+
)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
308
|
+
path_objs: list[Path],
|
|
309
|
+
config_file: str | None,
|
|
310
|
+
format: str,
|
|
311
|
+
min_continues: int | None,
|
|
312
|
+
recursive: bool,
|
|
313
|
+
parallel: bool,
|
|
314
|
+
verbose: bool,
|
|
315
|
+
project_root: Path | None = None,
|
|
316
|
+
) -> NoReturn:
|
|
317
|
+
"""Execute collection-pipeline lint."""
|
|
318
|
+
validate_paths_exist(path_objs)
|
|
319
|
+
orchestrator = _setup_pipeline_orchestrator(path_objs, config_file, verbose, project_root)
|
|
320
|
+
_apply_pipeline_config_override(orchestrator, min_continues, verbose)
|
|
321
|
+
pipeline_violations = _run_pipeline_lint(orchestrator, path_objs, recursive, parallel)
|
|
322
|
+
|
|
323
|
+
if verbose:
|
|
324
|
+
logger.info(f"Found {len(pipeline_violations)} collection-pipeline violation(s)")
|
|
325
|
+
|
|
326
|
+
format_violations(pipeline_violations, format)
|
|
327
|
+
sys.exit(1 if pipeline_violations else 0)
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI commands for structure quality linters (nesting, srp)
|
|
3
|
+
|
|
4
|
+
Scope: Commands that analyze code structure for quality issues
|
|
5
|
+
|
|
6
|
+
Overview: Provides CLI commands for structure quality linting: nesting checks for excessive nesting
|
|
7
|
+
depth in control flow statements, and srp detects Single Responsibility Principle violations in
|
|
8
|
+
classes. Each command supports standard options (config, format, recursive) plus linter-specific
|
|
9
|
+
options (max-depth, max-methods, max-loc) and integrates with the orchestrator for execution.
|
|
10
|
+
|
|
11
|
+
Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities,
|
|
12
|
+
src.cli.linters.shared for linter-specific helpers
|
|
13
|
+
|
|
14
|
+
Exports: nesting command, srp command
|
|
15
|
+
|
|
16
|
+
Interfaces: Click CLI commands registered to main CLI group
|
|
17
|
+
|
|
18
|
+
Implementation: Click decorators for command definition, orchestrator-based linting execution
|
|
19
|
+
|
|
20
|
+
SRP Exception: CLI command modules follow Click framework patterns requiring similar command
|
|
21
|
+
structure across all linter commands. This is intentional design for consistency.
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import sys
|
|
29
|
+
from contextlib import suppress
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
32
|
+
|
|
33
|
+
import click
|
|
34
|
+
|
|
35
|
+
from src.cli.linters.shared import (
|
|
36
|
+
ensure_config_section,
|
|
37
|
+
extract_command_context,
|
|
38
|
+
set_config_value,
|
|
39
|
+
)
|
|
40
|
+
from src.cli.main import cli
|
|
41
|
+
from src.cli.utils import (
|
|
42
|
+
execute_linting_on_paths,
|
|
43
|
+
format_option,
|
|
44
|
+
handle_linting_error,
|
|
45
|
+
parallel_option,
|
|
46
|
+
setup_base_orchestrator,
|
|
47
|
+
validate_paths_exist,
|
|
48
|
+
)
|
|
49
|
+
from src.core.cli_utils import format_violations
|
|
50
|
+
from src.core.types import Violation
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from src.orchestrator.core import Orchestrator
|
|
54
|
+
|
|
55
|
+
# Configure module logger
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Nesting Command
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _setup_nesting_orchestrator(
|
|
65
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
66
|
+
) -> "Orchestrator":
|
|
67
|
+
"""Set up orchestrator for nesting command."""
|
|
68
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _apply_nesting_config_override(
|
|
72
|
+
orchestrator: "Orchestrator", max_depth: int | None, verbose: bool
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Apply max_depth override to orchestrator config."""
|
|
75
|
+
if max_depth is None:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
nesting_config = ensure_config_section(orchestrator, "nesting")
|
|
79
|
+
nesting_config["max_nesting_depth"] = max_depth
|
|
80
|
+
_apply_nesting_to_languages(nesting_config, max_depth)
|
|
81
|
+
|
|
82
|
+
if verbose:
|
|
83
|
+
logger.debug(f"Overriding max_nesting_depth to {max_depth}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _apply_nesting_to_languages(nesting_config: dict, max_depth: int) -> None:
|
|
87
|
+
"""Apply max_depth to language-specific configs."""
|
|
88
|
+
for lang in ["python", "typescript", "javascript"]:
|
|
89
|
+
with suppress(KeyError):
|
|
90
|
+
nesting_config[lang]["max_nesting_depth"] = max_depth
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _run_nesting_lint(
|
|
94
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
95
|
+
) -> list[Violation]:
|
|
96
|
+
"""Execute nesting lint on files or directories."""
|
|
97
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
98
|
+
return [v for v in all_violations if "nesting" in v.rule_id]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@cli.command("nesting")
|
|
102
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
103
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
104
|
+
@format_option
|
|
105
|
+
@click.option("--max-depth", type=int, help="Override max nesting depth (default: 4)")
|
|
106
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
107
|
+
@parallel_option
|
|
108
|
+
@click.pass_context
|
|
109
|
+
def nesting( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
110
|
+
ctx: click.Context,
|
|
111
|
+
paths: tuple[str, ...],
|
|
112
|
+
config_file: str | None,
|
|
113
|
+
format: str,
|
|
114
|
+
max_depth: int | None,
|
|
115
|
+
recursive: bool,
|
|
116
|
+
parallel: bool,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Check for excessive nesting depth in code.
|
|
119
|
+
|
|
120
|
+
Analyzes Python and TypeScript files for deeply nested code structures
|
|
121
|
+
(if/for/while/try statements) and reports violations.
|
|
122
|
+
|
|
123
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
|
|
127
|
+
\b
|
|
128
|
+
# Check current directory (all files recursively)
|
|
129
|
+
thai-lint nesting
|
|
130
|
+
|
|
131
|
+
\b
|
|
132
|
+
# Check specific directory
|
|
133
|
+
thai-lint nesting src/
|
|
134
|
+
|
|
135
|
+
\b
|
|
136
|
+
# Check single file
|
|
137
|
+
thai-lint nesting src/app.py
|
|
138
|
+
|
|
139
|
+
\b
|
|
140
|
+
# Check multiple files
|
|
141
|
+
thai-lint nesting src/app.py src/utils.py tests/test_app.py
|
|
142
|
+
|
|
143
|
+
\b
|
|
144
|
+
# Check mix of files and directories
|
|
145
|
+
thai-lint nesting src/app.py tests/
|
|
146
|
+
|
|
147
|
+
\b
|
|
148
|
+
# Use custom max depth
|
|
149
|
+
thai-lint nesting --max-depth 3 src/
|
|
150
|
+
|
|
151
|
+
\b
|
|
152
|
+
# Get JSON output
|
|
153
|
+
thai-lint nesting --format json .
|
|
154
|
+
|
|
155
|
+
\b
|
|
156
|
+
# Use custom config file
|
|
157
|
+
thai-lint nesting --config .thailint.yaml src/
|
|
158
|
+
"""
|
|
159
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
_execute_nesting_lint(
|
|
163
|
+
cmd_ctx.path_objs,
|
|
164
|
+
config_file,
|
|
165
|
+
format,
|
|
166
|
+
max_depth,
|
|
167
|
+
recursive,
|
|
168
|
+
parallel,
|
|
169
|
+
cmd_ctx.verbose,
|
|
170
|
+
cmd_ctx.project_root,
|
|
171
|
+
)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _execute_nesting_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
177
|
+
path_objs: list[Path],
|
|
178
|
+
config_file: str | None,
|
|
179
|
+
format: str,
|
|
180
|
+
max_depth: int | None,
|
|
181
|
+
recursive: bool,
|
|
182
|
+
parallel: bool,
|
|
183
|
+
verbose: bool,
|
|
184
|
+
project_root: Path | None = None,
|
|
185
|
+
) -> NoReturn:
|
|
186
|
+
"""Execute nesting lint."""
|
|
187
|
+
validate_paths_exist(path_objs)
|
|
188
|
+
orchestrator = _setup_nesting_orchestrator(path_objs, config_file, verbose, project_root)
|
|
189
|
+
_apply_nesting_config_override(orchestrator, max_depth, verbose)
|
|
190
|
+
nesting_violations = _run_nesting_lint(orchestrator, path_objs, recursive, parallel)
|
|
191
|
+
|
|
192
|
+
if verbose:
|
|
193
|
+
logger.info(f"Found {len(nesting_violations)} nesting violation(s)")
|
|
194
|
+
|
|
195
|
+
format_violations(nesting_violations, format)
|
|
196
|
+
sys.exit(1 if nesting_violations else 0)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# =============================================================================
|
|
200
|
+
# SRP Command
|
|
201
|
+
# =============================================================================
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _setup_srp_orchestrator(
|
|
205
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
206
|
+
) -> "Orchestrator":
|
|
207
|
+
"""Set up orchestrator for SRP command."""
|
|
208
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _apply_srp_config_override(
|
|
212
|
+
orchestrator: "Orchestrator", max_methods: int | None, max_loc: int | None, verbose: bool
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Apply max_methods and max_loc overrides to orchestrator config."""
|
|
215
|
+
if max_methods is None and max_loc is None:
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
srp_config = ensure_config_section(orchestrator, "srp")
|
|
219
|
+
set_config_value(srp_config, "max_methods", max_methods, verbose)
|
|
220
|
+
set_config_value(srp_config, "max_loc", max_loc, verbose)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _run_srp_lint(
|
|
224
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
225
|
+
) -> list[Violation]:
|
|
226
|
+
"""Execute SRP lint on files or directories."""
|
|
227
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
228
|
+
return [v for v in all_violations if "srp" in v.rule_id]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@cli.command("srp")
|
|
232
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
233
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
234
|
+
@format_option
|
|
235
|
+
@click.option("--max-methods", type=int, help="Override max methods per class (default: 7)")
|
|
236
|
+
@click.option("--max-loc", type=int, help="Override max lines of code per class (default: 200)")
|
|
237
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
238
|
+
@parallel_option
|
|
239
|
+
@click.pass_context
|
|
240
|
+
def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
241
|
+
ctx: click.Context,
|
|
242
|
+
paths: tuple[str, ...],
|
|
243
|
+
config_file: str | None,
|
|
244
|
+
format: str,
|
|
245
|
+
max_methods: int | None,
|
|
246
|
+
max_loc: int | None,
|
|
247
|
+
recursive: bool,
|
|
248
|
+
parallel: bool,
|
|
249
|
+
) -> None:
|
|
250
|
+
"""Check for Single Responsibility Principle violations.
|
|
251
|
+
|
|
252
|
+
Analyzes Python and TypeScript classes for SRP violations using heuristics:
|
|
253
|
+
- Method count exceeding threshold (default: 7)
|
|
254
|
+
- Lines of code exceeding threshold (default: 200)
|
|
255
|
+
- Responsibility keywords in class names (Manager, Handler, Processor, etc.)
|
|
256
|
+
|
|
257
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
258
|
+
|
|
259
|
+
Examples:
|
|
260
|
+
|
|
261
|
+
\b
|
|
262
|
+
# Check current directory (all files recursively)
|
|
263
|
+
thai-lint srp
|
|
264
|
+
|
|
265
|
+
\b
|
|
266
|
+
# Check specific directory
|
|
267
|
+
thai-lint srp src/
|
|
268
|
+
|
|
269
|
+
\b
|
|
270
|
+
# Check single file
|
|
271
|
+
thai-lint srp src/app.py
|
|
272
|
+
|
|
273
|
+
\b
|
|
274
|
+
# Check multiple files
|
|
275
|
+
thai-lint srp src/app.py src/service.py tests/test_app.py
|
|
276
|
+
|
|
277
|
+
\b
|
|
278
|
+
# Use custom thresholds
|
|
279
|
+
thai-lint srp --max-methods 10 --max-loc 300 src/
|
|
280
|
+
|
|
281
|
+
\b
|
|
282
|
+
# Get JSON output
|
|
283
|
+
thai-lint srp --format json .
|
|
284
|
+
|
|
285
|
+
\b
|
|
286
|
+
# Use custom config file
|
|
287
|
+
thai-lint srp --config .thailint.yaml src/
|
|
288
|
+
"""
|
|
289
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
_execute_srp_lint(
|
|
293
|
+
cmd_ctx.path_objs,
|
|
294
|
+
config_file,
|
|
295
|
+
format,
|
|
296
|
+
max_methods,
|
|
297
|
+
max_loc,
|
|
298
|
+
recursive,
|
|
299
|
+
parallel,
|
|
300
|
+
cmd_ctx.verbose,
|
|
301
|
+
cmd_ctx.project_root,
|
|
302
|
+
)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
308
|
+
path_objs: list[Path],
|
|
309
|
+
config_file: str | None,
|
|
310
|
+
format: str,
|
|
311
|
+
max_methods: int | None,
|
|
312
|
+
max_loc: int | None,
|
|
313
|
+
recursive: bool,
|
|
314
|
+
parallel: bool,
|
|
315
|
+
verbose: bool,
|
|
316
|
+
project_root: Path | None = None,
|
|
317
|
+
) -> NoReturn:
|
|
318
|
+
"""Execute SRP lint."""
|
|
319
|
+
validate_paths_exist(path_objs)
|
|
320
|
+
orchestrator = _setup_srp_orchestrator(path_objs, config_file, verbose, project_root)
|
|
321
|
+
_apply_srp_config_override(orchestrator, max_methods, max_loc, verbose)
|
|
322
|
+
srp_violations = _run_srp_lint(orchestrator, path_objs, recursive, parallel)
|
|
323
|
+
|
|
324
|
+
if verbose:
|
|
325
|
+
logger.info(f"Found {len(srp_violations)} SRP violation(s)")
|
|
326
|
+
|
|
327
|
+
format_violations(srp_violations, format)
|
|
328
|
+
sys.exit(1 if srp_violations else 0)
|