thailint 0.12.0__py3-none-any.whl → 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +3 -0
- src/cli/config.py +12 -12
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +9 -0
- src/cli/linters/code_patterns.py +107 -257
- src/cli/linters/code_smells.py +48 -165
- src/cli/linters/documentation.py +21 -95
- src/cli/linters/performance.py +274 -0
- src/cli/linters/shared.py +232 -6
- src/cli/linters/structure.py +26 -21
- src/cli/linters/structure_quality.py +28 -21
- src/cli_main.py +3 -0
- src/config.py +2 -1
- src/core/base.py +3 -2
- src/core/cli_utils.py +3 -1
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/rule_discovery.py +5 -1
- src/core/violation_builder.py +3 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +225 -383
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +12 -0
- src/linters/collection_pipeline/continue_analyzer.py +2 -8
- src/linters/collection_pipeline/detector.py +262 -32
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +18 -35
- src/linters/collection_pipeline/suggestion_builder.py +68 -1
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +7 -4
- src/linters/dry/cache.py +7 -2
- src/linters/dry/config.py +7 -1
- src/linters/dry/constant_matcher.py +34 -25
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +48 -25
- src/linters/dry/python_analyzer.py +18 -10
- src/linters/dry/python_constant_extractor.py +51 -52
- src/linters/dry/single_statement_detector.py +14 -12
- src/linters/dry/token_hasher.py +115 -115
- src/linters/dry/typescript_analyzer.py +11 -6
- src/linters/dry/typescript_constant_extractor.py +4 -0
- src/linters/dry/typescript_statement_detector.py +208 -208
- src/linters/dry/typescript_value_extractor.py +3 -0
- src/linters/dry/violation_filter.py +1 -4
- src/linters/dry/violation_generator.py +1 -4
- src/linters/file_header/atemporal_detector.py +58 -40
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/config.py +14 -0
- src/linters/file_header/field_validator.py +5 -8
- src/linters/file_header/linter.py +19 -12
- src/linters/file_header/markdown_parser.py +6 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/linter.py +22 -8
- src/linters/file_placement/pattern_matcher.py +21 -4
- src/linters/file_placement/pattern_validator.py +21 -7
- src/linters/file_placement/rule_checker.py +2 -2
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +66 -0
- src/linters/lazy_ignores/directive_utils.py +121 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +135 -0
- src/linters/lazy_ignores/python_analyzer.py +205 -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 +69 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +131 -0
- src/linters/lbyl/__init__.py +29 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/pattern_detectors/__init__.py +25 -0
- src/linters/lbyl/pattern_detectors/base.py +46 -0
- src/linters/magic_numbers/context_analyzer.py +227 -229
- src/linters/magic_numbers/linter.py +20 -15
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -16
- src/linters/method_property/config.py +4 -1
- src/linters/method_property/linter.py +5 -10
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/linter.py +11 -6
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- 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/linter.py +6 -4
- src/linters/print_statements/python_analyzer.py +85 -81
- src/linters/print_statements/typescript_analyzer.py +6 -15
- src/linters/srp/heuristics.py +4 -4
- src/linters/srp/linter.py +12 -12
- src/linters/srp/violation_builder.py +0 -4
- src/linters/stateless_class/linter.py +30 -36
- src/linters/stateless_class/python_analyzer.py +11 -20
- src/linters/stringly_typed/config.py +4 -5
- src/linters/stringly_typed/context_filter.py +410 -410
- src/linters/stringly_typed/function_call_violation_builder.py +93 -95
- src/linters/stringly_typed/linter.py +48 -16
- src/linters/stringly_typed/python/analyzer.py +5 -1
- src/linters/stringly_typed/python/call_tracker.py +8 -5
- src/linters/stringly_typed/python/comparison_tracker.py +10 -5
- src/linters/stringly_typed/python/condition_extractor.py +3 -0
- src/linters/stringly_typed/python/conditional_detector.py +4 -1
- src/linters/stringly_typed/python/match_analyzer.py +8 -2
- src/linters/stringly_typed/python/validation_detector.py +3 -0
- src/linters/stringly_typed/storage.py +14 -14
- src/linters/stringly_typed/typescript/call_tracker.py +9 -3
- src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
- src/linters/stringly_typed/violation_generator.py +288 -259
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.14.0.dist-info/METADATA +185 -0
- thailint-0.14.0.dist-info/RECORD +199 -0
- thailint-0.12.0.dist-info/METADATA +0 -1667
- thailint-0.12.0.dist-info/RECORD +0 -164
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
src/cli/linters/code_patterns.py
CHANGED
|
@@ -1,43 +1,30 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Purpose: CLI commands for code pattern linters (print-statements, method-property, stateless-class)
|
|
2
|
+
Purpose: CLI commands for code pattern linters (print-statements, method-property, stateless-class, lazy-ignores)
|
|
3
3
|
|
|
4
4
|
Scope: Commands that detect code patterns and anti-patterns in Python code
|
|
5
5
|
|
|
6
6
|
Overview: Provides CLI commands for code pattern linting: print-statements detects print() and
|
|
7
7
|
console.log calls that should use proper logging, method-property finds methods that should be
|
|
8
|
-
@property decorators,
|
|
9
|
-
functions
|
|
10
|
-
with the orchestrator for execution.
|
|
8
|
+
@property decorators, stateless-class detects classes without state that should be module
|
|
9
|
+
functions, and lazy-ignores detects unjustified linting suppressions. Each command supports
|
|
10
|
+
standard options (config, format, recursive) and integrates with the orchestrator for execution.
|
|
11
11
|
|
|
12
12
|
Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities
|
|
13
13
|
|
|
14
|
-
Exports: print_statements command, method_property command, stateless_class command
|
|
14
|
+
Exports: print_statements command, method_property command, stateless_class command, lazy_ignores command
|
|
15
15
|
|
|
16
16
|
Interfaces: Click CLI commands registered to main CLI group
|
|
17
17
|
|
|
18
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
19
|
"""
|
|
23
|
-
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
24
20
|
|
|
25
21
|
import logging
|
|
26
22
|
import sys
|
|
27
23
|
from pathlib import Path
|
|
28
24
|
from typing import TYPE_CHECKING, NoReturn
|
|
29
25
|
|
|
30
|
-
import
|
|
31
|
-
|
|
32
|
-
from src.cli.main import cli
|
|
33
|
-
from src.cli.utils import (
|
|
34
|
-
execute_linting_on_paths,
|
|
35
|
-
format_option,
|
|
36
|
-
get_project_root_from_context,
|
|
37
|
-
handle_linting_error,
|
|
38
|
-
setup_base_orchestrator,
|
|
39
|
-
validate_paths_exist,
|
|
40
|
-
)
|
|
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
|
|
41
28
|
from src.core.cli_utils import format_violations
|
|
42
29
|
from src.core.types import Violation
|
|
43
30
|
|
|
@@ -68,90 +55,32 @@ def _run_print_statements_lint(
|
|
|
68
55
|
return [v for v in all_violations if "print-statement" in v.rule_id]
|
|
69
56
|
|
|
70
57
|
|
|
71
|
-
|
|
72
|
-
@click.argument("paths", nargs=-1, type=click.Path())
|
|
73
|
-
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
74
|
-
@format_option
|
|
75
|
-
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
76
|
-
@click.pass_context
|
|
77
|
-
def print_statements( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
78
|
-
ctx: click.Context,
|
|
79
|
-
paths: tuple[str, ...],
|
|
80
|
-
config_file: str | None,
|
|
81
|
-
format: str,
|
|
82
|
-
recursive: bool,
|
|
83
|
-
) -> None:
|
|
84
|
-
"""Check for print/console statements in code.
|
|
85
|
-
|
|
86
|
-
Detects print() calls in Python and console.log/warn/error/debug/info calls
|
|
87
|
-
in TypeScript/JavaScript that should be replaced with proper logging.
|
|
88
|
-
|
|
89
|
-
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
90
|
-
|
|
91
|
-
Examples:
|
|
92
|
-
|
|
93
|
-
\b
|
|
94
|
-
# Check current directory (all files recursively)
|
|
95
|
-
thai-lint print-statements
|
|
96
|
-
|
|
97
|
-
\b
|
|
98
|
-
# Check specific directory
|
|
99
|
-
thai-lint print-statements src/
|
|
100
|
-
|
|
101
|
-
\b
|
|
102
|
-
# Check single file
|
|
103
|
-
thai-lint print-statements src/app.py
|
|
104
|
-
|
|
105
|
-
\b
|
|
106
|
-
# Check multiple files
|
|
107
|
-
thai-lint print-statements src/app.py src/utils.ts tests/test_app.py
|
|
108
|
-
|
|
109
|
-
\b
|
|
110
|
-
# Get JSON output
|
|
111
|
-
thai-lint print-statements --format json .
|
|
112
|
-
|
|
113
|
-
\b
|
|
114
|
-
# Use custom config file
|
|
115
|
-
thai-lint print-statements --config .thailint.yaml src/
|
|
116
|
-
"""
|
|
117
|
-
verbose: bool = ctx.obj.get("verbose", False)
|
|
118
|
-
project_root = get_project_root_from_context(ctx)
|
|
119
|
-
|
|
120
|
-
if not paths:
|
|
121
|
-
paths = (".",)
|
|
122
|
-
|
|
123
|
-
path_objs = [Path(p) for p in paths]
|
|
124
|
-
|
|
125
|
-
try:
|
|
126
|
-
_execute_print_statements_lint(
|
|
127
|
-
path_objs, config_file, format, recursive, verbose, project_root
|
|
128
|
-
)
|
|
129
|
-
except Exception as e:
|
|
130
|
-
handle_linting_error(e, verbose)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _execute_print_statements_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
134
|
-
path_objs: list[Path],
|
|
135
|
-
config_file: str | None,
|
|
136
|
-
format: str,
|
|
137
|
-
recursive: bool,
|
|
138
|
-
verbose: bool,
|
|
139
|
-
project_root: Path | None = None,
|
|
140
|
-
) -> NoReturn:
|
|
58
|
+
def _execute_print_statements_lint(params: ExecuteParams) -> NoReturn:
|
|
141
59
|
"""Execute print-statements lint."""
|
|
142
|
-
validate_paths_exist(path_objs)
|
|
60
|
+
validate_paths_exist(params.path_objs)
|
|
143
61
|
orchestrator = _setup_print_statements_orchestrator(
|
|
144
|
-
path_objs, config_file, verbose, project_root
|
|
62
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
63
|
+
)
|
|
64
|
+
print_statements_violations = _run_print_statements_lint(
|
|
65
|
+
orchestrator, params.path_objs, params.recursive
|
|
145
66
|
)
|
|
146
|
-
print_statements_violations = _run_print_statements_lint(orchestrator, path_objs, recursive)
|
|
147
67
|
|
|
148
|
-
if verbose:
|
|
68
|
+
if params.verbose:
|
|
149
69
|
logger.info(f"Found {len(print_statements_violations)} print statement violation(s)")
|
|
150
70
|
|
|
151
|
-
format_violations(print_statements_violations, format)
|
|
71
|
+
format_violations(print_statements_violations, params.format)
|
|
152
72
|
sys.exit(1 if print_statements_violations else 0)
|
|
153
73
|
|
|
154
74
|
|
|
75
|
+
print_statements = create_linter_command(
|
|
76
|
+
"print-statements",
|
|
77
|
+
_execute_print_statements_lint,
|
|
78
|
+
"Check for print/console statements in code.",
|
|
79
|
+
"Detects print() calls in Python and console.log/warn/error/debug/info calls\n"
|
|
80
|
+
" in TypeScript/JavaScript that should be replaced with proper logging.",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
155
84
|
# =============================================================================
|
|
156
85
|
# Method Property Command
|
|
157
86
|
# =============================================================================
|
|
@@ -172,97 +101,33 @@ def _run_method_property_lint(
|
|
|
172
101
|
return [v for v in all_violations if "method-property" in v.rule_id]
|
|
173
102
|
|
|
174
103
|
|
|
175
|
-
|
|
176
|
-
@click.argument("paths", nargs=-1, type=click.Path())
|
|
177
|
-
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
178
|
-
@format_option
|
|
179
|
-
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
180
|
-
@click.pass_context
|
|
181
|
-
def method_property(
|
|
182
|
-
ctx: click.Context,
|
|
183
|
-
paths: tuple[str, ...],
|
|
184
|
-
config_file: str | None,
|
|
185
|
-
format: str,
|
|
186
|
-
recursive: bool,
|
|
187
|
-
) -> None:
|
|
188
|
-
"""Check for methods that should be @property decorators.
|
|
189
|
-
|
|
190
|
-
Detects Python methods that could be converted to properties following
|
|
191
|
-
Pythonic conventions:
|
|
192
|
-
- Methods returning only self._attribute or self.attribute
|
|
193
|
-
- get_* prefixed methods (Java-style getters)
|
|
194
|
-
- Simple computed values with no side effects
|
|
195
|
-
|
|
196
|
-
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
197
|
-
|
|
198
|
-
Examples:
|
|
199
|
-
|
|
200
|
-
\b
|
|
201
|
-
# Check current directory (all files recursively)
|
|
202
|
-
thai-lint method-property
|
|
203
|
-
|
|
204
|
-
\b
|
|
205
|
-
# Check specific directory
|
|
206
|
-
thai-lint method-property src/
|
|
207
|
-
|
|
208
|
-
\b
|
|
209
|
-
# Check single file
|
|
210
|
-
thai-lint method-property src/models.py
|
|
211
|
-
|
|
212
|
-
\b
|
|
213
|
-
# Check multiple files
|
|
214
|
-
thai-lint method-property src/models.py src/services.py
|
|
215
|
-
|
|
216
|
-
\b
|
|
217
|
-
# Get JSON output
|
|
218
|
-
thai-lint method-property --format json .
|
|
219
|
-
|
|
220
|
-
\b
|
|
221
|
-
# Get SARIF output for CI/CD integration
|
|
222
|
-
thai-lint method-property --format sarif src/
|
|
223
|
-
|
|
224
|
-
\b
|
|
225
|
-
# Use custom config file
|
|
226
|
-
thai-lint method-property --config .thailint.yaml src/
|
|
227
|
-
"""
|
|
228
|
-
verbose: bool = ctx.obj.get("verbose", False)
|
|
229
|
-
project_root = get_project_root_from_context(ctx)
|
|
230
|
-
|
|
231
|
-
if not paths:
|
|
232
|
-
paths = (".",)
|
|
233
|
-
|
|
234
|
-
path_objs = [Path(p) for p in paths]
|
|
235
|
-
|
|
236
|
-
try:
|
|
237
|
-
_execute_method_property_lint(
|
|
238
|
-
path_objs, config_file, format, recursive, verbose, project_root
|
|
239
|
-
)
|
|
240
|
-
except Exception as e:
|
|
241
|
-
handle_linting_error(e, verbose)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def _execute_method_property_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
245
|
-
path_objs: list[Path],
|
|
246
|
-
config_file: str | None,
|
|
247
|
-
format: str,
|
|
248
|
-
recursive: bool,
|
|
249
|
-
verbose: bool,
|
|
250
|
-
project_root: Path | None = None,
|
|
251
|
-
) -> NoReturn:
|
|
104
|
+
def _execute_method_property_lint(params: ExecuteParams) -> NoReturn:
|
|
252
105
|
"""Execute method-property lint."""
|
|
253
|
-
validate_paths_exist(path_objs)
|
|
106
|
+
validate_paths_exist(params.path_objs)
|
|
254
107
|
orchestrator = _setup_method_property_orchestrator(
|
|
255
|
-
path_objs, config_file, verbose, project_root
|
|
108
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
109
|
+
)
|
|
110
|
+
method_property_violations = _run_method_property_lint(
|
|
111
|
+
orchestrator, params.path_objs, params.recursive
|
|
256
112
|
)
|
|
257
|
-
method_property_violations = _run_method_property_lint(orchestrator, path_objs, recursive)
|
|
258
113
|
|
|
259
|
-
if verbose:
|
|
114
|
+
if params.verbose:
|
|
260
115
|
logger.info(f"Found {len(method_property_violations)} method-property violation(s)")
|
|
261
116
|
|
|
262
|
-
format_violations(method_property_violations, format)
|
|
117
|
+
format_violations(method_property_violations, params.format)
|
|
263
118
|
sys.exit(1 if method_property_violations else 0)
|
|
264
119
|
|
|
265
120
|
|
|
121
|
+
method_property = create_linter_command(
|
|
122
|
+
"method-property",
|
|
123
|
+
_execute_method_property_lint,
|
|
124
|
+
"Check for methods that should be @property decorators.",
|
|
125
|
+
"Detects Python methods that could be converted to properties following\n"
|
|
126
|
+
" Pythonic conventions: methods returning self._attribute, get_* prefixed\n"
|
|
127
|
+
" methods (Java-style getters), or simple computed values with no side effects.",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
266
131
|
# =============================================================================
|
|
267
132
|
# Stateless Class Command
|
|
268
133
|
# =============================================================================
|
|
@@ -283,90 +148,75 @@ def _run_stateless_class_lint(
|
|
|
283
148
|
return [v for v in all_violations if "stateless-class" in v.rule_id]
|
|
284
149
|
|
|
285
150
|
|
|
286
|
-
|
|
287
|
-
@click.argument("paths", nargs=-1, type=click.Path())
|
|
288
|
-
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
289
|
-
@format_option
|
|
290
|
-
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
291
|
-
@click.pass_context
|
|
292
|
-
def stateless_class(
|
|
293
|
-
ctx: click.Context,
|
|
294
|
-
paths: tuple[str, ...],
|
|
295
|
-
config_file: str | None,
|
|
296
|
-
format: str,
|
|
297
|
-
recursive: bool,
|
|
298
|
-
) -> None:
|
|
299
|
-
"""Check for stateless classes that should be module functions.
|
|
300
|
-
|
|
301
|
-
Detects Python classes that have no constructor (__init__), no instance
|
|
302
|
-
state, and 2+ methods - indicating they should be refactored to module-level
|
|
303
|
-
functions instead of using a class as a namespace.
|
|
304
|
-
|
|
305
|
-
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
306
|
-
|
|
307
|
-
Examples:
|
|
308
|
-
|
|
309
|
-
\b
|
|
310
|
-
# Check current directory (all files recursively)
|
|
311
|
-
thai-lint stateless-class
|
|
312
|
-
|
|
313
|
-
\b
|
|
314
|
-
# Check specific directory
|
|
315
|
-
thai-lint stateless-class src/
|
|
316
|
-
|
|
317
|
-
\b
|
|
318
|
-
# Check single file
|
|
319
|
-
thai-lint stateless-class src/utils.py
|
|
320
|
-
|
|
321
|
-
\b
|
|
322
|
-
# Check multiple files
|
|
323
|
-
thai-lint stateless-class src/utils.py src/helpers.py
|
|
324
|
-
|
|
325
|
-
\b
|
|
326
|
-
# Get JSON output
|
|
327
|
-
thai-lint stateless-class --format json .
|
|
328
|
-
|
|
329
|
-
\b
|
|
330
|
-
# Get SARIF output for CI/CD integration
|
|
331
|
-
thai-lint stateless-class --format sarif src/
|
|
332
|
-
|
|
333
|
-
\b
|
|
334
|
-
# Use custom config file
|
|
335
|
-
thai-lint stateless-class --config .thailint.yaml src/
|
|
336
|
-
"""
|
|
337
|
-
verbose: bool = ctx.obj.get("verbose", False)
|
|
338
|
-
project_root = get_project_root_from_context(ctx)
|
|
339
|
-
|
|
340
|
-
if not paths:
|
|
341
|
-
paths = (".",)
|
|
342
|
-
|
|
343
|
-
path_objs = [Path(p) for p in paths]
|
|
344
|
-
|
|
345
|
-
try:
|
|
346
|
-
_execute_stateless_class_lint(
|
|
347
|
-
path_objs, config_file, format, recursive, verbose, project_root
|
|
348
|
-
)
|
|
349
|
-
except Exception as e:
|
|
350
|
-
handle_linting_error(e, verbose)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
def _execute_stateless_class_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
354
|
-
path_objs: list[Path],
|
|
355
|
-
config_file: str | None,
|
|
356
|
-
format: str,
|
|
357
|
-
recursive: bool,
|
|
358
|
-
verbose: bool,
|
|
359
|
-
project_root: Path | None = None,
|
|
360
|
-
) -> NoReturn:
|
|
151
|
+
def _execute_stateless_class_lint(params: ExecuteParams) -> NoReturn:
|
|
361
152
|
"""Execute stateless-class lint."""
|
|
362
|
-
validate_paths_exist(path_objs)
|
|
153
|
+
validate_paths_exist(params.path_objs)
|
|
363
154
|
orchestrator = _setup_stateless_class_orchestrator(
|
|
364
|
-
path_objs, config_file, verbose, project_root
|
|
155
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
156
|
+
)
|
|
157
|
+
stateless_class_violations = _run_stateless_class_lint(
|
|
158
|
+
orchestrator, params.path_objs, params.recursive
|
|
365
159
|
)
|
|
366
|
-
stateless_class_violations = _run_stateless_class_lint(orchestrator, path_objs, recursive)
|
|
367
160
|
|
|
368
|
-
if verbose:
|
|
161
|
+
if params.verbose:
|
|
369
162
|
logger.info(f"Found {len(stateless_class_violations)} stateless-class violation(s)")
|
|
370
163
|
|
|
371
|
-
format_violations(stateless_class_violations, format)
|
|
164
|
+
format_violations(stateless_class_violations, params.format)
|
|
372
165
|
sys.exit(1 if stateless_class_violations else 0)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
stateless_class = create_linter_command(
|
|
169
|
+
"stateless-class",
|
|
170
|
+
_execute_stateless_class_lint,
|
|
171
|
+
"Check for stateless classes that should be module functions.",
|
|
172
|
+
"Detects Python classes that have no constructor (__init__), no instance\n"
|
|
173
|
+
" state, and 2+ methods - indicating they should be refactored to module-level\n"
|
|
174
|
+
" functions instead of using a class as a namespace.",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# =============================================================================
|
|
179
|
+
# Lazy Ignores Command
|
|
180
|
+
# =============================================================================
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _setup_lazy_ignores_orchestrator(
|
|
184
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
185
|
+
) -> "Orchestrator":
|
|
186
|
+
"""Set up orchestrator for lazy-ignores command."""
|
|
187
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _run_lazy_ignores_lint(
|
|
191
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
|
|
192
|
+
) -> list[Violation]:
|
|
193
|
+
"""Execute lazy-ignores lint on files or directories."""
|
|
194
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
195
|
+
return [v for v in all_violations if v.rule_id.startswith("lazy-ignores")]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _execute_lazy_ignores_lint(params: ExecuteParams) -> NoReturn:
|
|
199
|
+
"""Execute lazy-ignores lint."""
|
|
200
|
+
validate_paths_exist(params.path_objs)
|
|
201
|
+
orchestrator = _setup_lazy_ignores_orchestrator(
|
|
202
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
203
|
+
)
|
|
204
|
+
lazy_ignores_violations = _run_lazy_ignores_lint(
|
|
205
|
+
orchestrator, params.path_objs, params.recursive
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if params.verbose:
|
|
209
|
+
logger.info(f"Found {len(lazy_ignores_violations)} lazy-ignores violation(s)")
|
|
210
|
+
|
|
211
|
+
format_violations(lazy_ignores_violations, params.format)
|
|
212
|
+
sys.exit(1 if lazy_ignores_violations else 0)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
lazy_ignores = create_linter_command(
|
|
216
|
+
"lazy-ignores",
|
|
217
|
+
_execute_lazy_ignores_lint,
|
|
218
|
+
"Check for unjustified linting suppressions.",
|
|
219
|
+
"Detects ignore directives (noqa, type:ignore, pylint:disable, nosec) that lack\n"
|
|
220
|
+
" corresponding entries in the file header's Suppressions section. Enforces a\n"
|
|
221
|
+
" header-based suppression model requiring human approval for all linting bypasses.",
|
|
222
|
+
)
|
src/cli/linters/code_smells.py
CHANGED
|
@@ -18,10 +18,10 @@ Interfaces: Click CLI commands registered to main CLI group
|
|
|
18
18
|
|
|
19
19
|
Implementation: Click decorators for command definition, orchestrator-based linting execution
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
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)
|
|
23
24
|
"""
|
|
24
|
-
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
25
25
|
|
|
26
26
|
import logging
|
|
27
27
|
import sys
|
|
@@ -31,7 +31,12 @@ from typing import TYPE_CHECKING, Any, NoReturn
|
|
|
31
31
|
import click
|
|
32
32
|
import yaml
|
|
33
33
|
|
|
34
|
-
from src.cli.linters.shared import
|
|
34
|
+
from src.cli.linters.shared import (
|
|
35
|
+
ExecuteParams,
|
|
36
|
+
create_linter_command,
|
|
37
|
+
ensure_config_section,
|
|
38
|
+
set_config_value,
|
|
39
|
+
)
|
|
35
40
|
from src.cli.main import cli
|
|
36
41
|
from src.cli.utils import (
|
|
37
42
|
execute_linting_on_paths,
|
|
@@ -52,7 +57,7 @@ logger = logging.getLogger(__name__)
|
|
|
52
57
|
|
|
53
58
|
|
|
54
59
|
# =============================================================================
|
|
55
|
-
# DRY Command
|
|
60
|
+
# DRY Command (custom options - cannot use create_linter_command)
|
|
56
61
|
# =============================================================================
|
|
57
62
|
|
|
58
63
|
|
|
@@ -257,92 +262,32 @@ def _run_magic_numbers_lint(
|
|
|
257
262
|
return [v for v in all_violations if "magic-number" in v.rule_id]
|
|
258
263
|
|
|
259
264
|
|
|
260
|
-
|
|
261
|
-
@click.argument("paths", nargs=-1, type=click.Path())
|
|
262
|
-
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
263
|
-
@format_option
|
|
264
|
-
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
265
|
-
@click.pass_context
|
|
266
|
-
def magic_numbers( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
267
|
-
ctx: click.Context,
|
|
268
|
-
paths: tuple[str, ...],
|
|
269
|
-
config_file: str | None,
|
|
270
|
-
format: str,
|
|
271
|
-
recursive: bool,
|
|
272
|
-
) -> None:
|
|
273
|
-
"""Check for magic numbers in code.
|
|
274
|
-
|
|
275
|
-
Detects unnamed numeric literals in Python and TypeScript/JavaScript code
|
|
276
|
-
that should be extracted as named constants for better readability.
|
|
277
|
-
|
|
278
|
-
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
279
|
-
|
|
280
|
-
Examples:
|
|
281
|
-
|
|
282
|
-
\b
|
|
283
|
-
# Check current directory (all files recursively)
|
|
284
|
-
thai-lint magic-numbers
|
|
285
|
-
|
|
286
|
-
\b
|
|
287
|
-
# Check specific directory
|
|
288
|
-
thai-lint magic-numbers src/
|
|
289
|
-
|
|
290
|
-
\b
|
|
291
|
-
# Check single file
|
|
292
|
-
thai-lint magic-numbers src/app.py
|
|
293
|
-
|
|
294
|
-
\b
|
|
295
|
-
# Check multiple files
|
|
296
|
-
thai-lint magic-numbers src/app.py src/utils.py tests/test_app.py
|
|
297
|
-
|
|
298
|
-
\b
|
|
299
|
-
# Check mix of files and directories
|
|
300
|
-
thai-lint magic-numbers src/app.py tests/
|
|
301
|
-
|
|
302
|
-
\b
|
|
303
|
-
# Get JSON output
|
|
304
|
-
thai-lint magic-numbers --format json .
|
|
305
|
-
|
|
306
|
-
\b
|
|
307
|
-
# Use custom config file
|
|
308
|
-
thai-lint magic-numbers --config .thailint.yaml src/
|
|
309
|
-
"""
|
|
310
|
-
verbose: bool = ctx.obj.get("verbose", False)
|
|
311
|
-
project_root = get_project_root_from_context(ctx)
|
|
312
|
-
|
|
313
|
-
if not paths:
|
|
314
|
-
paths = (".",)
|
|
315
|
-
|
|
316
|
-
path_objs = [Path(p) for p in paths]
|
|
317
|
-
|
|
318
|
-
try:
|
|
319
|
-
_execute_magic_numbers_lint(
|
|
320
|
-
path_objs, config_file, format, recursive, verbose, project_root
|
|
321
|
-
)
|
|
322
|
-
except Exception as e:
|
|
323
|
-
handle_linting_error(e, verbose)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def _execute_magic_numbers_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
327
|
-
path_objs: list[Path],
|
|
328
|
-
config_file: str | None,
|
|
329
|
-
format: str,
|
|
330
|
-
recursive: bool,
|
|
331
|
-
verbose: bool,
|
|
332
|
-
project_root: Path | None = None,
|
|
333
|
-
) -> NoReturn:
|
|
265
|
+
def _execute_magic_numbers_lint(params: ExecuteParams) -> NoReturn:
|
|
334
266
|
"""Execute magic-numbers lint."""
|
|
335
|
-
validate_paths_exist(path_objs)
|
|
336
|
-
orchestrator = _setup_magic_numbers_orchestrator(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
267
|
+
validate_paths_exist(params.path_objs)
|
|
268
|
+
orchestrator = _setup_magic_numbers_orchestrator(
|
|
269
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
270
|
+
)
|
|
271
|
+
magic_numbers_violations = _run_magic_numbers_lint(
|
|
272
|
+
orchestrator, params.path_objs, params.recursive
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if params.verbose:
|
|
340
276
|
logger.info(f"Found {len(magic_numbers_violations)} magic number violation(s)")
|
|
341
277
|
|
|
342
|
-
format_violations(magic_numbers_violations, format)
|
|
278
|
+
format_violations(magic_numbers_violations, params.format)
|
|
343
279
|
sys.exit(1 if magic_numbers_violations else 0)
|
|
344
280
|
|
|
345
281
|
|
|
282
|
+
magic_numbers = create_linter_command(
|
|
283
|
+
"magic-numbers",
|
|
284
|
+
_execute_magic_numbers_lint,
|
|
285
|
+
"Check for magic numbers in code.",
|
|
286
|
+
"Detects unnamed numeric literals in Python and TypeScript/JavaScript code\n"
|
|
287
|
+
" that should be extracted as named constants for better readability.",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
346
291
|
# =============================================================================
|
|
347
292
|
# Stringly-Typed Command
|
|
348
293
|
# =============================================================================
|
|
@@ -363,88 +308,26 @@ def _run_stringly_typed_lint(
|
|
|
363
308
|
return [v for v in all_violations if "stringly-typed" in v.rule_id]
|
|
364
309
|
|
|
365
310
|
|
|
366
|
-
|
|
367
|
-
@click.argument("paths", nargs=-1, type=click.Path())
|
|
368
|
-
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
369
|
-
@format_option
|
|
370
|
-
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
371
|
-
@click.pass_context
|
|
372
|
-
def stringly_typed( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
373
|
-
ctx: click.Context,
|
|
374
|
-
paths: tuple[str, ...],
|
|
375
|
-
config_file: str | None,
|
|
376
|
-
format: str,
|
|
377
|
-
recursive: bool,
|
|
378
|
-
) -> None:
|
|
379
|
-
"""Check for stringly-typed patterns in code.
|
|
380
|
-
|
|
381
|
-
Detects string patterns in Python and TypeScript/JavaScript code that should
|
|
382
|
-
use enums or typed alternatives. Finds membership validation, equality chains,
|
|
383
|
-
and function calls with limited string values across multiple files.
|
|
384
|
-
|
|
385
|
-
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
386
|
-
|
|
387
|
-
Examples:
|
|
388
|
-
|
|
389
|
-
\b
|
|
390
|
-
# Check current directory (all files recursively)
|
|
391
|
-
thai-lint stringly-typed
|
|
392
|
-
|
|
393
|
-
\b
|
|
394
|
-
# Check specific directory
|
|
395
|
-
thai-lint stringly-typed src/
|
|
396
|
-
|
|
397
|
-
\b
|
|
398
|
-
# Check single file
|
|
399
|
-
thai-lint stringly-typed src/handlers.py
|
|
400
|
-
|
|
401
|
-
\b
|
|
402
|
-
# Check multiple files
|
|
403
|
-
thai-lint stringly-typed src/handlers.py src/services.py
|
|
404
|
-
|
|
405
|
-
\b
|
|
406
|
-
# Get JSON output
|
|
407
|
-
thai-lint stringly-typed --format json .
|
|
408
|
-
|
|
409
|
-
\b
|
|
410
|
-
# Get SARIF output for IDE integration
|
|
411
|
-
thai-lint stringly-typed --format sarif .
|
|
412
|
-
|
|
413
|
-
\b
|
|
414
|
-
# Use custom config file
|
|
415
|
-
thai-lint stringly-typed --config .thailint.yaml src/
|
|
416
|
-
"""
|
|
417
|
-
verbose: bool = ctx.obj.get("verbose", False)
|
|
418
|
-
project_root = get_project_root_from_context(ctx)
|
|
419
|
-
|
|
420
|
-
if not paths:
|
|
421
|
-
paths = (".",)
|
|
422
|
-
|
|
423
|
-
path_objs = [Path(p) for p in paths]
|
|
424
|
-
|
|
425
|
-
try:
|
|
426
|
-
_execute_stringly_typed_lint(
|
|
427
|
-
path_objs, config_file, format, recursive, verbose, project_root
|
|
428
|
-
)
|
|
429
|
-
except Exception as e:
|
|
430
|
-
handle_linting_error(e, verbose)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
def _execute_stringly_typed_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
434
|
-
path_objs: list[Path],
|
|
435
|
-
config_file: str | None,
|
|
436
|
-
format: str,
|
|
437
|
-
recursive: bool,
|
|
438
|
-
verbose: bool,
|
|
439
|
-
project_root: Path | None = None,
|
|
440
|
-
) -> NoReturn:
|
|
311
|
+
def _execute_stringly_typed_lint(params: ExecuteParams) -> NoReturn:
|
|
441
312
|
"""Execute stringly-typed lint."""
|
|
442
|
-
validate_paths_exist(path_objs)
|
|
443
|
-
orchestrator = _setup_stringly_typed_orchestrator(
|
|
444
|
-
|
|
313
|
+
validate_paths_exist(params.path_objs)
|
|
314
|
+
orchestrator = _setup_stringly_typed_orchestrator(
|
|
315
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
316
|
+
)
|
|
317
|
+
stringly_violations = _run_stringly_typed_lint(orchestrator, params.path_objs, params.recursive)
|
|
445
318
|
|
|
446
|
-
if verbose:
|
|
319
|
+
if params.verbose:
|
|
447
320
|
logger.info(f"Found {len(stringly_violations)} stringly-typed violation(s)")
|
|
448
321
|
|
|
449
|
-
format_violations(stringly_violations, format)
|
|
322
|
+
format_violations(stringly_violations, params.format)
|
|
450
323
|
sys.exit(1 if stringly_violations else 0)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
stringly_typed = create_linter_command(
|
|
327
|
+
"stringly-typed",
|
|
328
|
+
_execute_stringly_typed_lint,
|
|
329
|
+
"Check for stringly-typed patterns in code.",
|
|
330
|
+
"Detects string patterns in Python and TypeScript/JavaScript code that should\n"
|
|
331
|
+
" use enums or typed alternatives. Finds membership validation, equality chains,\n"
|
|
332
|
+
" and function calls with limited string values across multiple files.",
|
|
333
|
+
)
|