thailint 0.5.0__py3-none-any.whl → 0.15.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,419 @@
|
|
|
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 _has_spaces(values: set[str]) -> bool:
|
|
58
|
+
"""Check if any value contains spaces (indicates SQL/templates, not enums)."""
|
|
59
|
+
return any(" " in v for v in values)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_enum_candidate(pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
|
|
63
|
+
"""Check if pattern's value count is within enum range."""
|
|
64
|
+
value_count = len(pattern.string_values)
|
|
65
|
+
return config.min_values_for_enum <= value_count <= config.max_values_for_enum
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_pattern_allowed(pattern: StoredPattern, config: StringlyTypedConfig) -> bool:
|
|
69
|
+
"""Check if pattern's string set is in allowed list."""
|
|
70
|
+
return _is_allowed_value_set(set(pattern.string_values), config)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _should_skip_pattern_values(values: set[str], config: StringlyTypedConfig) -> bool:
|
|
74
|
+
"""Check if pattern values should be skipped."""
|
|
75
|
+
return (
|
|
76
|
+
_is_allowed_value_set(values, config)
|
|
77
|
+
or context_filter.are_all_values_excluded(values)
|
|
78
|
+
or _has_spaces(values)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _should_skip_patterns(patterns: list[StoredPattern], config: StringlyTypedConfig) -> bool:
|
|
83
|
+
"""Check if pattern group should be skipped based on config."""
|
|
84
|
+
if not patterns:
|
|
85
|
+
return True
|
|
86
|
+
first = patterns[0]
|
|
87
|
+
if not _is_enum_candidate(first, config):
|
|
88
|
+
return True
|
|
89
|
+
return _should_skip_pattern_values(set(first.string_values), config)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _should_skip_comparison(unique_values: set[str], config: StringlyTypedConfig) -> bool:
|
|
93
|
+
"""Check if a comparison pattern should be skipped based on config."""
|
|
94
|
+
if len(unique_values) > config.max_values_for_enum:
|
|
95
|
+
return True
|
|
96
|
+
if _is_allowed_value_set(unique_values, config):
|
|
97
|
+
return True
|
|
98
|
+
if context_filter.are_all_values_excluded(unique_values):
|
|
99
|
+
return True
|
|
100
|
+
if _has_spaces(unique_values):
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Variable suffixes that indicate false positive comparisons
|
|
106
|
+
_EXCLUDED_VARIABLE_SUFFIXES: tuple[str, ...] = (
|
|
107
|
+
".value", # Enum value access
|
|
108
|
+
".method", # HTTP method
|
|
109
|
+
".type", # Tree-sitter node types
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _should_skip_variable(variable_name: str) -> bool:
|
|
114
|
+
"""Check if a variable name indicates a false positive comparison."""
|
|
115
|
+
# Check excluded suffixes
|
|
116
|
+
if any(variable_name.endswith(s) for s in _EXCLUDED_VARIABLE_SUFFIXES):
|
|
117
|
+
return True
|
|
118
|
+
# Test assertion patterns (underscore prefix is common in comprehensions/lambdas)
|
|
119
|
+
return variable_name.startswith("_.")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --- Pure helper functions for message building ---
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_cross_references(pattern: StoredPattern, all_patterns: list[StoredPattern]) -> str:
|
|
126
|
+
"""Build cross-reference string for other files."""
|
|
127
|
+
refs = [
|
|
128
|
+
f"{other.file_path.name}:{other.line_number}"
|
|
129
|
+
for other in all_patterns
|
|
130
|
+
if other.file_path != pattern.file_path
|
|
131
|
+
]
|
|
132
|
+
return ", ".join(refs)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _build_message(pattern: StoredPattern, all_patterns: list[StoredPattern]) -> str:
|
|
136
|
+
"""Build violation message with cross-file references."""
|
|
137
|
+
file_count = len({p.file_path for p in all_patterns})
|
|
138
|
+
values_str = ", ".join(f"'{v}'" for v in sorted(pattern.string_values))
|
|
139
|
+
other_refs = _build_cross_references(pattern, all_patterns)
|
|
140
|
+
|
|
141
|
+
message = f"Stringly-typed pattern with values [{values_str}] appears in {file_count} files."
|
|
142
|
+
if other_refs:
|
|
143
|
+
message += f" Also found in: {other_refs}."
|
|
144
|
+
|
|
145
|
+
return message
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _build_suggestion(pattern: StoredPattern) -> str:
|
|
149
|
+
"""Build fix suggestion for the pattern."""
|
|
150
|
+
values_count = len(pattern.string_values)
|
|
151
|
+
var_info = f" for '{pattern.variable_name}'" if pattern.variable_name else ""
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
f"Consider defining an enum or type union{var_info} with the "
|
|
155
|
+
f"{values_count} possible values instead of using string literals."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _build_comparison_cross_references(
|
|
160
|
+
comparison: StoredComparison,
|
|
161
|
+
all_comparisons: list[StoredComparison],
|
|
162
|
+
) -> str:
|
|
163
|
+
"""Build cross-reference string for other comparison locations."""
|
|
164
|
+
refs = [
|
|
165
|
+
f"{other.file_path.name}:{other.line_number}"
|
|
166
|
+
for other in all_comparisons
|
|
167
|
+
if other.file_path != comparison.file_path
|
|
168
|
+
]
|
|
169
|
+
return ", ".join(refs)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _build_comparison_message(
|
|
173
|
+
comparison: StoredComparison,
|
|
174
|
+
all_comparisons: list[StoredComparison],
|
|
175
|
+
unique_values: set[str],
|
|
176
|
+
) -> str:
|
|
177
|
+
"""Build violation message for scattered comparison."""
|
|
178
|
+
file_count = len({c.file_path for c in all_comparisons})
|
|
179
|
+
values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
|
|
180
|
+
other_refs = _build_comparison_cross_references(comparison, all_comparisons)
|
|
181
|
+
|
|
182
|
+
message = (
|
|
183
|
+
f"Variable '{comparison.variable_name}' is compared to {len(unique_values)} "
|
|
184
|
+
f"different string values [{values_str}] across {file_count} file(s)."
|
|
185
|
+
)
|
|
186
|
+
if other_refs:
|
|
187
|
+
message += f" Also compared in: {other_refs}."
|
|
188
|
+
|
|
189
|
+
return message
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _build_comparison_suggestion(comparison: StoredComparison, unique_values: set[str]) -> str:
|
|
193
|
+
"""Build fix suggestion for scattered comparison."""
|
|
194
|
+
return (
|
|
195
|
+
f"Consider defining an enum for '{comparison.variable_name}' with the "
|
|
196
|
+
f"{len(unique_values)} possible values instead of using string literals "
|
|
197
|
+
f"in scattered comparisons."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# --- Pure helper function for violation building ---
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _build_violation(
|
|
205
|
+
pattern: StoredPattern, all_patterns: list[StoredPattern], rule_id: str
|
|
206
|
+
) -> Violation:
|
|
207
|
+
"""Build a violation for a pattern with cross-references."""
|
|
208
|
+
message = _build_message(pattern, all_patterns)
|
|
209
|
+
suggestion = _build_suggestion(pattern)
|
|
210
|
+
|
|
211
|
+
return Violation(
|
|
212
|
+
rule_id=rule_id,
|
|
213
|
+
file_path=str(pattern.file_path),
|
|
214
|
+
line=pattern.line_number,
|
|
215
|
+
column=pattern.column,
|
|
216
|
+
message=message,
|
|
217
|
+
severity=Severity.ERROR,
|
|
218
|
+
suggestion=suggestion,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _build_comparison_violation(
|
|
223
|
+
comparison: StoredComparison,
|
|
224
|
+
all_comparisons: list[StoredComparison],
|
|
225
|
+
unique_values: set[str],
|
|
226
|
+
) -> Violation:
|
|
227
|
+
"""Build a violation for a scattered string comparison."""
|
|
228
|
+
message = _build_comparison_message(comparison, all_comparisons, unique_values)
|
|
229
|
+
suggestion = _build_comparison_suggestion(comparison, unique_values)
|
|
230
|
+
|
|
231
|
+
return Violation(
|
|
232
|
+
rule_id="stringly-typed.scattered-comparison",
|
|
233
|
+
file_path=str(comparison.file_path),
|
|
234
|
+
line=comparison.line_number,
|
|
235
|
+
column=comparison.column,
|
|
236
|
+
message=message,
|
|
237
|
+
severity=Severity.ERROR,
|
|
238
|
+
suggestion=suggestion,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# --- Helper functions for pattern processing ---
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _process_pattern_group(
|
|
246
|
+
patterns: list[StoredPattern],
|
|
247
|
+
config: StringlyTypedConfig,
|
|
248
|
+
rule_id: str,
|
|
249
|
+
violations: list[Violation],
|
|
250
|
+
covered_variables: set[str],
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Process a group of patterns with the same hash."""
|
|
253
|
+
if _should_skip_patterns(patterns, config):
|
|
254
|
+
return
|
|
255
|
+
violations.extend(_build_violation(p, patterns, rule_id) for p in patterns)
|
|
256
|
+
for pattern in patterns:
|
|
257
|
+
if pattern.variable_name:
|
|
258
|
+
covered_variables.add(pattern.variable_name)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# --- Helper functions for function call processing ---
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _is_valid_function(name: str, idx: int, vals: set[str], config: StringlyTypedConfig) -> bool:
|
|
265
|
+
"""Check if a function passes all validity filters."""
|
|
266
|
+
if _is_allowed_value_set(vals, config):
|
|
267
|
+
return False
|
|
268
|
+
if _has_spaces(vals):
|
|
269
|
+
return False
|
|
270
|
+
return context_filter.should_include(name, idx, vals)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _get_valid_functions(
|
|
274
|
+
storage: StringlyTypedStorage,
|
|
275
|
+
config: StringlyTypedConfig,
|
|
276
|
+
) -> list[tuple[str, int, set[str]]]:
|
|
277
|
+
"""Get functions that pass all filters."""
|
|
278
|
+
min_files = config.min_occurrences if config.require_cross_file else 1
|
|
279
|
+
limited_funcs = storage.get_limited_value_functions(
|
|
280
|
+
min_values=config.min_values_for_enum,
|
|
281
|
+
max_values=config.max_values_for_enum,
|
|
282
|
+
min_files=min_files,
|
|
283
|
+
)
|
|
284
|
+
return [(n, i, v) for n, i, v in limited_funcs if _is_valid_function(n, i, v, config)]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _build_call_violations(
|
|
288
|
+
valid_funcs: list[tuple[str, int, set[str]]],
|
|
289
|
+
storage: StringlyTypedStorage,
|
|
290
|
+
) -> list[Violation]:
|
|
291
|
+
"""Build violations for valid function patterns."""
|
|
292
|
+
violations: list[Violation] = []
|
|
293
|
+
for function_name, param_index, unique_values in valid_funcs:
|
|
294
|
+
calls = storage.get_calls_by_function(function_name, param_index)
|
|
295
|
+
violations.extend(build_function_call_violations(calls, unique_values))
|
|
296
|
+
return violations
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# --- Helper functions for comparison processing ---
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _get_variables_to_check(
|
|
303
|
+
storage: StringlyTypedStorage,
|
|
304
|
+
config: StringlyTypedConfig,
|
|
305
|
+
) -> list[tuple[str, set[str]]]:
|
|
306
|
+
"""Get variables with multiple values that should be checked."""
|
|
307
|
+
min_files = config.min_occurrences if config.require_cross_file else 1
|
|
308
|
+
return storage.get_variables_with_multiple_values(
|
|
309
|
+
min_values=config.min_values_for_enum,
|
|
310
|
+
min_files=min_files,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _process_variable( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
315
|
+
variable_name: str,
|
|
316
|
+
unique_values: set[str],
|
|
317
|
+
storage: StringlyTypedStorage,
|
|
318
|
+
config: StringlyTypedConfig,
|
|
319
|
+
covered_variables: set[str],
|
|
320
|
+
violations: list[Violation],
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Process comparisons for a single variable."""
|
|
323
|
+
if variable_name in covered_variables:
|
|
324
|
+
return
|
|
325
|
+
if _should_skip_variable(variable_name):
|
|
326
|
+
return
|
|
327
|
+
if _should_skip_comparison(unique_values, config):
|
|
328
|
+
return
|
|
329
|
+
comparisons = storage.get_comparisons_by_variable(variable_name)
|
|
330
|
+
violations.extend(
|
|
331
|
+
_build_comparison_violation(c, comparisons, unique_values) for c in comparisons
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# --- ViolationGenerator class ---
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class ViolationGenerator:
|
|
339
|
+
"""Generates violations from cross-file stringly-typed patterns."""
|
|
340
|
+
|
|
341
|
+
def __init__(self) -> None:
|
|
342
|
+
"""Initialize with helper filters."""
|
|
343
|
+
self._ignore_checker = IgnoreChecker()
|
|
344
|
+
|
|
345
|
+
def generate_violations(
|
|
346
|
+
self,
|
|
347
|
+
storage: StringlyTypedStorage,
|
|
348
|
+
rule_id: str,
|
|
349
|
+
config: StringlyTypedConfig,
|
|
350
|
+
) -> list[Violation]:
|
|
351
|
+
"""Generate violations from storage.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
storage: Pattern storage instance
|
|
355
|
+
rule_id: Rule identifier for violations
|
|
356
|
+
config: Stringly-typed configuration with thresholds
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
List of violations for patterns appearing in multiple files
|
|
360
|
+
"""
|
|
361
|
+
violations: list[Violation] = []
|
|
362
|
+
pattern_violations, covered_vars = self._generate_pattern_violations(
|
|
363
|
+
storage, rule_id, config
|
|
364
|
+
)
|
|
365
|
+
violations.extend(pattern_violations)
|
|
366
|
+
violations.extend(self._generate_function_call_violations(storage, config))
|
|
367
|
+
violations.extend(self._generate_comparison_violations(storage, config, covered_vars))
|
|
368
|
+
|
|
369
|
+
# Apply path-based ignore patterns from config
|
|
370
|
+
violations = _filter_by_ignore(violations, config.ignore)
|
|
371
|
+
|
|
372
|
+
# Apply inline ignore directives via IgnoreChecker
|
|
373
|
+
violations = self._ignore_checker.filter_violations(violations)
|
|
374
|
+
|
|
375
|
+
return violations
|
|
376
|
+
|
|
377
|
+
def _generate_pattern_violations(
|
|
378
|
+
self,
|
|
379
|
+
storage: StringlyTypedStorage,
|
|
380
|
+
rule_id: str,
|
|
381
|
+
config: StringlyTypedConfig,
|
|
382
|
+
) -> tuple[list[Violation], set[str]]:
|
|
383
|
+
"""Generate violations for duplicate validation patterns."""
|
|
384
|
+
duplicate_hashes = storage.get_duplicate_hashes(min_files=config.min_occurrences)
|
|
385
|
+
violations: list[Violation] = []
|
|
386
|
+
covered_variables: set[str] = set()
|
|
387
|
+
|
|
388
|
+
for hash_value in duplicate_hashes:
|
|
389
|
+
patterns = storage.get_patterns_by_hash(hash_value)
|
|
390
|
+
_process_pattern_group(patterns, config, rule_id, violations, covered_variables)
|
|
391
|
+
|
|
392
|
+
return violations, covered_variables
|
|
393
|
+
|
|
394
|
+
def _generate_function_call_violations(
|
|
395
|
+
self,
|
|
396
|
+
storage: StringlyTypedStorage,
|
|
397
|
+
config: StringlyTypedConfig,
|
|
398
|
+
) -> list[Violation]:
|
|
399
|
+
"""Generate violations for function call patterns."""
|
|
400
|
+
valid_funcs = _get_valid_functions(storage, config)
|
|
401
|
+
return _build_call_violations(valid_funcs, storage)
|
|
402
|
+
|
|
403
|
+
def _generate_comparison_violations(
|
|
404
|
+
self,
|
|
405
|
+
storage: StringlyTypedStorage,
|
|
406
|
+
config: StringlyTypedConfig,
|
|
407
|
+
covered_variables: set[str] | None = None,
|
|
408
|
+
) -> list[Violation]:
|
|
409
|
+
"""Generate violations for scattered string comparisons."""
|
|
410
|
+
covered_variables = covered_variables or set()
|
|
411
|
+
variables = _get_variables_to_check(storage, config)
|
|
412
|
+
|
|
413
|
+
violations: list[Violation] = []
|
|
414
|
+
for variable_name, unique_values in variables:
|
|
415
|
+
_process_variable(
|
|
416
|
+
variable_name, unique_values, storage, config, covered_variables, violations
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return violations
|