thailint 0.10.0__py3-none-any.whl → 0.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. src/__init__.py +1 -0
  2. src/cli/__init__.py +27 -0
  3. src/cli/__main__.py +22 -0
  4. src/cli/config.py +478 -0
  5. src/cli/linters/__init__.py +58 -0
  6. src/cli/linters/code_patterns.py +372 -0
  7. src/cli/linters/code_smells.py +450 -0
  8. src/cli/linters/documentation.py +155 -0
  9. src/cli/linters/shared.py +89 -0
  10. src/cli/linters/structure.py +313 -0
  11. src/cli/linters/structure_quality.py +316 -0
  12. src/cli/main.py +120 -0
  13. src/cli/utils.py +395 -0
  14. src/cli_main.py +34 -0
  15. src/core/types.py +13 -0
  16. src/core/violation_utils.py +69 -0
  17. src/linter_config/ignore.py +32 -16
  18. src/linters/collection_pipeline/linter.py +2 -2
  19. src/linters/dry/block_filter.py +97 -1
  20. src/linters/dry/cache.py +94 -6
  21. src/linters/dry/config.py +47 -10
  22. src/linters/dry/constant.py +92 -0
  23. src/linters/dry/constant_matcher.py +214 -0
  24. src/linters/dry/constant_violation_builder.py +98 -0
  25. src/linters/dry/linter.py +89 -48
  26. src/linters/dry/python_analyzer.py +12 -415
  27. src/linters/dry/python_constant_extractor.py +101 -0
  28. src/linters/dry/single_statement_detector.py +415 -0
  29. src/linters/dry/token_hasher.py +5 -5
  30. src/linters/dry/typescript_analyzer.py +5 -354
  31. src/linters/dry/typescript_constant_extractor.py +134 -0
  32. src/linters/dry/typescript_statement_detector.py +255 -0
  33. src/linters/dry/typescript_value_extractor.py +66 -0
  34. src/linters/file_header/linter.py +2 -2
  35. src/linters/file_placement/linter.py +2 -2
  36. src/linters/file_placement/pattern_matcher.py +19 -5
  37. src/linters/magic_numbers/linter.py +8 -67
  38. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  39. src/linters/nesting/linter.py +12 -9
  40. src/linters/print_statements/linter.py +7 -24
  41. src/linters/srp/class_analyzer.py +9 -9
  42. src/linters/srp/heuristics.py +2 -2
  43. src/linters/srp/linter.py +2 -2
  44. src/linters/stateless_class/linter.py +2 -2
  45. src/linters/stringly_typed/__init__.py +36 -0
  46. src/linters/stringly_typed/config.py +190 -0
  47. src/linters/stringly_typed/context_filter.py +451 -0
  48. src/linters/stringly_typed/function_call_violation_builder.py +137 -0
  49. src/linters/stringly_typed/ignore_checker.py +102 -0
  50. src/linters/stringly_typed/ignore_utils.py +51 -0
  51. src/linters/stringly_typed/linter.py +344 -0
  52. src/linters/stringly_typed/python/__init__.py +33 -0
  53. src/linters/stringly_typed/python/analyzer.py +344 -0
  54. src/linters/stringly_typed/python/call_tracker.py +172 -0
  55. src/linters/stringly_typed/python/comparison_tracker.py +252 -0
  56. src/linters/stringly_typed/python/condition_extractor.py +131 -0
  57. src/linters/stringly_typed/python/conditional_detector.py +176 -0
  58. src/linters/stringly_typed/python/constants.py +21 -0
  59. src/linters/stringly_typed/python/match_analyzer.py +88 -0
  60. src/linters/stringly_typed/python/validation_detector.py +186 -0
  61. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  62. src/linters/stringly_typed/storage.py +630 -0
  63. src/linters/stringly_typed/storage_initializer.py +45 -0
  64. src/linters/stringly_typed/typescript/__init__.py +28 -0
  65. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  66. src/linters/stringly_typed/typescript/call_tracker.py +329 -0
  67. src/linters/stringly_typed/typescript/comparison_tracker.py +372 -0
  68. src/linters/stringly_typed/violation_generator.py +376 -0
  69. src/orchestrator/core.py +241 -12
  70. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/METADATA +9 -3
  71. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/RECORD +74 -28
  72. thailint-0.12.0.dist-info/entry_points.txt +4 -0
  73. src/cli.py +0 -2141
  74. thailint-0.10.0.dist-info/entry_points.txt +0 -4
  75. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/WHEEL +0 -0
  76. {thailint-0.10.0.dist-info → thailint-0.12.0.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: FunctionCallFilter class
15
+
16
+ Interfaces: FunctionCallFilter.should_include(function_name, param_index, string_values) -> bool
17
+
18
+ Implementation: Blocklist-based filtering with function name patterns, parameter position rules,
19
+ and string value pattern detection
20
+ """
21
+
22
+ import re
23
+
24
+
25
+ class FunctionCallFilter: # thailint: ignore srp - data-heavy class with extensive exclusion pattern lists
26
+ """Filters function call violations to reduce false positives.
27
+
28
+ Uses a blocklist approach to exclude known false positive patterns:
29
+ - Dictionary/object access methods (.get, .set, .pop)
30
+ - String processing methods (split, replace, strip, strftime)
31
+ - Logging and output functions (logger.*, print, echo)
32
+ - Framework-specific patterns (Pydantic validators, TypeVar)
33
+ - External API functions (boto3, HTTP clients)
34
+ - File and path operations
35
+ """
36
+
37
+ # Function name suffixes that always indicate false positives
38
+ _EXCLUDED_FUNCTION_SUFFIXES: tuple[str, ...] = (
39
+ # Exception constructors - error messages are inherently unique strings
40
+ "Error",
41
+ "Exception",
42
+ "Warning",
43
+ )
44
+
45
+ # Function name patterns to always exclude (case-insensitive suffix/contains match)
46
+ _EXCLUDED_FUNCTION_PATTERNS: tuple[str, ...] = (
47
+ # Dictionary/object access - these are metadata access, not domain values
48
+ ".get",
49
+ ".set",
50
+ ".pop",
51
+ ".setdefault",
52
+ ".update",
53
+ # List/collection operations
54
+ ".append",
55
+ ".extend",
56
+ ".insert",
57
+ ".add",
58
+ ".remove",
59
+ ".push",
60
+ ".set",
61
+ ".has",
62
+ "hasItem",
63
+ "push",
64
+ # String processing - delimiters and format strings
65
+ # Note: both .method and method forms to catch both method calls and standalone functions
66
+ ".split",
67
+ "split",
68
+ ".rsplit",
69
+ ".replace",
70
+ "replace",
71
+ ".strip",
72
+ ".rstrip",
73
+ ".lstrip",
74
+ ".startswith",
75
+ "startswith",
76
+ ".startsWith",
77
+ "startsWith",
78
+ ".endswith",
79
+ "endswith",
80
+ ".endsWith",
81
+ "endsWith",
82
+ ".includes",
83
+ "includes",
84
+ ".indexOf",
85
+ "indexOf",
86
+ ".lastIndexOf",
87
+ ".match",
88
+ ".search",
89
+ ".format",
90
+ ".join",
91
+ "join",
92
+ ".encode",
93
+ ".decode",
94
+ ".lower",
95
+ ".upper",
96
+ ".trim",
97
+ ".trimStart",
98
+ ".trimEnd",
99
+ ".padStart",
100
+ ".padEnd",
101
+ "strftime",
102
+ "strptime",
103
+ # Logging and output - human-readable messages
104
+ "logger.debug",
105
+ "logger.info",
106
+ "logger.warning",
107
+ "logger.error",
108
+ "logger.critical",
109
+ "logger.exception",
110
+ "logging.debug",
111
+ "logging.info",
112
+ "logging.warning",
113
+ "logging.error",
114
+ "print",
115
+ "echo",
116
+ "console.print",
117
+ "console.log",
118
+ "typer.echo",
119
+ "click.echo",
120
+ # Regex - pattern strings
121
+ "re.sub",
122
+ "re.match",
123
+ "re.search",
124
+ "re.compile",
125
+ "re.findall",
126
+ "re.split",
127
+ # Environment variables
128
+ "os.environ.get",
129
+ "os.getenv",
130
+ "environ.get",
131
+ "getenv",
132
+ # File operations
133
+ "open",
134
+ "Path",
135
+ # Framework validators - must be strings matching field names
136
+ "field_validator",
137
+ "validator",
138
+ "computed_field",
139
+ # Type system - required Python syntax
140
+ "TypeVar",
141
+ "Generic",
142
+ "cast",
143
+ # Numeric - string representations of numbers
144
+ "Decimal",
145
+ "int",
146
+ "float",
147
+ # Exception constructors - error messages
148
+ "ValueError",
149
+ "TypeError",
150
+ "KeyError",
151
+ "AttributeError",
152
+ "RuntimeError",
153
+ "Exception",
154
+ "raise",
155
+ "APIException",
156
+ "HTTPException",
157
+ "ValidationError",
158
+ # CLI frameworks - short flags, option names, prompts
159
+ "typer.Option",
160
+ "typer.Argument",
161
+ "typer.confirm",
162
+ "typer.prompt",
163
+ "click.option",
164
+ "click.argument",
165
+ "click.confirm",
166
+ "click.prompt",
167
+ ".command",
168
+ # HTTP/API clients - external protocol strings
169
+ "requests.get",
170
+ "requests.post",
171
+ "requests.put",
172
+ "requests.delete",
173
+ "requests.patch",
174
+ "httpx.get",
175
+ "httpx.post",
176
+ "axios.get",
177
+ "axios.post",
178
+ "axios.put",
179
+ "axios.delete",
180
+ "axios.patch",
181
+ "client.get",
182
+ "client.post",
183
+ "client.put",
184
+ "client.delete",
185
+ "session.client",
186
+ "session.resource",
187
+ "_request",
188
+ # Browser/DOM APIs - CSS selectors and data URLs
189
+ "document.querySelector",
190
+ "document.querySelectorAll",
191
+ "document.getElementById",
192
+ "document.getElementsByClassName",
193
+ "canvas.toDataURL",
194
+ "canvas.toBlob",
195
+ "createElement",
196
+ "getAttribute",
197
+ "setAttribute",
198
+ "addEventListener",
199
+ "removeEventListener",
200
+ "localStorage.getItem",
201
+ "localStorage.setItem",
202
+ "sessionStorage.getItem",
203
+ "sessionStorage.setItem",
204
+ "window.confirm",
205
+ "window.alert",
206
+ "window.prompt",
207
+ "confirm",
208
+ "alert",
209
+ "prompt",
210
+ # React hooks - internal state identifiers
211
+ "useRef",
212
+ "useState",
213
+ "useCallback",
214
+ "useMemo",
215
+ # AWS SDK - service names and API parameters
216
+ "boto3.client",
217
+ "boto3.resource",
218
+ "generate_presigned_url",
219
+ # AWS CDK - infrastructure as code (broad patterns)
220
+ "s3.",
221
+ "ec2.",
222
+ "logs.",
223
+ "route53.",
224
+ "lambda_.",
225
+ "_lambda.",
226
+ "tasks.",
227
+ "iam.",
228
+ "dynamodb.",
229
+ "sqs.",
230
+ "sns.",
231
+ "apigateway.",
232
+ "cloudfront.",
233
+ "cdk.",
234
+ "sfn.",
235
+ "acm.",
236
+ "cloudwatch.",
237
+ "secretsmanager.",
238
+ "cr.",
239
+ "pipes.",
240
+ "rds.",
241
+ "elasticache.",
242
+ "from_lookup",
243
+ "generate_resource_name",
244
+ "CfnPipe",
245
+ "CfnOutput",
246
+ # FastAPI/Starlette routing
247
+ "router.get",
248
+ "router.post",
249
+ "router.put",
250
+ "router.delete",
251
+ "router.patch",
252
+ "app.get",
253
+ "app.post",
254
+ "app.put",
255
+ "app.delete",
256
+ "@app.",
257
+ "@router.",
258
+ # DynamoDB attribute access
259
+ "Key",
260
+ "Attr",
261
+ "ConditionExpression",
262
+ # Azure CLI - external tool invocation
263
+ "az",
264
+ # Database/ORM - schema definitions
265
+ "op.add_column",
266
+ "op.drop_column",
267
+ "op.create_table",
268
+ "op.alter_column",
269
+ "sa.Column",
270
+ "sa.PrimaryKeyConstraint",
271
+ "sa.ForeignKeyConstraint",
272
+ "Column",
273
+ "relationship",
274
+ "postgresql.ENUM",
275
+ "ENUM",
276
+ # Python built-ins
277
+ "getattr",
278
+ "setattr",
279
+ "hasattr",
280
+ "delattr",
281
+ "isinstance",
282
+ "issubclass",
283
+ # Pydantic/dataclass fields
284
+ "Field",
285
+ "PrivateAttr",
286
+ # UI frameworks - display text
287
+ "QLabel",
288
+ "QPushButton",
289
+ "QMessageBox",
290
+ "QCheckBox",
291
+ "setWindowTitle",
292
+ "setText",
293
+ "setToolTip",
294
+ "setPlaceholderText",
295
+ "setStatusTip",
296
+ "Static",
297
+ "Label",
298
+ "Button",
299
+ # Table/grid display - formatting
300
+ "table.add_row",
301
+ "add_row",
302
+ "add_column",
303
+ "Table",
304
+ "Panel",
305
+ "Console",
306
+ # Testing - mocks and fixtures
307
+ "monkeypatch.setattr",
308
+ "patch",
309
+ "Mock",
310
+ "MagicMock",
311
+ "PropertyMock",
312
+ # Storybook - action handlers
313
+ "action",
314
+ "fn",
315
+ # React state setters - UI state names
316
+ "setMessage",
317
+ "setError",
318
+ "setLoading",
319
+ "setStatus",
320
+ "setText",
321
+ # API clients - external endpoints
322
+ "API.",
323
+ "api.",
324
+ # CSS/styling
325
+ "setStyleSheet",
326
+ "add_class",
327
+ "remove_class",
328
+ # JSON/serialization - output identifiers
329
+ "_output",
330
+ "json.dumps",
331
+ "json.loads",
332
+ # Health checks - framework pattern
333
+ "register_health_check",
334
+ )
335
+
336
+ # Function names where second parameter (index 1) should be excluded
337
+ # These are typically default values, not keys
338
+ _EXCLUDE_PARAM_INDEX_1: tuple[str, ...] = (
339
+ ".get",
340
+ "os.environ.get",
341
+ "environ.get",
342
+ "getattr",
343
+ "os.getenv",
344
+ "getenv",
345
+ )
346
+
347
+ # String value patterns that indicate false positives
348
+ _EXCLUDED_VALUE_PATTERNS: tuple[re.Pattern[str], ...] = (
349
+ # strftime format strings
350
+ re.compile(r"^%[A-Za-z%-]+$"),
351
+ # Single character delimiters
352
+ re.compile(r"^[\n\t\r,;:|/\\.\-_]$"),
353
+ # Empty string or whitespace only
354
+ re.compile(r"^\s*$"),
355
+ # HTTP methods (external protocol)
356
+ re.compile(r"^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$"),
357
+ # Numeric strings (should use Decimal or int)
358
+ re.compile(r"^-?\d+\.?\d*$"),
359
+ # Short CLI flags
360
+ re.compile(r"^-[a-zA-Z]$"),
361
+ # CSS/Rich markup
362
+ re.compile(r"^\[/?[a-z]+\]"),
363
+ # File modes (only multi-char modes to avoid false positives on single letters)
364
+ re.compile(r"^[rwa][bt]\+?$|^[rwa]\+$"),
365
+ )
366
+
367
+ def should_include(
368
+ self,
369
+ function_name: str,
370
+ param_index: int,
371
+ unique_values: set[str],
372
+ ) -> bool:
373
+ """Determine if a function call pattern should be included in violations.
374
+
375
+ Args:
376
+ function_name: Name of the function being called
377
+ param_index: Index of the parameter (0-based)
378
+ unique_values: Set of unique string values passed to this parameter
379
+
380
+ Returns:
381
+ True if this pattern should generate a violation, False to filter it out
382
+ """
383
+ # Check function name patterns
384
+ if self._is_excluded_function(function_name):
385
+ return False
386
+
387
+ # Check parameter position for specific functions
388
+ if self._is_excluded_param_position(function_name, param_index):
389
+ return False
390
+
391
+ # Check if all values match excluded patterns
392
+ if self._all_values_excluded(unique_values):
393
+ return False
394
+
395
+ return True
396
+
397
+ def are_all_values_excluded(self, unique_values: set[str]) -> bool:
398
+ """Check if all values match excluded patterns (numeric strings, delimiters, etc.).
399
+
400
+ Public interface for value-based filtering used by violation generator.
401
+
402
+ Args:
403
+ unique_values: Set of unique string values to check
404
+
405
+ Returns:
406
+ True if all values match excluded patterns, False otherwise
407
+ """
408
+ return self._all_values_excluded(unique_values)
409
+
410
+ def _is_excluded_function(self, function_name: str) -> bool:
411
+ """Check if function name matches any excluded pattern."""
412
+ # Check suffix patterns (e.g., *Error, *Exception)
413
+ if self._matches_suffix(function_name):
414
+ return True
415
+ return self._matches_pattern(function_name.lower())
416
+
417
+ def _matches_suffix(self, function_name: str) -> bool:
418
+ """Check if function name ends with an excluded suffix."""
419
+ return any(function_name.endswith(s) for s in self._EXCLUDED_FUNCTION_SUFFIXES)
420
+
421
+ def _matches_pattern(self, func_lower: str) -> bool:
422
+ """Check if function name matches any excluded pattern."""
423
+ for pattern in self._EXCLUDED_FUNCTION_PATTERNS:
424
+ pattern_lower = pattern.lower()
425
+ if pattern_lower in func_lower or func_lower.endswith(pattern_lower):
426
+ return True
427
+ return False
428
+
429
+ def _is_excluded_param_position(self, function_name: str, param_index: int) -> bool:
430
+ """Check if this parameter position should be excluded for this function."""
431
+ if param_index != 1:
432
+ return False
433
+
434
+ func_lower = function_name.lower()
435
+ for pattern in self._EXCLUDE_PARAM_INDEX_1:
436
+ if pattern.lower() in func_lower or func_lower.endswith(pattern.lower()):
437
+ return True
438
+ return False
439
+
440
+ def _all_values_excluded(self, unique_values: set[str]) -> bool:
441
+ """Check if all values in the set match excluded patterns."""
442
+ if not unique_values:
443
+ return True
444
+ return all(self._is_excluded_value(value) for value in unique_values)
445
+
446
+ def _is_excluded_value(self, value: str) -> bool:
447
+ """Check if a single value matches any excluded pattern."""
448
+ for pattern in self._EXCLUDED_VALUE_PATTERNS:
449
+ if pattern.match(value):
450
+ return True
451
+ return False
@@ -0,0 +1,137 @@
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: FunctionCallViolationBuilder class
14
+
15
+ Interfaces: FunctionCallViolationBuilder.build_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_cross_references(call: StoredFunctionCall, all_calls: list[StoredFunctionCall]) -> str:
28
+ """Build cross-reference string for other function call locations.
29
+
30
+ Args:
31
+ call: Current call
32
+ all_calls: All calls with same function/param
33
+
34
+ Returns:
35
+ Comma-separated list of file:line references
36
+ """
37
+ refs = []
38
+ for other in all_calls:
39
+ if other.file_path != call.file_path or other.line_number != call.line_number:
40
+ refs.append(f"{Path(other.file_path).name}:{other.line_number}")
41
+
42
+ return ", ".join(refs[:5]) # Limit to 5 references
43
+
44
+
45
+ class FunctionCallViolationBuilder:
46
+ """Builds violations for function call patterns with limited string values."""
47
+
48
+ def build_violations(
49
+ self, calls: list[StoredFunctionCall], unique_values: set[str]
50
+ ) -> list[Violation]:
51
+ """Build violations for all calls to a function with limited values.
52
+
53
+ Args:
54
+ calls: All calls to the function/param
55
+ unique_values: Set of unique string values passed
56
+
57
+ Returns:
58
+ List of violations for each call site
59
+ """
60
+ return [self._build_violation(call, calls, unique_values) for call in calls]
61
+
62
+ def _build_violation(
63
+ self,
64
+ call: StoredFunctionCall,
65
+ all_calls: list[StoredFunctionCall],
66
+ unique_values: set[str],
67
+ ) -> Violation:
68
+ """Build a single violation for a function call.
69
+
70
+ Args:
71
+ call: The specific call to create violation for
72
+ all_calls: All calls to the same function/param
73
+ unique_values: Set of unique string values passed
74
+
75
+ Returns:
76
+ Violation instance
77
+ """
78
+ message = self._build_message(call, all_calls, unique_values)
79
+ suggestion = self._build_suggestion(call, unique_values)
80
+
81
+ return Violation(
82
+ rule_id="stringly-typed.limited-values",
83
+ file_path=str(call.file_path),
84
+ line=call.line_number,
85
+ column=call.column,
86
+ message=message,
87
+ severity=Severity.ERROR,
88
+ suggestion=suggestion,
89
+ )
90
+
91
+ def _build_message(
92
+ self,
93
+ call: StoredFunctionCall,
94
+ all_calls: list[StoredFunctionCall],
95
+ unique_values: set[str],
96
+ ) -> str:
97
+ """Build violation message for function call pattern.
98
+
99
+ Args:
100
+ call: Current function call
101
+ all_calls: All calls to the same function/param
102
+ unique_values: Set of unique values passed
103
+
104
+ Returns:
105
+ Human-readable violation message
106
+ """
107
+ file_count = len({c.file_path for c in all_calls})
108
+ values_str = ", ".join(f"'{v}'" for v in sorted(unique_values))
109
+ param_desc = f"parameter {call.param_index}" if call.param_index > 0 else "first parameter"
110
+
111
+ message = (
112
+ f"Function '{call.function_name}' {param_desc} is called with "
113
+ f"only {len(unique_values)} unique string values [{values_str}] "
114
+ f"across {file_count} file(s)."
115
+ )
116
+
117
+ other_refs = _build_cross_references(call, all_calls)
118
+ if other_refs:
119
+ message += f" Also called in: {other_refs}."
120
+
121
+ return message
122
+
123
+ def _build_suggestion(self, call: StoredFunctionCall, unique_values: set[str]) -> str:
124
+ """Build fix suggestion for function call pattern.
125
+
126
+ Args:
127
+ call: The function call
128
+ unique_values: Set of unique values passed
129
+
130
+ Returns:
131
+ Human-readable suggestion
132
+ """
133
+ return (
134
+ f"Consider defining an enum or type union with the "
135
+ f"{len(unique_values)} possible values for '{call.function_name}' "
136
+ f"parameter {call.param_index}."
137
+ )
@@ -0,0 +1,102 @@
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 pathlib import Path
23
+
24
+ from src.core.types import Violation
25
+ from src.linter_config.ignore import get_ignore_parser
26
+
27
+
28
+ class IgnoreChecker:
29
+ """Checks for ignore directives in stringly-typed linter violations.
30
+
31
+ Wraps the centralized IgnoreDirectiveParser to filter stringly-typed
32
+ violations based on inline ignore comments.
33
+ """
34
+
35
+ def __init__(self, project_root: Path | None = None) -> None:
36
+ """Initialize with project root for ignore parser.
37
+
38
+ Args:
39
+ project_root: Optional project root directory. Defaults to cwd.
40
+ """
41
+ self._ignore_parser = get_ignore_parser(project_root)
42
+ self._file_content_cache: dict[str, str] = {}
43
+
44
+ def filter_violations(self, violations: list[Violation]) -> list[Violation]:
45
+ """Filter violations based on ignore directives.
46
+
47
+ Args:
48
+ violations: List of violations to filter
49
+
50
+ Returns:
51
+ List of violations not suppressed by ignore directives
52
+ """
53
+ return [v for v in violations if not self._should_ignore(v)]
54
+
55
+ def _should_ignore(self, violation: Violation) -> bool:
56
+ """Check if a violation should be ignored.
57
+
58
+ Args:
59
+ violation: Violation to check
60
+
61
+ Returns:
62
+ True if violation should be ignored
63
+ """
64
+ file_content = self._get_file_content(violation.file_path)
65
+ return self._ignore_parser.should_ignore_violation(violation, file_content)
66
+
67
+ def _get_file_content(self, file_path: str) -> str:
68
+ """Get file content with caching.
69
+
70
+ Args:
71
+ file_path: Path to file
72
+
73
+ Returns:
74
+ File content or empty string if unreadable
75
+ """
76
+ if file_path in self._file_content_cache:
77
+ return self._file_content_cache[file_path]
78
+
79
+ content = self._read_file_content(file_path)
80
+ self._file_content_cache[file_path] = content
81
+ return content
82
+
83
+ def _read_file_content(self, file_path: str) -> str:
84
+ """Read file content from disk.
85
+
86
+ Args:
87
+ file_path: Path to file
88
+
89
+ Returns:
90
+ File content or empty string if unreadable
91
+ """
92
+ try:
93
+ path = Path(file_path)
94
+ if path.exists():
95
+ return path.read_text(encoding="utf-8")
96
+ except (OSError, UnicodeDecodeError):
97
+ pass
98
+ return ""
99
+
100
+ def clear_cache(self) -> None:
101
+ """Clear file content cache."""
102
+ self._file_content_cache.clear()