thailint 0.11.0__py3-none-any.whl → 0.13.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 +3 -0
- src/cli/linters/code_patterns.py +113 -5
- src/cli/linters/code_smells.py +118 -7
- src/cli/linters/documentation.py +3 -0
- src/cli/linters/structure.py +3 -0
- src/cli/linters/structure_quality.py +3 -0
- src/cli/utils.py +29 -9
- 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 +4 -0
- 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 +4 -0
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -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 +201 -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 +67 -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 -0
- src/linters/method_property/linter.py +5 -4
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- 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/__init__.py +22 -9
- src/linters/stringly_typed/config.py +32 -8
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +102 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +9 -5
- src/linters/stringly_typed/python/analyzer.py +159 -9
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +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 +630 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +405 -0
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +166 -0
- src/utils/project_root.py +3 -0
- thailint-0.13.0.dist-info/METADATA +184 -0
- thailint-0.13.0.dist-info/RECORD +189 -0
- thailint-0.11.0.dist-info/METADATA +0 -1661
- thailint-0.11.0.dist-info/RECORD +0 -150
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
src/cli/linters/code_smells.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Purpose: CLI commands for code smell linters (dry, magic-numbers)
|
|
2
|
+
Purpose: CLI commands for code smell linters (dry, magic-numbers, stringly-typed)
|
|
3
3
|
|
|
4
|
-
Scope: Commands that detect code smells like duplicate code and
|
|
4
|
+
Scope: Commands that detect code smells like duplicate code, magic numbers, and stringly-typed patterns
|
|
5
5
|
|
|
6
6
|
Overview: Provides CLI commands for code smell detection: dry finds duplicate code blocks using
|
|
7
|
-
token-based hashing with SQLite caching,
|
|
8
|
-
should be extracted as named constants
|
|
9
|
-
|
|
10
|
-
the orchestrator for execution.
|
|
7
|
+
token-based hashing with SQLite caching, magic-numbers detects unnamed numeric literals that
|
|
8
|
+
should be extracted as named constants, and stringly-typed detects string patterns that should
|
|
9
|
+
use enums. Each command supports standard options (config, format, recursive) plus linter-specific
|
|
10
|
+
options and integrates with the orchestrator for execution.
|
|
11
11
|
|
|
12
12
|
Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities,
|
|
13
13
|
src.cli.linters.shared for linter-specific helpers, yaml for config loading
|
|
14
14
|
|
|
15
|
-
Exports: dry command, magic_numbers command
|
|
15
|
+
Exports: dry command, magic_numbers command, stringly_typed command
|
|
16
16
|
|
|
17
17
|
Interfaces: Click CLI commands registered to main CLI group
|
|
18
18
|
|
|
@@ -20,6 +20,10 @@ Implementation: Click decorators for command definition, orchestrator-based lint
|
|
|
20
20
|
|
|
21
21
|
SRP Exception: CLI command modules follow Click framework patterns requiring similar command
|
|
22
22
|
structure across all linter commands. This is intentional design for consistency.
|
|
23
|
+
|
|
24
|
+
Suppressions:
|
|
25
|
+
too-many-arguments: Click commands require many parameters by framework design
|
|
26
|
+
too-many-positional-arguments: Click positional params match CLI arg structure
|
|
23
27
|
"""
|
|
24
28
|
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
25
29
|
|
|
@@ -341,3 +345,110 @@ def _execute_magic_numbers_lint( # pylint: disable=too-many-arguments,too-many-
|
|
|
341
345
|
|
|
342
346
|
format_violations(magic_numbers_violations, format)
|
|
343
347
|
sys.exit(1 if magic_numbers_violations else 0)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# =============================================================================
|
|
351
|
+
# Stringly-Typed Command
|
|
352
|
+
# =============================================================================
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _setup_stringly_typed_orchestrator(
|
|
356
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
357
|
+
) -> "Orchestrator":
|
|
358
|
+
"""Set up orchestrator for stringly-typed command."""
|
|
359
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _run_stringly_typed_lint(
|
|
363
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
|
|
364
|
+
) -> list[Violation]:
|
|
365
|
+
"""Execute stringly-typed lint on files or directories."""
|
|
366
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
367
|
+
return [v for v in all_violations if "stringly-typed" in v.rule_id]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@cli.command("stringly-typed")
|
|
371
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
372
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
373
|
+
@format_option
|
|
374
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
375
|
+
@click.pass_context
|
|
376
|
+
def stringly_typed( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
377
|
+
ctx: click.Context,
|
|
378
|
+
paths: tuple[str, ...],
|
|
379
|
+
config_file: str | None,
|
|
380
|
+
format: str,
|
|
381
|
+
recursive: bool,
|
|
382
|
+
) -> None:
|
|
383
|
+
"""Check for stringly-typed patterns in code.
|
|
384
|
+
|
|
385
|
+
Detects string patterns in Python and TypeScript/JavaScript code that should
|
|
386
|
+
use enums or typed alternatives. Finds membership validation, equality chains,
|
|
387
|
+
and function calls with limited string values across multiple files.
|
|
388
|
+
|
|
389
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
390
|
+
|
|
391
|
+
Examples:
|
|
392
|
+
|
|
393
|
+
\b
|
|
394
|
+
# Check current directory (all files recursively)
|
|
395
|
+
thai-lint stringly-typed
|
|
396
|
+
|
|
397
|
+
\b
|
|
398
|
+
# Check specific directory
|
|
399
|
+
thai-lint stringly-typed src/
|
|
400
|
+
|
|
401
|
+
\b
|
|
402
|
+
# Check single file
|
|
403
|
+
thai-lint stringly-typed src/handlers.py
|
|
404
|
+
|
|
405
|
+
\b
|
|
406
|
+
# Check multiple files
|
|
407
|
+
thai-lint stringly-typed src/handlers.py src/services.py
|
|
408
|
+
|
|
409
|
+
\b
|
|
410
|
+
# Get JSON output
|
|
411
|
+
thai-lint stringly-typed --format json .
|
|
412
|
+
|
|
413
|
+
\b
|
|
414
|
+
# Get SARIF output for IDE integration
|
|
415
|
+
thai-lint stringly-typed --format sarif .
|
|
416
|
+
|
|
417
|
+
\b
|
|
418
|
+
# Use custom config file
|
|
419
|
+
thai-lint stringly-typed --config .thailint.yaml src/
|
|
420
|
+
"""
|
|
421
|
+
verbose: bool = ctx.obj.get("verbose", False)
|
|
422
|
+
project_root = get_project_root_from_context(ctx)
|
|
423
|
+
|
|
424
|
+
if not paths:
|
|
425
|
+
paths = (".",)
|
|
426
|
+
|
|
427
|
+
path_objs = [Path(p) for p in paths]
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
_execute_stringly_typed_lint(
|
|
431
|
+
path_objs, config_file, format, recursive, verbose, project_root
|
|
432
|
+
)
|
|
433
|
+
except Exception as e:
|
|
434
|
+
handle_linting_error(e, verbose)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _execute_stringly_typed_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
438
|
+
path_objs: list[Path],
|
|
439
|
+
config_file: str | None,
|
|
440
|
+
format: str,
|
|
441
|
+
recursive: bool,
|
|
442
|
+
verbose: bool,
|
|
443
|
+
project_root: Path | None = None,
|
|
444
|
+
) -> NoReturn:
|
|
445
|
+
"""Execute stringly-typed lint."""
|
|
446
|
+
validate_paths_exist(path_objs)
|
|
447
|
+
orchestrator = _setup_stringly_typed_orchestrator(path_objs, config_file, verbose, project_root)
|
|
448
|
+
stringly_violations = _run_stringly_typed_lint(orchestrator, path_objs, recursive)
|
|
449
|
+
|
|
450
|
+
if verbose:
|
|
451
|
+
logger.info(f"Found {len(stringly_violations)} stringly-typed violation(s)")
|
|
452
|
+
|
|
453
|
+
format_violations(stringly_violations, format)
|
|
454
|
+
sys.exit(1 if stringly_violations else 0)
|
src/cli/linters/documentation.py
CHANGED
|
@@ -19,6 +19,9 @@ Implementation: Click decorators for command definition, orchestrator-based lint
|
|
|
19
19
|
|
|
20
20
|
SRP Exception: CLI command modules follow Click framework patterns requiring similar command
|
|
21
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
|
|
22
25
|
"""
|
|
23
26
|
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
24
27
|
|
src/cli/linters/structure.py
CHANGED
|
@@ -19,6 +19,9 @@ Implementation: Click decorators for command definition, orchestrator-based lint
|
|
|
19
19
|
|
|
20
20
|
SRP Exception: CLI command modules follow Click framework patterns requiring similar command
|
|
21
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
|
|
22
25
|
"""
|
|
23
26
|
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
24
27
|
|
|
@@ -19,6 +19,9 @@ Implementation: Click decorators for command definition, orchestrator-based lint
|
|
|
19
19
|
|
|
20
20
|
SRP Exception: CLI command modules follow Click framework patterns requiring similar command
|
|
21
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
|
|
22
25
|
"""
|
|
23
26
|
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
24
27
|
|
src/cli/utils.py
CHANGED
|
@@ -172,34 +172,54 @@ def _autodetect_project_root(
|
|
|
172
172
|
return auto_root
|
|
173
173
|
|
|
174
174
|
|
|
175
|
-
def get_project_root_from_context(ctx: click.Context) -> Path:
|
|
175
|
+
def get_project_root_from_context(ctx: click.Context) -> Path | None:
|
|
176
176
|
"""Get or determine project root from Click context.
|
|
177
177
|
|
|
178
178
|
This function defers the actual determination until needed to avoid
|
|
179
179
|
importing pyprojroot in test environments where it may not be available.
|
|
180
180
|
|
|
181
|
+
Returns None when no explicit root is specified (via --project-root or --config),
|
|
182
|
+
allowing the orchestrator to auto-detect from target paths instead of CWD.
|
|
183
|
+
|
|
181
184
|
Args:
|
|
182
185
|
ctx: Click context containing CLI options
|
|
183
186
|
|
|
184
187
|
Returns:
|
|
185
|
-
Path to determined project root
|
|
188
|
+
Path to determined project root, or None for auto-detection from target paths
|
|
186
189
|
"""
|
|
187
190
|
# Check if already determined and cached
|
|
188
191
|
if "project_root" in ctx.obj:
|
|
189
|
-
|
|
190
|
-
return
|
|
192
|
+
cached: Path | None = ctx.obj["project_root"]
|
|
193
|
+
return cached
|
|
194
|
+
|
|
195
|
+
project_root = _determine_project_root_for_context(ctx)
|
|
196
|
+
ctx.obj["project_root"] = project_root
|
|
197
|
+
return project_root
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _determine_project_root_for_context(ctx: click.Context) -> Path | None:
|
|
201
|
+
"""Determine project root from context options.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
ctx: Click context containing CLI options
|
|
191
205
|
|
|
192
|
-
|
|
206
|
+
Returns:
|
|
207
|
+
Path if explicit root or config specified, None for auto-detection
|
|
208
|
+
"""
|
|
193
209
|
explicit_root = ctx.obj.get("cli_project_root")
|
|
194
210
|
config_path = ctx.obj.get("cli_config_path")
|
|
195
211
|
verbose = ctx.obj.get("verbose", False)
|
|
196
212
|
|
|
197
|
-
|
|
213
|
+
if explicit_root:
|
|
214
|
+
return _resolve_explicit_project_root(explicit_root, verbose)
|
|
198
215
|
|
|
199
|
-
|
|
200
|
-
|
|
216
|
+
if config_path:
|
|
217
|
+
return _infer_root_from_config(config_path, verbose)
|
|
201
218
|
|
|
202
|
-
return
|
|
219
|
+
# No explicit root - return None for auto-detection from target paths
|
|
220
|
+
if verbose:
|
|
221
|
+
logger.debug("No explicit project root, will auto-detect from target paths")
|
|
222
|
+
return None
|
|
203
223
|
|
|
204
224
|
|
|
205
225
|
# =============================================================================
|
src/cli_main.py
CHANGED
|
@@ -17,6 +17,9 @@ Exports: cli (main command group with all commands registered)
|
|
|
17
17
|
Interfaces: Click CLI commands, integration with Orchestrator for linting execution
|
|
18
18
|
|
|
19
19
|
Implementation: Module imports trigger command registration via Click decorator side effects
|
|
20
|
+
|
|
21
|
+
Suppressions:
|
|
22
|
+
- F401: Module re-exports and imports trigger Click command registration via decorator side effects
|
|
20
23
|
"""
|
|
21
24
|
|
|
22
25
|
# Import the main CLI group from the modular package
|
src/config.py
CHANGED
|
@@ -26,6 +26,7 @@ from typing import Any
|
|
|
26
26
|
import yaml
|
|
27
27
|
|
|
28
28
|
from src.core.config_parser import ConfigParseError, parse_config_file
|
|
29
|
+
from src.core.constants import CONFIG_EXTENSIONS
|
|
29
30
|
|
|
30
31
|
logger = logging.getLogger(__name__)
|
|
31
32
|
|
|
@@ -170,7 +171,7 @@ def _validate_before_save(config: dict[str, Any]) -> None:
|
|
|
170
171
|
|
|
171
172
|
def _write_config_file(config: dict[str, Any], path: Path) -> None:
|
|
172
173
|
"""Write config to file based on extension."""
|
|
173
|
-
if path.suffix in
|
|
174
|
+
if path.suffix in CONFIG_EXTENSIONS:
|
|
174
175
|
_write_yaml_config(config, path)
|
|
175
176
|
elif path.suffix == ".json":
|
|
176
177
|
_write_json_config(config, path)
|
src/core/base.py
CHANGED
|
@@ -31,6 +31,7 @@ from abc import ABC, abstractmethod
|
|
|
31
31
|
from pathlib import Path
|
|
32
32
|
from typing import Any
|
|
33
33
|
|
|
34
|
+
from .constants import Language
|
|
34
35
|
from .types import Violation
|
|
35
36
|
|
|
36
37
|
|
|
@@ -176,10 +177,10 @@ class MultiLanguageLintRule(BaseLintRule):
|
|
|
176
177
|
if not config.enabled:
|
|
177
178
|
return []
|
|
178
179
|
|
|
179
|
-
if context.language ==
|
|
180
|
+
if context.language == Language.PYTHON:
|
|
180
181
|
return self._check_python(context, config)
|
|
181
182
|
|
|
182
|
-
if context.language in (
|
|
183
|
+
if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
|
|
183
184
|
return self._check_typescript(context, config)
|
|
184
185
|
|
|
185
186
|
return []
|
src/core/cli_utils.py
CHANGED
|
@@ -26,6 +26,8 @@ from typing import Any
|
|
|
26
26
|
|
|
27
27
|
import click
|
|
28
28
|
|
|
29
|
+
from src.core.constants import CONFIG_EXTENSIONS
|
|
30
|
+
|
|
29
31
|
|
|
30
32
|
def common_linter_options(func: Callable) -> Callable:
|
|
31
33
|
"""Add common linter CLI options to command.
|
|
@@ -103,7 +105,7 @@ def _load_config_by_format(config_file: Path) -> dict[str, Any]:
|
|
|
103
105
|
Returns:
|
|
104
106
|
Loaded configuration dictionary
|
|
105
107
|
"""
|
|
106
|
-
if config_file.suffix in
|
|
108
|
+
if config_file.suffix in CONFIG_EXTENSIONS:
|
|
107
109
|
return _load_yaml_config(config_file)
|
|
108
110
|
if config_file.suffix == ".json":
|
|
109
111
|
return _load_json_config(config_file)
|
src/core/config_parser.py
CHANGED
|
@@ -27,6 +27,8 @@ from typing import Any, TextIO
|
|
|
27
27
|
|
|
28
28
|
import yaml
|
|
29
29
|
|
|
30
|
+
from src.core.constants import CONFIG_EXTENSIONS
|
|
31
|
+
|
|
30
32
|
|
|
31
33
|
class ConfigParseError(Exception):
|
|
32
34
|
"""Configuration file parsing errors."""
|
|
@@ -113,11 +115,12 @@ def parse_config_file(path: Path, encoding: str = "utf-8") -> dict[str, Any]:
|
|
|
113
115
|
"""
|
|
114
116
|
suffix = path.suffix.lower()
|
|
115
117
|
|
|
116
|
-
|
|
118
|
+
valid_suffixes = (*CONFIG_EXTENSIONS, ".json")
|
|
119
|
+
if suffix not in valid_suffixes:
|
|
117
120
|
raise ConfigParseError(f"Unsupported config format: {suffix}")
|
|
118
121
|
|
|
119
122
|
with path.open(encoding=encoding) as f:
|
|
120
|
-
if suffix in
|
|
123
|
+
if suffix in CONFIG_EXTENSIONS:
|
|
121
124
|
config = parse_yaml(f, path)
|
|
122
125
|
else:
|
|
123
126
|
config = parse_json(f, path)
|
src/core/constants.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Core constants and enums used across the thai-lint codebase
|
|
3
|
+
|
|
4
|
+
Scope: Centralized definitions for language names, storage modes, config extensions
|
|
5
|
+
|
|
6
|
+
Overview: Provides type-safe enums and constants for consistent stringly-typed patterns
|
|
7
|
+
across the codebase. Includes Language enum for programming language detection,
|
|
8
|
+
StorageMode for cache storage options, and CONFIG_EXTENSIONS for config file
|
|
9
|
+
discovery. Using enums ensures compile-time safety and IDE autocompletion.
|
|
10
|
+
|
|
11
|
+
Dependencies: enum module
|
|
12
|
+
|
|
13
|
+
Exports: Language enum, StorageMode enum, CONFIG_EXTENSIONS, IgnoreDirective enum,
|
|
14
|
+
HEADER_SCAN_LINES, MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
15
|
+
|
|
16
|
+
Interfaces: Use enum values instead of string literals throughout codebase
|
|
17
|
+
|
|
18
|
+
Implementation: Standard Python enums with string values for compatibility
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from enum import Enum
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Language(str, Enum):
|
|
25
|
+
"""Supported programming languages for linting."""
|
|
26
|
+
|
|
27
|
+
PYTHON = "python"
|
|
28
|
+
TYPESCRIPT = "typescript"
|
|
29
|
+
JAVASCRIPT = "javascript"
|
|
30
|
+
MARKDOWN = "markdown"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StorageMode(str, Enum):
|
|
34
|
+
"""Storage modes for DRY linter cache."""
|
|
35
|
+
|
|
36
|
+
MEMORY = "memory"
|
|
37
|
+
TEMPFILE = "tempfile"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class IgnoreDirective(str, Enum):
|
|
41
|
+
"""Inline ignore directive types."""
|
|
42
|
+
|
|
43
|
+
IGNORE = "ignore"
|
|
44
|
+
IGNORE_FILE = "ignore-file"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Valid config file extensions
|
|
48
|
+
CONFIG_EXTENSIONS: tuple[str, str] = (".yaml", ".yml")
|
|
49
|
+
|
|
50
|
+
# Number of lines to scan at file start for ignore directives and headers
|
|
51
|
+
HEADER_SCAN_LINES: int = 10
|
|
52
|
+
|
|
53
|
+
# Maximum depth for attribute chain traversal (e.g., obj.attr.attr2.attr3)
|
|
54
|
+
MAX_ATTRIBUTE_CHAIN_DEPTH: int = 3
|
src/core/linter_utils.py
CHANGED
|
@@ -16,6 +16,10 @@ Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content
|
|
|
16
16
|
Interfaces: All functions take BaseLintContext and return typed values (dict, str, bool, Any)
|
|
17
17
|
|
|
18
18
|
Implementation: Type-safe metadata access with fallbacks, generic config loading with language support
|
|
19
|
+
|
|
20
|
+
Suppressions:
|
|
21
|
+
- invalid-name: T type variable follows Python generic naming convention
|
|
22
|
+
- type:ignore[return-value]: Generic config factory with runtime type checking
|
|
19
23
|
"""
|
|
20
24
|
|
|
21
25
|
from typing import Any, Protocol, TypeVar
|
src/core/rule_discovery.py
CHANGED
|
@@ -19,12 +19,15 @@ Implementation: Package traversal with pkgutil, class introspection with inspect
|
|
|
19
19
|
|
|
20
20
|
import importlib
|
|
21
21
|
import inspect
|
|
22
|
+
import logging
|
|
22
23
|
import pkgutil
|
|
23
24
|
from types import ModuleType
|
|
24
25
|
from typing import Any
|
|
25
26
|
|
|
26
27
|
from .base import BaseLintRule
|
|
27
28
|
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
28
31
|
|
|
29
32
|
def discover_from_package(package_path: str) -> list[BaseLintRule]:
|
|
30
33
|
"""Discover rules from a package and its modules.
|
|
@@ -37,7 +40,8 @@ def discover_from_package(package_path: str) -> list[BaseLintRule]:
|
|
|
37
40
|
"""
|
|
38
41
|
try:
|
|
39
42
|
package = importlib.import_module(package_path)
|
|
40
|
-
except ImportError:
|
|
43
|
+
except ImportError as e:
|
|
44
|
+
logger.debug("Failed to import package %s: %s", package_path, e)
|
|
41
45
|
return []
|
|
42
46
|
|
|
43
47
|
if not hasattr(package, "__path__"):
|
src/core/violation_builder.py
CHANGED
|
@@ -21,6 +21,9 @@ Interfaces: ViolationInfo(rule_id, file_path, line, message, column, severity),
|
|
|
21
21
|
|
|
22
22
|
Implementation: Uses dataclass for type-safe violation info, functions provide build logic
|
|
23
23
|
that constructs Violation objects with proper defaults
|
|
24
|
+
|
|
25
|
+
Suppressions:
|
|
26
|
+
- too-many-arguments,too-many-positional-arguments: Violation fields as parameters
|
|
24
27
|
"""
|
|
25
28
|
|
|
26
29
|
from dataclasses import dataclass
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Ignore directive marker detection for thailint comments
|
|
3
|
+
|
|
4
|
+
Scope: Detection of thailint and design-lint ignore markers in source code
|
|
5
|
+
|
|
6
|
+
Overview: Provides functions for detecting various ignore directive markers in code
|
|
7
|
+
comments. Supports file-level ignores, line-level ignores, block ignores, and
|
|
8
|
+
next-line ignores. Works with both Python (#) and JavaScript (//) comment styles.
|
|
9
|
+
All checks are case-insensitive.
|
|
10
|
+
|
|
11
|
+
Dependencies: None (pure string operations)
|
|
12
|
+
|
|
13
|
+
Exports: Marker detection functions for various ignore directive types
|
|
14
|
+
|
|
15
|
+
Interfaces: has_*_marker(line) -> bool for each marker type
|
|
16
|
+
|
|
17
|
+
Implementation: String-based pattern detection with case-insensitive matching
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def has_ignore_directive_marker(line: str) -> bool:
|
|
22
|
+
"""Check if line contains a file-level ignore directive marker.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
line: Line of code to check
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
True if line has ignore-file marker
|
|
29
|
+
"""
|
|
30
|
+
line_lower = line.lower()
|
|
31
|
+
return "# thailint: ignore-file" in line_lower or "# design-lint: ignore-file" in line_lower
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def has_line_ignore_marker(code: str) -> bool:
|
|
35
|
+
"""Check if code line has an inline ignore marker.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
code: Line of code to check
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if line has inline ignore marker
|
|
42
|
+
"""
|
|
43
|
+
code_lower = code.lower()
|
|
44
|
+
return (
|
|
45
|
+
"# thailint: ignore" in code_lower
|
|
46
|
+
or "# design-lint: ignore" in code_lower
|
|
47
|
+
or "// thailint: ignore" in code_lower
|
|
48
|
+
or "// design-lint: ignore" in code_lower
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def has_ignore_next_line_marker(line: str) -> bool:
|
|
53
|
+
"""Check if line has ignore-next-line marker.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
line: Line of code to check
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if line has ignore-next-line marker
|
|
60
|
+
"""
|
|
61
|
+
return "# thailint: ignore-next-line" in line or "# design-lint: ignore-next-line" in line
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def has_ignore_start_marker(line: str) -> bool:
|
|
65
|
+
"""Check if line has ignore-start comment marker.
|
|
66
|
+
|
|
67
|
+
Only matches actual comment lines (starting with # or //), not strings
|
|
68
|
+
containing the marker text.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
line: Line of code to check
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if line is a proper ignore-start comment
|
|
75
|
+
"""
|
|
76
|
+
stripped = line.strip().lower()
|
|
77
|
+
if not (stripped.startswith("#") or stripped.startswith("//")):
|
|
78
|
+
return False
|
|
79
|
+
return "ignore-start" in stripped and ("thailint:" in stripped or "design-lint:" in stripped)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def has_ignore_end_marker(line: str) -> bool:
|
|
83
|
+
"""Check if line has ignore-end comment marker.
|
|
84
|
+
|
|
85
|
+
Only matches actual comment lines (starting with # or //), not strings
|
|
86
|
+
containing the marker text.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
line: Line of code to check
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True if line is a proper ignore-end comment
|
|
93
|
+
"""
|
|
94
|
+
stripped = line.strip().lower()
|
|
95
|
+
if not (stripped.startswith("#") or stripped.startswith("//")):
|
|
96
|
+
return False
|
|
97
|
+
return "ignore-end" in stripped and ("thailint:" in stripped or "design-lint:" in stripped)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def check_general_ignore(line: str) -> bool:
|
|
101
|
+
"""Check if line has general ignore directive (no specific rules).
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
line: Line containing ignore directive
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
True if no specific rules are specified (not bracket syntax)
|
|
108
|
+
"""
|
|
109
|
+
return "ignore-file[" not in line
|