thailint 0.11.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 +118 -7
- src/cli/linters/documentation.py +3 -0
- src/cli/linters/structure.py +3 -0
- src/cli/linters/structure_quality.py +3 -0
- src/cli/utils.py +29 -9
- 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/__init__.py +22 -9
- src/linters/stringly_typed/config.py +32 -8
- 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 +102 -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 +9 -5
- src/linters/stringly_typed/python/analyzer.py +159 -9
- 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 +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 +630 -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 +405 -0
- 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.11.0.dist-info/METADATA +0 -1661
- thailint-0.11.0.dist-info/RECORD +0 -150
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Violation generation from cross-file stringly-typed patterns
|
|
3
|
+
|
|
4
|
+
Scope: Generates violations from duplicate pattern hashes, function call patterns, and
|
|
5
|
+
scattered string comparisons
|
|
6
|
+
|
|
7
|
+
Overview: Handles violation generation for stringly-typed patterns that appear across multiple
|
|
8
|
+
files. Queries storage for duplicate hashes, retrieves patterns for each hash, builds
|
|
9
|
+
violations with cross-references to other files, and filters patterns based on enum value
|
|
10
|
+
thresholds. Delegates function call violation generation to FunctionCallViolationBuilder.
|
|
11
|
+
Generates violations for scattered string comparisons (e.g., `if env == "production"`)
|
|
12
|
+
where a variable is compared to multiple unique string values across files.
|
|
13
|
+
Applies inline ignore directives via IgnoreChecker to filter suppressed violations.
|
|
14
|
+
Separates violation generation logic from main linter rule to maintain SRP compliance.
|
|
15
|
+
|
|
16
|
+
Dependencies: StringlyTypedStorage, StoredPattern, StoredComparison, StringlyTypedConfig,
|
|
17
|
+
Violation, Severity, build_function_call_violations, IgnoreChecker
|
|
18
|
+
|
|
19
|
+
Exports: ViolationGenerator class, pure helper functions for message building
|
|
20
|
+
|
|
21
|
+
Interfaces: ViolationGenerator.generate_violations(storage, rule_id, config) -> list[Violation]
|
|
22
|
+
|
|
23
|
+
Implementation: Queries storage, validates pattern thresholds, builds violations with
|
|
24
|
+
cross-file references, delegates function call violations to builder, generates
|
|
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
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from src.core.types import Severity, Violation
|
|
34
|
+
|
|
35
|
+
from . import context_filter
|
|
36
|
+
from .config import StringlyTypedConfig
|
|
37
|
+
from .function_call_violation_builder import build_function_call_violations
|
|
38
|
+
from .ignore_checker import IgnoreChecker
|
|
39
|
+
from .ignore_utils import is_ignored
|
|
40
|
+
from .storage import StoredComparison, StoredPattern, StringlyTypedStorage
|
|
41
|
+
|
|
42
|
+
# --- Pure helper functions for filtering ---
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _filter_by_ignore(violations: list[Violation], ignore: list[str]) -> list[Violation]:
|
|
46
|
+
"""Filter violations by ignore patterns."""
|
|
47
|
+
if not ignore:
|
|
48
|
+
return violations
|
|
49
|
+
return [v for v in violations if not is_ignored(v.file_path, ignore)]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _is_allowed_value_set(values: set[str], config: StringlyTypedConfig) -> bool:
|
|
53
|
+
"""Check if a set of values is in the allowed list."""
|
|
54
|
+
return any(values == set(allowed) for allowed in config.allowed_string_sets)
|
|
55
|
+
|
|
56
|
+
|
|
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:
|
|
325
|
+
"""Generates violations from cross-file stringly-typed patterns."""
|
|
326
|
+
|
|
327
|
+
def __init__(self) -> None:
|
|
328
|
+
"""Initialize with helper filters."""
|
|
329
|
+
self._ignore_checker = IgnoreChecker()
|
|
330
|
+
|
|
331
|
+
def generate_violations(
|
|
332
|
+
self,
|
|
333
|
+
storage: StringlyTypedStorage,
|
|
334
|
+
rule_id: str,
|
|
335
|
+
config: StringlyTypedConfig,
|
|
336
|
+
) -> list[Violation]:
|
|
337
|
+
"""Generate violations from storage.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
storage: Pattern storage instance
|
|
341
|
+
rule_id: Rule identifier for violations
|
|
342
|
+
config: Stringly-typed configuration with thresholds
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
List of violations for patterns appearing in multiple files
|
|
346
|
+
"""
|
|
347
|
+
violations: list[Violation] = []
|
|
348
|
+
pattern_violations, covered_vars = self._generate_pattern_violations(
|
|
349
|
+
storage, rule_id, config
|
|
350
|
+
)
|
|
351
|
+
violations.extend(pattern_violations)
|
|
352
|
+
violations.extend(self._generate_function_call_violations(storage, config))
|
|
353
|
+
violations.extend(self._generate_comparison_violations(storage, config, covered_vars))
|
|
354
|
+
|
|
355
|
+
# Apply path-based ignore patterns from config
|
|
356
|
+
violations = _filter_by_ignore(violations, config.ignore)
|
|
357
|
+
|
|
358
|
+
# Apply inline ignore directives via IgnoreChecker
|
|
359
|
+
violations = self._ignore_checker.filter_violations(violations)
|
|
360
|
+
|
|
361
|
+
return violations
|
|
362
|
+
|
|
363
|
+
def _generate_pattern_violations(
|
|
364
|
+
self,
|
|
365
|
+
storage: StringlyTypedStorage,
|
|
366
|
+
rule_id: str,
|
|
367
|
+
config: StringlyTypedConfig,
|
|
368
|
+
) -> tuple[list[Violation], set[str]]:
|
|
369
|
+
"""Generate violations for duplicate validation patterns."""
|
|
370
|
+
duplicate_hashes = storage.get_duplicate_hashes(min_files=config.min_occurrences)
|
|
371
|
+
violations: list[Violation] = []
|
|
372
|
+
covered_variables: set[str] = set()
|
|
373
|
+
|
|
374
|
+
for hash_value in duplicate_hashes:
|
|
375
|
+
patterns = storage.get_patterns_by_hash(hash_value)
|
|
376
|
+
_process_pattern_group(patterns, config, rule_id, violations, covered_variables)
|
|
377
|
+
|
|
378
|
+
return violations, covered_variables
|
|
379
|
+
|
|
380
|
+
def _generate_function_call_violations(
|
|
381
|
+
self,
|
|
382
|
+
storage: StringlyTypedStorage,
|
|
383
|
+
config: StringlyTypedConfig,
|
|
384
|
+
) -> list[Violation]:
|
|
385
|
+
"""Generate violations for function call patterns."""
|
|
386
|
+
valid_funcs = _get_valid_functions(storage, config)
|
|
387
|
+
return _build_call_violations(valid_funcs, storage)
|
|
388
|
+
|
|
389
|
+
def _generate_comparison_violations(
|
|
390
|
+
self,
|
|
391
|
+
storage: StringlyTypedStorage,
|
|
392
|
+
config: StringlyTypedConfig,
|
|
393
|
+
covered_variables: set[str] | None = None,
|
|
394
|
+
) -> list[Violation]:
|
|
395
|
+
"""Generate violations for scattered string comparisons."""
|
|
396
|
+
covered_variables = covered_variables or set()
|
|
397
|
+
variables = _get_variables_to_check(storage, config)
|
|
398
|
+
|
|
399
|
+
violations: list[Violation] = []
|
|
400
|
+
for variable_name, unique_values in variables:
|
|
401
|
+
_process_variable(
|
|
402
|
+
variable_name, unique_values, storage, config, covered_variables, violations
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
return violations
|
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]:
|
|
@@ -132,6 +132,172 @@ print-statements:
|
|
|
132
132
|
# - "scripts/**"
|
|
133
133
|
# - "**/debug.py"
|
|
134
134
|
|
|
135
|
+
# ============================================================================
|
|
136
|
+
# STRINGLY-TYPED LINTER
|
|
137
|
+
# ============================================================================
|
|
138
|
+
# Detects "stringly typed" code patterns where strings are used instead of
|
|
139
|
+
# proper enums - e.g., if env == "production": ... repeated across files
|
|
140
|
+
#
|
|
141
|
+
stringly-typed:
|
|
142
|
+
enabled: true
|
|
143
|
+
|
|
144
|
+
# Minimum occurrences across files to flag a violation
|
|
145
|
+
# Default: 2
|
|
146
|
+
min_occurrences: 2
|
|
147
|
+
|
|
148
|
+
# Minimum unique string values to suggest creating an enum
|
|
149
|
+
# Default: 2
|
|
150
|
+
min_values_for_enum: 2
|
|
151
|
+
|
|
152
|
+
# Maximum unique string values to suggest an enum (above this, probably not enum-worthy)
|
|
153
|
+
# Default: 6
|
|
154
|
+
max_values_for_enum: 6
|
|
155
|
+
|
|
156
|
+
# Whether to require cross-file occurrences to flag violations
|
|
157
|
+
# Default: true
|
|
158
|
+
require_cross_file: true
|
|
159
|
+
|
|
160
|
+
# -------------------------------------------------------------------------
|
|
161
|
+
# OPTIONAL: String value sets that are acceptable (won't be flagged)
|
|
162
|
+
# -------------------------------------------------------------------------
|
|
163
|
+
# allowed_string_sets:
|
|
164
|
+
# - ["debug", "info", "warning", "error"] # Log levels
|
|
165
|
+
# - ["ASC", "DESC"] # Sort directions
|
|
166
|
+
|
|
167
|
+
# -------------------------------------------------------------------------
|
|
168
|
+
# OPTIONAL: Variable names to exclude from analysis
|
|
169
|
+
# -------------------------------------------------------------------------
|
|
170
|
+
# exclude_variables:
|
|
171
|
+
# - log_level
|
|
172
|
+
# - severity
|
|
173
|
+
|
|
174
|
+
# ============================================================================
|
|
175
|
+
# FILE HEADER LINTER
|
|
176
|
+
# ============================================================================
|
|
177
|
+
# Validates that files have proper documentation headers
|
|
178
|
+
#
|
|
179
|
+
file-header:
|
|
180
|
+
enabled: true
|
|
181
|
+
|
|
182
|
+
# Enforce atemporal language (no "currently", "now", dates)
|
|
183
|
+
# Default: true
|
|
184
|
+
enforce_atemporal: true
|
|
185
|
+
|
|
186
|
+
# -------------------------------------------------------------------------
|
|
187
|
+
# OPTIONAL: Override required fields by language
|
|
188
|
+
# -------------------------------------------------------------------------
|
|
189
|
+
# required_fields:
|
|
190
|
+
# python: [Purpose, Scope, Overview, Dependencies, Exports, Interfaces, Implementation]
|
|
191
|
+
# typescript: [Purpose, Scope, Overview, Dependencies, Exports, Props/Interfaces, State/Behavior]
|
|
192
|
+
# bash: [Purpose, Scope, Overview, Dependencies, Exports, Usage, Environment]
|
|
193
|
+
# markdown: [purpose, scope, overview, audience, status]
|
|
194
|
+
# css: [Purpose, Scope, Overview, Dependencies, Exports, Interfaces, Environment]
|
|
195
|
+
|
|
196
|
+
# -------------------------------------------------------------------------
|
|
197
|
+
# OPTIONAL: File patterns to ignore
|
|
198
|
+
# -------------------------------------------------------------------------
|
|
199
|
+
# ignore:
|
|
200
|
+
# - "test/**"
|
|
201
|
+
# - "**/migrations/**"
|
|
202
|
+
# - "**/__init__.py"
|
|
203
|
+
|
|
204
|
+
# ============================================================================
|
|
205
|
+
# METHOD PROPERTY LINTER
|
|
206
|
+
# ============================================================================
|
|
207
|
+
# Detects methods that should be @property (no args, simple return)
|
|
208
|
+
#
|
|
209
|
+
method-property:
|
|
210
|
+
enabled: true
|
|
211
|
+
|
|
212
|
+
# Maximum statements in method body to suggest @property
|
|
213
|
+
# Default: 3
|
|
214
|
+
max_body_statements: 3
|
|
215
|
+
|
|
216
|
+
# -------------------------------------------------------------------------
|
|
217
|
+
# OPTIONAL: Methods to ignore (exact names)
|
|
218
|
+
# -------------------------------------------------------------------------
|
|
219
|
+
# ignore_methods:
|
|
220
|
+
# - "__str__"
|
|
221
|
+
# - "__repr__"
|
|
222
|
+
|
|
223
|
+
# -------------------------------------------------------------------------
|
|
224
|
+
# OPTIONAL: Additional action verb prefixes to exclude
|
|
225
|
+
# -------------------------------------------------------------------------
|
|
226
|
+
# exclude_prefixes:
|
|
227
|
+
# - "fetch_"
|
|
228
|
+
# - "load_"
|
|
229
|
+
|
|
230
|
+
# -------------------------------------------------------------------------
|
|
231
|
+
# OPTIONAL: File patterns to ignore
|
|
232
|
+
# -------------------------------------------------------------------------
|
|
233
|
+
# ignore:
|
|
234
|
+
# - "tests/**"
|
|
235
|
+
|
|
236
|
+
# ============================================================================
|
|
237
|
+
# STATELESS CLASS LINTER
|
|
238
|
+
# ============================================================================
|
|
239
|
+
# Detects classes with no instance state (should be modules or functions)
|
|
240
|
+
#
|
|
241
|
+
stateless-class:
|
|
242
|
+
enabled: true
|
|
243
|
+
|
|
244
|
+
# Minimum methods to flag a stateless class
|
|
245
|
+
# Default: 2
|
|
246
|
+
min_methods: 2
|
|
247
|
+
|
|
248
|
+
# -------------------------------------------------------------------------
|
|
249
|
+
# OPTIONAL: File patterns to ignore
|
|
250
|
+
# -------------------------------------------------------------------------
|
|
251
|
+
# ignore:
|
|
252
|
+
# - "tests/**"
|
|
253
|
+
|
|
254
|
+
# ============================================================================
|
|
255
|
+
# COLLECTION PIPELINE LINTER
|
|
256
|
+
# ============================================================================
|
|
257
|
+
# Detects "embedded loop filtering" anti-pattern (if/continue in loops)
|
|
258
|
+
# Suggests using filter(), list comprehensions, or generator expressions
|
|
259
|
+
#
|
|
260
|
+
pipeline:
|
|
261
|
+
enabled: true
|
|
262
|
+
|
|
263
|
+
# Minimum if/continue patterns in a loop to flag
|
|
264
|
+
# Default: 1
|
|
265
|
+
min_continues: 1
|
|
266
|
+
|
|
267
|
+
# -------------------------------------------------------------------------
|
|
268
|
+
# OPTIONAL: File patterns to ignore
|
|
269
|
+
# -------------------------------------------------------------------------
|
|
270
|
+
# ignore:
|
|
271
|
+
# - "tests/**"
|
|
272
|
+
|
|
273
|
+
# ============================================================================
|
|
274
|
+
# LAZY IGNORES LINTER
|
|
275
|
+
# ============================================================================
|
|
276
|
+
# Detects unjustified linting suppressions (noqa, type: ignore, etc.)
|
|
277
|
+
# without proper documentation in file headers
|
|
278
|
+
#
|
|
279
|
+
lazy-ignores:
|
|
280
|
+
enabled: true
|
|
281
|
+
|
|
282
|
+
# Pattern-specific toggles
|
|
283
|
+
check_noqa: true
|
|
284
|
+
check_type_ignore: true
|
|
285
|
+
check_pylint_disable: true
|
|
286
|
+
check_nosec: true
|
|
287
|
+
check_ts_ignore: true
|
|
288
|
+
check_eslint_disable: true
|
|
289
|
+
check_thailint_ignore: true
|
|
290
|
+
check_test_skips: true
|
|
291
|
+
|
|
292
|
+
# Check for orphaned suppressions (documented but not used)
|
|
293
|
+
check_orphaned: true
|
|
294
|
+
|
|
295
|
+
# -------------------------------------------------------------------------
|
|
296
|
+
# OPTIONAL: File patterns to ignore
|
|
297
|
+
# -------------------------------------------------------------------------
|
|
298
|
+
# ignore_patterns:
|
|
299
|
+
# - "tests/**"
|
|
300
|
+
|
|
135
301
|
# ============================================================================
|
|
136
302
|
# GLOBAL SETTINGS
|
|
137
303
|
# ============================================================================
|
src/utils/project_root.py
CHANGED
|
@@ -15,6 +15,9 @@ Exports: is_project_root(), get_project_root()
|
|
|
15
15
|
Interfaces: Path-based functions for checking and finding project roots
|
|
16
16
|
|
|
17
17
|
Implementation: pyprojroot delegation with manual fallback for test environments
|
|
18
|
+
|
|
19
|
+
Suppressions:
|
|
20
|
+
- type:ignore[arg-type]: pyprojroot external library typing issue with Path conversion
|
|
18
21
|
"""
|
|
19
22
|
|
|
20
23
|
from pathlib import Path
|