thailint 0.13.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.
Files changed (32) hide show
  1. src/cli/linters/__init__.py +6 -0
  2. src/cli/linters/code_patterns.py +75 -333
  3. src/cli/linters/code_smells.py +47 -168
  4. src/cli/linters/documentation.py +21 -98
  5. src/cli/linters/performance.py +274 -0
  6. src/cli/linters/shared.py +232 -6
  7. src/cli/linters/structure.py +23 -21
  8. src/cli/linters/structure_quality.py +25 -21
  9. src/core/linter_utils.py +91 -6
  10. src/linters/file_header/atemporal_detector.py +54 -40
  11. src/linters/file_header/config.py +14 -0
  12. src/linters/lazy_ignores/python_analyzer.py +5 -1
  13. src/linters/lazy_ignores/types.py +2 -0
  14. src/linters/method_property/config.py +0 -1
  15. src/linters/method_property/linter.py +0 -6
  16. src/linters/nesting/linter.py +11 -6
  17. src/linters/nesting/violation_builder.py +1 -0
  18. src/linters/performance/__init__.py +91 -0
  19. src/linters/performance/config.py +43 -0
  20. src/linters/performance/constants.py +49 -0
  21. src/linters/performance/linter.py +149 -0
  22. src/linters/performance/python_analyzer.py +365 -0
  23. src/linters/performance/regex_analyzer.py +312 -0
  24. src/linters/performance/regex_linter.py +139 -0
  25. src/linters/performance/typescript_analyzer.py +236 -0
  26. src/linters/performance/violation_builder.py +160 -0
  27. src/templates/thailint_config_template.yaml +30 -0
  28. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/METADATA +3 -2
  29. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/RECORD +32 -22
  30. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
  31. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
  32. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -27,6 +27,7 @@ from src.cli.linters import ( # noqa: F401
27
27
  code_patterns,
28
28
  code_smells,
29
29
  documentation,
30
+ performance,
30
31
  structure,
31
32
  structure_quality,
32
33
  )
@@ -39,6 +40,7 @@ from src.cli.linters.code_patterns import (
39
40
  )
40
41
  from src.cli.linters.code_smells import dry, magic_numbers
41
42
  from src.cli.linters.documentation import file_header
43
+ from src.cli.linters.performance import perf, regex_in_loop, string_concat_loop
42
44
  from src.cli.linters.structure import file_placement, pipeline
43
45
  from src.cli.linters.structure_quality import nesting, srp
44
46
 
@@ -58,4 +60,8 @@ __all__ = [
58
60
  "pipeline",
59
61
  # Documentation commands
60
62
  "file_header",
63
+ # Performance commands
64
+ "perf",
65
+ "string_concat_loop",
66
+ "regex_in_loop",
61
67
  ]
@@ -16,31 +16,15 @@ Exports: print_statements command, method_property command, stateless_class comm
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
-
23
- Suppressions:
24
- - too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
25
19
  """
26
- # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
27
20
 
28
21
  import logging
29
22
  import sys
30
23
  from pathlib import Path
31
24
  from typing import TYPE_CHECKING, NoReturn
32
25
 
33
- import click
34
-
35
- from src.cli.main import cli
36
- from src.cli.utils import (
37
- execute_linting_on_paths,
38
- format_option,
39
- get_project_root_from_context,
40
- handle_linting_error,
41
- setup_base_orchestrator,
42
- validate_paths_exist,
43
- )
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
44
28
  from src.core.cli_utils import format_violations
45
29
  from src.core.types import Violation
46
30
 
@@ -71,90 +55,32 @@ def _run_print_statements_lint(
71
55
  return [v for v in all_violations if "print-statement" in v.rule_id]
72
56
 
73
57
 
74
- @cli.command("print-statements")
75
- @click.argument("paths", nargs=-1, type=click.Path())
76
- @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
77
- @format_option
78
- @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
79
- @click.pass_context
80
- def print_statements( # pylint: disable=too-many-arguments,too-many-positional-arguments
81
- ctx: click.Context,
82
- paths: tuple[str, ...],
83
- config_file: str | None,
84
- format: str,
85
- recursive: bool,
86
- ) -> None:
87
- """Check for print/console statements in code.
88
-
89
- Detects print() calls in Python and console.log/warn/error/debug/info calls
90
- in TypeScript/JavaScript that should be replaced with proper logging.
91
-
92
- PATHS: Files or directories to lint (defaults to current directory if none provided)
93
-
94
- Examples:
95
-
96
- \b
97
- # Check current directory (all files recursively)
98
- thai-lint print-statements
99
-
100
- \b
101
- # Check specific directory
102
- thai-lint print-statements src/
103
-
104
- \b
105
- # Check single file
106
- thai-lint print-statements src/app.py
107
-
108
- \b
109
- # Check multiple files
110
- thai-lint print-statements src/app.py src/utils.ts tests/test_app.py
111
-
112
- \b
113
- # Get JSON output
114
- thai-lint print-statements --format json .
115
-
116
- \b
117
- # Use custom config file
118
- thai-lint print-statements --config .thailint.yaml src/
119
- """
120
- verbose: bool = ctx.obj.get("verbose", False)
121
- project_root = get_project_root_from_context(ctx)
122
-
123
- if not paths:
124
- paths = (".",)
125
-
126
- path_objs = [Path(p) for p in paths]
127
-
128
- try:
129
- _execute_print_statements_lint(
130
- path_objs, config_file, format, recursive, verbose, project_root
131
- )
132
- except Exception as e:
133
- handle_linting_error(e, verbose)
134
-
135
-
136
- def _execute_print_statements_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
137
- path_objs: list[Path],
138
- config_file: str | None,
139
- format: str,
140
- recursive: bool,
141
- verbose: bool,
142
- project_root: Path | None = None,
143
- ) -> NoReturn:
58
+ def _execute_print_statements_lint(params: ExecuteParams) -> NoReturn:
144
59
  """Execute print-statements lint."""
145
- validate_paths_exist(path_objs)
60
+ validate_paths_exist(params.path_objs)
146
61
  orchestrator = _setup_print_statements_orchestrator(
147
- 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
148
66
  )
149
- print_statements_violations = _run_print_statements_lint(orchestrator, path_objs, recursive)
150
67
 
151
- if verbose:
68
+ if params.verbose:
152
69
  logger.info(f"Found {len(print_statements_violations)} print statement violation(s)")
153
70
 
154
- format_violations(print_statements_violations, format)
71
+ format_violations(print_statements_violations, params.format)
155
72
  sys.exit(1 if print_statements_violations else 0)
156
73
 
157
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
+
158
84
  # =============================================================================
159
85
  # Method Property Command
160
86
  # =============================================================================
@@ -175,97 +101,33 @@ def _run_method_property_lint(
175
101
  return [v for v in all_violations if "method-property" in v.rule_id]
176
102
 
177
103
 
178
- @cli.command("method-property")
179
- @click.argument("paths", nargs=-1, type=click.Path())
180
- @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
181
- @format_option
182
- @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
183
- @click.pass_context
184
- def method_property(
185
- ctx: click.Context,
186
- paths: tuple[str, ...],
187
- config_file: str | None,
188
- format: str,
189
- recursive: bool,
190
- ) -> None:
191
- """Check for methods that should be @property decorators.
192
-
193
- Detects Python methods that could be converted to properties following
194
- Pythonic conventions:
195
- - Methods returning only self._attribute or self.attribute
196
- - get_* prefixed methods (Java-style getters)
197
- - Simple computed values with no side effects
198
-
199
- PATHS: Files or directories to lint (defaults to current directory if none provided)
200
-
201
- Examples:
202
-
203
- \b
204
- # Check current directory (all files recursively)
205
- thai-lint method-property
206
-
207
- \b
208
- # Check specific directory
209
- thai-lint method-property src/
210
-
211
- \b
212
- # Check single file
213
- thai-lint method-property src/models.py
214
-
215
- \b
216
- # Check multiple files
217
- thai-lint method-property src/models.py src/services.py
218
-
219
- \b
220
- # Get JSON output
221
- thai-lint method-property --format json .
222
-
223
- \b
224
- # Get SARIF output for CI/CD integration
225
- thai-lint method-property --format sarif src/
226
-
227
- \b
228
- # Use custom config file
229
- thai-lint method-property --config .thailint.yaml src/
230
- """
231
- verbose: bool = ctx.obj.get("verbose", False)
232
- project_root = get_project_root_from_context(ctx)
233
-
234
- if not paths:
235
- paths = (".",)
236
-
237
- path_objs = [Path(p) for p in paths]
238
-
239
- try:
240
- _execute_method_property_lint(
241
- path_objs, config_file, format, recursive, verbose, project_root
242
- )
243
- except Exception as e:
244
- handle_linting_error(e, verbose)
245
-
246
-
247
- def _execute_method_property_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
248
- path_objs: list[Path],
249
- config_file: str | None,
250
- format: str,
251
- recursive: bool,
252
- verbose: bool,
253
- project_root: Path | None = None,
254
- ) -> NoReturn:
104
+ def _execute_method_property_lint(params: ExecuteParams) -> NoReturn:
255
105
  """Execute method-property lint."""
256
- validate_paths_exist(path_objs)
106
+ validate_paths_exist(params.path_objs)
257
107
  orchestrator = _setup_method_property_orchestrator(
258
- 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
259
112
  )
260
- method_property_violations = _run_method_property_lint(orchestrator, path_objs, recursive)
261
113
 
262
- if verbose:
114
+ if params.verbose:
263
115
  logger.info(f"Found {len(method_property_violations)} method-property violation(s)")
264
116
 
265
- format_violations(method_property_violations, format)
117
+ format_violations(method_property_violations, params.format)
266
118
  sys.exit(1 if method_property_violations else 0)
267
119
 
268
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
+
269
131
  # =============================================================================
270
132
  # Stateless Class Command
271
133
  # =============================================================================
@@ -286,95 +148,33 @@ def _run_stateless_class_lint(
286
148
  return [v for v in all_violations if "stateless-class" in v.rule_id]
287
149
 
288
150
 
289
- @cli.command("stateless-class")
290
- @click.argument("paths", nargs=-1, type=click.Path())
291
- @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
292
- @format_option
293
- @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
294
- @click.pass_context
295
- def stateless_class(
296
- ctx: click.Context,
297
- paths: tuple[str, ...],
298
- config_file: str | None,
299
- format: str,
300
- recursive: bool,
301
- ) -> None:
302
- """Check for stateless classes that should be module functions.
303
-
304
- Detects Python classes that have no constructor (__init__), no instance
305
- state, and 2+ methods - indicating they should be refactored to module-level
306
- functions instead of using a class as a namespace.
307
-
308
- PATHS: Files or directories to lint (defaults to current directory if none provided)
309
-
310
- Examples:
311
-
312
- \b
313
- # Check current directory (all files recursively)
314
- thai-lint stateless-class
315
-
316
- \b
317
- # Check specific directory
318
- thai-lint stateless-class src/
319
-
320
- \b
321
- # Check single file
322
- thai-lint stateless-class src/utils.py
323
-
324
- \b
325
- # Check multiple files
326
- thai-lint stateless-class src/utils.py src/helpers.py
327
-
328
- \b
329
- # Get JSON output
330
- thai-lint stateless-class --format json .
331
-
332
- \b
333
- # Get SARIF output for CI/CD integration
334
- thai-lint stateless-class --format sarif src/
335
-
336
- \b
337
- # Use custom config file
338
- thai-lint stateless-class --config .thailint.yaml src/
339
- """
340
- verbose: bool = ctx.obj.get("verbose", False)
341
- project_root = get_project_root_from_context(ctx)
342
-
343
- if not paths:
344
- paths = (".",)
345
-
346
- path_objs = [Path(p) for p in paths]
347
-
348
- try:
349
- _execute_stateless_class_lint(
350
- path_objs, config_file, format, recursive, verbose, project_root
351
- )
352
- except Exception as e:
353
- handle_linting_error(e, verbose)
354
-
355
-
356
- def _execute_stateless_class_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
357
- path_objs: list[Path],
358
- config_file: str | None,
359
- format: str,
360
- recursive: bool,
361
- verbose: bool,
362
- project_root: Path | None = None,
363
- ) -> NoReturn:
151
+ def _execute_stateless_class_lint(params: ExecuteParams) -> NoReturn:
364
152
  """Execute stateless-class lint."""
365
- validate_paths_exist(path_objs)
153
+ validate_paths_exist(params.path_objs)
366
154
  orchestrator = _setup_stateless_class_orchestrator(
367
- 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
368
159
  )
369
- stateless_class_violations = _run_stateless_class_lint(orchestrator, path_objs, recursive)
370
160
 
371
- if verbose:
161
+ if params.verbose:
372
162
  logger.info(f"Found {len(stateless_class_violations)} stateless-class violation(s)")
373
163
 
374
- format_violations(stateless_class_violations, format)
164
+ format_violations(stateless_class_violations, params.format)
375
165
  sys.exit(1 if stateless_class_violations else 0)
376
166
 
377
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
+
378
178
  # =============================================================================
379
179
  # Lazy Ignores Command
380
180
  # =============================================================================
@@ -395,86 +195,28 @@ def _run_lazy_ignores_lint(
395
195
  return [v for v in all_violations if v.rule_id.startswith("lazy-ignores")]
396
196
 
397
197
 
398
- @cli.command("lazy-ignores")
399
- @click.argument("paths", nargs=-1, type=click.Path())
400
- @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
401
- @format_option
402
- @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
403
- @click.pass_context
404
- def lazy_ignores(
405
- ctx: click.Context,
406
- paths: tuple[str, ...],
407
- config_file: str | None,
408
- format: str,
409
- recursive: bool,
410
- ) -> None:
411
- """Check for unjustified linting suppressions.
412
-
413
- Detects ignore directives (noqa, type:ignore, pylint:disable, nosec) that lack
414
- corresponding entries in the file header's Suppressions section. Enforces a
415
- header-based suppression model requiring human approval for all linting bypasses.
416
-
417
- PATHS: Files or directories to lint (defaults to current directory if none provided)
418
-
419
- Examples:
420
-
421
- \b
422
- # Check current directory (all files recursively)
423
- thai-lint lazy-ignores
424
-
425
- \b
426
- # Check specific directory
427
- thai-lint lazy-ignores src/
428
-
429
- \b
430
- # Check single file
431
- thai-lint lazy-ignores src/routes.py
432
-
433
- \b
434
- # Check multiple files
435
- thai-lint lazy-ignores src/routes.py src/utils.py
436
-
437
- \b
438
- # Get JSON output
439
- thai-lint lazy-ignores --format json .
440
-
441
- \b
442
- # Get SARIF output for CI/CD integration
443
- thai-lint lazy-ignores --format sarif src/
444
-
445
- \b
446
- # Use custom config file
447
- thai-lint lazy-ignores --config .thailint.yaml src/
448
- """
449
- verbose: bool = ctx.obj.get("verbose", False)
450
- project_root = get_project_root_from_context(ctx)
451
-
452
- if not paths:
453
- paths = (".",)
454
-
455
- path_objs = [Path(p) for p in paths]
456
-
457
- try:
458
- _execute_lazy_ignores_lint(path_objs, config_file, format, recursive, verbose, project_root)
459
- except Exception as e:
460
- handle_linting_error(e, verbose)
461
-
462
-
463
- def _execute_lazy_ignores_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
464
- path_objs: list[Path],
465
- config_file: str | None,
466
- format: str,
467
- recursive: bool,
468
- verbose: bool,
469
- project_root: Path | None = None,
470
- ) -> NoReturn:
198
+ def _execute_lazy_ignores_lint(params: ExecuteParams) -> NoReturn:
471
199
  """Execute lazy-ignores lint."""
472
- validate_paths_exist(path_objs)
473
- orchestrator = _setup_lazy_ignores_orchestrator(path_objs, config_file, verbose, project_root)
474
- lazy_ignores_violations = _run_lazy_ignores_lint(orchestrator, path_objs, recursive)
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
+ )
475
207
 
476
- if verbose:
208
+ if params.verbose:
477
209
  logger.info(f"Found {len(lazy_ignores_violations)} lazy-ignores violation(s)")
478
210
 
479
- format_violations(lazy_ignores_violations, format)
211
+ format_violations(lazy_ignores_violations, params.format)
480
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
+ )