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/structure.py
CHANGED
|
@@ -19,8 +19,10 @@ 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
|
-
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
24
26
|
|
|
25
27
|
import json
|
|
26
28
|
import logging
|
|
@@ -30,13 +32,16 @@ from typing import TYPE_CHECKING, Any, NoReturn
|
|
|
30
32
|
|
|
31
33
|
import click
|
|
32
34
|
|
|
33
|
-
from src.cli.linters.shared import
|
|
35
|
+
from src.cli.linters.shared import (
|
|
36
|
+
ensure_config_section,
|
|
37
|
+
extract_command_context,
|
|
38
|
+
set_config_value,
|
|
39
|
+
)
|
|
34
40
|
from src.cli.main import cli
|
|
35
41
|
from src.cli.utils import (
|
|
36
42
|
execute_linting_on_paths,
|
|
37
43
|
format_option,
|
|
38
44
|
get_or_detect_project_root,
|
|
39
|
-
get_project_root_from_context,
|
|
40
45
|
handle_linting_error,
|
|
41
46
|
load_config_file,
|
|
42
47
|
setup_base_orchestrator,
|
|
@@ -152,20 +157,20 @@ def file_placement( # pylint: disable=too-many-arguments,too-many-positional-ar
|
|
|
152
157
|
# Inline JSON rules
|
|
153
158
|
thai-lint file-placement --rules '{"allow": [".*\\.py$"]}' .
|
|
154
159
|
"""
|
|
155
|
-
|
|
156
|
-
project_root = get_project_root_from_context(ctx)
|
|
157
|
-
|
|
158
|
-
if not paths:
|
|
159
|
-
paths = (".",)
|
|
160
|
-
|
|
161
|
-
path_objs = [Path(p) for p in paths]
|
|
160
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
162
161
|
|
|
163
162
|
try:
|
|
164
163
|
_execute_file_placement_lint(
|
|
165
|
-
path_objs,
|
|
164
|
+
cmd_ctx.path_objs,
|
|
165
|
+
config_file,
|
|
166
|
+
rules,
|
|
167
|
+
format,
|
|
168
|
+
recursive,
|
|
169
|
+
cmd_ctx.verbose,
|
|
170
|
+
cmd_ctx.project_root,
|
|
166
171
|
)
|
|
167
172
|
except Exception as e:
|
|
168
|
-
handle_linting_error(e, verbose)
|
|
173
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
169
174
|
|
|
170
175
|
|
|
171
176
|
def _execute_file_placement_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
@@ -275,20 +280,20 @@ def pipeline( # pylint: disable=too-many-arguments,too-many-positional-argument
|
|
|
275
280
|
# Use custom config file
|
|
276
281
|
thai-lint pipeline --config .thailint.yaml src/
|
|
277
282
|
"""
|
|
278
|
-
|
|
279
|
-
project_root = get_project_root_from_context(ctx)
|
|
280
|
-
|
|
281
|
-
if not paths:
|
|
282
|
-
paths = (".",)
|
|
283
|
-
|
|
284
|
-
path_objs = [Path(p) for p in paths]
|
|
283
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
285
284
|
|
|
286
285
|
try:
|
|
287
286
|
_execute_pipeline_lint(
|
|
288
|
-
path_objs,
|
|
287
|
+
cmd_ctx.path_objs,
|
|
288
|
+
config_file,
|
|
289
|
+
format,
|
|
290
|
+
min_continues,
|
|
291
|
+
recursive,
|
|
292
|
+
cmd_ctx.verbose,
|
|
293
|
+
cmd_ctx.project_root,
|
|
289
294
|
)
|
|
290
295
|
except Exception as e:
|
|
291
|
-
handle_linting_error(e, verbose)
|
|
296
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
292
297
|
|
|
293
298
|
|
|
294
299
|
def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
@@ -19,8 +19,10 @@ 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
|
-
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
24
26
|
|
|
25
27
|
import logging
|
|
26
28
|
import sys
|
|
@@ -29,12 +31,15 @@ from typing import TYPE_CHECKING, NoReturn
|
|
|
29
31
|
|
|
30
32
|
import click
|
|
31
33
|
|
|
32
|
-
from src.cli.linters.shared import
|
|
34
|
+
from src.cli.linters.shared import (
|
|
35
|
+
ensure_config_section,
|
|
36
|
+
extract_command_context,
|
|
37
|
+
set_config_value,
|
|
38
|
+
)
|
|
33
39
|
from src.cli.main import cli
|
|
34
40
|
from src.cli.utils import (
|
|
35
41
|
execute_linting_on_paths,
|
|
36
42
|
format_option,
|
|
37
|
-
get_project_root_from_context,
|
|
38
43
|
handle_linting_error,
|
|
39
44
|
parallel_option,
|
|
40
45
|
setup_base_orchestrator,
|
|
@@ -150,20 +155,21 @@ def nesting( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
150
155
|
# Use custom config file
|
|
151
156
|
thai-lint nesting --config .thailint.yaml src/
|
|
152
157
|
"""
|
|
153
|
-
|
|
154
|
-
project_root = get_project_root_from_context(ctx)
|
|
155
|
-
|
|
156
|
-
if not paths:
|
|
157
|
-
paths = (".",)
|
|
158
|
-
|
|
159
|
-
path_objs = [Path(p) for p in paths]
|
|
158
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
160
159
|
|
|
161
160
|
try:
|
|
162
161
|
_execute_nesting_lint(
|
|
163
|
-
path_objs,
|
|
162
|
+
cmd_ctx.path_objs,
|
|
163
|
+
config_file,
|
|
164
|
+
format,
|
|
165
|
+
max_depth,
|
|
166
|
+
recursive,
|
|
167
|
+
parallel,
|
|
168
|
+
cmd_ctx.verbose,
|
|
169
|
+
cmd_ctx.project_root,
|
|
164
170
|
)
|
|
165
171
|
except Exception as e:
|
|
166
|
-
handle_linting_error(e, verbose)
|
|
172
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
167
173
|
|
|
168
174
|
|
|
169
175
|
def _execute_nesting_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
@@ -277,20 +283,21 @@ def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
277
283
|
# Use custom config file
|
|
278
284
|
thai-lint srp --config .thailint.yaml src/
|
|
279
285
|
"""
|
|
280
|
-
|
|
281
|
-
project_root = get_project_root_from_context(ctx)
|
|
282
|
-
|
|
283
|
-
if not paths:
|
|
284
|
-
paths = (".",)
|
|
285
|
-
|
|
286
|
-
path_objs = [Path(p) for p in paths]
|
|
286
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
287
287
|
|
|
288
288
|
try:
|
|
289
289
|
_execute_srp_lint(
|
|
290
|
-
path_objs,
|
|
290
|
+
cmd_ctx.path_objs,
|
|
291
|
+
config_file,
|
|
292
|
+
format,
|
|
293
|
+
max_methods,
|
|
294
|
+
max_loc,
|
|
295
|
+
recursive,
|
|
296
|
+
cmd_ctx.verbose,
|
|
297
|
+
cmd_ctx.project_root,
|
|
291
298
|
)
|
|
292
299
|
except Exception as e:
|
|
293
|
-
handle_linting_error(e, verbose)
|
|
300
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
294
301
|
|
|
295
302
|
|
|
296
303
|
def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
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
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Purpose: Shared utility functions for linter framework patterns
|
|
3
3
|
|
|
4
|
-
Scope: Common config loading, metadata access,
|
|
4
|
+
Scope: Common config loading, metadata access, context validation, and AST parsing utilities
|
|
5
5
|
|
|
6
6
|
Overview: Provides reusable helper functions to eliminate duplication across linter implementations.
|
|
7
7
|
Includes utilities for loading configuration from context metadata with language-specific overrides,
|
|
8
|
-
extracting metadata fields safely with type validation,
|
|
9
|
-
common patterns used by srp, nesting, dry,
|
|
10
|
-
while maintaining type safety
|
|
8
|
+
extracting metadata fields safely with type validation, validating context state, and parsing
|
|
9
|
+
Python AST with syntax error handling. Standardizes common patterns used by srp, nesting, dry,
|
|
10
|
+
performance, and file_placement linters. Reduces boilerplate code while maintaining type safety
|
|
11
|
+
and proper error handling.
|
|
11
12
|
|
|
12
|
-
Dependencies: BaseLintContext from src.core.base
|
|
13
|
+
Dependencies: BaseLintContext from src.core.base, ast for Python parsing
|
|
13
14
|
|
|
14
|
-
Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content
|
|
15
|
+
Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content, parse_python_ast,
|
|
16
|
+
with_parsed_python
|
|
15
17
|
|
|
16
18
|
Interfaces: All functions take BaseLintContext and return typed values (dict, str, bool, Any)
|
|
17
19
|
|
|
18
20
|
Implementation: Type-safe metadata access with fallbacks, generic config loading with language support
|
|
21
|
+
|
|
22
|
+
Suppressions:
|
|
23
|
+
- invalid-name: T type variable follows Python generic naming convention
|
|
24
|
+
- type:ignore[return-value]: Generic config factory with runtime type checking
|
|
25
|
+
- unnecessary-ellipsis: Protocol method bodies use ellipsis per PEP 544
|
|
26
|
+
- B101: Assert used to narrow type after parse_python_ast returns non-None tree
|
|
19
27
|
"""
|
|
20
28
|
|
|
29
|
+
import ast
|
|
30
|
+
from collections.abc import Callable
|
|
21
31
|
from typing import Any, Protocol, TypeVar
|
|
22
32
|
|
|
23
33
|
from src.core.base import BaseLintContext
|
|
34
|
+
from src.core.types import Violation
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Protocol for violation builders that support syntax error handling
|
|
38
|
+
class SyntaxErrorViolationBuilder(Protocol):
|
|
39
|
+
"""Protocol for violation builders that can create syntax error violations."""
|
|
40
|
+
|
|
41
|
+
def create_syntax_error_violation(
|
|
42
|
+
self, error: SyntaxError, context: BaseLintContext
|
|
43
|
+
) -> Violation:
|
|
44
|
+
"""Create a violation for a syntax error."""
|
|
45
|
+
... # pylint: disable=unnecessary-ellipsis
|
|
24
46
|
|
|
25
47
|
|
|
26
48
|
# Protocol for config classes that support from_dict
|
|
@@ -166,3 +188,70 @@ def should_process_file(context: BaseLintContext) -> bool:
|
|
|
166
188
|
True if file has both content and path available
|
|
167
189
|
"""
|
|
168
190
|
return has_file_content(context) and has_file_path(context)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def parse_python_ast(
|
|
194
|
+
context: BaseLintContext,
|
|
195
|
+
violation_builder: SyntaxErrorViolationBuilder,
|
|
196
|
+
) -> tuple[ast.Module | None, list[Violation]]:
|
|
197
|
+
"""Parse Python AST from context, handling syntax errors gracefully.
|
|
198
|
+
|
|
199
|
+
Provides a standard pattern for Python linters to parse AST and handle
|
|
200
|
+
syntax errors by returning a violation instead of crashing.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
context: Lint context containing file content
|
|
204
|
+
violation_builder: Builder to create syntax error violations
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Tuple of (ast_tree, violations):
|
|
208
|
+
- On success: (ast.Module, [])
|
|
209
|
+
- On syntax error: (None, [syntax_error_violation])
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
tree, errors = parse_python_ast(context, self._violation_builder)
|
|
213
|
+
if errors:
|
|
214
|
+
return errors
|
|
215
|
+
# ... use tree for analysis
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
tree = ast.parse(context.file_content or "")
|
|
219
|
+
return tree, []
|
|
220
|
+
except SyntaxError as e:
|
|
221
|
+
violation = violation_builder.create_syntax_error_violation(e, context)
|
|
222
|
+
return None, [violation]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def with_parsed_python(
|
|
226
|
+
context: BaseLintContext,
|
|
227
|
+
violation_builder: SyntaxErrorViolationBuilder,
|
|
228
|
+
on_success: Callable[[ast.Module], list[Violation]],
|
|
229
|
+
) -> list[Violation]:
|
|
230
|
+
"""Parse Python and call on_success with the AST, or return parse errors.
|
|
231
|
+
|
|
232
|
+
Eliminates the repeated parse-check-assert pattern across Python linters.
|
|
233
|
+
On parse success, calls on_success with a guaranteed non-None AST tree.
|
|
234
|
+
On parse failure, returns syntax error violations.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
context: Lint context containing file content
|
|
238
|
+
violation_builder: Builder to create syntax error violations
|
|
239
|
+
on_success: Callback receiving the parsed AST tree, returns violations
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Violations from on_success callback, or syntax error violations
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
def _check_python(self, context, config):
|
|
246
|
+
return with_parsed_python(
|
|
247
|
+
context,
|
|
248
|
+
self._violation_builder,
|
|
249
|
+
lambda tree: self._analyze_tree(tree, config, context),
|
|
250
|
+
)
|
|
251
|
+
"""
|
|
252
|
+
tree, errors = parse_python_ast(context, violation_builder)
|
|
253
|
+
if errors:
|
|
254
|
+
return errors
|
|
255
|
+
# tree is guaranteed non-None when errors is empty (parse_python_ast contract)
|
|
256
|
+
assert tree is not None # nosec B101
|
|
257
|
+
return on_success(tree)
|
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
|