thailint 0.2.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 +44 -27
- src/core/base.py +95 -5
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +36 -6
- 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 +125 -22
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +142 -94
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +68 -21
- 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 +20 -82
- src/linters/dry/file_analyzer.py +15 -50
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +182 -54
- src/linters/dry/python_analyzer.py +108 -336
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +129 -71
- src/linters/dry/typescript_analyzer.py +68 -380
- 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 +9 -5
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +105 -0
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +140 -0
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +72 -0
- src/linters/file_header/linter.py +309 -0
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +42 -0
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +79 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +74 -31
- 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/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +249 -0
- src/linters/magic_numbers/linter.py +462 -0
- src/linters/magic_numbers/python_analyzer.py +64 -0
- src/linters/magic_numbers/typescript_analyzer.py +215 -0
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/magic_numbers/violation_builder.py +98 -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/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +31 -34
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -11
- 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/__init__.py +53 -0
- src/linters/print_statements/config.py +78 -0
- src/linters/print_statements/linter.py +413 -0
- src/linters/print_statements/python_analyzer.py +153 -0
- src/linters/print_statements/typescript_analyzer.py +125 -0
- src/linters/print_statements/violation_builder.py +96 -0
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/config.py +12 -6
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +47 -39
- 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 +264 -16
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +354 -0
- src/utils/project_root.py +138 -16
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1055
- thailint-0.2.0.dist-info/METADATA +0 -980
- thailint-0.2.0.dist-info/RECORD +0 -75
- thailint-0.2.0.dist-info/entry_points.txt +0 -4
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Context-aware filtering for stringly-typed function call violations
|
|
3
|
+
|
|
4
|
+
Scope: Filter out false positive function call patterns based on function names and contexts
|
|
5
|
+
|
|
6
|
+
Overview: Implements a blocklist-based filtering approach to reduce false positives in the
|
|
7
|
+
stringly-typed linter's function call detection. Excludes known false positive patterns
|
|
8
|
+
including dictionary access methods, string processing functions, logging calls, framework
|
|
9
|
+
validators, and external API functions. Uses function name pattern matching and parameter
|
|
10
|
+
position filtering to achieve <5% false positive rate.
|
|
11
|
+
|
|
12
|
+
Dependencies: re module for pattern matching
|
|
13
|
+
|
|
14
|
+
Exports: should_include, are_all_values_excluded functions
|
|
15
|
+
|
|
16
|
+
Interfaces: should_include(function_name, param_index, string_values) -> bool,
|
|
17
|
+
are_all_values_excluded(unique_values) -> bool
|
|
18
|
+
|
|
19
|
+
Implementation: Blocklist-based filtering with function name patterns, parameter position rules,
|
|
20
|
+
and string value pattern detection
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
# Function name suffixes that always indicate false positives
|
|
26
|
+
_EXCLUDED_FUNCTION_SUFFIXES: tuple[str, ...] = (
|
|
27
|
+
# Exception constructors - error messages are inherently unique strings
|
|
28
|
+
"Error",
|
|
29
|
+
"Exception",
|
|
30
|
+
"Warning",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Function name patterns to always exclude (case-insensitive suffix/contains match)
|
|
34
|
+
_EXCLUDED_FUNCTION_PATTERNS: tuple[str, ...] = (
|
|
35
|
+
# Dictionary/object access - these are metadata access, not domain values
|
|
36
|
+
".get",
|
|
37
|
+
".set",
|
|
38
|
+
".pop",
|
|
39
|
+
".setdefault",
|
|
40
|
+
".update",
|
|
41
|
+
# List/collection operations
|
|
42
|
+
".append",
|
|
43
|
+
".extend",
|
|
44
|
+
".insert",
|
|
45
|
+
".add",
|
|
46
|
+
".remove",
|
|
47
|
+
".push",
|
|
48
|
+
".set",
|
|
49
|
+
".has",
|
|
50
|
+
"hasItem",
|
|
51
|
+
"push",
|
|
52
|
+
# String processing - delimiters and format strings
|
|
53
|
+
# Note: both .method and method forms to catch both method calls and standalone functions
|
|
54
|
+
".split",
|
|
55
|
+
"split",
|
|
56
|
+
".rsplit",
|
|
57
|
+
".replace",
|
|
58
|
+
"replace",
|
|
59
|
+
".strip",
|
|
60
|
+
".rstrip",
|
|
61
|
+
".lstrip",
|
|
62
|
+
".startswith",
|
|
63
|
+
"startswith",
|
|
64
|
+
".startsWith",
|
|
65
|
+
"startsWith",
|
|
66
|
+
".endswith",
|
|
67
|
+
"endswith",
|
|
68
|
+
".endsWith",
|
|
69
|
+
"endsWith",
|
|
70
|
+
".includes",
|
|
71
|
+
"includes",
|
|
72
|
+
".indexOf",
|
|
73
|
+
"indexOf",
|
|
74
|
+
".lastIndexOf",
|
|
75
|
+
".match",
|
|
76
|
+
".search",
|
|
77
|
+
".format",
|
|
78
|
+
".join",
|
|
79
|
+
"join",
|
|
80
|
+
".encode",
|
|
81
|
+
".decode",
|
|
82
|
+
".lower",
|
|
83
|
+
".upper",
|
|
84
|
+
".trim",
|
|
85
|
+
".trimStart",
|
|
86
|
+
".trimEnd",
|
|
87
|
+
".padStart",
|
|
88
|
+
".padEnd",
|
|
89
|
+
"strftime",
|
|
90
|
+
"strptime",
|
|
91
|
+
# Logging and output - human-readable messages
|
|
92
|
+
"logger.debug",
|
|
93
|
+
"logger.info",
|
|
94
|
+
"logger.warning",
|
|
95
|
+
"logger.error",
|
|
96
|
+
"logger.critical",
|
|
97
|
+
"logger.exception",
|
|
98
|
+
"logging.debug",
|
|
99
|
+
"logging.info",
|
|
100
|
+
"logging.warning",
|
|
101
|
+
"logging.error",
|
|
102
|
+
"print",
|
|
103
|
+
"echo",
|
|
104
|
+
"console.print",
|
|
105
|
+
"console.log",
|
|
106
|
+
"typer.echo",
|
|
107
|
+
"click.echo",
|
|
108
|
+
# Regex - pattern strings
|
|
109
|
+
"re.sub",
|
|
110
|
+
"re.match",
|
|
111
|
+
"re.search",
|
|
112
|
+
"re.compile",
|
|
113
|
+
"re.findall",
|
|
114
|
+
"re.split",
|
|
115
|
+
# Environment variables
|
|
116
|
+
"os.environ.get",
|
|
117
|
+
"os.getenv",
|
|
118
|
+
"environ.get",
|
|
119
|
+
"getenv",
|
|
120
|
+
# File operations
|
|
121
|
+
"open",
|
|
122
|
+
"Path",
|
|
123
|
+
# Framework validators - must be strings matching field names
|
|
124
|
+
"field_validator",
|
|
125
|
+
"validator",
|
|
126
|
+
"computed_field",
|
|
127
|
+
# Type system - required Python syntax
|
|
128
|
+
"TypeVar",
|
|
129
|
+
"Generic",
|
|
130
|
+
"cast",
|
|
131
|
+
# Numeric - string representations of numbers
|
|
132
|
+
"Decimal",
|
|
133
|
+
"int",
|
|
134
|
+
"float",
|
|
135
|
+
# Exception constructors - error messages
|
|
136
|
+
"ValueError",
|
|
137
|
+
"TypeError",
|
|
138
|
+
"KeyError",
|
|
139
|
+
"AttributeError",
|
|
140
|
+
"RuntimeError",
|
|
141
|
+
"Exception",
|
|
142
|
+
"raise",
|
|
143
|
+
"APIException",
|
|
144
|
+
"HTTPException",
|
|
145
|
+
"ValidationError",
|
|
146
|
+
# CLI frameworks - short flags, option names, prompts
|
|
147
|
+
"typer.Option",
|
|
148
|
+
"typer.Argument",
|
|
149
|
+
"typer.confirm",
|
|
150
|
+
"typer.prompt",
|
|
151
|
+
"click.option",
|
|
152
|
+
"click.argument",
|
|
153
|
+
"click.confirm",
|
|
154
|
+
"click.prompt",
|
|
155
|
+
".command",
|
|
156
|
+
# HTTP/API clients - external protocol strings
|
|
157
|
+
"requests.get",
|
|
158
|
+
"requests.post",
|
|
159
|
+
"requests.put",
|
|
160
|
+
"requests.delete",
|
|
161
|
+
"requests.patch",
|
|
162
|
+
"httpx.get",
|
|
163
|
+
"httpx.post",
|
|
164
|
+
"axios.get",
|
|
165
|
+
"axios.post",
|
|
166
|
+
"axios.put",
|
|
167
|
+
"axios.delete",
|
|
168
|
+
"axios.patch",
|
|
169
|
+
"client.get",
|
|
170
|
+
"client.post",
|
|
171
|
+
"client.put",
|
|
172
|
+
"client.delete",
|
|
173
|
+
"session.client",
|
|
174
|
+
"session.resource",
|
|
175
|
+
"_request",
|
|
176
|
+
# Browser/DOM APIs - CSS selectors and data URLs
|
|
177
|
+
"document.querySelector",
|
|
178
|
+
"document.querySelectorAll",
|
|
179
|
+
"document.getElementById",
|
|
180
|
+
"document.getElementsByClassName",
|
|
181
|
+
"canvas.toDataURL",
|
|
182
|
+
"canvas.toBlob",
|
|
183
|
+
"createElement",
|
|
184
|
+
"getAttribute",
|
|
185
|
+
"setAttribute",
|
|
186
|
+
"addEventListener",
|
|
187
|
+
"removeEventListener",
|
|
188
|
+
"localStorage.getItem",
|
|
189
|
+
"localStorage.setItem",
|
|
190
|
+
"sessionStorage.getItem",
|
|
191
|
+
"sessionStorage.setItem",
|
|
192
|
+
"window.confirm",
|
|
193
|
+
"window.alert",
|
|
194
|
+
"window.prompt",
|
|
195
|
+
"confirm",
|
|
196
|
+
"alert",
|
|
197
|
+
"prompt",
|
|
198
|
+
# React hooks - internal state identifiers
|
|
199
|
+
"useRef",
|
|
200
|
+
"useState",
|
|
201
|
+
"useCallback",
|
|
202
|
+
"useMemo",
|
|
203
|
+
# AWS SDK - service names and API parameters
|
|
204
|
+
"boto3.client",
|
|
205
|
+
"boto3.resource",
|
|
206
|
+
"generate_presigned_url",
|
|
207
|
+
# AWS CDK - infrastructure as code (broad patterns)
|
|
208
|
+
"s3.",
|
|
209
|
+
"ec2.",
|
|
210
|
+
"logs.",
|
|
211
|
+
"route53.",
|
|
212
|
+
"lambda_.",
|
|
213
|
+
"_lambda.",
|
|
214
|
+
"tasks.",
|
|
215
|
+
"iam.",
|
|
216
|
+
"dynamodb.",
|
|
217
|
+
"sqs.",
|
|
218
|
+
"sns.",
|
|
219
|
+
"apigateway.",
|
|
220
|
+
"cloudfront.",
|
|
221
|
+
"cdk.",
|
|
222
|
+
"sfn.",
|
|
223
|
+
"acm.",
|
|
224
|
+
"cloudwatch.",
|
|
225
|
+
"secretsmanager.",
|
|
226
|
+
"cr.",
|
|
227
|
+
"pipes.",
|
|
228
|
+
"rds.",
|
|
229
|
+
"elasticache.",
|
|
230
|
+
"from_lookup",
|
|
231
|
+
"generate_resource_name",
|
|
232
|
+
"CfnPipe",
|
|
233
|
+
"CfnOutput",
|
|
234
|
+
# FastAPI/Starlette routing
|
|
235
|
+
"router.get",
|
|
236
|
+
"router.post",
|
|
237
|
+
"router.put",
|
|
238
|
+
"router.delete",
|
|
239
|
+
"router.patch",
|
|
240
|
+
"app.get",
|
|
241
|
+
"app.post",
|
|
242
|
+
"app.put",
|
|
243
|
+
"app.delete",
|
|
244
|
+
"@app.",
|
|
245
|
+
"@router.",
|
|
246
|
+
# DynamoDB attribute access
|
|
247
|
+
"Key",
|
|
248
|
+
"Attr",
|
|
249
|
+
"ConditionExpression",
|
|
250
|
+
# Azure CLI - external tool invocation
|
|
251
|
+
"az",
|
|
252
|
+
# Database/ORM - schema definitions
|
|
253
|
+
"op.add_column",
|
|
254
|
+
"op.drop_column",
|
|
255
|
+
"op.create_table",
|
|
256
|
+
"op.alter_column",
|
|
257
|
+
"sa.Column",
|
|
258
|
+
"sa.PrimaryKeyConstraint",
|
|
259
|
+
"sa.ForeignKeyConstraint",
|
|
260
|
+
"Column",
|
|
261
|
+
"relationship",
|
|
262
|
+
"postgresql.ENUM",
|
|
263
|
+
"ENUM",
|
|
264
|
+
# Python built-ins
|
|
265
|
+
"getattr",
|
|
266
|
+
"setattr",
|
|
267
|
+
"hasattr",
|
|
268
|
+
"delattr",
|
|
269
|
+
"isinstance",
|
|
270
|
+
"issubclass",
|
|
271
|
+
# Pydantic/dataclass fields
|
|
272
|
+
"Field",
|
|
273
|
+
"PrivateAttr",
|
|
274
|
+
# UI frameworks - display text
|
|
275
|
+
"QLabel",
|
|
276
|
+
"QPushButton",
|
|
277
|
+
"QMessageBox",
|
|
278
|
+
"QCheckBox",
|
|
279
|
+
"setWindowTitle",
|
|
280
|
+
"setText",
|
|
281
|
+
"setToolTip",
|
|
282
|
+
"setPlaceholderText",
|
|
283
|
+
"setStatusTip",
|
|
284
|
+
"Static",
|
|
285
|
+
"Label",
|
|
286
|
+
"Button",
|
|
287
|
+
# Table/grid display - formatting
|
|
288
|
+
"table.add_row",
|
|
289
|
+
"add_row",
|
|
290
|
+
"add_column",
|
|
291
|
+
"Table",
|
|
292
|
+
"Panel",
|
|
293
|
+
"Console",
|
|
294
|
+
# Testing - mocks and fixtures
|
|
295
|
+
"monkeypatch.setattr",
|
|
296
|
+
"patch",
|
|
297
|
+
"Mock",
|
|
298
|
+
"MagicMock",
|
|
299
|
+
"PropertyMock",
|
|
300
|
+
# String parsing methods - internal message processing
|
|
301
|
+
".index",
|
|
302
|
+
".find",
|
|
303
|
+
".rfind",
|
|
304
|
+
".rindex",
|
|
305
|
+
# Storybook - action handlers
|
|
306
|
+
"action",
|
|
307
|
+
"fn",
|
|
308
|
+
# React state setters - UI state names
|
|
309
|
+
"setMessage",
|
|
310
|
+
"setError",
|
|
311
|
+
"setLoading",
|
|
312
|
+
"setStatus",
|
|
313
|
+
"setText",
|
|
314
|
+
# API clients - external endpoints
|
|
315
|
+
"API.",
|
|
316
|
+
"api.",
|
|
317
|
+
# CSS/styling
|
|
318
|
+
"setStyleSheet",
|
|
319
|
+
"add_class",
|
|
320
|
+
"remove_class",
|
|
321
|
+
# JSON/serialization - output identifiers
|
|
322
|
+
"_output",
|
|
323
|
+
"json.dumps",
|
|
324
|
+
"json.loads",
|
|
325
|
+
# Health checks - framework pattern
|
|
326
|
+
"register_health_check",
|
|
327
|
+
# Config management - dynamic config keys
|
|
328
|
+
"ensure_config_section",
|
|
329
|
+
"set_config_value",
|
|
330
|
+
"get_config_value",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Function names where second parameter (index 1) should be excluded
|
|
334
|
+
# These are typically default values, not keys
|
|
335
|
+
_EXCLUDE_PARAM_INDEX_1: tuple[str, ...] = (
|
|
336
|
+
".get",
|
|
337
|
+
"os.environ.get",
|
|
338
|
+
"environ.get",
|
|
339
|
+
"getattr",
|
|
340
|
+
"os.getenv",
|
|
341
|
+
"getenv",
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# String value patterns that indicate false positives
|
|
345
|
+
_EXCLUDED_VALUE_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
346
|
+
# strftime format strings
|
|
347
|
+
re.compile(r"^%[A-Za-z%-]+$"),
|
|
348
|
+
# Single character delimiters
|
|
349
|
+
re.compile(r"^[\n\t\r,;:|/\\.\-_]$"),
|
|
350
|
+
# Empty string or whitespace only
|
|
351
|
+
re.compile(r"^\s*$"),
|
|
352
|
+
# HTTP methods (external protocol)
|
|
353
|
+
re.compile(r"^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$"),
|
|
354
|
+
# Numeric strings (should use Decimal or int)
|
|
355
|
+
re.compile(r"^-?\d+\.?\d*$"),
|
|
356
|
+
# Short CLI flags
|
|
357
|
+
re.compile(r"^-[a-zA-Z]$"),
|
|
358
|
+
# CSS/Rich markup
|
|
359
|
+
re.compile(r"^\[/?[a-z]+\]"),
|
|
360
|
+
# File modes (only multi-char modes to avoid false positives on single letters)
|
|
361
|
+
re.compile(r"^[rwa][bt]\+?$|^[rwa]\+$"),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def should_include(
|
|
366
|
+
function_name: str,
|
|
367
|
+
param_index: int,
|
|
368
|
+
unique_values: set[str],
|
|
369
|
+
) -> bool:
|
|
370
|
+
"""Determine if a function call pattern should be included in violations.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
function_name: Name of the function being called
|
|
374
|
+
param_index: Index of the parameter (0-based)
|
|
375
|
+
unique_values: Set of unique string values passed to this parameter
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
True if this pattern should generate a violation, False to filter it out
|
|
379
|
+
"""
|
|
380
|
+
# Check function name patterns
|
|
381
|
+
if _is_excluded_function(function_name):
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
# Check parameter position for specific functions
|
|
385
|
+
if _is_excluded_param_position(function_name, param_index):
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
# Check if all values match excluded patterns
|
|
389
|
+
if _all_values_excluded(unique_values):
|
|
390
|
+
return False
|
|
391
|
+
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def are_all_values_excluded(unique_values: set[str]) -> bool:
|
|
396
|
+
"""Check if all values match excluded patterns (numeric strings, delimiters, etc.).
|
|
397
|
+
|
|
398
|
+
Public interface for value-based filtering used by violation generator.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
unique_values: Set of unique string values to check
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
True if all values match excluded patterns, False otherwise
|
|
405
|
+
"""
|
|
406
|
+
return _all_values_excluded(unique_values)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _is_excluded_function(function_name: str) -> bool:
|
|
410
|
+
"""Check if function name matches any excluded pattern."""
|
|
411
|
+
# Check suffix patterns (e.g., *Error, *Exception)
|
|
412
|
+
if _matches_suffix(function_name):
|
|
413
|
+
return True
|
|
414
|
+
return _matches_pattern(function_name.lower())
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _matches_suffix(function_name: str) -> bool:
|
|
418
|
+
"""Check if function name ends with an excluded suffix."""
|
|
419
|
+
return any(function_name.endswith(s) for s in _EXCLUDED_FUNCTION_SUFFIXES)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _matches_pattern(func_lower: str) -> bool:
|
|
423
|
+
"""Check if function name matches any excluded pattern."""
|
|
424
|
+
return any(
|
|
425
|
+
pattern.lower() in func_lower or func_lower.endswith(pattern.lower())
|
|
426
|
+
for pattern in _EXCLUDED_FUNCTION_PATTERNS
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _is_excluded_param_position(function_name: str, param_index: int) -> bool:
|
|
431
|
+
"""Check if this parameter position should be excluded for this function."""
|
|
432
|
+
if param_index != 1:
|
|
433
|
+
return False
|
|
434
|
+
|
|
435
|
+
func_lower = function_name.lower()
|
|
436
|
+
return any(
|
|
437
|
+
pattern.lower() in func_lower or func_lower.endswith(pattern.lower())
|
|
438
|
+
for pattern in _EXCLUDE_PARAM_INDEX_1
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _all_values_excluded(unique_values: set[str]) -> bool:
|
|
443
|
+
"""Check if all values in the set match excluded patterns."""
|
|
444
|
+
if not unique_values:
|
|
445
|
+
return True
|
|
446
|
+
return all(_is_excluded_value(value) for value in unique_values)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _is_excluded_value(value: str) -> bool:
|
|
450
|
+
"""Check if a single value matches any excluded pattern."""
|
|
451
|
+
return any(pattern.match(value) for pattern in _EXCLUDED_VALUE_PATTERNS)
|
|
@@ -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()
|