thailint 0.10.0__py3-none-any.whl → 0.12.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.
Files changed (76) hide show
  1. src/__init__.py +1 -0
  2. src/cli/__init__.py +27 -0
  3. src/cli/__main__.py +22 -0
  4. src/cli/config.py +478 -0
  5. src/cli/linters/__init__.py +58 -0
  6. src/cli/linters/code_patterns.py +372 -0
  7. src/cli/linters/code_smells.py +450 -0
  8. src/cli/linters/documentation.py +155 -0
  9. src/cli/linters/shared.py +89 -0
  10. src/cli/linters/structure.py +313 -0
  11. src/cli/linters/structure_quality.py +316 -0
  12. src/cli/main.py +120 -0
  13. src/cli/utils.py +395 -0
  14. src/cli_main.py +34 -0
  15. src/core/types.py +13 -0
  16. src/core/violation_utils.py +69 -0
  17. src/linter_config/ignore.py +32 -16
  18. src/linters/collection_pipeline/linter.py +2 -2
  19. src/linters/dry/block_filter.py +97 -1
  20. src/linters/dry/cache.py +94 -6
  21. src/linters/dry/config.py +47 -10
  22. src/linters/dry/constant.py +92 -0
  23. src/linters/dry/constant_matcher.py +214 -0
  24. src/linters/dry/constant_violation_builder.py +98 -0
  25. src/linters/dry/linter.py +89 -48
  26. src/linters/dry/python_analyzer.py +12 -415
  27. src/linters/dry/python_constant_extractor.py +101 -0
  28. src/linters/dry/single_statement_detector.py +415 -0
  29. src/linters/dry/token_hasher.py +5 -5
  30. src/linters/dry/typescript_analyzer.py +5 -354
  31. src/linters/dry/typescript_constant_extractor.py +134 -0
  32. src/linters/dry/typescript_statement_detector.py +255 -0
  33. src/linters/dry/typescript_value_extractor.py +66 -0
  34. src/linters/file_header/linter.py +2 -2
  35. src/linters/file_placement/linter.py +2 -2
  36. src/linters/file_placement/pattern_matcher.py +19 -5
  37. src/linters/magic_numbers/linter.py +8 -67
  38. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  39. src/linters/nesting/linter.py +12 -9
  40. src/linters/print_statements/linter.py +7 -24
  41. src/linters/srp/class_analyzer.py +9 -9
  42. src/linters/srp/heuristics.py +2 -2
  43. src/linters/srp/linter.py +2 -2
  44. src/linters/stateless_class/linter.py +2 -2
  45. src/linters/stringly_typed/__init__.py +36 -0
  46. src/linters/stringly_typed/config.py +190 -0
  47. src/linters/stringly_typed/context_filter.py +451 -0
  48. src/linters/stringly_typed/function_call_violation_builder.py +137 -0
  49. src/linters/stringly_typed/ignore_checker.py +102 -0
  50. src/linters/stringly_typed/ignore_utils.py +51 -0
  51. src/linters/stringly_typed/linter.py +344 -0
  52. src/linters/stringly_typed/python/__init__.py +33 -0
  53. src/linters/stringly_typed/python/analyzer.py +344 -0
  54. src/linters/stringly_typed/python/call_tracker.py +172 -0
  55. src/linters/stringly_typed/python/comparison_tracker.py +252 -0
  56. src/linters/stringly_typed/python/condition_extractor.py +131 -0
  57. src/linters/stringly_typed/python/conditional_detector.py +176 -0
  58. src/linters/stringly_typed/python/constants.py +21 -0
  59. src/linters/stringly_typed/python/match_analyzer.py +88 -0
  60. src/linters/stringly_typed/python/validation_detector.py +186 -0
  61. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  62. src/linters/stringly_typed/storage.py +630 -0
  63. src/linters/stringly_typed/storage_initializer.py +45 -0
  64. src/linters/stringly_typed/typescript/__init__.py +28 -0
  65. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  66. src/linters/stringly_typed/typescript/call_tracker.py +329 -0
  67. src/linters/stringly_typed/typescript/comparison_tracker.py +372 -0
  68. src/linters/stringly_typed/violation_generator.py +376 -0
  69. src/orchestrator/core.py +241 -12
  70. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/METADATA +9 -3
  71. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/RECORD +74 -28
  72. thailint-0.12.0.dist-info/entry_points.txt +4 -0
  73. src/cli.py +0 -2141
  74. thailint-0.10.0.dist-info/entry_points.txt +0 -4
  75. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/WHEEL +0 -0
  76. {thailint-0.10.0.dist-info → thailint-0.12.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)