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,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI commands for code smell linters (dry, magic-numbers, stringly-typed)
|
|
3
|
+
|
|
4
|
+
Scope: Commands that detect code smells like duplicate code, magic numbers, and stringly-typed patterns
|
|
5
|
+
|
|
6
|
+
Overview: Provides CLI commands for code smell detection: dry finds duplicate code blocks using
|
|
7
|
+
token-based hashing with SQLite caching, magic-numbers detects unnamed numeric literals that
|
|
8
|
+
should be extracted as named constants, and stringly-typed detects string patterns that should
|
|
9
|
+
use enums. Each command supports standard options (config, format, recursive) plus linter-specific
|
|
10
|
+
options and integrates with the orchestrator for execution.
|
|
11
|
+
|
|
12
|
+
Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities,
|
|
13
|
+
src.cli.linters.shared for linter-specific helpers, yaml for config loading
|
|
14
|
+
|
|
15
|
+
Exports: dry command, magic_numbers command, stringly_typed command
|
|
16
|
+
|
|
17
|
+
Interfaces: Click CLI commands registered to main CLI group
|
|
18
|
+
|
|
19
|
+
Implementation: Click decorators for command definition, orchestrator-based linting execution
|
|
20
|
+
|
|
21
|
+
Suppressions:
|
|
22
|
+
- too-many-arguments,too-many-positional-arguments: Click commands with custom options require
|
|
23
|
+
many parameters by framework design (dry command has 8 params for extra options)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import TYPE_CHECKING, Any, NoReturn
|
|
30
|
+
|
|
31
|
+
import click
|
|
32
|
+
import yaml
|
|
33
|
+
|
|
34
|
+
from src.cli.linters.shared import (
|
|
35
|
+
ExecuteParams,
|
|
36
|
+
create_linter_command,
|
|
37
|
+
ensure_config_section,
|
|
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_project_root_from_context,
|
|
45
|
+
handle_linting_error,
|
|
46
|
+
parallel_option,
|
|
47
|
+
setup_base_orchestrator,
|
|
48
|
+
validate_paths_exist,
|
|
49
|
+
)
|
|
50
|
+
from src.core.cli_utils import format_violations
|
|
51
|
+
from src.core.types import Violation
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from src.orchestrator.core import Orchestrator
|
|
55
|
+
|
|
56
|
+
# Configure module logger
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# DRY Command (custom options - cannot use create_linter_command)
|
|
62
|
+
# =============================================================================
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _setup_dry_orchestrator(
|
|
66
|
+
path_objs: list[Path],
|
|
67
|
+
config_file: str | None,
|
|
68
|
+
verbose: bool,
|
|
69
|
+
project_root: Path | None = None,
|
|
70
|
+
) -> "Orchestrator":
|
|
71
|
+
"""Set up orchestrator for DRY linting."""
|
|
72
|
+
return setup_base_orchestrator(path_objs, None, verbose, project_root)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _load_dry_config_file(orchestrator: "Orchestrator", config_file: str, verbose: bool) -> None:
|
|
76
|
+
"""Load DRY configuration from file."""
|
|
77
|
+
config_path = Path(config_file)
|
|
78
|
+
if not config_path.exists():
|
|
79
|
+
click.echo(f"Error: Config file not found: {config_file}", err=True)
|
|
80
|
+
sys.exit(2)
|
|
81
|
+
|
|
82
|
+
with config_path.open("r", encoding="utf-8") as f:
|
|
83
|
+
config: dict[str, Any] = yaml.safe_load(f)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
dry_config = config["dry"]
|
|
87
|
+
except KeyError:
|
|
88
|
+
return # No DRY config in file
|
|
89
|
+
orchestrator.config.update({"dry": dry_config})
|
|
90
|
+
if verbose:
|
|
91
|
+
logger.info(f"Loaded DRY config from {config_file}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _apply_dry_config_override(
|
|
95
|
+
orchestrator: "Orchestrator", min_lines: int | None, no_cache: bool, verbose: bool
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Apply CLI option overrides to DRY config."""
|
|
98
|
+
dry_config = ensure_config_section(orchestrator, "dry")
|
|
99
|
+
set_config_value(dry_config, "min_duplicate_lines", min_lines, verbose)
|
|
100
|
+
if no_cache:
|
|
101
|
+
set_config_value(dry_config, "cache_enabled", False, verbose)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _clear_dry_cache(orchestrator: "Orchestrator", verbose: bool) -> None:
|
|
105
|
+
"""Clear DRY cache before running."""
|
|
106
|
+
cache_path_str = orchestrator.config.get("dry", {}).get("cache_path", ".thailint-cache/dry.db")
|
|
107
|
+
cache_path = orchestrator.project_root / cache_path_str
|
|
108
|
+
|
|
109
|
+
if cache_path.exists():
|
|
110
|
+
cache_path.unlink()
|
|
111
|
+
if verbose:
|
|
112
|
+
logger.info(f"Cleared cache: {cache_path}")
|
|
113
|
+
elif verbose:
|
|
114
|
+
logger.info("Cache file does not exist, nothing to clear")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _run_dry_lint(
|
|
118
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
119
|
+
) -> list[Violation]:
|
|
120
|
+
"""Run DRY linting and return violations."""
|
|
121
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
122
|
+
return [v for v in all_violations if v.rule_id.startswith("dry.")]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@cli.command("dry")
|
|
126
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
127
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
128
|
+
@format_option
|
|
129
|
+
@click.option("--min-lines", type=int, help="Override min duplicate lines threshold")
|
|
130
|
+
@click.option("--no-cache", is_flag=True, help="Disable SQLite cache (force rehash)")
|
|
131
|
+
@click.option("--clear-cache", is_flag=True, help="Clear cache before running")
|
|
132
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
133
|
+
@parallel_option
|
|
134
|
+
@click.pass_context
|
|
135
|
+
def dry( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
136
|
+
ctx: click.Context,
|
|
137
|
+
paths: tuple[str, ...],
|
|
138
|
+
config_file: str | None,
|
|
139
|
+
format: str,
|
|
140
|
+
min_lines: int | None,
|
|
141
|
+
no_cache: bool,
|
|
142
|
+
clear_cache: bool,
|
|
143
|
+
recursive: bool,
|
|
144
|
+
parallel: bool,
|
|
145
|
+
) -> None:
|
|
146
|
+
# Justification for Pylint disables:
|
|
147
|
+
# - too-many-arguments/positional: CLI requires 1 ctx + 1 arg + 6 options = 8 params
|
|
148
|
+
"""
|
|
149
|
+
Check for duplicate code (DRY principle violations).
|
|
150
|
+
|
|
151
|
+
Detects duplicate code blocks across your project using token-based hashing
|
|
152
|
+
with SQLite caching for fast incremental scans.
|
|
153
|
+
|
|
154
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
|
|
158
|
+
\b
|
|
159
|
+
# Check current directory (all files recursively)
|
|
160
|
+
thai-lint dry
|
|
161
|
+
|
|
162
|
+
\b
|
|
163
|
+
# Check specific directory
|
|
164
|
+
thai-lint dry src/
|
|
165
|
+
|
|
166
|
+
\b
|
|
167
|
+
# Check single file
|
|
168
|
+
thai-lint dry src/app.py
|
|
169
|
+
|
|
170
|
+
\b
|
|
171
|
+
# Check multiple files
|
|
172
|
+
thai-lint dry src/app.py src/service.py tests/test_app.py
|
|
173
|
+
|
|
174
|
+
\b
|
|
175
|
+
# Use custom config file
|
|
176
|
+
thai-lint dry --config .thailint.yaml src/
|
|
177
|
+
|
|
178
|
+
\b
|
|
179
|
+
# Override minimum duplicate lines threshold
|
|
180
|
+
thai-lint dry --min-lines 5 .
|
|
181
|
+
|
|
182
|
+
\b
|
|
183
|
+
# Disable cache (force re-analysis)
|
|
184
|
+
thai-lint dry --no-cache .
|
|
185
|
+
|
|
186
|
+
\b
|
|
187
|
+
# Clear cache before running
|
|
188
|
+
thai-lint dry --clear-cache .
|
|
189
|
+
|
|
190
|
+
\b
|
|
191
|
+
# Get JSON output
|
|
192
|
+
thai-lint dry --format json .
|
|
193
|
+
"""
|
|
194
|
+
verbose: bool = ctx.obj.get("verbose", False)
|
|
195
|
+
project_root = get_project_root_from_context(ctx)
|
|
196
|
+
|
|
197
|
+
if not paths:
|
|
198
|
+
paths = (".",)
|
|
199
|
+
|
|
200
|
+
path_objs = [Path(p) for p in paths]
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
_execute_dry_lint(
|
|
204
|
+
path_objs,
|
|
205
|
+
config_file,
|
|
206
|
+
format,
|
|
207
|
+
min_lines,
|
|
208
|
+
no_cache,
|
|
209
|
+
clear_cache,
|
|
210
|
+
recursive,
|
|
211
|
+
parallel,
|
|
212
|
+
verbose,
|
|
213
|
+
project_root,
|
|
214
|
+
)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
handle_linting_error(e, verbose)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _execute_dry_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
220
|
+
path_objs: list[Path],
|
|
221
|
+
config_file: str | None,
|
|
222
|
+
format: str,
|
|
223
|
+
min_lines: int | None,
|
|
224
|
+
no_cache: bool,
|
|
225
|
+
clear_cache: bool,
|
|
226
|
+
recursive: bool,
|
|
227
|
+
parallel: bool,
|
|
228
|
+
verbose: bool,
|
|
229
|
+
project_root: Path | None = None,
|
|
230
|
+
) -> NoReturn:
|
|
231
|
+
"""Execute DRY linting."""
|
|
232
|
+
validate_paths_exist(path_objs)
|
|
233
|
+
orchestrator = _setup_dry_orchestrator(path_objs, config_file, verbose, project_root)
|
|
234
|
+
|
|
235
|
+
if config_file:
|
|
236
|
+
_load_dry_config_file(orchestrator, config_file, verbose)
|
|
237
|
+
|
|
238
|
+
_apply_dry_config_override(orchestrator, min_lines, no_cache, verbose)
|
|
239
|
+
|
|
240
|
+
if clear_cache:
|
|
241
|
+
_clear_dry_cache(orchestrator, verbose)
|
|
242
|
+
|
|
243
|
+
dry_violations = _run_dry_lint(orchestrator, path_objs, recursive, parallel)
|
|
244
|
+
|
|
245
|
+
if verbose:
|
|
246
|
+
logger.info(f"Found {len(dry_violations)} DRY violation(s)")
|
|
247
|
+
|
|
248
|
+
format_violations(dry_violations, format)
|
|
249
|
+
sys.exit(1 if dry_violations else 0)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# =============================================================================
|
|
253
|
+
# Magic Numbers Command
|
|
254
|
+
# =============================================================================
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _setup_magic_numbers_orchestrator(
|
|
258
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
259
|
+
) -> "Orchestrator":
|
|
260
|
+
"""Set up orchestrator for magic-numbers command."""
|
|
261
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _run_magic_numbers_lint(
|
|
265
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
266
|
+
) -> list[Violation]:
|
|
267
|
+
"""Execute magic-numbers lint on files or directories."""
|
|
268
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
269
|
+
return [v for v in all_violations if "magic-number" in v.rule_id]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _execute_magic_numbers_lint(params: ExecuteParams) -> NoReturn:
|
|
273
|
+
"""Execute magic-numbers lint."""
|
|
274
|
+
validate_paths_exist(params.path_objs)
|
|
275
|
+
orchestrator = _setup_magic_numbers_orchestrator(
|
|
276
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
277
|
+
)
|
|
278
|
+
magic_numbers_violations = _run_magic_numbers_lint(
|
|
279
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if params.verbose:
|
|
283
|
+
logger.info(f"Found {len(magic_numbers_violations)} magic number violation(s)")
|
|
284
|
+
|
|
285
|
+
format_violations(magic_numbers_violations, params.format)
|
|
286
|
+
sys.exit(1 if magic_numbers_violations else 0)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
magic_numbers = create_linter_command(
|
|
290
|
+
"magic-numbers",
|
|
291
|
+
_execute_magic_numbers_lint,
|
|
292
|
+
"Check for magic numbers in code.",
|
|
293
|
+
"Detects unnamed numeric literals in Python and TypeScript/JavaScript code\n"
|
|
294
|
+
" that should be extracted as named constants for better readability.",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# =============================================================================
|
|
299
|
+
# Stringly-Typed Command
|
|
300
|
+
# =============================================================================
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _setup_stringly_typed_orchestrator(
|
|
304
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
305
|
+
) -> "Orchestrator":
|
|
306
|
+
"""Set up orchestrator for stringly-typed command."""
|
|
307
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _run_stringly_typed_lint(
|
|
311
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
312
|
+
) -> list[Violation]:
|
|
313
|
+
"""Execute stringly-typed lint on files or directories."""
|
|
314
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
315
|
+
return [v for v in all_violations if "stringly-typed" in v.rule_id]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _execute_stringly_typed_lint(params: ExecuteParams) -> NoReturn:
|
|
319
|
+
"""Execute stringly-typed lint."""
|
|
320
|
+
validate_paths_exist(params.path_objs)
|
|
321
|
+
orchestrator = _setup_stringly_typed_orchestrator(
|
|
322
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
323
|
+
)
|
|
324
|
+
stringly_violations = _run_stringly_typed_lint(
|
|
325
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if params.verbose:
|
|
329
|
+
logger.info(f"Found {len(stringly_violations)} stringly-typed violation(s)")
|
|
330
|
+
|
|
331
|
+
format_violations(stringly_violations, params.format)
|
|
332
|
+
sys.exit(1 if stringly_violations else 0)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
stringly_typed = create_linter_command(
|
|
336
|
+
"stringly-typed",
|
|
337
|
+
_execute_stringly_typed_lint,
|
|
338
|
+
"Check for stringly-typed patterns in code.",
|
|
339
|
+
"Detects string patterns in Python and TypeScript/JavaScript code that should\n"
|
|
340
|
+
" use enums or typed alternatives. Finds membership validation, equality chains,\n"
|
|
341
|
+
" and function calls with limited string values across multiple files.",
|
|
342
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI commands for documentation linters (file-header)
|
|
3
|
+
|
|
4
|
+
Scope: Commands that validate documentation standards in source files
|
|
5
|
+
|
|
6
|
+
Overview: Provides CLI commands for documentation linting: file-header validates that source files
|
|
7
|
+
have proper documentation headers with required fields (Purpose, Scope, Overview, etc.) and
|
|
8
|
+
detects temporal language patterns (dates, temporal qualifiers, state change references).
|
|
9
|
+
Supports Python, TypeScript, JavaScript, Bash, Markdown, and CSS files. Integrates with the
|
|
10
|
+
orchestrator for execution.
|
|
11
|
+
|
|
12
|
+
Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities
|
|
13
|
+
|
|
14
|
+
Exports: file_header 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
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
25
|
+
|
|
26
|
+
from src.cli.linters.shared import ExecuteParams, create_linter_command
|
|
27
|
+
from src.cli.utils import execute_linting_on_paths, setup_base_orchestrator, validate_paths_exist
|
|
28
|
+
from src.core.cli_utils import format_violations
|
|
29
|
+
from src.core.types import Violation
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from src.orchestrator.core import Orchestrator
|
|
33
|
+
|
|
34
|
+
# Configure module logger
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# =============================================================================
|
|
39
|
+
# File Header Command
|
|
40
|
+
# =============================================================================
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _setup_file_header_orchestrator(
|
|
44
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
45
|
+
) -> "Orchestrator":
|
|
46
|
+
"""Set up orchestrator for file-header command."""
|
|
47
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run_file_header_lint(
|
|
51
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
52
|
+
) -> list[Violation]:
|
|
53
|
+
"""Execute file-header lint on files or directories."""
|
|
54
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
55
|
+
return [v for v in all_violations if "file-header" in v.rule_id]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _execute_file_header_lint(params: ExecuteParams) -> NoReturn:
|
|
59
|
+
"""Execute file-header lint."""
|
|
60
|
+
validate_paths_exist(params.path_objs)
|
|
61
|
+
orchestrator = _setup_file_header_orchestrator(
|
|
62
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
63
|
+
)
|
|
64
|
+
file_header_violations = _run_file_header_lint(
|
|
65
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if params.verbose:
|
|
69
|
+
logger.info(f"Found {len(file_header_violations)} file header violation(s)")
|
|
70
|
+
|
|
71
|
+
format_violations(file_header_violations, params.format)
|
|
72
|
+
sys.exit(1 if file_header_violations else 0)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
file_header = create_linter_command(
|
|
76
|
+
"file-header",
|
|
77
|
+
_execute_file_header_lint,
|
|
78
|
+
"Check file headers for mandatory fields and atemporal language.",
|
|
79
|
+
"Validates that source files have proper documentation headers containing\n"
|
|
80
|
+
" required fields (Purpose, Scope, Overview, etc.) and don't use temporal\n"
|
|
81
|
+
" language (dates, 'currently', 'now', etc.). Supports Python, TypeScript,\n"
|
|
82
|
+
" JavaScript, Bash, Markdown, and CSS files.",
|
|
83
|
+
)
|