thailint 0.11.0__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. src/analyzers/__init__.py +4 -3
  2. src/analyzers/ast_utils.py +54 -0
  3. src/analyzers/typescript_base.py +4 -0
  4. src/cli/__init__.py +3 -0
  5. src/cli/config.py +12 -12
  6. src/cli/config_merge.py +241 -0
  7. src/cli/linters/__init__.py +3 -0
  8. src/cli/linters/code_patterns.py +113 -5
  9. src/cli/linters/code_smells.py +118 -7
  10. src/cli/linters/documentation.py +3 -0
  11. src/cli/linters/structure.py +3 -0
  12. src/cli/linters/structure_quality.py +3 -0
  13. src/cli/utils.py +29 -9
  14. src/cli_main.py +3 -0
  15. src/config.py +2 -1
  16. src/core/base.py +3 -2
  17. src/core/cli_utils.py +3 -1
  18. src/core/config_parser.py +5 -2
  19. src/core/constants.py +54 -0
  20. src/core/linter_utils.py +4 -0
  21. src/core/rule_discovery.py +5 -1
  22. src/core/violation_builder.py +3 -0
  23. src/linter_config/directive_markers.py +109 -0
  24. src/linter_config/ignore.py +225 -383
  25. src/linter_config/pattern_utils.py +65 -0
  26. src/linter_config/rule_matcher.py +89 -0
  27. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  28. src/linters/collection_pipeline/ast_utils.py +40 -0
  29. src/linters/collection_pipeline/config.py +12 -0
  30. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  31. src/linters/collection_pipeline/detector.py +262 -32
  32. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  33. src/linters/collection_pipeline/linter.py +18 -35
  34. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  35. src/linters/dry/base_token_analyzer.py +16 -9
  36. src/linters/dry/block_filter.py +7 -4
  37. src/linters/dry/cache.py +7 -2
  38. src/linters/dry/config.py +7 -1
  39. src/linters/dry/constant_matcher.py +34 -25
  40. src/linters/dry/file_analyzer.py +4 -2
  41. src/linters/dry/inline_ignore.py +7 -16
  42. src/linters/dry/linter.py +48 -25
  43. src/linters/dry/python_analyzer.py +18 -10
  44. src/linters/dry/python_constant_extractor.py +51 -52
  45. src/linters/dry/single_statement_detector.py +14 -12
  46. src/linters/dry/token_hasher.py +115 -115
  47. src/linters/dry/typescript_analyzer.py +11 -6
  48. src/linters/dry/typescript_constant_extractor.py +4 -0
  49. src/linters/dry/typescript_statement_detector.py +208 -208
  50. src/linters/dry/typescript_value_extractor.py +3 -0
  51. src/linters/dry/violation_filter.py +1 -4
  52. src/linters/dry/violation_generator.py +1 -4
  53. src/linters/file_header/atemporal_detector.py +4 -0
  54. src/linters/file_header/base_parser.py +4 -0
  55. src/linters/file_header/bash_parser.py +4 -0
  56. src/linters/file_header/field_validator.py +5 -8
  57. src/linters/file_header/linter.py +19 -12
  58. src/linters/file_header/markdown_parser.py +6 -0
  59. src/linters/file_placement/config_loader.py +3 -1
  60. src/linters/file_placement/linter.py +22 -8
  61. src/linters/file_placement/pattern_matcher.py +21 -4
  62. src/linters/file_placement/pattern_validator.py +21 -7
  63. src/linters/file_placement/rule_checker.py +2 -2
  64. src/linters/lazy_ignores/__init__.py +43 -0
  65. src/linters/lazy_ignores/config.py +66 -0
  66. src/linters/lazy_ignores/directive_utils.py +121 -0
  67. src/linters/lazy_ignores/header_parser.py +177 -0
  68. src/linters/lazy_ignores/linter.py +158 -0
  69. src/linters/lazy_ignores/matcher.py +135 -0
  70. src/linters/lazy_ignores/python_analyzer.py +201 -0
  71. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  72. src/linters/lazy_ignores/skip_detector.py +298 -0
  73. src/linters/lazy_ignores/types.py +67 -0
  74. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  75. src/linters/lazy_ignores/violation_builder.py +131 -0
  76. src/linters/lbyl/__init__.py +29 -0
  77. src/linters/lbyl/config.py +63 -0
  78. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  79. src/linters/lbyl/pattern_detectors/base.py +46 -0
  80. src/linters/magic_numbers/context_analyzer.py +227 -229
  81. src/linters/magic_numbers/linter.py +20 -15
  82. src/linters/magic_numbers/python_analyzer.py +4 -16
  83. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  84. src/linters/method_property/config.py +4 -0
  85. src/linters/method_property/linter.py +5 -4
  86. src/linters/method_property/python_analyzer.py +5 -4
  87. src/linters/method_property/violation_builder.py +3 -0
  88. src/linters/nesting/typescript_analyzer.py +6 -12
  89. src/linters/nesting/typescript_function_extractor.py +0 -4
  90. src/linters/print_statements/linter.py +6 -4
  91. src/linters/print_statements/python_analyzer.py +85 -81
  92. src/linters/print_statements/typescript_analyzer.py +6 -15
  93. src/linters/srp/heuristics.py +4 -4
  94. src/linters/srp/linter.py +12 -12
  95. src/linters/srp/violation_builder.py +0 -4
  96. src/linters/stateless_class/linter.py +30 -36
  97. src/linters/stateless_class/python_analyzer.py +11 -20
  98. src/linters/stringly_typed/__init__.py +22 -9
  99. src/linters/stringly_typed/config.py +32 -8
  100. src/linters/stringly_typed/context_filter.py +451 -0
  101. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  102. src/linters/stringly_typed/ignore_checker.py +102 -0
  103. src/linters/stringly_typed/ignore_utils.py +51 -0
  104. src/linters/stringly_typed/linter.py +376 -0
  105. src/linters/stringly_typed/python/__init__.py +9 -5
  106. src/linters/stringly_typed/python/analyzer.py +159 -9
  107. src/linters/stringly_typed/python/call_tracker.py +175 -0
  108. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  109. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  110. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  111. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  112. src/linters/stringly_typed/python/validation_detector.py +3 -0
  113. src/linters/stringly_typed/storage.py +630 -0
  114. src/linters/stringly_typed/storage_initializer.py +45 -0
  115. src/linters/stringly_typed/typescript/__init__.py +28 -0
  116. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  117. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  118. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  119. src/linters/stringly_typed/violation_generator.py +405 -0
  120. src/orchestrator/core.py +13 -4
  121. src/templates/thailint_config_template.yaml +166 -0
  122. src/utils/project_root.py +3 -0
  123. thailint-0.13.0.dist-info/METADATA +184 -0
  124. thailint-0.13.0.dist-info/RECORD +189 -0
  125. thailint-0.11.0.dist-info/METADATA +0 -1661
  126. thailint-0.11.0.dist-info/RECORD +0 -150
  127. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/WHEEL +0 -0
  128. {thailint-0.11.0.dist-info → thailint-0.13.0.dist-info}/entry_points.txt +0 -0
  129. {thailint-0.11.0.dist-info → thailint-0.13.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: 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,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()