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,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Build violations for function call patterns with limited string values
|
|
3
|
+
|
|
4
|
+
Scope: Function call violation message and suggestion generation
|
|
5
|
+
|
|
6
|
+
Overview: Handles building violation objects for function calls that consistently receive
|
|
7
|
+
a limited set of string values, suggesting they should use enums. Generates messages
|
|
8
|
+
with cross-file references and actionable suggestions. Separated from main violation
|
|
9
|
+
generator to maintain SRP compliance with focused responsibility.
|
|
10
|
+
|
|
11
|
+
Dependencies: Violation, Severity, StoredFunctionCall, StringlyTypedConfig
|
|
12
|
+
|
|
13
|
+
Exports: build_function_call_violations function
|
|
14
|
+
|
|
15
|
+
Interfaces: build_function_call_violations(calls, unique_values) -> list[Violation]
|
|
16
|
+
|
|
17
|
+
Implementation: Builds violations with cross-file references and enum suggestions
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from src.core.types import Severity, Violation
|
|
23
|
+
|
|
24
|
+
from .storage import StoredFunctionCall
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_function_call_violations(
|
|
28
|
+
calls: list[StoredFunctionCall], unique_values: set[str]
|
|
29
|
+
) -> list[Violation]:
|
|
30
|
+
"""Build violations for all calls to a function with limited values.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
calls: All calls to the function/param
|
|
34
|
+
unique_values: Set of unique string values passed
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of violations for each call site
|
|
38
|
+
"""
|
|
39
|
+
return [_build_violation(call, calls, unique_values) for call in calls]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_cross_references(call: StoredFunctionCall, all_calls: list[StoredFunctionCall]) -> str:
|
|
43
|
+
"""Build cross-reference string for other function call locations.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
call: Current call
|
|
47
|
+
all_calls: All calls with same function/param
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Comma-separated list of file:line references
|
|
51
|
+
"""
|
|
52
|
+
refs = []
|
|
53
|
+
for other in all_calls:
|
|
54
|
+
if other.file_path != call.file_path or other.line_number != call.line_number:
|
|
55
|
+
refs.append(f"{Path(other.file_path).name}:{other.line_number}")
|
|
56
|
+
|
|
57
|
+
return ", ".join(refs[:5]) # Limit to 5 references
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _build_violation(
|
|
61
|
+
call: StoredFunctionCall,
|
|
62
|
+
all_calls: list[StoredFunctionCall],
|
|
63
|
+
unique_values: set[str],
|
|
64
|
+
) -> Violation:
|
|
65
|
+
"""Build a single violation for a function call.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
call: The specific call to create violation for
|
|
69
|
+
all_calls: All calls to the same function/param
|
|
70
|
+
unique_values: Set of unique string values passed
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Violation instance
|
|
74
|
+
"""
|
|
75
|
+
message = _build_message(call, all_calls, unique_values)
|
|
76
|
+
suggestion = _build_suggestion(call, unique_values)
|
|
77
|
+
|
|
78
|
+
return Violation(
|
|
79
|
+
rule_id="stringly-typed.limited-values",
|
|
80
|
+
file_path=str(call.file_path),
|
|
81
|
+
line=call.line_number,
|
|
82
|
+
column=call.column,
|
|
83
|
+
message=message,
|
|
84
|
+
severity=Severity.ERROR,
|
|
85
|
+
suggestion=suggestion,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _build_message(
|
|
90
|
+
call: StoredFunctionCall,
|
|
91
|
+
all_calls: list[StoredFunctionCall],
|
|
92
|
+
unique_values: set[str],
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Build violation message for function call pattern.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
call: Current function call
|
|
98
|
+
all_calls: All calls to the same function/param
|
|
99
|
+
unique_values: Set of unique values passed
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Human-readable violation message
|
|
103
|
+
"""
|
|
104
|
+
file_count = len({c.file_path for c in all_calls})
|
|
105
|
+
values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
|
|
106
|
+
param_desc = f"parameter {call.param_index}" if call.param_index > 0 else "first parameter"
|
|
107
|
+
|
|
108
|
+
message = (
|
|
109
|
+
f"Function '{call.function_name}' {param_desc} is called with "
|
|
110
|
+
f"only {len(unique_values)} unique string values [{values_str}] "
|
|
111
|
+
f"across {file_count} file(s)."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
other_refs = _build_cross_references(call, all_calls)
|
|
115
|
+
if other_refs:
|
|
116
|
+
message += f" Also called in: {other_refs}."
|
|
117
|
+
|
|
118
|
+
return message
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _build_suggestion(call: StoredFunctionCall, unique_values: set[str]) -> str:
|
|
122
|
+
"""Build fix suggestion for function call pattern.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
call: The function call
|
|
126
|
+
unique_values: Set of unique values passed
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Human-readable suggestion
|
|
130
|
+
"""
|
|
131
|
+
return (
|
|
132
|
+
f"Consider defining an enum or type union with the "
|
|
133
|
+
f"{len(unique_values)} possible values for '{call.function_name}' "
|
|
134
|
+
f"parameter {call.param_index}."
|
|
135
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Ignore directive checking for stringly-typed linter violations
|
|
3
|
+
|
|
4
|
+
Scope: Line-level, block-level, and file-level ignore directive support
|
|
5
|
+
|
|
6
|
+
Overview: Provides ignore directive checking functionality for the stringly-typed linter.
|
|
7
|
+
Wraps the centralized IgnoreDirectiveParser to filter violations based on inline comments
|
|
8
|
+
like `# thailint: ignore[stringly-typed]`. Supports line-level, block-level
|
|
9
|
+
(ignore-start/ignore-end), file-level (ignore-file), and next-line directives.
|
|
10
|
+
Handles both Python (# comment) and TypeScript (// comment) syntax.
|
|
11
|
+
|
|
12
|
+
Dependencies: IgnoreDirectiveParser from src.linter_config.ignore, Violation type, pathlib
|
|
13
|
+
|
|
14
|
+
Exports: IgnoreChecker class
|
|
15
|
+
|
|
16
|
+
Interfaces: IgnoreChecker.filter_violations(violations) -> list[Violation]
|
|
17
|
+
|
|
18
|
+
Implementation: Uses cached IgnoreDirectiveParser singleton, reads file content on demand,
|
|
19
|
+
supports both stringly-typed.* and stringly-typed specific rule matching
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from contextlib import suppress
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from src.core.types import Violation
|
|
26
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class IgnoreChecker:
|
|
30
|
+
"""Checks for ignore directives in stringly-typed linter violations.
|
|
31
|
+
|
|
32
|
+
Wraps the centralized IgnoreDirectiveParser to filter stringly-typed
|
|
33
|
+
violations based on inline ignore comments.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, project_root: Path | None = None) -> None:
|
|
37
|
+
"""Initialize with project root for ignore parser.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
project_root: Optional project root directory. Defaults to cwd.
|
|
41
|
+
"""
|
|
42
|
+
self._ignore_parser = get_ignore_parser(project_root)
|
|
43
|
+
self._file_content_cache: dict[str, str] = {}
|
|
44
|
+
|
|
45
|
+
def filter_violations(self, violations: list[Violation]) -> list[Violation]:
|
|
46
|
+
"""Filter violations based on ignore directives.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
violations: List of violations to filter
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of violations not suppressed by ignore directives
|
|
53
|
+
"""
|
|
54
|
+
return [v for v in violations if not self._should_ignore(v)]
|
|
55
|
+
|
|
56
|
+
def _should_ignore(self, violation: Violation) -> bool:
|
|
57
|
+
"""Check if a violation should be ignored.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
violation: Violation to check
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if violation should be ignored
|
|
64
|
+
"""
|
|
65
|
+
file_content = self._get_file_content(violation.file_path)
|
|
66
|
+
return self._ignore_parser.should_ignore_violation(violation, file_content)
|
|
67
|
+
|
|
68
|
+
def _get_file_content(self, file_path: str) -> str:
|
|
69
|
+
"""Get file content with caching.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
file_path: Path to file
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
File content or empty string if unreadable
|
|
76
|
+
"""
|
|
77
|
+
with suppress(KeyError):
|
|
78
|
+
return self._file_content_cache[file_path]
|
|
79
|
+
|
|
80
|
+
content = self._read_file_content(file_path)
|
|
81
|
+
self._file_content_cache[file_path] = content
|
|
82
|
+
return content
|
|
83
|
+
|
|
84
|
+
def _read_file_content(self, file_path: str) -> str:
|
|
85
|
+
"""Read file content from disk.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
file_path: Path to file
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
File content or empty string if unreadable
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
return Path(file_path).read_text(encoding="utf-8")
|
|
95
|
+
except (OSError, UnicodeDecodeError):
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
def clear_cache(self) -> None:
|
|
99
|
+
"""Clear file content cache."""
|
|
100
|
+
self._file_content_cache.clear()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Shared ignore pattern matching utilities
|
|
3
|
+
|
|
4
|
+
Scope: Common ignore pattern checking for stringly-typed linter components
|
|
5
|
+
|
|
6
|
+
Overview: Provides shared utility functions for checking if file paths match ignore patterns.
|
|
7
|
+
Used by both the main linter and violation generator to avoid duplicating ignore pattern
|
|
8
|
+
matching logic. Centralizes the ignore pattern matching algorithm.
|
|
9
|
+
|
|
10
|
+
Dependencies: pathlib.Path, fnmatch
|
|
11
|
+
|
|
12
|
+
Exports: is_ignored function
|
|
13
|
+
|
|
14
|
+
Interfaces: is_ignored(file_path, ignore_patterns) -> bool
|
|
15
|
+
|
|
16
|
+
Implementation: Glob pattern matching with fnmatch for flexible ignore patterns
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import fnmatch
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_ignored(file_path: str | Path, ignore_patterns: list[str]) -> bool:
|
|
24
|
+
"""Check if file path matches any ignore pattern.
|
|
25
|
+
|
|
26
|
+
Supports glob patterns like:
|
|
27
|
+
- **/tests/** - matches any file in tests directories
|
|
28
|
+
- **/*_test.py - matches any file ending in _test.py
|
|
29
|
+
- tests/ - simple substring match
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
file_path: Path to check (string or Path object)
|
|
33
|
+
ignore_patterns: List of patterns to match against
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if file should be ignored
|
|
37
|
+
"""
|
|
38
|
+
if not ignore_patterns:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
path_str = str(file_path)
|
|
42
|
+
|
|
43
|
+
for pattern in ignore_patterns:
|
|
44
|
+
# Use fnmatch for glob-style patterns
|
|
45
|
+
if fnmatch.fnmatch(path_str, pattern):
|
|
46
|
+
return True
|
|
47
|
+
# Also check if pattern appears as substring (for simple patterns)
|
|
48
|
+
if pattern in path_str:
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
return False
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main stringly-typed linter rule with cross-file detection
|
|
3
|
+
|
|
4
|
+
Scope: StringlyTypedRule implementing MultiLanguageLintRule for cross-file pattern detection
|
|
5
|
+
|
|
6
|
+
Overview: Implements stringly-typed linter rule following MultiLanguageLintRule interface with
|
|
7
|
+
cross-file detection using SQLite storage. Orchestrates pattern detection by delegating to
|
|
8
|
+
language-specific analyzers (Python, TypeScript). During check() phase, patterns are collected
|
|
9
|
+
into storage. During finalize() phase, storage is queried for patterns appearing across
|
|
10
|
+
multiple files and violations are generated. Maintains minimal orchestration logic to comply
|
|
11
|
+
with SRP.
|
|
12
|
+
|
|
13
|
+
Dependencies: MultiLanguageLintRule, BaseLintContext, PythonStringlyTypedAnalyzer,
|
|
14
|
+
StringlyTypedStorage, StorageInitializer, ViolationGenerator, StringlyTypedConfig
|
|
15
|
+
|
|
16
|
+
Exports: StringlyTypedRule class
|
|
17
|
+
|
|
18
|
+
Interfaces: StringlyTypedRule.check(context) -> list[Violation],
|
|
19
|
+
StringlyTypedRule.finalize() -> list[Violation]
|
|
20
|
+
|
|
21
|
+
Implementation: Two-phase pattern: check() stores data, finalize() generates violations.
|
|
22
|
+
Delegates all logic to helper classes, maintains only orchestration and state.
|
|
23
|
+
|
|
24
|
+
Suppressions:
|
|
25
|
+
- B101: Type narrowing assertions after guards (storage initialized, file_path/content set)
|
|
26
|
+
- srp: Rule class orchestrates cross-file detection with storage, analyzers, and generators.
|
|
27
|
+
Splitting would fragment the two-phase detection workflow.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
36
|
+
from src.core.linter_utils import load_linter_config
|
|
37
|
+
from src.core.types import Violation
|
|
38
|
+
|
|
39
|
+
from .config import StringlyTypedConfig
|
|
40
|
+
from .ignore_utils import is_ignored
|
|
41
|
+
from .python.analyzer import (
|
|
42
|
+
AnalysisResult,
|
|
43
|
+
ComparisonResult,
|
|
44
|
+
FunctionCallResult,
|
|
45
|
+
PythonStringlyTypedAnalyzer,
|
|
46
|
+
)
|
|
47
|
+
from .storage import StoredComparison, StoredFunctionCall, StoredPattern, StringlyTypedStorage
|
|
48
|
+
from .storage_initializer import StorageInitializer
|
|
49
|
+
from .typescript.analyzer import TypeScriptStringlyTypedAnalyzer
|
|
50
|
+
from .violation_generator import ViolationGenerator
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def compute_string_set_hash(values: set[str]) -> int:
|
|
54
|
+
"""Compute consistent hash for a set of strings.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
values: Set of string values to hash
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Hash value based on sorted, lowercased strings
|
|
61
|
+
"""
|
|
62
|
+
return hash(tuple(sorted(s.lower() for s in values)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_ready_for_analysis(context: BaseLintContext, storage: StringlyTypedStorage | None) -> bool:
|
|
66
|
+
"""Check if context and storage are ready for analysis."""
|
|
67
|
+
return bool(context.file_path and context.file_content and storage)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _convert_to_stored_pattern(result: AnalysisResult) -> StoredPattern:
|
|
71
|
+
"""Convert AnalysisResult to StoredPattern.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
result: Analysis result from language analyzer
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
StoredPattern for storage
|
|
78
|
+
"""
|
|
79
|
+
return StoredPattern(
|
|
80
|
+
file_path=result.file_path,
|
|
81
|
+
line_number=result.line_number,
|
|
82
|
+
column=result.column,
|
|
83
|
+
variable_name=result.variable_name,
|
|
84
|
+
string_set_hash=compute_string_set_hash(result.string_values),
|
|
85
|
+
string_values=sorted(result.string_values),
|
|
86
|
+
pattern_type=result.pattern_type,
|
|
87
|
+
details=result.details,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _convert_to_stored_function_call(result: FunctionCallResult) -> StoredFunctionCall:
|
|
92
|
+
"""Convert FunctionCallResult to StoredFunctionCall.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
result: Function call result from language analyzer
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
StoredFunctionCall for storage
|
|
99
|
+
"""
|
|
100
|
+
return StoredFunctionCall(
|
|
101
|
+
file_path=result.file_path,
|
|
102
|
+
line_number=result.line_number,
|
|
103
|
+
column=result.column,
|
|
104
|
+
function_name=result.function_name,
|
|
105
|
+
param_index=result.param_index,
|
|
106
|
+
string_value=result.string_value,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _convert_to_stored_comparison(result: ComparisonResult) -> StoredComparison:
|
|
111
|
+
"""Convert ComparisonResult to StoredComparison.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
result: Comparison result from language analyzer
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
StoredComparison for storage
|
|
118
|
+
"""
|
|
119
|
+
return StoredComparison(
|
|
120
|
+
file_path=result.file_path,
|
|
121
|
+
line_number=result.line_number,
|
|
122
|
+
column=result.column,
|
|
123
|
+
variable_name=result.variable_name,
|
|
124
|
+
compared_value=result.compared_value,
|
|
125
|
+
operator=result.operator,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class StringlyTypedComponents:
|
|
131
|
+
"""Component dependencies for stringly-typed linter."""
|
|
132
|
+
|
|
133
|
+
storage_initializer: StorageInitializer
|
|
134
|
+
violation_generator: ViolationGenerator
|
|
135
|
+
python_analyzer: PythonStringlyTypedAnalyzer
|
|
136
|
+
typescript_analyzer: TypeScriptStringlyTypedAnalyzer
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class StringlyTypedRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
140
|
+
"""Detects stringly-typed patterns across project files.
|
|
141
|
+
|
|
142
|
+
Uses two-phase pattern:
|
|
143
|
+
1. check() - Collects patterns into SQLite storage (returns empty list)
|
|
144
|
+
2. finalize() - Queries storage and generates violations for cross-file patterns
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(self) -> None:
|
|
148
|
+
"""Initialize the stringly-typed rule with helper components."""
|
|
149
|
+
self._storage: StringlyTypedStorage | None = None
|
|
150
|
+
self._initialized = False
|
|
151
|
+
self._config: StringlyTypedConfig | None = None
|
|
152
|
+
|
|
153
|
+
# Helper components grouped to reduce instance attributes
|
|
154
|
+
self._helpers = StringlyTypedComponents(
|
|
155
|
+
storage_initializer=StorageInitializer(),
|
|
156
|
+
violation_generator=ViolationGenerator(),
|
|
157
|
+
python_analyzer=PythonStringlyTypedAnalyzer(),
|
|
158
|
+
typescript_analyzer=TypeScriptStringlyTypedAnalyzer(),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def _active_storage(self) -> StringlyTypedStorage:
|
|
163
|
+
"""Get storage, asserting it has been initialized.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The initialized storage instance.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
AssertionError: If storage has not been initialized.
|
|
170
|
+
"""
|
|
171
|
+
assert self._storage is not None, "Storage not initialized" # nosec B101
|
|
172
|
+
return self._storage
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def rule_id(self) -> str:
|
|
176
|
+
"""Unique identifier for this rule."""
|
|
177
|
+
return "stringly-typed.repeated-validation"
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def rule_name(self) -> str:
|
|
181
|
+
"""Human-readable name for this rule."""
|
|
182
|
+
return "Stringly-Typed Pattern"
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def description(self) -> str:
|
|
186
|
+
"""Description of what this rule checks."""
|
|
187
|
+
return "Detects stringly-typed code patterns that should use enums"
|
|
188
|
+
|
|
189
|
+
def _load_config(self, context: BaseLintContext) -> StringlyTypedConfig:
|
|
190
|
+
"""Load configuration from context.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
context: Lint context with metadata
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
StringlyTypedConfig instance
|
|
197
|
+
"""
|
|
198
|
+
return load_linter_config(context, "stringly_typed", StringlyTypedConfig)
|
|
199
|
+
|
|
200
|
+
def _check_python(
|
|
201
|
+
self, context: BaseLintContext, config: StringlyTypedConfig
|
|
202
|
+
) -> list[Violation]:
|
|
203
|
+
"""Analyze Python code and store patterns.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
context: Lint context with file content
|
|
207
|
+
config: Stringly-typed configuration
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Empty list (violations generated in finalize)
|
|
211
|
+
"""
|
|
212
|
+
self._ensure_storage_initialized(context, config)
|
|
213
|
+
self._analyze_python_file(context, config)
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
def _check_typescript(
|
|
217
|
+
self, context: BaseLintContext, config: StringlyTypedConfig
|
|
218
|
+
) -> list[Violation]:
|
|
219
|
+
"""Analyze TypeScript code and store patterns.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
context: Lint context with file content
|
|
223
|
+
config: Stringly-typed configuration
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Empty list (violations generated in finalize)
|
|
227
|
+
"""
|
|
228
|
+
self._ensure_storage_initialized(context, config)
|
|
229
|
+
self._analyze_typescript_file(context, config)
|
|
230
|
+
return []
|
|
231
|
+
|
|
232
|
+
def _analyze_typescript_file(
|
|
233
|
+
self, context: BaseLintContext, config: StringlyTypedConfig
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Analyze TypeScript file and store patterns.
|
|
236
|
+
|
|
237
|
+
Uses single-parse optimization to avoid duplicate parsing overhead.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
context: Lint context with file content
|
|
241
|
+
config: Stringly-typed configuration
|
|
242
|
+
"""
|
|
243
|
+
if not self._should_analyze(context, config):
|
|
244
|
+
return
|
|
245
|
+
# _should_analyze ensures file_path and file_content are set
|
|
246
|
+
assert context.file_path is not None # nosec B101
|
|
247
|
+
assert context.file_content is not None # nosec B101
|
|
248
|
+
|
|
249
|
+
self._helpers.typescript_analyzer.config = config
|
|
250
|
+
call_results, comparison_results = self._helpers.typescript_analyzer.analyze_all(
|
|
251
|
+
context.file_content, context.file_path
|
|
252
|
+
)
|
|
253
|
+
self._store_typescript_results(call_results, comparison_results)
|
|
254
|
+
|
|
255
|
+
def _store_typescript_results(
|
|
256
|
+
self,
|
|
257
|
+
call_results: list[FunctionCallResult],
|
|
258
|
+
comparison_results: list[ComparisonResult],
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Store TypeScript analysis results.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
call_results: Function call patterns found
|
|
264
|
+
comparison_results: Comparison patterns found
|
|
265
|
+
"""
|
|
266
|
+
stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
|
|
267
|
+
self._active_storage.add_function_calls(stored_calls)
|
|
268
|
+
stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
|
|
269
|
+
self._active_storage.add_comparisons(stored_comparisons)
|
|
270
|
+
|
|
271
|
+
def _ensure_storage_initialized(
|
|
272
|
+
self, context: BaseLintContext, config: StringlyTypedConfig
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Initialize storage and analyzers on first call.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
context: Lint context
|
|
278
|
+
config: Stringly-typed configuration
|
|
279
|
+
"""
|
|
280
|
+
if not self._initialized:
|
|
281
|
+
self._storage = self._helpers.storage_initializer.initialize(context, config)
|
|
282
|
+
self._config = config
|
|
283
|
+
self._initialized = True
|
|
284
|
+
|
|
285
|
+
def _analyze_python_file(self, context: BaseLintContext, config: StringlyTypedConfig) -> None:
|
|
286
|
+
"""Analyze Python file and store patterns.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
context: Lint context with file content
|
|
290
|
+
config: Stringly-typed configuration
|
|
291
|
+
"""
|
|
292
|
+
if not self._should_analyze(context, config):
|
|
293
|
+
return
|
|
294
|
+
# _should_analyze ensures file_path and file_content are set
|
|
295
|
+
assert context.file_path is not None # nosec B101
|
|
296
|
+
assert context.file_content is not None # nosec B101
|
|
297
|
+
|
|
298
|
+
file_path = context.file_path
|
|
299
|
+
file_content = context.file_content
|
|
300
|
+
self._helpers.python_analyzer.config = config
|
|
301
|
+
|
|
302
|
+
self._store_validation_patterns(file_content, file_path)
|
|
303
|
+
self._store_function_calls(file_content, file_path)
|
|
304
|
+
self._store_comparisons(file_content, file_path)
|
|
305
|
+
|
|
306
|
+
def _should_analyze(self, context: BaseLintContext, config: StringlyTypedConfig) -> bool:
|
|
307
|
+
"""Check if file should be analyzed.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
context: Lint context
|
|
311
|
+
config: Configuration
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
True if file should be analyzed
|
|
315
|
+
"""
|
|
316
|
+
if not _is_ready_for_analysis(context, self._storage):
|
|
317
|
+
return False
|
|
318
|
+
# _is_ready_for_analysis ensures file_path is set
|
|
319
|
+
assert context.file_path is not None # nosec B101
|
|
320
|
+
return not is_ignored(context.file_path, config.ignore)
|
|
321
|
+
|
|
322
|
+
def _store_validation_patterns(self, file_content: str, file_path: Path) -> None:
|
|
323
|
+
"""Analyze and store validation patterns.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
file_content: Python source code
|
|
327
|
+
file_path: Path to file
|
|
328
|
+
"""
|
|
329
|
+
results = self._helpers.python_analyzer.analyze(file_content, file_path)
|
|
330
|
+
self._active_storage.add_patterns([_convert_to_stored_pattern(r) for r in results])
|
|
331
|
+
|
|
332
|
+
def _store_function_calls(self, file_content: str, file_path: Path) -> None:
|
|
333
|
+
"""Analyze and store function call patterns.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
file_content: Python source code
|
|
337
|
+
file_path: Path to file
|
|
338
|
+
"""
|
|
339
|
+
call_results = self._helpers.python_analyzer.analyze_function_calls(file_content, file_path)
|
|
340
|
+
stored_calls = [_convert_to_stored_function_call(r) for r in call_results]
|
|
341
|
+
self._active_storage.add_function_calls(stored_calls)
|
|
342
|
+
|
|
343
|
+
def _store_comparisons(self, file_content: str, file_path: Path) -> None:
|
|
344
|
+
"""Analyze and store Python comparison patterns.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
file_content: Python source code
|
|
348
|
+
file_path: Path to file
|
|
349
|
+
"""
|
|
350
|
+
comparison_results = self._helpers.python_analyzer.analyze_comparisons(
|
|
351
|
+
file_content, file_path
|
|
352
|
+
)
|
|
353
|
+
stored_comparisons = [_convert_to_stored_comparison(r) for r in comparison_results]
|
|
354
|
+
self._active_storage.add_comparisons(stored_comparisons)
|
|
355
|
+
|
|
356
|
+
def finalize(self) -> list[Violation]:
|
|
357
|
+
"""Generate violations after all files processed.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
List of violations for patterns appearing in multiple files
|
|
361
|
+
"""
|
|
362
|
+
if not self._storage or not self._config:
|
|
363
|
+
return []
|
|
364
|
+
|
|
365
|
+
# Generate violations from cross-file patterns
|
|
366
|
+
violations = self._helpers.violation_generator.generate_violations(
|
|
367
|
+
self._storage, self.rule_id, self._config
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Cleanup and reset state for next run
|
|
371
|
+
self._storage.close()
|
|
372
|
+
self._storage = None
|
|
373
|
+
self._config = None
|
|
374
|
+
self._initialized = False
|
|
375
|
+
|
|
376
|
+
return violations
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Python-specific detection for stringly-typed patterns
|
|
3
|
+
|
|
4
|
+
Scope: Python AST analysis for membership validation, equality chains, and function calls
|
|
5
|
+
|
|
6
|
+
Overview: Exposes Python analysis components for detecting stringly-typed patterns in Python
|
|
7
|
+
source code. Includes validation_detector for finding 'x in ("a", "b")' patterns,
|
|
8
|
+
conditional_detector for finding if/elif chains and match statements, call_tracker for
|
|
9
|
+
finding function calls with string arguments, and analyzer for coordinating detection
|
|
10
|
+
across Python files. Uses AST traversal to identify where plain strings are used instead
|
|
11
|
+
of proper enums or typed alternatives.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for Python AST parsing
|
|
14
|
+
|
|
15
|
+
Exports: MembershipValidationDetector, ConditionalPatternDetector, FunctionCallTracker,
|
|
16
|
+
PythonStringlyTypedAnalyzer
|
|
17
|
+
|
|
18
|
+
Interfaces: Detector and analyzer classes for Python stringly-typed pattern detection
|
|
19
|
+
|
|
20
|
+
Implementation: AST NodeVisitor pattern for traversing Python syntax trees
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .analyzer import PythonStringlyTypedAnalyzer
|
|
24
|
+
from .call_tracker import FunctionCallTracker
|
|
25
|
+
from .conditional_detector import ConditionalPatternDetector
|
|
26
|
+
from .validation_detector import MembershipValidationDetector
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ConditionalPatternDetector",
|
|
30
|
+
"FunctionCallTracker",
|
|
31
|
+
"MembershipValidationDetector",
|
|
32
|
+
"PythonStringlyTypedAnalyzer",
|
|
33
|
+
]
|