thailint 0.2.0__py3-none-any.whl → 0.5.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 (52) hide show
  1. src/cli.py +646 -36
  2. src/config.py +6 -2
  3. src/core/base.py +90 -5
  4. src/core/config_parser.py +31 -4
  5. src/linters/dry/block_filter.py +5 -2
  6. src/linters/dry/cache.py +46 -92
  7. src/linters/dry/config.py +17 -13
  8. src/linters/dry/duplicate_storage.py +17 -80
  9. src/linters/dry/file_analyzer.py +11 -48
  10. src/linters/dry/linter.py +5 -12
  11. src/linters/dry/python_analyzer.py +188 -37
  12. src/linters/dry/storage_initializer.py +9 -18
  13. src/linters/dry/token_hasher.py +63 -9
  14. src/linters/dry/typescript_analyzer.py +7 -5
  15. src/linters/dry/violation_filter.py +4 -1
  16. src/linters/file_header/__init__.py +24 -0
  17. src/linters/file_header/atemporal_detector.py +87 -0
  18. src/linters/file_header/config.py +66 -0
  19. src/linters/file_header/field_validator.py +69 -0
  20. src/linters/file_header/linter.py +313 -0
  21. src/linters/file_header/python_parser.py +86 -0
  22. src/linters/file_header/violation_builder.py +78 -0
  23. src/linters/file_placement/linter.py +15 -4
  24. src/linters/magic_numbers/__init__.py +48 -0
  25. src/linters/magic_numbers/config.py +82 -0
  26. src/linters/magic_numbers/context_analyzer.py +247 -0
  27. src/linters/magic_numbers/linter.py +516 -0
  28. src/linters/magic_numbers/python_analyzer.py +76 -0
  29. src/linters/magic_numbers/typescript_analyzer.py +218 -0
  30. src/linters/magic_numbers/violation_builder.py +98 -0
  31. src/linters/nesting/__init__.py +6 -2
  32. src/linters/nesting/config.py +6 -3
  33. src/linters/nesting/linter.py +8 -19
  34. src/linters/nesting/typescript_analyzer.py +1 -0
  35. src/linters/print_statements/__init__.py +53 -0
  36. src/linters/print_statements/config.py +83 -0
  37. src/linters/print_statements/linter.py +430 -0
  38. src/linters/print_statements/python_analyzer.py +155 -0
  39. src/linters/print_statements/typescript_analyzer.py +135 -0
  40. src/linters/print_statements/violation_builder.py +98 -0
  41. src/linters/srp/__init__.py +3 -3
  42. src/linters/srp/config.py +12 -6
  43. src/linters/srp/linter.py +33 -24
  44. src/orchestrator/core.py +12 -2
  45. src/templates/thailint_config_template.yaml +158 -0
  46. src/utils/project_root.py +135 -16
  47. {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/METADATA +387 -81
  48. thailint-0.5.0.dist-info/RECORD +96 -0
  49. {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
  50. thailint-0.2.0.dist-info/RECORD +0 -75
  51. {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
  52. {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,516 @@
1
+ """
2
+ Purpose: Main magic numbers linter rule implementation
3
+
4
+ Scope: MagicNumberRule class implementing BaseLintRule interface
5
+
6
+ Overview: Implements magic numbers linter rule following BaseLintRule interface. Orchestrates
7
+ configuration loading, Python AST analysis, context detection, and violation building through
8
+ focused helper classes. Detects numeric literals that should be extracted to named constants.
9
+ Supports configurable allowed_numbers set and max_small_integer threshold. Handles ignore
10
+ directives for suppressing specific violations. Main rule class acts as coordinator for magic
11
+ number checking workflow across Python code files. Method count (17) exceeds SRP limit (8)
12
+ because refactoring for A-grade complexity requires extracting helper methods. Class maintains
13
+ single responsibility of magic number detection - all methods support this core purpose.
14
+
15
+ Dependencies: BaseLintRule, BaseLintContext, PythonMagicNumberAnalyzer, ContextAnalyzer,
16
+ ViolationBuilder, MagicNumberConfig, IgnoreDirectiveParser
17
+
18
+ Exports: MagicNumberRule class
19
+
20
+ Interfaces: MagicNumberRule.check(context) -> list[Violation], properties for rule metadata
21
+
22
+ Implementation: Composition pattern with helper classes, AST-based analysis with configurable
23
+ allowed numbers and context detection
24
+ """
25
+
26
+ import ast
27
+ from pathlib import Path
28
+
29
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
30
+ from src.core.linter_utils import load_linter_config
31
+ from src.core.types import Violation
32
+ from src.linter_config.ignore import IgnoreDirectiveParser
33
+
34
+ from .config import MagicNumberConfig
35
+ from .context_analyzer import ContextAnalyzer
36
+ from .python_analyzer import PythonMagicNumberAnalyzer
37
+ from .typescript_analyzer import TypeScriptMagicNumberAnalyzer
38
+ from .violation_builder import ViolationBuilder
39
+
40
+
41
+ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
42
+ """Detects magic numbers that should be replaced with named constants."""
43
+
44
+ def __init__(self) -> None:
45
+ """Initialize the magic numbers rule."""
46
+ self._ignore_parser = IgnoreDirectiveParser()
47
+ self._violation_builder = ViolationBuilder(self.rule_id)
48
+ self._context_analyzer = ContextAnalyzer()
49
+
50
+ @property
51
+ def rule_id(self) -> str:
52
+ """Unique identifier for this rule."""
53
+ return "magic-numbers.numeric-literal"
54
+
55
+ @property
56
+ def rule_name(self) -> str:
57
+ """Human-readable name for this rule."""
58
+ return "Magic Numbers"
59
+
60
+ @property
61
+ def description(self) -> str:
62
+ """Description of what this rule checks."""
63
+ return "Numeric literals should be replaced with named constants for better maintainability"
64
+
65
+ def _load_config(self, context: BaseLintContext) -> MagicNumberConfig:
66
+ """Load configuration from context.
67
+
68
+ Args:
69
+ context: Lint context
70
+
71
+ Returns:
72
+ MagicNumberConfig instance
73
+ """
74
+ # Try test-style config first
75
+ test_config = self._try_load_test_config(context)
76
+ if test_config is not None:
77
+ return test_config
78
+
79
+ # Try production config
80
+ prod_config = self._try_load_production_config(context)
81
+ if prod_config is not None:
82
+ return prod_config
83
+
84
+ # Use defaults
85
+ return MagicNumberConfig()
86
+
87
+ def _try_load_test_config(self, context: BaseLintContext) -> MagicNumberConfig | None:
88
+ """Try to load test-style configuration."""
89
+ if not hasattr(context, "config"):
90
+ return None
91
+ config_attr = context.config
92
+ if config_attr is None or not isinstance(config_attr, dict):
93
+ return None
94
+ return MagicNumberConfig.from_dict(config_attr, context.language)
95
+
96
+ def _try_load_production_config(self, context: BaseLintContext) -> MagicNumberConfig | None:
97
+ """Try to load production configuration."""
98
+ if not hasattr(context, "metadata") or not isinstance(context.metadata, dict):
99
+ return None
100
+
101
+ # Try both hyphenated and underscored keys for backward compatibility
102
+ # The config parser normalizes keys when loading from YAML, but
103
+ # direct metadata injection (tests) may use either format
104
+ metadata = context.metadata
105
+
106
+ # Try underscore version first (normalized format)
107
+ if "magic_numbers" in metadata:
108
+ return load_linter_config(context, "magic_numbers", MagicNumberConfig)
109
+
110
+ # Fallback to hyphenated version (for direct test injection)
111
+ if "magic-numbers" in metadata:
112
+ return load_linter_config(context, "magic-numbers", MagicNumberConfig)
113
+
114
+ # No config found, return None to use defaults
115
+ return None
116
+
117
+ def _is_file_ignored(self, context: BaseLintContext, config: MagicNumberConfig) -> bool:
118
+ """Check if file matches ignore patterns.
119
+
120
+ Args:
121
+ context: Lint context
122
+ config: Magic numbers configuration
123
+
124
+ Returns:
125
+ True if file should be ignored
126
+ """
127
+ if not config.ignore:
128
+ return False
129
+
130
+ if not context.file_path:
131
+ return False
132
+
133
+ file_path = Path(context.file_path)
134
+ for pattern in config.ignore:
135
+ if self._matches_pattern(file_path, pattern):
136
+ return True
137
+ return False
138
+
139
+ def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
140
+ """Check if file path matches a glob pattern.
141
+
142
+ Args:
143
+ file_path: Path to check
144
+ pattern: Glob pattern (e.g., "test/**", "**/test_*.py", "specific/file.py")
145
+
146
+ Returns:
147
+ True if path matches pattern
148
+ """
149
+ # Try glob pattern matching first (handles **, *, etc.)
150
+ if file_path.match(pattern):
151
+ return True
152
+
153
+ # Also check if pattern is a substring (for partial path matching)
154
+ if pattern in str(file_path):
155
+ return True
156
+
157
+ return False
158
+
159
+ def _check_python(self, context: BaseLintContext, config: MagicNumberConfig) -> list[Violation]:
160
+ """Check Python code for magic number violations.
161
+
162
+ Args:
163
+ context: Lint context with Python file information
164
+ config: Magic numbers configuration
165
+
166
+ Returns:
167
+ List of violations found in Python code
168
+ """
169
+ if self._is_file_ignored(context, config):
170
+ return []
171
+
172
+ tree = self._parse_python_code(context.file_content)
173
+ if tree is None:
174
+ return []
175
+
176
+ numeric_literals = self._find_numeric_literals(tree)
177
+ return self._collect_violations(numeric_literals, context, config)
178
+
179
+ def _parse_python_code(self, code: str | None) -> ast.AST | None:
180
+ """Parse Python code into AST."""
181
+ try:
182
+ return ast.parse(code or "")
183
+ except SyntaxError:
184
+ return None
185
+
186
+ def _find_numeric_literals(self, tree: ast.AST) -> list:
187
+ """Find all numeric literals in AST."""
188
+ analyzer = PythonMagicNumberAnalyzer()
189
+ return analyzer.find_numeric_literals(tree)
190
+
191
+ def _collect_violations(
192
+ self, numeric_literals: list, context: BaseLintContext, config: MagicNumberConfig
193
+ ) -> list[Violation]:
194
+ """Collect violations from numeric literals."""
195
+ violations = []
196
+ for literal_info in numeric_literals:
197
+ violation = self._try_create_violation(literal_info, context, config)
198
+ if violation is not None:
199
+ violations.append(violation)
200
+ return violations
201
+
202
+ def _try_create_violation(
203
+ self, literal_info: tuple, context: BaseLintContext, config: MagicNumberConfig
204
+ ) -> Violation | None:
205
+ """Try to create a violation for a numeric literal.
206
+
207
+ Args:
208
+ literal_info: Tuple of (node, parent, value, line_number)
209
+ context: Lint context
210
+ config: Configuration
211
+ """
212
+ node, parent, value, line_number = literal_info
213
+ if not self._should_flag_number(value, (node, parent), config, context):
214
+ return None
215
+
216
+ violation = self._violation_builder.create_violation(
217
+ node, value, line_number, context.file_path
218
+ )
219
+ if self._should_ignore(violation, context):
220
+ return None
221
+
222
+ return violation
223
+
224
+ def _should_flag_number(
225
+ self,
226
+ value: int | float,
227
+ node_info: tuple[ast.Constant, ast.AST | None],
228
+ config: MagicNumberConfig,
229
+ context: BaseLintContext,
230
+ ) -> bool:
231
+ """Determine if a number should be flagged as a magic number.
232
+
233
+ Args:
234
+ value: The numeric value
235
+ node_info: Tuple of (node, parent) AST nodes
236
+ config: Configuration
237
+ context: Lint context
238
+
239
+ Returns:
240
+ True if the number should be flagged
241
+ """
242
+ if value in config.allowed_numbers:
243
+ return False
244
+
245
+ node, parent = node_info
246
+ config_dict = {
247
+ "max_small_integer": config.max_small_integer,
248
+ "allowed_numbers": config.allowed_numbers,
249
+ }
250
+
251
+ if self._context_analyzer.is_acceptable_context(
252
+ node, parent, context.file_path, config_dict
253
+ ):
254
+ return False
255
+
256
+ return True
257
+
258
+ def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
259
+ """Check if violation should be ignored based on inline directives.
260
+
261
+ Args:
262
+ violation: Violation to check
263
+ context: Lint context with file content
264
+
265
+ Returns:
266
+ True if violation should be ignored
267
+ """
268
+ # Check using standard ignore parser
269
+ if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
270
+ return True
271
+
272
+ # Workaround for generic ignore directives
273
+ return self._check_generic_ignore(violation, context)
274
+
275
+ def _check_generic_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
276
+ """Check for generic ignore directives (workaround for parser limitation).
277
+
278
+ Args:
279
+ violation: Violation to check
280
+ context: Lint context
281
+
282
+ Returns:
283
+ True if line has generic ignore directive
284
+ """
285
+ line_text = self._get_violation_line(violation, context)
286
+ if line_text is None:
287
+ return False
288
+
289
+ return self._has_generic_ignore_directive(line_text)
290
+
291
+ def _get_violation_line(self, violation: Violation, context: BaseLintContext) -> str | None:
292
+ """Get the line text for a violation."""
293
+ if not context.file_content:
294
+ return None
295
+
296
+ lines = context.file_content.splitlines()
297
+ if violation.line <= 0 or violation.line > len(lines):
298
+ return None
299
+
300
+ return lines[violation.line - 1].lower()
301
+
302
+ def _has_generic_ignore_directive(self, line_text: str) -> bool:
303
+ """Check if line has generic ignore directive."""
304
+ if self._has_generic_thailint_ignore(line_text):
305
+ return True
306
+ return self._has_noqa_directive(line_text)
307
+
308
+ def _has_generic_thailint_ignore(self, line_text: str) -> bool:
309
+ """Check for generic thailint: ignore (no brackets)."""
310
+ if "# thailint: ignore" not in line_text:
311
+ return False
312
+ after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
313
+ return "[" not in after_ignore
314
+
315
+ def _has_noqa_directive(self, line_text: str) -> bool:
316
+ """Check for noqa-style comments."""
317
+ return "# noqa" in line_text
318
+
319
+ def _check_typescript(
320
+ self, context: BaseLintContext, config: MagicNumberConfig
321
+ ) -> list[Violation]:
322
+ """Check TypeScript/JavaScript code for magic number violations.
323
+
324
+ Args:
325
+ context: Lint context with TypeScript/JavaScript file information
326
+ config: Magic numbers configuration
327
+
328
+ Returns:
329
+ List of violations found in TypeScript/JavaScript code
330
+ """
331
+ if self._is_file_ignored(context, config):
332
+ return []
333
+
334
+ analyzer = TypeScriptMagicNumberAnalyzer()
335
+ root_node = analyzer.parse_typescript(context.file_content or "")
336
+ if root_node is None:
337
+ return []
338
+
339
+ numeric_literals = analyzer.find_numeric_literals(root_node)
340
+ return self._collect_typescript_violations(numeric_literals, context, config, analyzer)
341
+
342
+ def _collect_typescript_violations(
343
+ self,
344
+ numeric_literals: list,
345
+ context: BaseLintContext,
346
+ config: MagicNumberConfig,
347
+ analyzer: TypeScriptMagicNumberAnalyzer,
348
+ ) -> list[Violation]:
349
+ """Collect violations from TypeScript numeric literals.
350
+
351
+ Args:
352
+ numeric_literals: List of (node, value, line_number) tuples
353
+ context: Lint context
354
+ config: Configuration
355
+ analyzer: TypeScript analyzer instance
356
+
357
+ Returns:
358
+ List of violations
359
+ """
360
+ violations = []
361
+ for node, value, line_number in numeric_literals:
362
+ violation = self._try_create_typescript_violation(
363
+ node, value, line_number, context, config, analyzer
364
+ )
365
+ if violation is not None:
366
+ violations.append(violation)
367
+ return violations
368
+
369
+ def _try_create_typescript_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
370
+ self,
371
+ node: object,
372
+ value: float | int,
373
+ line_number: int,
374
+ context: BaseLintContext,
375
+ config: MagicNumberConfig,
376
+ analyzer: TypeScriptMagicNumberAnalyzer,
377
+ ) -> Violation | None:
378
+ """Try to create a violation for a TypeScript numeric literal.
379
+
380
+ Args:
381
+ node: Tree-sitter node
382
+ value: Numeric value
383
+ line_number: Line number of literal
384
+ context: Lint context
385
+ config: Configuration
386
+ analyzer: TypeScript analyzer
387
+
388
+ Returns:
389
+ Violation or None if should not flag
390
+ """
391
+ if not self._should_flag_typescript_number(node, value, context, config, analyzer):
392
+ return None
393
+
394
+ violation = self._violation_builder.create_typescript_violation(
395
+ value, line_number, context.file_path
396
+ )
397
+ if self._should_ignore_typescript(violation, context):
398
+ return None
399
+
400
+ return violation
401
+
402
+ def _should_flag_typescript_number( # pylint: disable=too-many-arguments,too-many-positional-arguments
403
+ self,
404
+ node: object,
405
+ value: float | int,
406
+ context: BaseLintContext,
407
+ config: MagicNumberConfig,
408
+ analyzer: TypeScriptMagicNumberAnalyzer,
409
+ ) -> bool:
410
+ """Determine if a TypeScript number should be flagged.
411
+
412
+ Args:
413
+ node: Tree-sitter node
414
+ value: Numeric value
415
+ context: Lint context
416
+ config: Configuration
417
+ analyzer: TypeScript analyzer
418
+
419
+ Returns:
420
+ True if should flag as magic number
421
+ """
422
+ # Early return for allowed contexts
423
+ if self._is_typescript_allowed_context(value, context, config):
424
+ return False
425
+
426
+ # Check TypeScript-specific contexts
427
+ return not self._is_typescript_special_context(node, analyzer, context)
428
+
429
+ def _is_typescript_allowed_context(
430
+ self, value: float | int, context: BaseLintContext, config: MagicNumberConfig
431
+ ) -> bool:
432
+ """Check if number is in allowed context."""
433
+ return value in config.allowed_numbers or self._is_test_file(context.file_path)
434
+
435
+ def _is_typescript_special_context(
436
+ self, node: object, analyzer: TypeScriptMagicNumberAnalyzer, context: BaseLintContext
437
+ ) -> bool:
438
+ """Check if in TypeScript-specific special context."""
439
+ # Calls require type: ignore because node is typed as object but analyzer expects Node
440
+ in_enum = analyzer.is_enum_context(node) # type: ignore[arg-type]
441
+ in_const_def = analyzer.is_constant_definition(node, context.file_content or "") # type: ignore[arg-type]
442
+ return in_enum or in_const_def
443
+
444
+ def _is_test_file(self, file_path: object) -> bool:
445
+ """Check if file is a test file.
446
+
447
+ Args:
448
+ file_path: Path to check
449
+
450
+ Returns:
451
+ True if test file
452
+ """
453
+ path_str = str(file_path)
454
+ return any(
455
+ pattern in path_str
456
+ for pattern in [".test.", ".spec.", "test_", "_test.", "/tests/", "/test/"]
457
+ )
458
+
459
+ def _should_ignore_typescript(self, violation: Violation, context: BaseLintContext) -> bool:
460
+ """Check if TypeScript violation should be ignored.
461
+
462
+ Args:
463
+ violation: Violation to check
464
+ context: Lint context
465
+
466
+ Returns:
467
+ True if should ignore
468
+ """
469
+ # Check standard ignore directives
470
+ if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
471
+ return True
472
+
473
+ # Check TypeScript-style comments
474
+ return self._check_typescript_ignore(violation, context)
475
+
476
+ def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
477
+ """Check for TypeScript-style ignore directives.
478
+
479
+ Args:
480
+ violation: Violation to check
481
+ context: Lint context
482
+
483
+ Returns:
484
+ True if line has ignore directive
485
+ """
486
+ line_text = self._get_violation_line(violation, context)
487
+ if line_text is None:
488
+ return False
489
+
490
+ # Check for // thailint: ignore or // noqa
491
+ return self._has_typescript_ignore_directive(line_text)
492
+
493
+ def _has_typescript_ignore_directive(self, line_text: str) -> bool:
494
+ """Check if line has TypeScript-style ignore directive.
495
+
496
+ Args:
497
+ line_text: Line text to check
498
+
499
+ Returns:
500
+ True if has ignore directive
501
+ """
502
+ # Check for // thailint: ignore[magic-numbers]
503
+ if "// thailint: ignore[magic-numbers]" in line_text:
504
+ return True
505
+
506
+ # Check for // thailint: ignore (generic)
507
+ if "// thailint: ignore" in line_text:
508
+ after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
509
+ if "[" not in after_ignore:
510
+ return True
511
+
512
+ # Check for // noqa
513
+ if "// noqa" in line_text:
514
+ return True
515
+
516
+ return False
@@ -0,0 +1,76 @@
1
+ """
2
+ Purpose: Python AST analysis for finding numeric literal nodes
3
+
4
+ Scope: Python magic number detection through AST traversal
5
+
6
+ Overview: Provides PythonMagicNumberAnalyzer class that traverses Python AST to find all numeric
7
+ literal nodes (integers and floats). Uses ast.NodeVisitor pattern to walk the syntax tree and
8
+ collect Constant nodes containing numeric values along with their parent nodes and line numbers.
9
+ Returns structured data about each numeric literal including the AST node, parent node, numeric
10
+ value, and source location. This analyzer handles Python-specific AST structure and provides
11
+ the foundation for magic number detection by identifying all candidates before context filtering.
12
+
13
+ Dependencies: ast module for AST parsing and node types
14
+
15
+ Exports: PythonMagicNumberAnalyzer class
16
+
17
+ Interfaces: PythonMagicNumberAnalyzer.find_numeric_literals(tree) -> list[tuple],
18
+ returns list of (node, parent, value, line_number) tuples
19
+
20
+ Implementation: AST NodeVisitor pattern with parent tracking, filters for numeric Constant nodes
21
+ """
22
+
23
+ import ast
24
+ from typing import Any
25
+
26
+
27
+ class PythonMagicNumberAnalyzer(ast.NodeVisitor):
28
+ """Analyzes Python AST to find numeric literals."""
29
+
30
+ def __init__(self) -> None:
31
+ """Initialize the analyzer."""
32
+ self.numeric_literals: list[tuple[ast.Constant, ast.AST | None, Any, int]] = []
33
+ self.parent_map: dict[ast.AST, ast.AST] = {}
34
+
35
+ def find_numeric_literals(
36
+ self, tree: ast.AST
37
+ ) -> list[tuple[ast.Constant, ast.AST | None, Any, int]]:
38
+ """Find all numeric literals in the AST.
39
+
40
+ Args:
41
+ tree: The AST to analyze
42
+
43
+ Returns:
44
+ List of tuples (node, parent, value, line_number)
45
+ """
46
+ self.numeric_literals = []
47
+ self.parent_map = {}
48
+ self._build_parent_map(tree)
49
+ self.visit(tree)
50
+ return self.numeric_literals
51
+
52
+ def _build_parent_map(self, node: ast.AST, parent: ast.AST | None = None) -> None:
53
+ """Build a map of nodes to their parents.
54
+
55
+ Args:
56
+ node: Current AST node
57
+ parent: Parent of current node
58
+ """
59
+ if parent is not None:
60
+ self.parent_map[node] = parent
61
+
62
+ for child in ast.iter_child_nodes(node):
63
+ self._build_parent_map(child, node)
64
+
65
+ def visit_Constant(self, node: ast.Constant) -> None:
66
+ """Visit a Constant node and check if it's a numeric literal.
67
+
68
+ Args:
69
+ node: The Constant node to check
70
+ """
71
+ if isinstance(node.value, (int, float)):
72
+ parent = self.parent_map.get(node)
73
+ line_number = node.lineno if hasattr(node, "lineno") else 0
74
+ self.numeric_literals.append((node, parent, node.value, line_number))
75
+
76
+ self.generic_visit(node)