thailint 0.2.0__py3-none-any.whl → 0.15.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +44 -27
- src/core/base.py +95 -5
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +36 -6
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -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 +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +125 -22
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +142 -94
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +68 -21
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +20 -82
- src/linters/dry/file_analyzer.py +15 -50
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +182 -54
- src/linters/dry/python_analyzer.py +108 -336
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +129 -71
- src/linters/dry/typescript_analyzer.py +68 -380
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +9 -5
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +105 -0
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +140 -0
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +72 -0
- src/linters/file_header/linter.py +309 -0
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +42 -0
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +79 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +74 -31
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -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 +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +249 -0
- src/linters/magic_numbers/linter.py +462 -0
- src/linters/magic_numbers/python_analyzer.py +64 -0
- src/linters/magic_numbers/typescript_analyzer.py +215 -0
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +31 -34
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -11
- 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/__init__.py +53 -0
- src/linters/print_statements/config.py +78 -0
- src/linters/print_statements/linter.py +413 -0
- src/linters/print_statements/python_analyzer.py +153 -0
- src/linters/print_statements/typescript_analyzer.py +125 -0
- src/linters/print_statements/violation_builder.py +96 -0
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/config.py +12 -6
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +47 -39
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- 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 +100 -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 +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- 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 +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -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 +419 -0
- src/orchestrator/core.py +264 -16
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +354 -0
- src/utils/project_root.py +138 -16
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1055
- thailint-0.2.0.dist-info/METADATA +0 -980
- thailint-0.2.0.dist-info/RECORD +0 -75
- thailint-0.2.0.dist-info/entry_points.txt +0 -4
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
src/core/base.py
CHANGED
|
@@ -8,14 +8,17 @@ Overview: Establishes the contract that all linting plugins must follow through
|
|
|
8
8
|
Defines BaseLintRule which all concrete linting rules inherit from, specifying required
|
|
9
9
|
properties (rule_id, rule_name, description) and the check() method for violation detection.
|
|
10
10
|
Provides BaseLintContext as the interface for accessing file information during analysis,
|
|
11
|
-
exposing file_path, file_content, and language properties.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
exposing file_path, file_content, and language properties. Includes MultiLanguageLintRule
|
|
12
|
+
intermediate class implementing template method pattern for language dispatch, eliminating
|
|
13
|
+
code duplication across multi-language linters (nesting, srp, magic_numbers). These
|
|
14
|
+
abstractions enable the rule registry to discover and instantiate rules dynamically without
|
|
15
|
+
tight coupling, supporting the extensible plugin system where new rules can be added by
|
|
16
|
+
simply placing them in the appropriate directory structure.
|
|
15
17
|
|
|
16
18
|
Dependencies: abc for abstract base class support, pathlib for Path types, Violation from types
|
|
17
19
|
|
|
18
|
-
Exports: BaseLintRule (abstract rule interface), BaseLintContext (abstract context interface)
|
|
20
|
+
Exports: BaseLintRule (abstract rule interface), BaseLintContext (abstract context interface),
|
|
21
|
+
MultiLanguageLintRule (template method base for multi-language linters)
|
|
19
22
|
|
|
20
23
|
Interfaces: BaseLintRule.check(context) -> list[Violation], BaseLintContext properties
|
|
21
24
|
(file_path, file_content, language), all abstract methods must be implemented by subclasses
|
|
@@ -26,7 +29,9 @@ Implementation: ABC-based interface definitions with @abstractmethod decorators,
|
|
|
26
29
|
|
|
27
30
|
from abc import ABC, abstractmethod
|
|
28
31
|
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
29
33
|
|
|
34
|
+
from .constants import Language
|
|
30
35
|
from .types import Violation
|
|
31
36
|
|
|
32
37
|
|
|
@@ -132,3 +137,88 @@ class BaseLintRule(ABC):
|
|
|
132
137
|
List of violations found during finalization. Empty list by default.
|
|
133
138
|
"""
|
|
134
139
|
return []
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class MultiLanguageLintRule(BaseLintRule):
|
|
143
|
+
"""Base class for linting rules that support multiple programming languages.
|
|
144
|
+
|
|
145
|
+
Provides language dispatch pattern to eliminate code duplication across multi-language
|
|
146
|
+
linters. Subclasses implement language-specific checking methods rather than handling
|
|
147
|
+
dispatch logic themselves.
|
|
148
|
+
|
|
149
|
+
Subclasses must implement:
|
|
150
|
+
- _check_python(context, config) for Python language support
|
|
151
|
+
- _check_typescript(context, config) for TypeScript/JavaScript support
|
|
152
|
+
- _load_config(context) for configuration loading
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self) -> None:
|
|
156
|
+
"""Initialize the multi-language lint rule."""
|
|
157
|
+
pass # Base class for multi-language linters
|
|
158
|
+
|
|
159
|
+
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
160
|
+
"""Check for violations with automatic language dispatch.
|
|
161
|
+
|
|
162
|
+
Dispatches to language-specific checking methods based on context.language.
|
|
163
|
+
Handles common patterns like file content validation and config loading.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
context: Lint context with file information
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of violations found
|
|
170
|
+
"""
|
|
171
|
+
from .linter_utils import has_file_content
|
|
172
|
+
|
|
173
|
+
if not has_file_content(context):
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
config = self._load_config(context)
|
|
177
|
+
if not config.enabled:
|
|
178
|
+
return []
|
|
179
|
+
|
|
180
|
+
if context.language == Language.PYTHON:
|
|
181
|
+
return self._check_python(context, config)
|
|
182
|
+
|
|
183
|
+
if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
|
|
184
|
+
return self._check_typescript(context, config)
|
|
185
|
+
|
|
186
|
+
return []
|
|
187
|
+
|
|
188
|
+
@abstractmethod
|
|
189
|
+
def _load_config(self, context: BaseLintContext) -> Any:
|
|
190
|
+
"""Load configuration from context.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
context: Lint context
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Configuration object with at minimum an 'enabled' attribute
|
|
197
|
+
"""
|
|
198
|
+
raise NotImplementedError("Subclasses must implement _load_config")
|
|
199
|
+
|
|
200
|
+
@abstractmethod
|
|
201
|
+
def _check_python(self, context: BaseLintContext, config: Any) -> list[Violation]:
|
|
202
|
+
"""Check Python code for violations.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
context: Lint context with Python file information
|
|
206
|
+
config: Loaded configuration
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of violations found in Python code
|
|
210
|
+
"""
|
|
211
|
+
raise NotImplementedError("Subclasses must implement _check_python")
|
|
212
|
+
|
|
213
|
+
@abstractmethod
|
|
214
|
+
def _check_typescript(self, context: BaseLintContext, config: Any) -> list[Violation]:
|
|
215
|
+
"""Check TypeScript/JavaScript code for violations.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
context: Lint context with TypeScript/JavaScript file information
|
|
219
|
+
config: Loaded configuration
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of violations found in TypeScript/JavaScript code
|
|
223
|
+
"""
|
|
224
|
+
raise NotImplementedError("Subclasses must implement _check_typescript")
|
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)
|
|
@@ -146,10 +148,12 @@ def format_violations(violations: list, output_format: str) -> None:
|
|
|
146
148
|
|
|
147
149
|
Args:
|
|
148
150
|
violations: List of violation objects with rule_id, file_path, line, column, message, severity
|
|
149
|
-
output_format: Output format ("text" or "
|
|
151
|
+
output_format: Output format ("text", "json", or "sarif")
|
|
150
152
|
"""
|
|
151
153
|
if output_format == "json":
|
|
152
154
|
_output_json(violations)
|
|
155
|
+
elif output_format == "sarif":
|
|
156
|
+
_output_sarif(violations)
|
|
153
157
|
else:
|
|
154
158
|
_output_text(violations)
|
|
155
159
|
|
|
@@ -177,6 +181,19 @@ def _output_json(violations: list) -> None:
|
|
|
177
181
|
click.echo(json.dumps(output, indent=2))
|
|
178
182
|
|
|
179
183
|
|
|
184
|
+
def _output_sarif(violations: list) -> None:
|
|
185
|
+
"""Output violations in SARIF v2.1.0 format.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
violations: List of violation objects
|
|
189
|
+
"""
|
|
190
|
+
from src.formatters.sarif import SarifFormatter
|
|
191
|
+
|
|
192
|
+
formatter = SarifFormatter()
|
|
193
|
+
sarif_doc = formatter.format(violations)
|
|
194
|
+
click.echo(json.dumps(sarif_doc, indent=2))
|
|
195
|
+
|
|
196
|
+
|
|
180
197
|
def _output_text(violations: list) -> None:
|
|
181
198
|
"""Output violations in human-readable text format.
|
|
182
199
|
|
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."""
|
|
@@ -72,28 +74,56 @@ def parse_json(file_obj: TextIO, path: Path) -> dict[str, Any]:
|
|
|
72
74
|
raise ConfigParseError(f"Invalid JSON in {path}: {e}") from e
|
|
73
75
|
|
|
74
76
|
|
|
77
|
+
def _normalize_config_keys(config: dict[str, Any]) -> dict[str, Any]:
|
|
78
|
+
"""Normalize configuration keys from hyphens to underscores.
|
|
79
|
+
|
|
80
|
+
Converts top-level keys like "magic-numbers" to "magic_numbers" to match
|
|
81
|
+
internal linter expectations while maintaining backward compatibility with
|
|
82
|
+
both formats in config files.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
config: Configuration dictionary with potentially hyphenated keys
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Configuration dictionary with normalized (underscored) keys
|
|
89
|
+
"""
|
|
90
|
+
normalized = {}
|
|
91
|
+
for key, value in config.items():
|
|
92
|
+
# Replace hyphens with underscores in keys
|
|
93
|
+
normalized_key = key.replace("-", "_")
|
|
94
|
+
normalized[normalized_key] = value
|
|
95
|
+
return normalized
|
|
96
|
+
|
|
97
|
+
|
|
75
98
|
def parse_config_file(path: Path, encoding: str = "utf-8") -> dict[str, Any]:
|
|
76
99
|
"""Parse configuration file based on extension.
|
|
77
100
|
|
|
78
101
|
Supports .yaml, .yml, and .json formats. Automatically detects format
|
|
79
|
-
from file extension and uses appropriate parser.
|
|
102
|
+
from file extension and uses appropriate parser. Normalizes hyphenated
|
|
103
|
+
keys (e.g., "magic-numbers") to underscored keys (e.g., "magic_numbers")
|
|
104
|
+
for internal consistency.
|
|
80
105
|
|
|
81
106
|
Args:
|
|
82
107
|
path: Path to configuration file.
|
|
83
108
|
encoding: File encoding (default: utf-8).
|
|
84
109
|
|
|
85
110
|
Returns:
|
|
86
|
-
Parsed configuration dictionary.
|
|
111
|
+
Parsed configuration dictionary with normalized keys.
|
|
87
112
|
|
|
88
113
|
Raises:
|
|
89
114
|
ConfigParseError: If file format is unsupported or parsing fails.
|
|
90
115
|
"""
|
|
91
116
|
suffix = path.suffix.lower()
|
|
92
117
|
|
|
93
|
-
|
|
118
|
+
valid_suffixes = (*CONFIG_EXTENSIONS, ".json")
|
|
119
|
+
if suffix not in valid_suffixes:
|
|
94
120
|
raise ConfigParseError(f"Unsupported config format: {suffix}")
|
|
95
121
|
|
|
96
122
|
with path.open(encoding=encoding) as f:
|
|
97
|
-
if suffix in
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
if suffix in CONFIG_EXTENSIONS:
|
|
124
|
+
config = parse_yaml(f, path)
|
|
125
|
+
else:
|
|
126
|
+
config = parse_json(f, path)
|
|
127
|
+
|
|
128
|
+
# Normalize keys from hyphens to underscores
|
|
129
|
+
return _normalize_config_keys(config)
|
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)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Base class for Python-only linters with common boilerplate
|
|
3
|
+
|
|
4
|
+
Scope: Shared infrastructure for Python-only lint rules
|
|
5
|
+
|
|
6
|
+
Overview: Provides PythonOnlyLintRule abstract base class that handles common boilerplate
|
|
7
|
+
for Python-only linters. Subclasses implement the abstract properties and analysis
|
|
8
|
+
method while the base class handles language checking, config loading, and enabled
|
|
9
|
+
checking. This eliminates duplicate code across Python-only linters like CQS and LBYL.
|
|
10
|
+
|
|
11
|
+
Dependencies: BaseLintRule, BaseLintContext, Language, load_linter_config, has_file_content
|
|
12
|
+
|
|
13
|
+
Exports: PythonOnlyLintRule
|
|
14
|
+
|
|
15
|
+
Interfaces: Subclasses implement _config_key, _config_class, _analyze, and rule metadata
|
|
16
|
+
|
|
17
|
+
Implementation: Template method pattern for Python linter boilerplate
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from abc import abstractmethod
|
|
21
|
+
from typing import Any, Generic
|
|
22
|
+
|
|
23
|
+
from .base import BaseLintContext, BaseLintRule
|
|
24
|
+
from .constants import Language
|
|
25
|
+
from .linter_utils import ConfigType, has_file_content, load_linter_config
|
|
26
|
+
from .types import Violation
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PythonOnlyLintRule(BaseLintRule, Generic[ConfigType]):
|
|
30
|
+
"""Base class for Python-only linters with common boilerplate.
|
|
31
|
+
|
|
32
|
+
Handles language checking, config loading, and enabled checking.
|
|
33
|
+
Subclasses provide the config key, config class, and analysis logic.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: ConfigType | None = None) -> None:
|
|
37
|
+
"""Initialize with optional config override.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
config: Optional configuration override for testing
|
|
41
|
+
"""
|
|
42
|
+
self._config_override = config
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def _config_key(self) -> str:
|
|
47
|
+
"""Configuration key in metadata (e.g., 'cqs', 'lbyl')."""
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def _config_class(self) -> type[ConfigType]:
|
|
53
|
+
"""Configuration class type."""
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def _analyze(self, code: str, file_path: str, config: ConfigType) -> list[Violation]:
|
|
58
|
+
"""Perform linter-specific analysis.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
code: Python source code
|
|
62
|
+
file_path: Path to the file
|
|
63
|
+
config: Loaded configuration
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of violations found
|
|
67
|
+
"""
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
|
|
70
|
+
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
71
|
+
"""Check for violations in the given context.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
context: The lint context containing file information.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of violations found.
|
|
78
|
+
"""
|
|
79
|
+
if not self._should_analyze(context):
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
config = self._get_config(context)
|
|
83
|
+
if not self._is_enabled(config):
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
file_path = str(context.file_path) if context.file_path else "unknown"
|
|
87
|
+
return self._analyze(context.file_content or "", file_path, config)
|
|
88
|
+
|
|
89
|
+
def _should_analyze(self, context: BaseLintContext) -> bool:
|
|
90
|
+
"""Check if context should be analyzed."""
|
|
91
|
+
return context.language == Language.PYTHON and has_file_content(context)
|
|
92
|
+
|
|
93
|
+
def _get_config(self, context: BaseLintContext) -> ConfigType:
|
|
94
|
+
"""Get configuration, using override if provided."""
|
|
95
|
+
if self._config_override is not None:
|
|
96
|
+
return self._config_override
|
|
97
|
+
return load_linter_config(context, self._config_key, self._config_class)
|
|
98
|
+
|
|
99
|
+
def _is_enabled(self, config: Any) -> bool:
|
|
100
|
+
"""Check if linter is enabled in config."""
|
|
101
|
+
return getattr(config, "enabled", True)
|
src/core/registry.py
CHANGED
|
@@ -6,7 +6,7 @@ Scope: Dynamic rule management and discovery across all linter plugin packages
|
|
|
6
6
|
Overview: Implements rule registry that maintains a collection of registered linting rules indexed
|
|
7
7
|
by rule_id. Provides methods to register individual rules, retrieve rules by identifier, list
|
|
8
8
|
all available rules, and discover rules from packages using the RuleDiscovery helper. Enables
|
|
9
|
-
the extensible plugin architecture by allowing
|
|
9
|
+
the extensible plugin architecture by allowing dynamic rule registration without framework
|
|
10
10
|
modifications. Validates rule uniqueness and handles registration errors gracefully.
|
|
11
11
|
|
|
12
12
|
Dependencies: BaseLintRule, RuleDiscovery
|