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/linters/dry/linter.py
CHANGED
|
@@ -1,47 +1,59 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Purpose: Main DRY linter rule implementation with stateful caching
|
|
3
3
|
|
|
4
|
-
Scope: DRYRule class implementing BaseLintRule interface for duplicate code detection
|
|
4
|
+
Scope: DRYRule class implementing BaseLintRule interface for duplicate code and constant detection
|
|
5
5
|
|
|
6
6
|
Overview: Implements DRY linter rule following BaseLintRule interface with stateful caching design.
|
|
7
7
|
Orchestrates duplicate detection by delegating to specialized classes: ConfigLoader for config,
|
|
8
8
|
StorageInitializer for storage setup, FileAnalyzer for file analysis, and ViolationGenerator
|
|
9
|
-
for violation creation.
|
|
9
|
+
for violation creation. Also supports duplicate constant detection (opt-in) to identify when
|
|
10
|
+
the same constant is defined in multiple files. Maintains minimal orchestration logic to comply
|
|
11
|
+
with SRP.
|
|
10
12
|
|
|
11
13
|
Dependencies: BaseLintRule, BaseLintContext, ConfigLoader, StorageInitializer, FileAnalyzer,
|
|
12
|
-
DuplicateStorage, ViolationGenerator
|
|
14
|
+
DuplicateStorage, ViolationGenerator, extract_python_constants, TypeScriptConstantExtractor,
|
|
15
|
+
find_constant_groups, ConstantViolationBuilder
|
|
13
16
|
|
|
14
17
|
Exports: DRYRule class
|
|
15
18
|
|
|
16
19
|
Interfaces: DRYRule.check(context) -> list[Violation], finalize() -> list[Violation]
|
|
17
20
|
|
|
18
21
|
Implementation: Delegates all logic to helper classes, maintains only orchestration and state
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- too-many-instance-attributes: DRYComponents groups helper dependencies; DRYRule has 8
|
|
25
|
+
attributes due to stateful caching requirements (storage, config, constants, file contents
|
|
26
|
+
for ignore directive processing)
|
|
27
|
+
- B101: Type narrowing assertions after guards (storage initialized, file_path/content set)
|
|
19
28
|
"""
|
|
20
29
|
|
|
21
30
|
from __future__ import annotations
|
|
22
31
|
|
|
32
|
+
from collections.abc import Callable
|
|
23
33
|
from dataclasses import dataclass
|
|
24
34
|
from pathlib import Path
|
|
25
|
-
from typing import TYPE_CHECKING
|
|
26
35
|
|
|
27
36
|
from src.core.base import BaseLintContext, BaseLintRule
|
|
28
37
|
from src.core.linter_utils import should_process_file
|
|
29
38
|
from src.core.types import Violation
|
|
39
|
+
from src.linter_config.ignore import IgnoreDirectiveParser
|
|
30
40
|
|
|
31
41
|
from .config import DRYConfig
|
|
32
42
|
from .config_loader import ConfigLoader
|
|
43
|
+
from .constant import ConstantInfo
|
|
44
|
+
from .constant_matcher import find_constant_groups
|
|
45
|
+
from .constant_violation_builder import ConstantViolationBuilder
|
|
33
46
|
from .duplicate_storage import DuplicateStorage
|
|
34
47
|
from .file_analyzer import FileAnalyzer
|
|
35
48
|
from .inline_ignore import InlineIgnoreParser
|
|
49
|
+
from .python_constant_extractor import extract_python_constants
|
|
36
50
|
from .storage_initializer import StorageInitializer
|
|
37
|
-
from .
|
|
38
|
-
|
|
39
|
-
if TYPE_CHECKING:
|
|
40
|
-
from .cache import CodeBlock
|
|
51
|
+
from .typescript_constant_extractor import TypeScriptConstantExtractor
|
|
52
|
+
from .violation_generator import IgnoreContext, ViolationGenerator
|
|
41
53
|
|
|
42
54
|
|
|
43
55
|
@dataclass
|
|
44
|
-
class DRYComponents:
|
|
56
|
+
class DRYComponents: # pylint: disable=too-many-instance-attributes
|
|
45
57
|
"""Component dependencies for DRY linter."""
|
|
46
58
|
|
|
47
59
|
config_loader: ConfigLoader
|
|
@@ -49,9 +61,11 @@ class DRYComponents:
|
|
|
49
61
|
file_analyzer: FileAnalyzer
|
|
50
62
|
violation_generator: ViolationGenerator
|
|
51
63
|
inline_ignore: InlineIgnoreParser
|
|
64
|
+
typescript_extractor: TypeScriptConstantExtractor
|
|
65
|
+
constant_violation_builder: ConstantViolationBuilder
|
|
52
66
|
|
|
53
67
|
|
|
54
|
-
class DRYRule(BaseLintRule):
|
|
68
|
+
class DRYRule(BaseLintRule): # pylint: disable=too-many-instance-attributes
|
|
55
69
|
"""Detects duplicate code across project files."""
|
|
56
70
|
|
|
57
71
|
def __init__(self) -> None:
|
|
@@ -60,6 +74,13 @@ class DRYRule(BaseLintRule):
|
|
|
60
74
|
self._initialized = False
|
|
61
75
|
self._config: DRYConfig | None = None
|
|
62
76
|
self._file_analyzer: FileAnalyzer | None = None
|
|
77
|
+
self._project_root: Path | None = None
|
|
78
|
+
|
|
79
|
+
# Collected constants for cross-file detection: list of (file_path, ConstantInfo)
|
|
80
|
+
self._constants: list[tuple[Path, ConstantInfo]] = []
|
|
81
|
+
|
|
82
|
+
# Cache file contents for ignore directive checking during finalize
|
|
83
|
+
self._file_contents: dict[str, str] = {}
|
|
63
84
|
|
|
64
85
|
# Helper components grouped to reduce instance attributes
|
|
65
86
|
self._helpers = DRYComponents(
|
|
@@ -68,8 +89,22 @@ class DRYRule(BaseLintRule):
|
|
|
68
89
|
file_analyzer=FileAnalyzer(), # Placeholder, will be replaced with configured one
|
|
69
90
|
violation_generator=ViolationGenerator(),
|
|
70
91
|
inline_ignore=InlineIgnoreParser(),
|
|
92
|
+
typescript_extractor=TypeScriptConstantExtractor(),
|
|
93
|
+
constant_violation_builder=ConstantViolationBuilder(),
|
|
71
94
|
)
|
|
72
95
|
|
|
96
|
+
@property
|
|
97
|
+
def _active_storage(self) -> DuplicateStorage:
|
|
98
|
+
"""Get storage, asserting it has been initialized."""
|
|
99
|
+
assert self._storage is not None, "Storage not initialized" # nosec B101
|
|
100
|
+
return self._storage
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def _active_file_analyzer(self) -> FileAnalyzer:
|
|
104
|
+
"""Get file analyzer, asserting it has been initialized."""
|
|
105
|
+
assert self._file_analyzer is not None, "File analyzer not initialized" # nosec B101
|
|
106
|
+
return self._file_analyzer
|
|
107
|
+
|
|
73
108
|
@property
|
|
74
109
|
def rule_id(self) -> str:
|
|
75
110
|
"""Unique identifier for this rule."""
|
|
@@ -86,14 +121,7 @@ class DRYRule(BaseLintRule):
|
|
|
86
121
|
return "Detects duplicate code blocks across the project"
|
|
87
122
|
|
|
88
123
|
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
89
|
-
"""Analyze file and store blocks (collection phase).
|
|
90
|
-
|
|
91
|
-
Args:
|
|
92
|
-
context: Lint context with file information
|
|
93
|
-
|
|
94
|
-
Returns:
|
|
95
|
-
Empty list (violations returned in finalize())
|
|
96
|
-
"""
|
|
124
|
+
"""Analyze file and store blocks (collection phase)."""
|
|
97
125
|
if not should_process_file(context):
|
|
98
126
|
return []
|
|
99
127
|
|
|
@@ -101,18 +129,28 @@ class DRYRule(BaseLintRule):
|
|
|
101
129
|
if not config.enabled:
|
|
102
130
|
return []
|
|
103
131
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
132
|
+
self._config = self._config or config
|
|
133
|
+
self._process_file(context, config)
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
def _process_file(self, context: BaseLintContext, config: DRYConfig) -> None:
|
|
137
|
+
"""Process a single file for duplicates and constants."""
|
|
138
|
+
# should_process_file ensures file_path and file_content are set
|
|
139
|
+
assert context.file_path is not None # nosec B101
|
|
140
|
+
assert context.file_content is not None # nosec B101
|
|
107
141
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
self.
|
|
142
|
+
file_path = context.file_path
|
|
143
|
+
# Cache file content for ignore directive checking in finalize
|
|
144
|
+
self._file_contents[str(file_path)] = context.file_content
|
|
145
|
+
# Get project root from context metadata if available
|
|
146
|
+
if self._project_root is None:
|
|
147
|
+
self._project_root = self._get_project_root(context)
|
|
111
148
|
|
|
149
|
+
self._helpers.inline_ignore.parse_file(file_path, context.file_content)
|
|
112
150
|
self._ensure_storage_initialized(context, config)
|
|
113
151
|
self._analyze_and_store(context, config)
|
|
114
|
-
|
|
115
|
-
|
|
152
|
+
if config.detect_duplicate_constants:
|
|
153
|
+
self._extract_and_store_constants(context)
|
|
116
154
|
|
|
117
155
|
def _ensure_storage_initialized(self, context: BaseLintContext, config: DRYConfig) -> None:
|
|
118
156
|
"""Initialize storage and file analyzer on first call."""
|
|
@@ -124,40 +162,137 @@ class DRYRule(BaseLintRule):
|
|
|
124
162
|
|
|
125
163
|
def _analyze_and_store(self, context: BaseLintContext, config: DRYConfig) -> None:
|
|
126
164
|
"""Analyze file and store blocks."""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
165
|
+
if not self._can_analyze(context):
|
|
166
|
+
return
|
|
167
|
+
# _can_analyze ensures file_path and file_content are set
|
|
168
|
+
assert context.file_path is not None # nosec B101
|
|
169
|
+
assert context.file_content is not None # nosec B101
|
|
170
|
+
|
|
171
|
+
blocks = self._active_file_analyzer.analyze(
|
|
172
|
+
context.file_path,
|
|
173
|
+
context.file_content,
|
|
174
|
+
context.language,
|
|
175
|
+
config,
|
|
137
176
|
)
|
|
138
|
-
|
|
139
177
|
if blocks:
|
|
140
|
-
self.
|
|
178
|
+
self._active_storage.add_blocks(context.file_path, blocks)
|
|
179
|
+
|
|
180
|
+
def _can_analyze(self, context: BaseLintContext) -> bool:
|
|
181
|
+
"""Check if context is ready for analysis."""
|
|
182
|
+
return (
|
|
183
|
+
context.file_path is not None
|
|
184
|
+
and context.file_content is not None
|
|
185
|
+
and self._file_analyzer is not None
|
|
186
|
+
and self._storage is not None
|
|
187
|
+
)
|
|
141
188
|
|
|
142
|
-
def
|
|
143
|
-
"""
|
|
144
|
-
if
|
|
145
|
-
|
|
189
|
+
def _extract_and_store_constants(self, context: BaseLintContext) -> None:
|
|
190
|
+
"""Extract constants from file and store for cross-file detection."""
|
|
191
|
+
if context.file_path is None or context.file_content is None:
|
|
192
|
+
return
|
|
193
|
+
file_path = Path(context.file_path)
|
|
194
|
+
extract_fn = _get_extractor_for_language(context.language, self._helpers)
|
|
195
|
+
if extract_fn:
|
|
196
|
+
self._constants.extend((file_path, c) for c in extract_fn(context.file_content))
|
|
146
197
|
|
|
147
|
-
def
|
|
148
|
-
"""
|
|
198
|
+
def _get_project_root(self, context: BaseLintContext) -> Path | None:
|
|
199
|
+
"""Get project root from context if available.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
context: Lint context
|
|
149
203
|
|
|
150
204
|
Returns:
|
|
151
|
-
|
|
205
|
+
Project root path or None if not available
|
|
152
206
|
"""
|
|
207
|
+
# Try to get from metadata (orchestrator sets this)
|
|
208
|
+
if hasattr(context, "metadata") and isinstance(context.metadata, dict):
|
|
209
|
+
project_root = context.metadata.get("project_root")
|
|
210
|
+
if project_root:
|
|
211
|
+
return Path(project_root)
|
|
212
|
+
|
|
213
|
+
# Fallback: derive from file path
|
|
214
|
+
if context.file_path:
|
|
215
|
+
return Path(context.file_path).parent
|
|
216
|
+
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
def finalize(self) -> list[Violation]:
|
|
220
|
+
"""Generate violations after all files processed."""
|
|
153
221
|
if not self._storage or not self._config:
|
|
154
222
|
return []
|
|
155
223
|
|
|
224
|
+
# Create ignore context for violation filtering
|
|
225
|
+
ignore_parser = IgnoreDirectiveParser(self._project_root)
|
|
226
|
+
ignore_ctx = IgnoreContext(
|
|
227
|
+
inline_ignore=self._helpers.inline_ignore,
|
|
228
|
+
shared_parser=ignore_parser,
|
|
229
|
+
file_contents=self._file_contents,
|
|
230
|
+
)
|
|
231
|
+
|
|
156
232
|
violations = self._helpers.violation_generator.generate_violations(
|
|
157
|
-
self._storage, self.rule_id, self._config,
|
|
233
|
+
self._storage, self.rule_id, self._config, ignore_ctx
|
|
158
234
|
)
|
|
235
|
+
if self._config.detect_duplicate_constants and self._constants:
|
|
236
|
+
constant_violations = _generate_constant_violations(
|
|
237
|
+
self._constants, self._config, self._helpers, self.rule_id
|
|
238
|
+
)
|
|
239
|
+
# Filter constant violations through shared ignore parser
|
|
240
|
+
constant_violations = _filter_ignored_violations(
|
|
241
|
+
constant_violations, ignore_parser, self._file_contents
|
|
242
|
+
)
|
|
243
|
+
violations.extend(constant_violations)
|
|
159
244
|
|
|
160
|
-
# Clear inline ignore cache for next run
|
|
161
245
|
self._helpers.inline_ignore.clear()
|
|
162
|
-
|
|
246
|
+
self._constants = []
|
|
247
|
+
self._file_contents = {}
|
|
163
248
|
return violations
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
ConstantExtractorFn = Callable[[str], list[ConstantInfo]]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _get_extractor_for_language(
|
|
255
|
+
language: str | None, helpers: DRYComponents
|
|
256
|
+
) -> ConstantExtractorFn | None:
|
|
257
|
+
"""Get the appropriate constant extractor function for a language."""
|
|
258
|
+
extractors: dict[str, ConstantExtractorFn] = {
|
|
259
|
+
"python": extract_python_constants,
|
|
260
|
+
"typescript": helpers.typescript_extractor.extract,
|
|
261
|
+
"javascript": helpers.typescript_extractor.extract,
|
|
262
|
+
}
|
|
263
|
+
return extractors.get(language or "")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _generate_constant_violations(
|
|
267
|
+
constants: list[tuple[Path, ConstantInfo]],
|
|
268
|
+
config: DRYConfig,
|
|
269
|
+
helpers: DRYComponents,
|
|
270
|
+
rule_id: str,
|
|
271
|
+
) -> list[Violation]:
|
|
272
|
+
"""Generate violations for duplicate constants."""
|
|
273
|
+
groups = find_constant_groups(constants)
|
|
274
|
+
helpers.constant_violation_builder.min_occurrences = config.min_constant_occurrences
|
|
275
|
+
return helpers.constant_violation_builder.build_violations(groups, rule_id)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _filter_ignored_violations(
|
|
279
|
+
violations: list[Violation],
|
|
280
|
+
ignore_parser: IgnoreDirectiveParser,
|
|
281
|
+
file_contents: dict[str, str],
|
|
282
|
+
) -> list[Violation]:
|
|
283
|
+
"""Filter violations through the shared ignore directive parser.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
violations: List of violations to filter
|
|
287
|
+
ignore_parser: Shared ignore directive parser
|
|
288
|
+
file_contents: Cached file contents for checking ignore directives
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Filtered list of violations not matching ignore directives
|
|
292
|
+
"""
|
|
293
|
+
filtered = []
|
|
294
|
+
for violation in violations:
|
|
295
|
+
file_content = file_contents.get(violation.file_path, "")
|
|
296
|
+
if not ignore_parser.should_ignore_violation(violation, file_content):
|
|
297
|
+
filtered.append(violation)
|
|
298
|
+
return filtered
|