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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Type definitions for lazy-ignores linter
|
|
3
|
+
|
|
4
|
+
Scope: Data structures for ignore directives and suppression entries
|
|
5
|
+
|
|
6
|
+
Overview: Defines core types for the lazy-ignores linter including IgnoreType enum for
|
|
7
|
+
categorizing different suppression patterns, IgnoreDirective dataclass for representing
|
|
8
|
+
detected ignores in code, and SuppressionEntry dataclass for representing declared
|
|
9
|
+
suppressions in file headers. Supports Python (noqa, type:ignore, pylint, nosec, pyright),
|
|
10
|
+
TypeScript (@ts-ignore, eslint-disable), thai-lint (thailint:ignore), and test skip
|
|
11
|
+
patterns (pytest.mark.skip, it.skip, describe.skip).
|
|
12
|
+
|
|
13
|
+
Dependencies: dataclasses, enum, pathlib
|
|
14
|
+
|
|
15
|
+
Exports: IgnoreType, IgnoreDirective, SuppressionEntry
|
|
16
|
+
|
|
17
|
+
Interfaces: Frozen dataclasses for immutable ignore representation
|
|
18
|
+
|
|
19
|
+
Implementation: Enum-based categorization with frozen dataclasses for thread safety
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class IgnoreType(Enum):
|
|
28
|
+
"""Type of linting ignore directive."""
|
|
29
|
+
|
|
30
|
+
NOQA = "noqa"
|
|
31
|
+
TYPE_IGNORE = "type:ignore"
|
|
32
|
+
PYLINT_DISABLE = "pylint:disable"
|
|
33
|
+
NOSEC = "nosec"
|
|
34
|
+
PYRIGHT_IGNORE = "pyright:ignore"
|
|
35
|
+
TS_IGNORE = "ts-ignore"
|
|
36
|
+
TS_NOCHECK = "ts-nocheck"
|
|
37
|
+
TS_EXPECT_ERROR = "ts-expect-error"
|
|
38
|
+
ESLINT_DISABLE = "eslint-disable"
|
|
39
|
+
THAILINT_IGNORE = "thailint:ignore"
|
|
40
|
+
THAILINT_IGNORE_FILE = "thailint:ignore-file"
|
|
41
|
+
THAILINT_IGNORE_NEXT = "thailint:ignore-next-line"
|
|
42
|
+
THAILINT_IGNORE_BLOCK = "thailint:ignore-start"
|
|
43
|
+
# DRY ignore patterns
|
|
44
|
+
DRY_IGNORE_BLOCK = "dry:ignore-block"
|
|
45
|
+
# Test skip patterns
|
|
46
|
+
PYTEST_SKIP = "pytest:skip"
|
|
47
|
+
PYTEST_SKIPIF = "pytest:skipif"
|
|
48
|
+
JEST_SKIP = "jest:skip"
|
|
49
|
+
MOCHA_SKIP = "mocha:skip"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class IgnoreDirective:
|
|
54
|
+
"""Represents a linting ignore found in code."""
|
|
55
|
+
|
|
56
|
+
ignore_type: IgnoreType
|
|
57
|
+
rule_ids: tuple[str, ...] # Can have multiple: noqa: PLR0912, PLR0915
|
|
58
|
+
line: int
|
|
59
|
+
column: int
|
|
60
|
+
raw_text: str # Original comment text
|
|
61
|
+
file_path: Path
|
|
62
|
+
inline_justification: str | None = None # Justification after " - " delimiter
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class SuppressionEntry:
|
|
67
|
+
"""Represents a suppression declared in file header."""
|
|
68
|
+
|
|
69
|
+
rule_id: str # Normalized rule ID
|
|
70
|
+
justification: str
|
|
71
|
+
raw_text: str # Original header line
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Detect TypeScript/JavaScript linting ignore directives in source code
|
|
3
|
+
|
|
4
|
+
Scope: @ts-ignore, @ts-nocheck, @ts-expect-error, eslint-disable pattern detection
|
|
5
|
+
|
|
6
|
+
Overview: Provides TypeScriptIgnoreDetector class that scans TypeScript and JavaScript source
|
|
7
|
+
code for common linting ignore patterns. Detects TypeScript-specific patterns (@ts-ignore,
|
|
8
|
+
@ts-nocheck, @ts-expect-error) and ESLint patterns (eslint-disable-next-line, eslint-disable
|
|
9
|
+
block comments, eslint-disable-line). Handles both single-line (//) and block (/* */)
|
|
10
|
+
comment styles. Returns list of IgnoreDirective objects with line/column positions for
|
|
11
|
+
violation reporting.
|
|
12
|
+
|
|
13
|
+
Dependencies: re for pattern matching, pathlib for file paths, types module for dataclasses
|
|
14
|
+
|
|
15
|
+
Exports: TypeScriptIgnoreDetector
|
|
16
|
+
|
|
17
|
+
Interfaces: find_ignores(code: str, file_path: Path | str | None) -> list[IgnoreDirective]
|
|
18
|
+
|
|
19
|
+
Implementation: Regex-based line-by-line scanning with pattern-specific rule ID extraction
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from src.linters.lazy_ignores.directive_utils import create_directive, normalize_path
|
|
26
|
+
from src.linters.lazy_ignores.types import IgnoreDirective, IgnoreType
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TypeScriptIgnoreDetector:
|
|
30
|
+
"""Detects TypeScript/JavaScript linting ignore directives in source code."""
|
|
31
|
+
|
|
32
|
+
# Regex patterns for each ignore type
|
|
33
|
+
# Single-line comment patterns (//)
|
|
34
|
+
SINGLE_LINE_PATTERNS: dict[IgnoreType, re.Pattern[str]] = {
|
|
35
|
+
IgnoreType.TS_IGNORE: re.compile(
|
|
36
|
+
r"//\s*@ts-ignore(?:\s|$)",
|
|
37
|
+
),
|
|
38
|
+
IgnoreType.TS_NOCHECK: re.compile(
|
|
39
|
+
r"//\s*@ts-nocheck(?:\s|$)",
|
|
40
|
+
),
|
|
41
|
+
IgnoreType.TS_EXPECT_ERROR: re.compile(
|
|
42
|
+
r"//\s*@ts-expect-error(?:\s|$)",
|
|
43
|
+
),
|
|
44
|
+
IgnoreType.THAILINT_IGNORE: re.compile(
|
|
45
|
+
r"//\s*thailint:\s*ignore(?!-)(?:\[([^\]]+)\])?",
|
|
46
|
+
re.IGNORECASE,
|
|
47
|
+
),
|
|
48
|
+
IgnoreType.THAILINT_IGNORE_FILE: re.compile(
|
|
49
|
+
r"//\s*thailint:\s*ignore-file(?:\[([^\]]+)\])?",
|
|
50
|
+
re.IGNORECASE,
|
|
51
|
+
),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ESLint patterns (can be single-line or inline)
|
|
55
|
+
ESLINT_PATTERNS: dict[str, re.Pattern[str]] = {
|
|
56
|
+
"next-line": re.compile(
|
|
57
|
+
r"//\s*eslint-disable-next-line(?:\s+([a-zA-Z0-9\-/,\s]+))?(?:\s|$)",
|
|
58
|
+
),
|
|
59
|
+
"inline": re.compile(
|
|
60
|
+
r"//\s*eslint-disable-line(?:\s+([a-zA-Z0-9\-/,\s]+))?(?:\s|$)",
|
|
61
|
+
),
|
|
62
|
+
"block-start": re.compile(
|
|
63
|
+
r"/\*\s*eslint-disable(?:\s+([a-zA-Z0-9\-/,\s]+))?\s*\*/",
|
|
64
|
+
),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def find_ignores(self, code: str, file_path: Path | str | None = None) -> list[IgnoreDirective]:
|
|
68
|
+
"""Find all TypeScript/JavaScript ignore directives in code.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
code: TypeScript/JavaScript source code to scan
|
|
72
|
+
file_path: Optional path to the source file (Path or string)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of IgnoreDirective objects for each detected ignore pattern
|
|
76
|
+
"""
|
|
77
|
+
directives: list[IgnoreDirective] = []
|
|
78
|
+
effective_path = normalize_path(file_path)
|
|
79
|
+
|
|
80
|
+
for line_num, line in enumerate(code.splitlines(), start=1):
|
|
81
|
+
directives.extend(self._scan_line(line, line_num, effective_path))
|
|
82
|
+
|
|
83
|
+
return directives
|
|
84
|
+
|
|
85
|
+
def _scan_line(self, line: str, line_num: int, file_path: Path) -> list[IgnoreDirective]:
|
|
86
|
+
"""Scan a single line for ignore patterns.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
line: Line of code to scan
|
|
90
|
+
line_num: 1-indexed line number
|
|
91
|
+
file_path: Path to the source file
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of IgnoreDirective objects found on this line
|
|
95
|
+
"""
|
|
96
|
+
found: list[IgnoreDirective] = []
|
|
97
|
+
|
|
98
|
+
# Check TypeScript-specific patterns
|
|
99
|
+
found.extend(self._scan_typescript_patterns(line, line_num, file_path))
|
|
100
|
+
|
|
101
|
+
# Check ESLint patterns
|
|
102
|
+
found.extend(self._scan_eslint_patterns(line, line_num, file_path))
|
|
103
|
+
|
|
104
|
+
return found
|
|
105
|
+
|
|
106
|
+
def _scan_typescript_patterns(
|
|
107
|
+
self, line: str, line_num: int, file_path: Path
|
|
108
|
+
) -> list[IgnoreDirective]:
|
|
109
|
+
"""Scan for TypeScript-specific ignore patterns.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
line: Line of code to scan
|
|
113
|
+
line_num: 1-indexed line number
|
|
114
|
+
file_path: Path to the source file
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of IgnoreDirective objects for TypeScript patterns
|
|
118
|
+
"""
|
|
119
|
+
found: list[IgnoreDirective] = []
|
|
120
|
+
for ignore_type, pattern in self.SINGLE_LINE_PATTERNS.items():
|
|
121
|
+
match = pattern.search(line)
|
|
122
|
+
if match:
|
|
123
|
+
found.append(create_directive(match, ignore_type, line_num, file_path))
|
|
124
|
+
return found
|
|
125
|
+
|
|
126
|
+
def _scan_eslint_patterns(
|
|
127
|
+
self, line: str, line_num: int, file_path: Path
|
|
128
|
+
) -> list[IgnoreDirective]:
|
|
129
|
+
"""Scan for ESLint disable patterns.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
line: Line of code to scan
|
|
133
|
+
line_num: 1-indexed line number
|
|
134
|
+
file_path: Path to the source file
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
List of IgnoreDirective objects for ESLint patterns
|
|
138
|
+
"""
|
|
139
|
+
found: list[IgnoreDirective] = []
|
|
140
|
+
for pattern in self.ESLINT_PATTERNS.values():
|
|
141
|
+
match = pattern.search(line)
|
|
142
|
+
if match:
|
|
143
|
+
found.append(
|
|
144
|
+
create_directive(match, IgnoreType.ESLINT_DISABLE, line_num, file_path)
|
|
145
|
+
)
|
|
146
|
+
return found
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Build agent-friendly violation messages for lazy-ignores linter
|
|
3
|
+
|
|
4
|
+
Scope: Violation construction for unjustified ignores and orphaned header suppressions
|
|
5
|
+
|
|
6
|
+
Overview: Provides functions to construct Violation objects with AI-agent-friendly error
|
|
7
|
+
messages. Messages include explicit guidance for adding Suppressions section entries to
|
|
8
|
+
file headers and emphasize the requirement for human approval before adding suppressions.
|
|
9
|
+
Designed to help AI coding assistants understand the proper workflow for handling linting
|
|
10
|
+
suppressions rather than blindly adding ignore directives.
|
|
11
|
+
|
|
12
|
+
Dependencies: src.core.types for Violation dataclass
|
|
13
|
+
|
|
14
|
+
Exports: build_unjustified_violation, build_orphaned_violation
|
|
15
|
+
|
|
16
|
+
Interfaces: Two builder functions returning Violation objects
|
|
17
|
+
|
|
18
|
+
Implementation: Template-based message construction with rule ID formatting
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from src.core.types import Severity, Violation
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_unjustified_violation(
|
|
25
|
+
file_path: str,
|
|
26
|
+
line: int,
|
|
27
|
+
column: int,
|
|
28
|
+
rule_id: str,
|
|
29
|
+
raw_text: str,
|
|
30
|
+
) -> Violation:
|
|
31
|
+
"""Create violation for an ignore directive without header justification.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
file_path: Path to the file containing the violation.
|
|
35
|
+
line: Line number where the ignore was found (1-indexed).
|
|
36
|
+
column: Column number where the ignore starts (0-indexed).
|
|
37
|
+
rule_id: The rule ID(s) being suppressed (e.g., "PLR0912").
|
|
38
|
+
raw_text: The raw ignore directive text found in code.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Violation object with agent-friendly guidance message.
|
|
42
|
+
"""
|
|
43
|
+
message = (
|
|
44
|
+
f"Unjustified suppression found: {raw_text} "
|
|
45
|
+
f"(ASK PERMISSION before adding Suppressions header)"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
suggestion = _build_unjustified_suggestion(rule_id)
|
|
49
|
+
|
|
50
|
+
return Violation(
|
|
51
|
+
rule_id="lazy-ignores.unjustified",
|
|
52
|
+
file_path=file_path,
|
|
53
|
+
line=line,
|
|
54
|
+
column=column,
|
|
55
|
+
message=message,
|
|
56
|
+
severity=Severity.ERROR,
|
|
57
|
+
suggestion=suggestion,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _build_unjustified_suggestion(rule_id: str) -> str:
|
|
62
|
+
"""Build the suggestion text for unjustified violations.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
rule_id: The rule ID(s) being suppressed.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Formatted suggestion string with header instructions.
|
|
69
|
+
"""
|
|
70
|
+
# Handle multiple rules (e.g., "PLR0912, PLR0915")
|
|
71
|
+
rule_ids = [r.strip() for r in rule_id.split(",")]
|
|
72
|
+
|
|
73
|
+
suppression_entries = "\n".join(f" {rid}: [Your justification here]" for rid in rule_ids)
|
|
74
|
+
|
|
75
|
+
return f"""To fix, either:
|
|
76
|
+
|
|
77
|
+
1. Add an inline justification (10+ chars) after the ignore directive:
|
|
78
|
+
# noqa: {rule_ids[0]} - [Your justification here]
|
|
79
|
+
|
|
80
|
+
2. Or add an entry to the file header Suppressions section:
|
|
81
|
+
Suppressions:
|
|
82
|
+
{suppression_entries}
|
|
83
|
+
|
|
84
|
+
IMPORTANT: Adding suppressions requires human approval.
|
|
85
|
+
Do not add this entry without explicit permission from a human reviewer.
|
|
86
|
+
Ask first, then add if approved."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_orphaned_violation(
|
|
90
|
+
file_path: str,
|
|
91
|
+
header_line: int,
|
|
92
|
+
rule_id: str,
|
|
93
|
+
justification: str,
|
|
94
|
+
) -> Violation:
|
|
95
|
+
"""Create violation for a header entry without matching code ignore.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
file_path: Path to the file containing the orphaned entry.
|
|
99
|
+
header_line: Line number of the suppression in the header (1-indexed).
|
|
100
|
+
rule_id: The orphaned rule ID from the header.
|
|
101
|
+
justification: The justification text from the header.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Violation object suggesting removal of the orphaned entry.
|
|
105
|
+
"""
|
|
106
|
+
message = f"Orphaned suppression in header: {rule_id}: {justification}"
|
|
107
|
+
|
|
108
|
+
suggestion = _build_orphaned_suggestion(rule_id)
|
|
109
|
+
|
|
110
|
+
return Violation(
|
|
111
|
+
rule_id="lazy-ignores.orphaned",
|
|
112
|
+
file_path=file_path,
|
|
113
|
+
line=header_line,
|
|
114
|
+
column=0,
|
|
115
|
+
message=message,
|
|
116
|
+
severity=Severity.ERROR,
|
|
117
|
+
suggestion=suggestion,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _build_orphaned_suggestion(rule_id: str) -> str:
|
|
122
|
+
"""Build the suggestion text for orphaned violations.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
rule_id: The orphaned rule ID.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Formatted suggestion string with removal instructions.
|
|
129
|
+
"""
|
|
130
|
+
return f"""This rule is declared in the Suppressions section but no matching
|
|
131
|
+
ignore directive was found in the code.
|
|
132
|
+
|
|
133
|
+
Either:
|
|
134
|
+
1. Remove the entry for {rule_id} from the Suppressions section if the ignore was removed from code
|
|
135
|
+
2. Add the ignore directive if it's missing from the code"""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: LBYL (Look Before You Leap) linter package exports
|
|
3
|
+
|
|
4
|
+
Scope: Detect LBYL anti-patterns in Python code and suggest EAFP alternatives
|
|
5
|
+
|
|
6
|
+
Overview: Package providing LBYL pattern detection for Python code. Identifies common
|
|
7
|
+
anti-patterns where explicit checks are performed before operations (e.g., if key in
|
|
8
|
+
dict before dict[key]) and suggests EAFP (Easier to Ask Forgiveness than Permission)
|
|
9
|
+
alternatives using try/except blocks. Supports 8 pattern types including dict key
|
|
10
|
+
checking, hasattr, isinstance, file exists, length checks, None checks, string
|
|
11
|
+
validation, and division safety checks.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for Python parsing, src.core for base classes
|
|
14
|
+
|
|
15
|
+
Exports: LBYLConfig, LBYLPattern, BaseLBYLDetector, LBYLRule
|
|
16
|
+
|
|
17
|
+
Interfaces: LBYLConfig.from_dict() for YAML configuration loading
|
|
18
|
+
|
|
19
|
+
Implementation: AST-based pattern detection with configurable pattern toggles
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .config import LBYLConfig
|
|
23
|
+
from .linter import LBYLRule
|
|
24
|
+
from .pattern_detectors.base import BaseLBYLDetector, LBYLPattern
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"LBYLConfig",
|
|
28
|
+
"LBYLPattern",
|
|
29
|
+
"BaseLBYLDetector",
|
|
30
|
+
"LBYLRule",
|
|
31
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration dataclass for LBYL linter
|
|
3
|
+
|
|
4
|
+
Scope: Pattern toggles, ignore patterns, and validation
|
|
5
|
+
|
|
6
|
+
Overview: Provides LBYLConfig dataclass with pattern-specific toggles for each LBYL
|
|
7
|
+
pattern type (dict_key, hasattr, isinstance, file_exists, len_check, none_check,
|
|
8
|
+
string_validation, division_check). Some patterns like isinstance and none_check
|
|
9
|
+
are disabled by default due to many valid use cases. Configuration can be loaded
|
|
10
|
+
from dictionary (YAML) with sensible defaults.
|
|
11
|
+
|
|
12
|
+
Dependencies: dataclasses, typing
|
|
13
|
+
|
|
14
|
+
Exports: LBYLConfig
|
|
15
|
+
|
|
16
|
+
Interfaces: LBYLConfig.from_dict() for YAML configuration loading
|
|
17
|
+
|
|
18
|
+
Implementation: Dataclass with factory defaults and conservative default settings
|
|
19
|
+
|
|
20
|
+
Suppressions:
|
|
21
|
+
too-many-instance-attributes: Configuration dataclass requires many toggles
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class LBYLConfig: # pylint: disable=too-many-instance-attributes
|
|
30
|
+
"""Configuration for LBYL linter."""
|
|
31
|
+
|
|
32
|
+
enabled: bool = True
|
|
33
|
+
|
|
34
|
+
# Pattern toggles
|
|
35
|
+
detect_dict_key: bool = True
|
|
36
|
+
detect_hasattr: bool = True
|
|
37
|
+
detect_isinstance: bool = False # Disabled - many valid uses for type narrowing
|
|
38
|
+
detect_file_exists: bool = True
|
|
39
|
+
detect_len_check: bool = True
|
|
40
|
+
detect_none_check: bool = False # Disabled - many valid uses
|
|
41
|
+
detect_string_validation: bool = True
|
|
42
|
+
detect_division_check: bool = True
|
|
43
|
+
|
|
44
|
+
# File patterns to ignore
|
|
45
|
+
ignore: list[str] = field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "LBYLConfig":
|
|
49
|
+
"""Load configuration from dictionary."""
|
|
50
|
+
# Language parameter reserved for future multi-language support
|
|
51
|
+
_ = language
|
|
52
|
+
return cls(
|
|
53
|
+
enabled=config.get("enabled", True),
|
|
54
|
+
detect_dict_key=config.get("detect_dict_key", True),
|
|
55
|
+
detect_hasattr=config.get("detect_hasattr", True),
|
|
56
|
+
detect_isinstance=config.get("detect_isinstance", False),
|
|
57
|
+
detect_file_exists=config.get("detect_file_exists", True),
|
|
58
|
+
detect_len_check=config.get("detect_len_check", True),
|
|
59
|
+
detect_none_check=config.get("detect_none_check", False),
|
|
60
|
+
detect_string_validation=config.get("detect_string_validation", True),
|
|
61
|
+
detect_division_check=config.get("detect_division_check", True),
|
|
62
|
+
ignore=config.get("ignore", []),
|
|
63
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main LBYL linter rule implementing PythonOnlyLintRule interface
|
|
3
|
+
|
|
4
|
+
Scope: Entry point for LBYL anti-pattern detection in Python code
|
|
5
|
+
|
|
6
|
+
Overview: Provides LBYLRule class that implements the PythonOnlyLintRule interface for
|
|
7
|
+
detecting Look Before You Leap anti-patterns in Python code. Validates that files
|
|
8
|
+
are Python with content, loads configuration, and delegates analysis to
|
|
9
|
+
PythonLBYLAnalyzer. Returns violations with EAFP suggestions for detected patterns.
|
|
10
|
+
Supports disabling via configuration and pattern-specific toggles.
|
|
11
|
+
|
|
12
|
+
Dependencies: PythonOnlyLintRule, PythonLBYLAnalyzer, LBYLConfig
|
|
13
|
+
|
|
14
|
+
Exports: LBYLRule
|
|
15
|
+
|
|
16
|
+
Interfaces: check(context: BaseLintContext) -> list[Violation]
|
|
17
|
+
|
|
18
|
+
Implementation: Single-file analysis with config-driven pattern detection
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from src.core.python_lint_rule import PythonOnlyLintRule
|
|
22
|
+
from src.core.types import Violation
|
|
23
|
+
|
|
24
|
+
from .config import LBYLConfig
|
|
25
|
+
from .python_analyzer import PythonLBYLAnalyzer
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LBYLRule(PythonOnlyLintRule[LBYLConfig]):
|
|
29
|
+
"""Detects Look Before You Leap anti-patterns in Python code."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, config: LBYLConfig | None = None) -> None:
|
|
32
|
+
"""Initialize the LBYL rule."""
|
|
33
|
+
super().__init__(config)
|
|
34
|
+
self._analyzer = PythonLBYLAnalyzer()
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def rule_id(self) -> str:
|
|
38
|
+
"""Unique identifier for this rule."""
|
|
39
|
+
return "lbyl"
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def rule_name(self) -> str:
|
|
43
|
+
"""Human-readable name for this rule."""
|
|
44
|
+
return "Look Before You Leap"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def description(self) -> str:
|
|
48
|
+
"""Description of what this rule checks."""
|
|
49
|
+
return (
|
|
50
|
+
"Detects LBYL (Look Before You Leap) anti-patterns that should be "
|
|
51
|
+
"refactored to EAFP (Easier to Ask Forgiveness than Permission) style "
|
|
52
|
+
"using try/except blocks."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def _config_key(self) -> str:
|
|
57
|
+
"""Configuration key in metadata."""
|
|
58
|
+
return "lbyl"
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def _config_class(self) -> type[LBYLConfig]:
|
|
62
|
+
"""Configuration class type."""
|
|
63
|
+
return LBYLConfig
|
|
64
|
+
|
|
65
|
+
def _analyze(self, code: str, file_path: str, config: LBYLConfig) -> list[Violation]:
|
|
66
|
+
"""Analyze code for LBYL violations."""
|
|
67
|
+
return self._analyzer.analyze(code, file_path, config)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Pattern detector exports for LBYL linter
|
|
3
|
+
|
|
4
|
+
Scope: All AST-based pattern detectors for LBYL anti-pattern detection
|
|
5
|
+
|
|
6
|
+
Overview: Exports pattern detector classes for the LBYL linter. Each detector is an
|
|
7
|
+
AST NodeVisitor that identifies specific LBYL anti-patterns. Detectors include
|
|
8
|
+
dict key checking, hasattr, isinstance, file exists, length checks, None checks,
|
|
9
|
+
string validators, and division zero-checks.
|
|
10
|
+
|
|
11
|
+
Dependencies: ast module, base detector class
|
|
12
|
+
|
|
13
|
+
Exports: BaseLBYLDetector, LBYLPattern, DictKeyDetector, DictKeyPattern, HasattrDetector,
|
|
14
|
+
HasattrPattern, IsinstanceDetector, IsinstancePattern, FileExistsDetector,
|
|
15
|
+
FileExistsPattern, LenCheckDetector, LenCheckPattern, NoneCheckDetector,
|
|
16
|
+
NoneCheckPattern, StringValidatorDetector, StringValidatorPattern,
|
|
17
|
+
DivisionCheckDetector, DivisionCheckPattern
|
|
18
|
+
|
|
19
|
+
Interfaces: find_patterns(tree: ast.AST) -> list[LBYLPattern]
|
|
20
|
+
|
|
21
|
+
Implementation: Modular detector pattern for extensible LBYL detection
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .base import BaseLBYLDetector, LBYLPattern
|
|
25
|
+
from .dict_key_detector import DictKeyDetector, DictKeyPattern
|
|
26
|
+
from .division_check_detector import DivisionCheckDetector, DivisionCheckPattern
|
|
27
|
+
from .file_exists_detector import FileExistsDetector, FileExistsPattern
|
|
28
|
+
from .hasattr_detector import HasattrDetector, HasattrPattern
|
|
29
|
+
from .isinstance_detector import IsinstanceDetector, IsinstancePattern
|
|
30
|
+
from .len_check_detector import LenCheckDetector, LenCheckPattern
|
|
31
|
+
from .none_check_detector import NoneCheckDetector, NoneCheckPattern
|
|
32
|
+
from .string_validator_detector import StringValidatorDetector, StringValidatorPattern
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"BaseLBYLDetector",
|
|
36
|
+
"LBYLPattern",
|
|
37
|
+
"DictKeyDetector",
|
|
38
|
+
"DictKeyPattern",
|
|
39
|
+
"DivisionCheckDetector",
|
|
40
|
+
"DivisionCheckPattern",
|
|
41
|
+
"FileExistsDetector",
|
|
42
|
+
"FileExistsPattern",
|
|
43
|
+
"HasattrDetector",
|
|
44
|
+
"HasattrPattern",
|
|
45
|
+
"IsinstanceDetector",
|
|
46
|
+
"IsinstancePattern",
|
|
47
|
+
"LenCheckDetector",
|
|
48
|
+
"LenCheckPattern",
|
|
49
|
+
"NoneCheckDetector",
|
|
50
|
+
"NoneCheckPattern",
|
|
51
|
+
"StringValidatorDetector",
|
|
52
|
+
"StringValidatorPattern",
|
|
53
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Base class for LBYL pattern detectors
|
|
3
|
+
|
|
4
|
+
Scope: Abstract base providing common detector interface
|
|
5
|
+
|
|
6
|
+
Overview: Defines BaseLBYLDetector abstract class that all pattern detectors extend.
|
|
7
|
+
Inherits from ast.NodeVisitor for AST traversal. Defines LBYLPattern base dataclass
|
|
8
|
+
for representing detected patterns with line number and column information. Each
|
|
9
|
+
concrete detector implements find_patterns() to identify specific LBYL anti-patterns.
|
|
10
|
+
Uses Generic TypeVar for type-safe subclass pattern storage.
|
|
11
|
+
|
|
12
|
+
Dependencies: abc, ast, dataclasses, typing
|
|
13
|
+
|
|
14
|
+
Exports: BaseLBYLDetector, LBYLPattern
|
|
15
|
+
|
|
16
|
+
Interfaces: find_patterns(tree: ast.AST) -> list[LBYLPattern]
|
|
17
|
+
|
|
18
|
+
Implementation: Abstract base with NodeVisitor pattern for extensibility, Generic for type safety
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
from abc import ABC
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Generic, TypeVar
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class LBYLPattern:
|
|
29
|
+
"""Base pattern data for detected LBYL anti-patterns."""
|
|
30
|
+
|
|
31
|
+
line_number: int
|
|
32
|
+
column: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
PatternT = TypeVar("PatternT", bound=LBYLPattern)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BaseLBYLDetector(ast.NodeVisitor, ABC, Generic[PatternT]):
|
|
39
|
+
"""Base class for LBYL pattern detectors.
|
|
40
|
+
|
|
41
|
+
Subclasses must initialize self._patterns as an empty list in __init__
|
|
42
|
+
and populate it in visit methods. The _patterns attribute stores subclass-
|
|
43
|
+
specific pattern types (DictKeyPattern, HasattrPattern, etc.) which all
|
|
44
|
+
inherit from LBYLPattern.
|
|
45
|
+
|
|
46
|
+
Type Parameters:
|
|
47
|
+
PatternT: The specific pattern type used by this detector
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
_patterns: list[PatternT]
|
|
51
|
+
|
|
52
|
+
def find_patterns(self, tree: ast.AST) -> list[LBYLPattern]:
|
|
53
|
+
"""Find LBYL patterns in AST.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
tree: Python AST to analyze
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List of detected LBYL patterns
|
|
60
|
+
"""
|
|
61
|
+
self._patterns = []
|
|
62
|
+
self.visit(tree)
|
|
63
|
+
return list(self._patterns)
|