thailint 0.9.0__py3-none-any.whl → 0.11.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/__init__.py +1 -0
- src/cli/__init__.py +27 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +478 -0
- src/cli/linters/__init__.py +58 -0
- src/cli/linters/code_patterns.py +372 -0
- src/cli/linters/code_smells.py +343 -0
- src/cli/linters/documentation.py +155 -0
- src/cli/linters/shared.py +89 -0
- src/cli/linters/structure.py +313 -0
- src/cli/linters/structure_quality.py +316 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +375 -0
- src/cli_main.py +34 -0
- src/config.py +2 -3
- src/core/rule_discovery.py +43 -10
- src/core/types.py +13 -0
- src/core/violation_utils.py +69 -0
- src/linter_config/ignore.py +32 -16
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/config.py +63 -0
- src/linters/collection_pipeline/continue_analyzer.py +100 -0
- src/linters/collection_pipeline/detector.py +130 -0
- src/linters/collection_pipeline/linter.py +437 -0
- src/linters/collection_pipeline/suggestion_builder.py +63 -0
- src/linters/dry/block_filter.py +99 -9
- src/linters/dry/cache.py +94 -6
- src/linters/dry/config.py +47 -10
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +214 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/linter.py +89 -48
- src/linters/dry/python_analyzer.py +44 -431
- src/linters/dry/python_constant_extractor.py +101 -0
- src/linters/dry/single_statement_detector.py +415 -0
- src/linters/dry/token_hasher.py +5 -5
- src/linters/dry/typescript_analyzer.py +63 -382
- src/linters/dry/typescript_constant_extractor.py +134 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +66 -0
- src/linters/file_header/linter.py +9 -13
- src/linters/file_placement/linter.py +30 -10
- src/linters/file_placement/pattern_matcher.py +19 -5
- src/linters/magic_numbers/linter.py +8 -67
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/nesting/linter.py +12 -9
- src/linters/print_statements/linter.py +7 -24
- src/linters/srp/class_analyzer.py +9 -9
- src/linters/srp/heuristics.py +6 -5
- src/linters/srp/linter.py +4 -5
- src/linters/stateless_class/linter.py +2 -2
- src/linters/stringly_typed/__init__.py +23 -0
- src/linters/stringly_typed/config.py +165 -0
- src/linters/stringly_typed/python/__init__.py +29 -0
- src/linters/stringly_typed/python/analyzer.py +198 -0
- src/linters/stringly_typed/python/condition_extractor.py +131 -0
- src/linters/stringly_typed/python/conditional_detector.py +176 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +88 -0
- src/linters/stringly_typed/python/validation_detector.py +186 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/orchestrator/core.py +241 -12
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/METADATA +116 -3
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/RECORD +67 -29
- thailint-0.11.0.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -2014
- thailint-0.9.0.dist-info/entry_points.txt +0 -4
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/WHEEL +0 -0
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI commands for code pattern linters (print-statements, method-property, stateless-class)
|
|
3
|
+
|
|
4
|
+
Scope: Commands that detect code patterns and anti-patterns in Python code
|
|
5
|
+
|
|
6
|
+
Overview: Provides CLI commands for code pattern linting: print-statements detects print() and
|
|
7
|
+
console.log calls that should use proper logging, method-property finds methods that should be
|
|
8
|
+
@property decorators, and stateless-class detects classes without state that should be module
|
|
9
|
+
functions. Each command supports standard options (config, format, recursive) and integrates
|
|
10
|
+
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
|
+
|
|
14
|
+
Exports: print_statements command, method_property command, stateless_class 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
|
+
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
29
|
+
|
|
30
|
+
import click
|
|
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
|
+
)
|
|
41
|
+
from src.core.cli_utils import format_violations
|
|
42
|
+
from src.core.types import Violation
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from src.orchestrator.core import Orchestrator
|
|
46
|
+
|
|
47
|
+
# Configure module logger
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# =============================================================================
|
|
52
|
+
# Print Statements Command
|
|
53
|
+
# =============================================================================
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _setup_print_statements_orchestrator(
|
|
57
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
58
|
+
) -> "Orchestrator":
|
|
59
|
+
"""Set up orchestrator for print-statements command."""
|
|
60
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _run_print_statements_lint(
|
|
64
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
|
|
65
|
+
) -> list[Violation]:
|
|
66
|
+
"""Execute print-statements lint on files or directories."""
|
|
67
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
68
|
+
return [v for v in all_violations if "print-statement" in v.rule_id]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@cli.command("print-statements")
|
|
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:
|
|
141
|
+
"""Execute print-statements lint."""
|
|
142
|
+
validate_paths_exist(path_objs)
|
|
143
|
+
orchestrator = _setup_print_statements_orchestrator(
|
|
144
|
+
path_objs, config_file, verbose, project_root
|
|
145
|
+
)
|
|
146
|
+
print_statements_violations = _run_print_statements_lint(orchestrator, path_objs, recursive)
|
|
147
|
+
|
|
148
|
+
if verbose:
|
|
149
|
+
logger.info(f"Found {len(print_statements_violations)} print statement violation(s)")
|
|
150
|
+
|
|
151
|
+
format_violations(print_statements_violations, format)
|
|
152
|
+
sys.exit(1 if print_statements_violations else 0)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# =============================================================================
|
|
156
|
+
# Method Property Command
|
|
157
|
+
# =============================================================================
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _setup_method_property_orchestrator(
|
|
161
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
162
|
+
) -> "Orchestrator":
|
|
163
|
+
"""Set up orchestrator for method-property command."""
|
|
164
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _run_method_property_lint(
|
|
168
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
|
|
169
|
+
) -> list[Violation]:
|
|
170
|
+
"""Execute method-property lint on files or directories."""
|
|
171
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
172
|
+
return [v for v in all_violations if "method-property" in v.rule_id]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@cli.command("method-property")
|
|
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:
|
|
252
|
+
"""Execute method-property lint."""
|
|
253
|
+
validate_paths_exist(path_objs)
|
|
254
|
+
orchestrator = _setup_method_property_orchestrator(
|
|
255
|
+
path_objs, config_file, verbose, project_root
|
|
256
|
+
)
|
|
257
|
+
method_property_violations = _run_method_property_lint(orchestrator, path_objs, recursive)
|
|
258
|
+
|
|
259
|
+
if verbose:
|
|
260
|
+
logger.info(f"Found {len(method_property_violations)} method-property violation(s)")
|
|
261
|
+
|
|
262
|
+
format_violations(method_property_violations, format)
|
|
263
|
+
sys.exit(1 if method_property_violations else 0)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# =============================================================================
|
|
267
|
+
# Stateless Class Command
|
|
268
|
+
# =============================================================================
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _setup_stateless_class_orchestrator(
|
|
272
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
273
|
+
) -> "Orchestrator":
|
|
274
|
+
"""Set up orchestrator for stateless-class command."""
|
|
275
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _run_stateless_class_lint(
|
|
279
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
|
|
280
|
+
) -> list[Violation]:
|
|
281
|
+
"""Execute stateless-class lint on files or directories."""
|
|
282
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
283
|
+
return [v for v in all_violations if "stateless-class" in v.rule_id]
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@cli.command("stateless-class")
|
|
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:
|
|
361
|
+
"""Execute stateless-class lint."""
|
|
362
|
+
validate_paths_exist(path_objs)
|
|
363
|
+
orchestrator = _setup_stateless_class_orchestrator(
|
|
364
|
+
path_objs, config_file, verbose, project_root
|
|
365
|
+
)
|
|
366
|
+
stateless_class_violations = _run_stateless_class_lint(orchestrator, path_objs, recursive)
|
|
367
|
+
|
|
368
|
+
if verbose:
|
|
369
|
+
logger.info(f"Found {len(stateless_class_violations)} stateless-class violation(s)")
|
|
370
|
+
|
|
371
|
+
format_violations(stateless_class_violations, format)
|
|
372
|
+
sys.exit(1 if stateless_class_violations else 0)
|