thailint 0.5.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 +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- 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 +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- 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 +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- 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 +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- 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/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -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/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- 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/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- 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 +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
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."""
|
|
@@ -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)
|
|
@@ -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
|
src/core/rule_discovery.py
CHANGED
|
@@ -10,7 +10,7 @@ Overview: Provides automatic rule discovery functionality for the linter framewo
|
|
|
10
10
|
|
|
11
11
|
Dependencies: importlib, inspect, pkgutil, BaseLintRule
|
|
12
12
|
|
|
13
|
-
Exports: RuleDiscovery
|
|
13
|
+
Exports: discover_from_package function, RuleDiscovery class (compat)
|
|
14
14
|
|
|
15
15
|
Interfaces: discover_from_package(package_path) -> list[BaseLintRule]
|
|
16
16
|
|
|
@@ -19,114 +19,177 @@ 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
|
|
24
|
+
from types import ModuleType
|
|
23
25
|
from typing import Any
|
|
24
26
|
|
|
25
27
|
from .base import BaseLintRule
|
|
26
28
|
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
27
30
|
|
|
28
|
-
class RuleDiscovery:
|
|
29
|
-
"""Discovers linting rules from Python packages."""
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
def discover_from_package(package_path: str) -> list[BaseLintRule]:
|
|
33
|
+
"""Discover rules from a package and its modules.
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
Args:
|
|
36
|
+
package_path: Python package path (e.g., 'src.linters')
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
Returns:
|
|
39
|
+
List of discovered rule instances
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
package = importlib.import_module(package_path)
|
|
43
|
+
except ImportError as e:
|
|
44
|
+
logger.debug("Failed to import package %s: %s", package_path, e)
|
|
45
|
+
return []
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
if not hasattr(package, "__path__"):
|
|
48
|
+
return _discover_from_module(package_path)
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
return _discover_from_package_modules(package_path, package)
|
|
49
51
|
|
|
50
|
-
def _discover_from_package_modules(self, package_path: str, package: Any) -> list[BaseLintRule]:
|
|
51
|
-
"""Discover rules from all modules in a package.
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
package: Imported package object
|
|
53
|
+
def _discover_from_package_modules(package_path: str, package: Any) -> list[BaseLintRule]:
|
|
54
|
+
"""Discover rules from all modules in a package.
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
rules = []
|
|
61
|
-
for _, module_name, _ in pkgutil.iter_modules(package.__path__):
|
|
62
|
-
full_module_name = f"{package_path}.{module_name}"
|
|
63
|
-
module_rules = self._try_discover_from_module(full_module_name)
|
|
64
|
-
rules.extend(module_rules)
|
|
65
|
-
return rules
|
|
56
|
+
Args:
|
|
57
|
+
package_path: Package path
|
|
58
|
+
package: Imported package object
|
|
66
59
|
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
Returns:
|
|
61
|
+
List of discovered rules
|
|
62
|
+
"""
|
|
63
|
+
rules = []
|
|
64
|
+
for _, module_name, _ in pkgutil.iter_modules(package.__path__):
|
|
65
|
+
full_module_name = f"{package_path}.{module_name}"
|
|
66
|
+
module_rules = _try_discover_from_module(full_module_name)
|
|
67
|
+
rules.extend(module_rules)
|
|
68
|
+
return rules
|
|
69
69
|
|
|
70
|
-
Args:
|
|
71
|
-
module_name: Full module name
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"""
|
|
76
|
-
try:
|
|
77
|
-
return self._discover_from_module(module_name)
|
|
78
|
-
except (ImportError, AttributeError):
|
|
79
|
-
return []
|
|
71
|
+
def _try_discover_from_module(module_name: str) -> list[BaseLintRule]:
|
|
72
|
+
"""Try to discover rules from a module, return empty list on error.
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
Args:
|
|
75
|
+
module_name: Full module name
|
|
83
76
|
|
|
84
|
-
|
|
85
|
-
|
|
77
|
+
Returns:
|
|
78
|
+
List of discovered rules (empty on error)
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
return _discover_from_module(module_name)
|
|
82
|
+
except (ImportError, AttributeError):
|
|
83
|
+
return []
|
|
86
84
|
|
|
87
|
-
Returns:
|
|
88
|
-
List of discovered rule instances
|
|
89
|
-
"""
|
|
90
|
-
try:
|
|
91
|
-
module = importlib.import_module(module_path)
|
|
92
|
-
except (ImportError, AttributeError):
|
|
93
|
-
return []
|
|
94
|
-
|
|
95
|
-
rules = []
|
|
96
|
-
for _name, obj in inspect.getmembers(module):
|
|
97
|
-
if not self._is_rule_class(obj):
|
|
98
|
-
continue
|
|
99
|
-
rule_instance = self._try_instantiate_rule(obj)
|
|
100
|
-
if rule_instance:
|
|
101
|
-
rules.append(rule_instance)
|
|
102
|
-
return rules
|
|
103
|
-
|
|
104
|
-
def _try_instantiate_rule(self, rule_class: type[BaseLintRule]) -> BaseLintRule | None:
|
|
105
|
-
"""Try to instantiate a rule class.
|
|
106
85
|
|
|
107
|
-
|
|
108
|
-
|
|
86
|
+
def _discover_from_module(module_path: str) -> list[BaseLintRule]:
|
|
87
|
+
"""Discover rules from a specific module.
|
|
109
88
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
89
|
+
Args:
|
|
90
|
+
module_path: Full module path to search
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
List of discovered rule instances
|
|
94
|
+
"""
|
|
95
|
+
module = _try_import_module(module_path)
|
|
96
|
+
if module is None:
|
|
97
|
+
return []
|
|
98
|
+
return _extract_rules_from_module(module)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _try_import_module(module_path: str) -> ModuleType | None:
|
|
102
|
+
"""Try to import a module, returning None on failure.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
module_path: Full module path to import
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Module object or None if import fails
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
return importlib.import_module(module_path)
|
|
112
|
+
except (ImportError, AttributeError):
|
|
113
|
+
return None
|
|
117
114
|
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
|
|
116
|
+
def _extract_rules_from_module(module: ModuleType) -> list[BaseLintRule]:
|
|
117
|
+
"""Extract rule instances from a module.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
module: Imported module to scan
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of discovered rule instances
|
|
124
|
+
"""
|
|
125
|
+
rule_classes = [obj for _name, obj in inspect.getmembers(module) if _is_rule_class(obj)]
|
|
126
|
+
return _instantiate_rules(rule_classes)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _instantiate_rules(rule_classes: list[type[BaseLintRule]]) -> list[BaseLintRule]:
|
|
130
|
+
"""Instantiate a list of rule classes.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
rule_classes: List of rule classes to instantiate
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of successfully instantiated rules
|
|
137
|
+
"""
|
|
138
|
+
instances = (_try_instantiate_rule(cls) for cls in rule_classes)
|
|
139
|
+
return [inst for inst in instances if inst is not None]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _try_instantiate_rule(rule_class: type[BaseLintRule]) -> BaseLintRule | None:
|
|
143
|
+
"""Try to instantiate a rule class.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
rule_class: Rule class to instantiate
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Rule instance or None on error
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
return rule_class()
|
|
153
|
+
except (TypeError, AttributeError):
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _is_rule_class(obj: Any) -> bool:
|
|
158
|
+
"""Check if an object is a valid rule class.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
obj: Object to check
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if obj is a concrete BaseLintRule subclass
|
|
165
|
+
"""
|
|
166
|
+
return (
|
|
167
|
+
inspect.isclass(obj)
|
|
168
|
+
and issubclass(obj, BaseLintRule)
|
|
169
|
+
and obj is not BaseLintRule
|
|
170
|
+
and not inspect.isabstract(obj)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Legacy class wrapper for backward compatibility
|
|
175
|
+
class RuleDiscovery:
|
|
176
|
+
"""Discovers linting rules from Python packages.
|
|
177
|
+
|
|
178
|
+
Note: This class is a thin wrapper around module-level functions
|
|
179
|
+
for backward compatibility.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self) -> None:
|
|
183
|
+
"""Initialize the discovery service."""
|
|
184
|
+
pass # No state needed
|
|
185
|
+
|
|
186
|
+
def discover_from_package(self, package_path: str) -> list[BaseLintRule]:
|
|
187
|
+
"""Discover rules from a package and its modules.
|
|
120
188
|
|
|
121
189
|
Args:
|
|
122
|
-
|
|
190
|
+
package_path: Python package path (e.g., 'src.linters')
|
|
123
191
|
|
|
124
192
|
Returns:
|
|
125
|
-
|
|
193
|
+
List of discovered rule instances
|
|
126
194
|
"""
|
|
127
|
-
return (
|
|
128
|
-
inspect.isclass(obj)
|
|
129
|
-
and issubclass(obj, BaseLintRule)
|
|
130
|
-
and obj is not BaseLintRule
|
|
131
|
-
and not inspect.isabstract(obj)
|
|
132
|
-
)
|
|
195
|
+
return discover_from_package(package_path)
|
src/core/types.py
CHANGED
|
@@ -81,3 +81,16 @@ class Violation:
|
|
|
81
81
|
"severity": self.severity.value,
|
|
82
82
|
"suggestion": self.suggestion,
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_dict(cls, data: dict) -> "Violation":
|
|
87
|
+
"""Reconstruct Violation from dictionary (for parallel processing)."""
|
|
88
|
+
return cls(
|
|
89
|
+
rule_id=data["rule_id"],
|
|
90
|
+
file_path=data["file_path"],
|
|
91
|
+
line=data["line"],
|
|
92
|
+
column=data["column"],
|
|
93
|
+
message=data["message"],
|
|
94
|
+
severity=Severity(data["severity"]),
|
|
95
|
+
suggestion=data.get("suggestion"),
|
|
96
|
+
)
|