thailint 0.12.0__py3-none-any.whl → 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +3 -0
- src/cli/config.py +12 -12
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +3 -0
- src/cli/linters/code_patterns.py +113 -5
- src/cli/linters/code_smells.py +4 -0
- src/cli/linters/documentation.py +3 -0
- src/cli/linters/structure.py +3 -0
- src/cli/linters/structure_quality.py +3 -0
- src/cli_main.py +3 -0
- src/config.py +2 -1
- src/core/base.py +3 -2
- src/core/cli_utils.py +3 -1
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +4 -0
- src/core/rule_discovery.py +5 -1
- src/core/violation_builder.py +3 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +225 -383
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +12 -0
- src/linters/collection_pipeline/continue_analyzer.py +2 -8
- src/linters/collection_pipeline/detector.py +262 -32
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +18 -35
- src/linters/collection_pipeline/suggestion_builder.py +68 -1
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +7 -4
- src/linters/dry/cache.py +7 -2
- src/linters/dry/config.py +7 -1
- src/linters/dry/constant_matcher.py +34 -25
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +48 -25
- src/linters/dry/python_analyzer.py +18 -10
- src/linters/dry/python_constant_extractor.py +51 -52
- src/linters/dry/single_statement_detector.py +14 -12
- src/linters/dry/token_hasher.py +115 -115
- src/linters/dry/typescript_analyzer.py +11 -6
- src/linters/dry/typescript_constant_extractor.py +4 -0
- src/linters/dry/typescript_statement_detector.py +208 -208
- src/linters/dry/typescript_value_extractor.py +3 -0
- src/linters/dry/violation_filter.py +1 -4
- src/linters/dry/violation_generator.py +1 -4
- src/linters/file_header/atemporal_detector.py +4 -0
- src/linters/file_header/base_parser.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/field_validator.py +5 -8
- src/linters/file_header/linter.py +19 -12
- src/linters/file_header/markdown_parser.py +6 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/linter.py +22 -8
- src/linters/file_placement/pattern_matcher.py +21 -4
- src/linters/file_placement/pattern_validator.py +21 -7
- src/linters/file_placement/rule_checker.py +2 -2
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +66 -0
- src/linters/lazy_ignores/directive_utils.py +121 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +135 -0
- src/linters/lazy_ignores/python_analyzer.py +201 -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 +67 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +131 -0
- src/linters/lbyl/__init__.py +29 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/pattern_detectors/__init__.py +25 -0
- src/linters/lbyl/pattern_detectors/base.py +46 -0
- src/linters/magic_numbers/context_analyzer.py +227 -229
- src/linters/magic_numbers/linter.py +20 -15
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -16
- src/linters/method_property/config.py +4 -0
- src/linters/method_property/linter.py +5 -4
- src/linters/method_property/python_analyzer.py +5 -4
- src/linters/method_property/violation_builder.py +3 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/typescript_function_extractor.py +0 -4
- src/linters/print_statements/linter.py +6 -4
- src/linters/print_statements/python_analyzer.py +85 -81
- src/linters/print_statements/typescript_analyzer.py +6 -15
- src/linters/srp/heuristics.py +4 -4
- src/linters/srp/linter.py +12 -12
- src/linters/srp/violation_builder.py +0 -4
- src/linters/stateless_class/linter.py +30 -36
- src/linters/stateless_class/python_analyzer.py +11 -20
- src/linters/stringly_typed/config.py +4 -5
- src/linters/stringly_typed/context_filter.py +410 -410
- src/linters/stringly_typed/function_call_violation_builder.py +93 -95
- src/linters/stringly_typed/linter.py +48 -16
- src/linters/stringly_typed/python/analyzer.py +5 -1
- src/linters/stringly_typed/python/call_tracker.py +8 -5
- src/linters/stringly_typed/python/comparison_tracker.py +10 -5
- src/linters/stringly_typed/python/condition_extractor.py +3 -0
- src/linters/stringly_typed/python/conditional_detector.py +4 -1
- src/linters/stringly_typed/python/match_analyzer.py +8 -2
- src/linters/stringly_typed/python/validation_detector.py +3 -0
- src/linters/stringly_typed/storage.py +14 -14
- src/linters/stringly_typed/typescript/call_tracker.py +9 -3
- src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
- src/linters/stringly_typed/violation_generator.py +288 -259
- src/orchestrator/core.py +13 -4
- src/templates/thailint_config_template.yaml +166 -0
- src/utils/project_root.py +3 -0
- thailint-0.13.0.dist-info/METADATA +184 -0
- thailint-0.13.0.dist-info/RECORD +189 -0
- thailint-0.12.0.dist-info/METADATA +0 -1667
- thailint-0.12.0.dist-info/RECORD +0 -164
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.12.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -14,26 +14,33 @@ Overview: Handles violation generation for stringly-typed patterns that appear a
|
|
|
14
14
|
Separates violation generation logic from main linter rule to maintain SRP compliance.
|
|
15
15
|
|
|
16
16
|
Dependencies: StringlyTypedStorage, StoredPattern, StoredComparison, StringlyTypedConfig,
|
|
17
|
-
Violation, Severity,
|
|
17
|
+
Violation, Severity, build_function_call_violations, IgnoreChecker
|
|
18
18
|
|
|
19
|
-
Exports: ViolationGenerator class
|
|
19
|
+
Exports: ViolationGenerator class, pure helper functions for message building
|
|
20
20
|
|
|
21
21
|
Interfaces: ViolationGenerator.generate_violations(storage, rule_id, config) -> list[Violation]
|
|
22
22
|
|
|
23
23
|
Implementation: Queries storage, validates pattern thresholds, builds violations with
|
|
24
24
|
cross-file references, delegates function call violations to builder, generates
|
|
25
25
|
comparison violations from scattered string comparisons, filters by ignore directives
|
|
26
|
+
|
|
27
|
+
Suppressions:
|
|
28
|
+
- too-many-arguments,too-many-positional-arguments: _process_variable helper passes
|
|
29
|
+
accumulated state (storage, config, covered_variables, violations) to avoid
|
|
30
|
+
global state or complex return types
|
|
26
31
|
"""
|
|
27
32
|
|
|
28
33
|
from src.core.types import Severity, Violation
|
|
29
34
|
|
|
35
|
+
from . import context_filter
|
|
30
36
|
from .config import StringlyTypedConfig
|
|
31
|
-
from .
|
|
32
|
-
from .function_call_violation_builder import FunctionCallViolationBuilder
|
|
37
|
+
from .function_call_violation_builder import build_function_call_violations
|
|
33
38
|
from .ignore_checker import IgnoreChecker
|
|
34
39
|
from .ignore_utils import is_ignored
|
|
35
40
|
from .storage import StoredComparison, StoredPattern, StringlyTypedStorage
|
|
36
41
|
|
|
42
|
+
# --- Pure helper functions for filtering ---
|
|
43
|
+
|
|
37
44
|
|
|
38
45
|
def _filter_by_ignore(violations: list[Violation], ignore: list[str]) -> list[Violation]:
|
|
39
46
|
"""Filter violations by ignore patterns."""
|
|
@@ -47,13 +54,278 @@ def _is_allowed_value_set(values: set[str], config: StringlyTypedConfig) -> bool
|
|
|
47
54
|
return any(values == set(allowed) for allowed in config.allowed_string_sets)
|
|
48
55
|
|
|
49
56
|
|
|
50
|
-
|
|
57
|
+
def _is_enum_candidate(pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
|
|
58
|
+
"""Check if pattern's value count is within enum range."""
|
|
59
|
+
value_count = len(pattern.string_values)
|
|
60
|
+
return config.min_values_for_enum <= value_count <= config.max_values_for_enum
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_pattern_allowed(pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
|
|
64
|
+
"""Check if pattern's string set is in allowed list."""
|
|
65
|
+
return _is_allowed_value_set(set(pattern.string_values), config)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _should_skip_patterns(patterns: list[StoredPattern], config: StringlyTypedConfig) -> bool:
|
|
69
|
+
"""Check if pattern group should be skipped based on config."""
|
|
70
|
+
if not patterns:
|
|
71
|
+
return True
|
|
72
|
+
first = patterns[0]
|
|
73
|
+
if not _is_enum_candidate(first, config):
|
|
74
|
+
return True
|
|
75
|
+
if _is_pattern_allowed(first, config):
|
|
76
|
+
return True
|
|
77
|
+
# Skip if all values match excluded patterns (numeric strings, etc.)
|
|
78
|
+
if context_filter.are_all_values_excluded(set(first.string_values)):
|
|
79
|
+
return True
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _should_skip_comparison(unique_values: set[str], config: StringlyTypedConfig) -> bool:
|
|
84
|
+
"""Check if a comparison pattern should be skipped based on config."""
|
|
85
|
+
if len(unique_values) > config.max_values_for_enum:
|
|
86
|
+
return True
|
|
87
|
+
if _is_allowed_value_set(unique_values, config):
|
|
88
|
+
return True
|
|
89
|
+
if context_filter.are_all_values_excluded(unique_values):
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Variable suffixes that indicate false positive comparisons
|
|
95
|
+
_EXCLUDED_VARIABLE_SUFFIXES: tuple[str, ...] = (
|
|
96
|
+
".value", # Enum value access
|
|
97
|
+
".method", # HTTP method
|
|
98
|
+
".type", # Tree-sitter node types
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _should_skip_variable(variable_name: str) -> bool:
|
|
103
|
+
"""Check if a variable name indicates a false positive comparison."""
|
|
104
|
+
# Check excluded suffixes
|
|
105
|
+
if any(variable_name.endswith(s) for s in _EXCLUDED_VARIABLE_SUFFIXES):
|
|
106
|
+
return True
|
|
107
|
+
# Test assertion patterns (underscore prefix is common in comprehensions/lambdas)
|
|
108
|
+
return variable_name.startswith("_.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# --- Pure helper functions for message building ---
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _build_cross_references(pattern: StoredPattern, all_patterns: list[StoredPattern]) -> str:
|
|
115
|
+
"""Build cross-reference string for other files."""
|
|
116
|
+
refs = [
|
|
117
|
+
f"{other.file_path.name}:{other.line_number}"
|
|
118
|
+
for other in all_patterns
|
|
119
|
+
if other.file_path != pattern.file_path
|
|
120
|
+
]
|
|
121
|
+
return ", ".join(refs)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _build_message(pattern: StoredPattern, all_patterns: list[StoredPattern]) -> str:
|
|
125
|
+
"""Build violation message with cross-file references."""
|
|
126
|
+
file_count = len({p.file_path for p in all_patterns})
|
|
127
|
+
values_str = ", ".join(f"'{v}'" for v in sorted(pattern.string_values))
|
|
128
|
+
other_refs = _build_cross_references(pattern, all_patterns)
|
|
129
|
+
|
|
130
|
+
message = f"Stringly-typed pattern with values [{values_str}] appears in {file_count} files."
|
|
131
|
+
if other_refs:
|
|
132
|
+
message += f" Also found in: {other_refs}."
|
|
133
|
+
|
|
134
|
+
return message
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _build_suggestion(pattern: StoredPattern) -> str:
|
|
138
|
+
"""Build fix suggestion for the pattern."""
|
|
139
|
+
values_count = len(pattern.string_values)
|
|
140
|
+
var_info = f" for '{pattern.variable_name}'" if pattern.variable_name else ""
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
f"Consider defining an enum or type union{var_info} with the "
|
|
144
|
+
f"{values_count} possible values instead of using string literals."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _build_comparison_cross_references(
|
|
149
|
+
comparison: StoredComparison,
|
|
150
|
+
all_comparisons: list[StoredComparison],
|
|
151
|
+
) -> str:
|
|
152
|
+
"""Build cross-reference string for other comparison locations."""
|
|
153
|
+
refs = [
|
|
154
|
+
f"{other.file_path.name}:{other.line_number}"
|
|
155
|
+
for other in all_comparisons
|
|
156
|
+
if other.file_path != comparison.file_path
|
|
157
|
+
]
|
|
158
|
+
return ", ".join(refs)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _build_comparison_message(
|
|
162
|
+
comparison: StoredComparison,
|
|
163
|
+
all_comparisons: list[StoredComparison],
|
|
164
|
+
unique_values: set[str],
|
|
165
|
+
) -> str:
|
|
166
|
+
"""Build violation message for scattered comparison."""
|
|
167
|
+
file_count = len({c.file_path for c in all_comparisons})
|
|
168
|
+
values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
|
|
169
|
+
other_refs = _build_comparison_cross_references(comparison, all_comparisons)
|
|
170
|
+
|
|
171
|
+
message = (
|
|
172
|
+
f"Variable '{comparison.variable_name}' is compared to {len(unique_values)} "
|
|
173
|
+
f"different string values [{values_str}] across {file_count} file(s)."
|
|
174
|
+
)
|
|
175
|
+
if other_refs:
|
|
176
|
+
message += f" Also compared in: {other_refs}."
|
|
177
|
+
|
|
178
|
+
return message
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _build_comparison_suggestion(comparison: StoredComparison, unique_values: set[str]) -> str:
|
|
182
|
+
"""Build fix suggestion for scattered comparison."""
|
|
183
|
+
return (
|
|
184
|
+
f"Consider defining an enum for '{comparison.variable_name}' with the "
|
|
185
|
+
f"{len(unique_values)} possible values instead of using string literals "
|
|
186
|
+
f"in scattered comparisons."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# --- Pure helper function for violation building ---
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _build_violation(
|
|
194
|
+
pattern: StoredPattern, all_patterns: list[StoredPattern], rule_id: str
|
|
195
|
+
) -> Violation:
|
|
196
|
+
"""Build a violation for a pattern with cross-references."""
|
|
197
|
+
message = _build_message(pattern, all_patterns)
|
|
198
|
+
suggestion = _build_suggestion(pattern)
|
|
199
|
+
|
|
200
|
+
return Violation(
|
|
201
|
+
rule_id=rule_id,
|
|
202
|
+
file_path=str(pattern.file_path),
|
|
203
|
+
line=pattern.line_number,
|
|
204
|
+
column=pattern.column,
|
|
205
|
+
message=message,
|
|
206
|
+
severity=Severity.ERROR,
|
|
207
|
+
suggestion=suggestion,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _build_comparison_violation(
|
|
212
|
+
comparison: StoredComparison,
|
|
213
|
+
all_comparisons: list[StoredComparison],
|
|
214
|
+
unique_values: set[str],
|
|
215
|
+
) -> Violation:
|
|
216
|
+
"""Build a violation for a scattered string comparison."""
|
|
217
|
+
message = _build_comparison_message(comparison, all_comparisons, unique_values)
|
|
218
|
+
suggestion = _build_comparison_suggestion(comparison, unique_values)
|
|
219
|
+
|
|
220
|
+
return Violation(
|
|
221
|
+
rule_id="stringly-typed.scattered-comparison",
|
|
222
|
+
file_path=str(comparison.file_path),
|
|
223
|
+
line=comparison.line_number,
|
|
224
|
+
column=comparison.column,
|
|
225
|
+
message=message,
|
|
226
|
+
severity=Severity.ERROR,
|
|
227
|
+
suggestion=suggestion,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# --- Helper functions for pattern processing ---
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _process_pattern_group(
|
|
235
|
+
patterns: list[StoredPattern],
|
|
236
|
+
config: StringlyTypedConfig,
|
|
237
|
+
rule_id: str,
|
|
238
|
+
violations: list[Violation],
|
|
239
|
+
covered_variables: set[str],
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Process a group of patterns with the same hash."""
|
|
242
|
+
if _should_skip_patterns(patterns, config):
|
|
243
|
+
return
|
|
244
|
+
violations.extend(_build_violation(p, patterns, rule_id) for p in patterns)
|
|
245
|
+
for pattern in patterns:
|
|
246
|
+
if pattern.variable_name:
|
|
247
|
+
covered_variables.add(pattern.variable_name)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# --- Helper functions for function call processing ---
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _get_valid_functions(
|
|
254
|
+
storage: StringlyTypedStorage,
|
|
255
|
+
config: StringlyTypedConfig,
|
|
256
|
+
) -> list[tuple[str, int, set[str]]]:
|
|
257
|
+
"""Get functions that pass all filters."""
|
|
258
|
+
min_files = config.min_occurrences if config.require_cross_file else 1
|
|
259
|
+
limited_funcs = storage.get_limited_value_functions(
|
|
260
|
+
min_values=config.min_values_for_enum,
|
|
261
|
+
max_values=config.max_values_for_enum,
|
|
262
|
+
min_files=min_files,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return [
|
|
266
|
+
(name, idx, vals)
|
|
267
|
+
for name, idx, vals in limited_funcs
|
|
268
|
+
if not _is_allowed_value_set(vals, config)
|
|
269
|
+
and context_filter.should_include(name, idx, vals)
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _build_call_violations(
|
|
274
|
+
valid_funcs: list[tuple[str, int, set[str]]],
|
|
275
|
+
storage: StringlyTypedStorage,
|
|
276
|
+
) -> list[Violation]:
|
|
277
|
+
"""Build violations for valid function patterns."""
|
|
278
|
+
violations: list[Violation] = []
|
|
279
|
+
for function_name, param_index, unique_values in valid_funcs:
|
|
280
|
+
calls = storage.get_calls_by_function(function_name, param_index)
|
|
281
|
+
violations.extend(build_function_call_violations(calls, unique_values))
|
|
282
|
+
return violations
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# --- Helper functions for comparison processing ---
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _get_variables_to_check(
|
|
289
|
+
storage: StringlyTypedStorage,
|
|
290
|
+
config: StringlyTypedConfig,
|
|
291
|
+
) -> list[tuple[str, set[str]]]:
|
|
292
|
+
"""Get variables with multiple values that should be checked."""
|
|
293
|
+
min_files = config.min_occurrences if config.require_cross_file else 1
|
|
294
|
+
return storage.get_variables_with_multiple_values(
|
|
295
|
+
min_values=config.min_values_for_enum,
|
|
296
|
+
min_files=min_files,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _process_variable( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
301
|
+
variable_name: str,
|
|
302
|
+
unique_values: set[str],
|
|
303
|
+
storage: StringlyTypedStorage,
|
|
304
|
+
config: StringlyTypedConfig,
|
|
305
|
+
covered_variables: set[str],
|
|
306
|
+
violations: list[Violation],
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Process comparisons for a single variable."""
|
|
309
|
+
if variable_name in covered_variables:
|
|
310
|
+
return
|
|
311
|
+
if _should_skip_variable(variable_name):
|
|
312
|
+
return
|
|
313
|
+
if _should_skip_comparison(unique_values, config):
|
|
314
|
+
return
|
|
315
|
+
comparisons = storage.get_comparisons_by_variable(variable_name)
|
|
316
|
+
violations.extend(
|
|
317
|
+
_build_comparison_violation(c, comparisons, unique_values) for c in comparisons
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# --- ViolationGenerator class ---
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class ViolationGenerator:
|
|
51
325
|
"""Generates violations from cross-file stringly-typed patterns."""
|
|
52
326
|
|
|
53
327
|
def __init__(self) -> None:
|
|
54
|
-
"""Initialize with helper
|
|
55
|
-
self._call_builder = FunctionCallViolationBuilder()
|
|
56
|
-
self._call_filter = FunctionCallFilter()
|
|
328
|
+
"""Initialize with helper filters."""
|
|
57
329
|
self._ignore_checker = IgnoreChecker()
|
|
58
330
|
|
|
59
331
|
def generate_violations(
|
|
@@ -83,7 +355,7 @@ class ViolationGenerator: # thailint: ignore srp
|
|
|
83
355
|
# Apply path-based ignore patterns from config
|
|
84
356
|
violations = _filter_by_ignore(violations, config.ignore)
|
|
85
357
|
|
|
86
|
-
# Apply inline ignore directives
|
|
358
|
+
# Apply inline ignore directives via IgnoreChecker
|
|
87
359
|
violations = self._ignore_checker.filter_violations(violations)
|
|
88
360
|
|
|
89
361
|
return violations
|
|
@@ -94,139 +366,25 @@ class ViolationGenerator: # thailint: ignore srp
|
|
|
94
366
|
rule_id: str,
|
|
95
367
|
config: StringlyTypedConfig,
|
|
96
368
|
) -> tuple[list[Violation], set[str]]:
|
|
97
|
-
"""Generate violations for duplicate validation patterns.
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
Tuple of (violations list, set of variable names covered by these violations)
|
|
101
|
-
"""
|
|
369
|
+
"""Generate violations for duplicate validation patterns."""
|
|
102
370
|
duplicate_hashes = storage.get_duplicate_hashes(min_files=config.min_occurrences)
|
|
103
371
|
violations: list[Violation] = []
|
|
104
372
|
covered_variables: set[str] = set()
|
|
105
373
|
|
|
106
374
|
for hash_value in duplicate_hashes:
|
|
107
375
|
patterns = storage.get_patterns_by_hash(hash_value)
|
|
108
|
-
|
|
376
|
+
_process_pattern_group(patterns, config, rule_id, violations, covered_variables)
|
|
109
377
|
|
|
110
378
|
return violations, covered_variables
|
|
111
379
|
|
|
112
|
-
def _process_pattern_group( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
113
|
-
self,
|
|
114
|
-
patterns: list[StoredPattern],
|
|
115
|
-
config: StringlyTypedConfig,
|
|
116
|
-
rule_id: str,
|
|
117
|
-
violations: list[Violation],
|
|
118
|
-
covered_variables: set[str],
|
|
119
|
-
) -> None:
|
|
120
|
-
"""Process a group of patterns with the same hash."""
|
|
121
|
-
if self._should_skip_patterns(patterns, config):
|
|
122
|
-
return
|
|
123
|
-
violations.extend(self._build_violation(p, patterns, rule_id) for p in patterns)
|
|
124
|
-
# Track variable names to avoid duplicate comparison violations
|
|
125
|
-
for pattern in patterns:
|
|
126
|
-
if pattern.variable_name:
|
|
127
|
-
covered_variables.add(pattern.variable_name)
|
|
128
|
-
|
|
129
380
|
def _generate_function_call_violations(
|
|
130
381
|
self,
|
|
131
382
|
storage: StringlyTypedStorage,
|
|
132
383
|
config: StringlyTypedConfig,
|
|
133
384
|
) -> list[Violation]:
|
|
134
385
|
"""Generate violations for function call patterns."""
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
min_values=config.min_values_for_enum,
|
|
138
|
-
max_values=config.max_values_for_enum,
|
|
139
|
-
min_files=min_files,
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
violations: list[Violation] = []
|
|
143
|
-
for function_name, param_index, unique_values in limited_funcs:
|
|
144
|
-
if _is_allowed_value_set(unique_values, config):
|
|
145
|
-
continue
|
|
146
|
-
# Apply context-aware filtering to reduce false positives
|
|
147
|
-
if not self._call_filter.should_include(function_name, param_index, unique_values):
|
|
148
|
-
continue
|
|
149
|
-
calls = storage.get_calls_by_function(function_name, param_index)
|
|
150
|
-
violations.extend(self._call_builder.build_violations(calls, unique_values))
|
|
151
|
-
|
|
152
|
-
return violations
|
|
153
|
-
|
|
154
|
-
def _should_skip_patterns(
|
|
155
|
-
self, patterns: list[StoredPattern], config: StringlyTypedConfig
|
|
156
|
-
) -> bool:
|
|
157
|
-
"""Check if pattern group should be skipped based on config."""
|
|
158
|
-
if not patterns:
|
|
159
|
-
return True
|
|
160
|
-
first = patterns[0]
|
|
161
|
-
if not self._is_enum_candidate(first, config):
|
|
162
|
-
return True
|
|
163
|
-
if self._is_pattern_allowed(first, config):
|
|
164
|
-
return True
|
|
165
|
-
# Skip if all values match excluded patterns (numeric strings, etc.)
|
|
166
|
-
if self._call_filter.are_all_values_excluded(set(first.string_values)):
|
|
167
|
-
return True
|
|
168
|
-
return False
|
|
169
|
-
|
|
170
|
-
def _is_enum_candidate(self, pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
|
|
171
|
-
"""Check if pattern's value count is within enum range."""
|
|
172
|
-
value_count = len(pattern.string_values)
|
|
173
|
-
return config.min_values_for_enum <= value_count <= config.max_values_for_enum
|
|
174
|
-
|
|
175
|
-
def _is_pattern_allowed(self, pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
|
|
176
|
-
"""Check if pattern's string set is in allowed list."""
|
|
177
|
-
return _is_allowed_value_set(set(pattern.string_values), config)
|
|
178
|
-
|
|
179
|
-
def _build_violation(
|
|
180
|
-
self, pattern: StoredPattern, all_patterns: list[StoredPattern], rule_id: str
|
|
181
|
-
) -> Violation:
|
|
182
|
-
"""Build a violation for a pattern with cross-references."""
|
|
183
|
-
message = self._build_message(pattern, all_patterns)
|
|
184
|
-
suggestion = self._build_suggestion(pattern)
|
|
185
|
-
|
|
186
|
-
return Violation(
|
|
187
|
-
rule_id=rule_id,
|
|
188
|
-
file_path=str(pattern.file_path),
|
|
189
|
-
line=pattern.line_number,
|
|
190
|
-
column=pattern.column,
|
|
191
|
-
message=message,
|
|
192
|
-
severity=Severity.ERROR,
|
|
193
|
-
suggestion=suggestion,
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
def _build_message(self, pattern: StoredPattern, all_patterns: list[StoredPattern]) -> str:
|
|
197
|
-
"""Build violation message with cross-file references."""
|
|
198
|
-
file_count = len({p.file_path for p in all_patterns})
|
|
199
|
-
values_str = ", ".join(f"'{v}'" for v in sorted(pattern.string_values))
|
|
200
|
-
other_refs = self._build_cross_references(pattern, all_patterns)
|
|
201
|
-
|
|
202
|
-
message = (
|
|
203
|
-
f"Stringly-typed pattern with values [{values_str}] appears in {file_count} files."
|
|
204
|
-
)
|
|
205
|
-
if other_refs:
|
|
206
|
-
message += f" Also found in: {other_refs}."
|
|
207
|
-
|
|
208
|
-
return message
|
|
209
|
-
|
|
210
|
-
def _build_cross_references(
|
|
211
|
-
self, pattern: StoredPattern, all_patterns: list[StoredPattern]
|
|
212
|
-
) -> str:
|
|
213
|
-
"""Build cross-reference string for other files."""
|
|
214
|
-
refs = [
|
|
215
|
-
f"{other.file_path.name}:{other.line_number}"
|
|
216
|
-
for other in all_patterns
|
|
217
|
-
if other.file_path != pattern.file_path
|
|
218
|
-
]
|
|
219
|
-
return ", ".join(refs)
|
|
220
|
-
|
|
221
|
-
def _build_suggestion(self, pattern: StoredPattern) -> str:
|
|
222
|
-
"""Build fix suggestion for the pattern."""
|
|
223
|
-
values_count = len(pattern.string_values)
|
|
224
|
-
var_info = f" for '{pattern.variable_name}'" if pattern.variable_name else ""
|
|
225
|
-
|
|
226
|
-
return (
|
|
227
|
-
f"Consider defining an enum or type union{var_info} with the "
|
|
228
|
-
f"{values_count} possible values instead of using string literals."
|
|
229
|
-
)
|
|
386
|
+
valid_funcs = _get_valid_functions(storage, config)
|
|
387
|
+
return _build_call_violations(valid_funcs, storage)
|
|
230
388
|
|
|
231
389
|
def _generate_comparison_violations(
|
|
232
390
|
self,
|
|
@@ -234,143 +392,14 @@ class ViolationGenerator: # thailint: ignore srp
|
|
|
234
392
|
config: StringlyTypedConfig,
|
|
235
393
|
covered_variables: set[str] | None = None,
|
|
236
394
|
) -> list[Violation]:
|
|
237
|
-
"""Generate violations for scattered string comparisons.
|
|
238
|
-
|
|
239
|
-
Finds variables that are compared to multiple unique string values across
|
|
240
|
-
files (e.g., `if env == "production"` in one file and `if env == "staging"`
|
|
241
|
-
in another), suggesting they should use enums instead.
|
|
242
|
-
|
|
243
|
-
Args:
|
|
244
|
-
storage: Pattern storage instance
|
|
245
|
-
config: Stringly-typed configuration
|
|
246
|
-
covered_variables: Variable names already flagged by pattern violations (to deduplicate)
|
|
247
|
-
"""
|
|
395
|
+
"""Generate violations for scattered string comparisons."""
|
|
248
396
|
covered_variables = covered_variables or set()
|
|
249
|
-
|
|
250
|
-
variables = storage.get_variables_with_multiple_values(
|
|
251
|
-
min_values=config.min_values_for_enum,
|
|
252
|
-
min_files=min_files,
|
|
253
|
-
)
|
|
397
|
+
variables = _get_variables_to_check(storage, config)
|
|
254
398
|
|
|
255
399
|
violations: list[Violation] = []
|
|
256
400
|
for variable_name, unique_values in variables:
|
|
257
|
-
|
|
401
|
+
_process_variable(
|
|
258
402
|
variable_name, unique_values, storage, config, covered_variables, violations
|
|
259
403
|
)
|
|
260
404
|
|
|
261
405
|
return violations
|
|
262
|
-
|
|
263
|
-
def _process_variable_comparisons( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
264
|
-
self,
|
|
265
|
-
variable_name: str,
|
|
266
|
-
unique_values: set[str],
|
|
267
|
-
storage: StringlyTypedStorage,
|
|
268
|
-
config: StringlyTypedConfig,
|
|
269
|
-
covered_variables: set[str],
|
|
270
|
-
violations: list[Violation],
|
|
271
|
-
) -> None:
|
|
272
|
-
"""Process comparisons for a single variable."""
|
|
273
|
-
# Skip if already covered by equality chain or validation pattern
|
|
274
|
-
if variable_name in covered_variables:
|
|
275
|
-
return
|
|
276
|
-
# Skip false positive variable patterns (.value, .method, etc.)
|
|
277
|
-
if self._should_skip_variable(variable_name):
|
|
278
|
-
return
|
|
279
|
-
if self._should_skip_comparison(unique_values, config):
|
|
280
|
-
return
|
|
281
|
-
comparisons = storage.get_comparisons_by_variable(variable_name)
|
|
282
|
-
violations.extend(
|
|
283
|
-
self._build_comparison_violation(c, comparisons, unique_values) for c in comparisons
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
def _should_skip_comparison(self, unique_values: set[str], config: StringlyTypedConfig) -> bool:
|
|
287
|
-
"""Check if a comparison pattern should be skipped based on config."""
|
|
288
|
-
if len(unique_values) > config.max_values_for_enum:
|
|
289
|
-
return True
|
|
290
|
-
if _is_allowed_value_set(unique_values, config):
|
|
291
|
-
return True
|
|
292
|
-
if self._call_filter.are_all_values_excluded(unique_values):
|
|
293
|
-
return True
|
|
294
|
-
return False
|
|
295
|
-
|
|
296
|
-
def _should_skip_variable(self, variable_name: str) -> bool:
|
|
297
|
-
"""Check if a variable name indicates a false positive comparison.
|
|
298
|
-
|
|
299
|
-
Excludes:
|
|
300
|
-
- Variables ending with .value (enum value access)
|
|
301
|
-
- HTTP method variables (request.method, etc.)
|
|
302
|
-
- Variables that are likely test fixtures (underscore prefix patterns)
|
|
303
|
-
"""
|
|
304
|
-
# Enum value access - already using an enum
|
|
305
|
-
if variable_name.endswith(".value"):
|
|
306
|
-
return True
|
|
307
|
-
# HTTP method - standard protocol strings
|
|
308
|
-
if variable_name.endswith(".method"):
|
|
309
|
-
return True
|
|
310
|
-
# Test assertion patterns (underscore prefix is common in comprehensions/lambdas)
|
|
311
|
-
if variable_name.startswith("_."):
|
|
312
|
-
return True
|
|
313
|
-
return False
|
|
314
|
-
|
|
315
|
-
def _build_comparison_violation(
|
|
316
|
-
self,
|
|
317
|
-
comparison: StoredComparison,
|
|
318
|
-
all_comparisons: list[StoredComparison],
|
|
319
|
-
unique_values: set[str],
|
|
320
|
-
) -> Violation:
|
|
321
|
-
"""Build a violation for a scattered string comparison."""
|
|
322
|
-
message = self._build_comparison_message(comparison, all_comparisons, unique_values)
|
|
323
|
-
suggestion = self._build_comparison_suggestion(comparison, unique_values)
|
|
324
|
-
|
|
325
|
-
return Violation(
|
|
326
|
-
rule_id="stringly-typed.scattered-comparison",
|
|
327
|
-
file_path=str(comparison.file_path),
|
|
328
|
-
line=comparison.line_number,
|
|
329
|
-
column=comparison.column,
|
|
330
|
-
message=message,
|
|
331
|
-
severity=Severity.ERROR,
|
|
332
|
-
suggestion=suggestion,
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
def _build_comparison_message(
|
|
336
|
-
self,
|
|
337
|
-
comparison: StoredComparison,
|
|
338
|
-
all_comparisons: list[StoredComparison],
|
|
339
|
-
unique_values: set[str],
|
|
340
|
-
) -> str:
|
|
341
|
-
"""Build violation message for scattered comparison."""
|
|
342
|
-
file_count = len({c.file_path for c in all_comparisons})
|
|
343
|
-
values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
|
|
344
|
-
other_refs = self._build_comparison_cross_references(comparison, all_comparisons)
|
|
345
|
-
|
|
346
|
-
message = (
|
|
347
|
-
f"Variable '{comparison.variable_name}' is compared to {len(unique_values)} "
|
|
348
|
-
f"different string values [{values_str}] across {file_count} file(s)."
|
|
349
|
-
)
|
|
350
|
-
if other_refs:
|
|
351
|
-
message += f" Also compared in: {other_refs}."
|
|
352
|
-
|
|
353
|
-
return message
|
|
354
|
-
|
|
355
|
-
def _build_comparison_cross_references(
|
|
356
|
-
self,
|
|
357
|
-
comparison: StoredComparison,
|
|
358
|
-
all_comparisons: list[StoredComparison],
|
|
359
|
-
) -> str:
|
|
360
|
-
"""Build cross-reference string for other comparison locations."""
|
|
361
|
-
refs = [
|
|
362
|
-
f"{other.file_path.name}:{other.line_number}"
|
|
363
|
-
for other in all_comparisons
|
|
364
|
-
if other.file_path != comparison.file_path
|
|
365
|
-
]
|
|
366
|
-
return ", ".join(refs)
|
|
367
|
-
|
|
368
|
-
def _build_comparison_suggestion(
|
|
369
|
-
self, comparison: StoredComparison, unique_values: set[str]
|
|
370
|
-
) -> str:
|
|
371
|
-
"""Build fix suggestion for scattered comparison."""
|
|
372
|
-
return (
|
|
373
|
-
f"Consider defining an enum for '{comparison.variable_name}' with the "
|
|
374
|
-
f"{len(unique_values)} possible values instead of using string literals "
|
|
375
|
-
f"in scattered comparisons."
|
|
376
|
-
)
|
src/orchestrator/core.py
CHANGED
|
@@ -29,10 +29,15 @@ Implementation: Directory glob pattern matching for traversal (** for recursive,
|
|
|
29
29
|
ignore pattern checking before file processing, dynamic context creation per file,
|
|
30
30
|
rule filtering by applicability, violation collection and aggregation across files,
|
|
31
31
|
ProcessPoolExecutor for parallel file processing
|
|
32
|
+
|
|
33
|
+
Suppressions:
|
|
34
|
+
- srp: Orchestrator class coordinates multiple subsystems by design (registry, config, ignore,
|
|
35
|
+
language detection). Splitting would fragment the core linting workflow.
|
|
32
36
|
"""
|
|
33
37
|
|
|
34
38
|
from __future__ import annotations
|
|
35
39
|
|
|
40
|
+
import logging
|
|
36
41
|
import multiprocessing
|
|
37
42
|
import os
|
|
38
43
|
from concurrent.futures import Future, ProcessPoolExecutor, as_completed
|
|
@@ -46,6 +51,8 @@ from src.linter_config.loader import LinterConfigLoader
|
|
|
46
51
|
|
|
47
52
|
from .language_detector import detect_language
|
|
48
53
|
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
49
56
|
# Default max workers for parallel processing (capped to avoid resource contention)
|
|
50
57
|
DEFAULT_MAX_WORKERS = 8
|
|
51
58
|
|
|
@@ -163,7 +170,8 @@ def _lint_file_worker(args: tuple[Path, Path, dict]) -> list[dict]:
|
|
|
163
170
|
violations = orchestrator.lint_file(file_path)
|
|
164
171
|
# Convert to dicts for pickling
|
|
165
172
|
return [v.to_dict() for v in violations]
|
|
166
|
-
except Exception:
|
|
173
|
+
except Exception:
|
|
174
|
+
logger.exception("Worker error processing file: %s", file_path)
|
|
167
175
|
return []
|
|
168
176
|
|
|
169
177
|
|
|
@@ -334,8 +342,8 @@ class Orchestrator: # thailint: ignore[srp]
|
|
|
334
342
|
except ValueError:
|
|
335
343
|
# Re-raise configuration validation errors (these are user-facing)
|
|
336
344
|
raise
|
|
337
|
-
except Exception:
|
|
338
|
-
|
|
345
|
+
except Exception:
|
|
346
|
+
logger.exception("Rule %s failed on %s", rule.rule_id, context.file_path)
|
|
339
347
|
return []
|
|
340
348
|
|
|
341
349
|
def lint_directory(self, dir_path: Path, recursive: bool = True) -> list[Violation]:
|
|
@@ -410,7 +418,8 @@ class Orchestrator: # thailint: ignore[srp]
|
|
|
410
418
|
"""Extract violations from a completed future, handling errors."""
|
|
411
419
|
try:
|
|
412
420
|
return [Violation.from_dict(d) for d in future.result()]
|
|
413
|
-
except Exception:
|
|
421
|
+
except Exception:
|
|
422
|
+
logger.exception("Error extracting violations from worker future")
|
|
414
423
|
return []
|
|
415
424
|
|
|
416
425
|
def _finalize_rules(self) -> list[Violation]:
|