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.
- src/cli.py +646 -36
- src/config.py +6 -2
- src/core/base.py +90 -5
- src/core/config_parser.py +31 -4
- src/linters/dry/block_filter.py +5 -2
- src/linters/dry/cache.py +46 -92
- src/linters/dry/config.py +17 -13
- src/linters/dry/duplicate_storage.py +17 -80
- src/linters/dry/file_analyzer.py +11 -48
- src/linters/dry/linter.py +5 -12
- src/linters/dry/python_analyzer.py +188 -37
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +63 -9
- src/linters/dry/typescript_analyzer.py +7 -5
- src/linters/dry/violation_filter.py +4 -1
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +87 -0
- src/linters/file_header/config.py +66 -0
- src/linters/file_header/field_validator.py +69 -0
- src/linters/file_header/linter.py +313 -0
- src/linters/file_header/python_parser.py +86 -0
- src/linters/file_header/violation_builder.py +78 -0
- src/linters/file_placement/linter.py +15 -4
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +247 -0
- src/linters/magic_numbers/linter.py +516 -0
- src/linters/magic_numbers/python_analyzer.py +76 -0
- src/linters/magic_numbers/typescript_analyzer.py +218 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +8 -19
- src/linters/nesting/typescript_analyzer.py +1 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +83 -0
- src/linters/print_statements/linter.py +430 -0
- src/linters/print_statements/python_analyzer.py +155 -0
- src/linters/print_statements/typescript_analyzer.py +135 -0
- src/linters/print_statements/violation_builder.py +98 -0
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/config.py +12 -6
- src/linters/srp/linter.py +33 -24
- src/orchestrator/core.py +12 -2
- src/templates/thailint_config_template.yaml +158 -0
- src/utils/project_root.py +135 -16
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/METADATA +387 -81
- thailint-0.5.0.dist-info/RECORD +96 -0
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
- thailint-0.2.0.dist-info/RECORD +0 -75
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File: src/linters/file_header/violation_builder.py
|
|
3
|
+
Purpose: Builds violation messages for file header linter
|
|
4
|
+
Exports: ViolationBuilder class
|
|
5
|
+
Depends: Violation type from core
|
|
6
|
+
Implements: Message templates with context-specific details
|
|
7
|
+
Related: linter.py for builder usage, atemporal_detector.py for temporal violations
|
|
8
|
+
|
|
9
|
+
Overview:
|
|
10
|
+
Creates formatted violation messages for file header validation failures.
|
|
11
|
+
Handles missing fields, atemporal language, and other header issues with clear,
|
|
12
|
+
actionable messages. Provides consistent violation format across all validation types.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
builder = ViolationBuilder("file-header.validation")
|
|
16
|
+
violation = builder.build_missing_field("Purpose", "test.py", 1)
|
|
17
|
+
|
|
18
|
+
Notes: Follows standard violation format with rule_id, message, location, severity, suggestion
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from src.core.types import Severity, Violation
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ViolationBuilder:
|
|
25
|
+
"""Builds violation messages for file header issues."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, rule_id: str):
|
|
28
|
+
"""Initialize with rule ID.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
rule_id: Rule identifier for violations
|
|
32
|
+
"""
|
|
33
|
+
self.rule_id = rule_id
|
|
34
|
+
|
|
35
|
+
def build_missing_field(self, field_name: str, file_path: str, line: int = 1) -> Violation:
|
|
36
|
+
"""Build violation for missing mandatory field.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
field_name: Name of missing field
|
|
40
|
+
file_path: Path to file
|
|
41
|
+
line: Line number (default 1 for header)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Violation object describing missing field
|
|
45
|
+
"""
|
|
46
|
+
return Violation(
|
|
47
|
+
rule_id=self.rule_id,
|
|
48
|
+
message=f"Missing mandatory field: {field_name}",
|
|
49
|
+
file_path=file_path,
|
|
50
|
+
line=line,
|
|
51
|
+
column=1,
|
|
52
|
+
severity=Severity.ERROR,
|
|
53
|
+
suggestion=f"Add '{field_name}:' field to file header",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def build_atemporal_violation(
|
|
57
|
+
self, pattern: str, description: str, file_path: str, line: int
|
|
58
|
+
) -> Violation:
|
|
59
|
+
"""Build violation for temporal language.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
pattern: Matched regex pattern
|
|
63
|
+
description: Description of temporal language
|
|
64
|
+
file_path: Path to file
|
|
65
|
+
line: Line number of violation
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Violation object describing temporal language issue
|
|
69
|
+
"""
|
|
70
|
+
return Violation(
|
|
71
|
+
rule_id=self.rule_id,
|
|
72
|
+
message=f"Temporal language detected: {description}",
|
|
73
|
+
file_path=file_path,
|
|
74
|
+
line=line,
|
|
75
|
+
column=1,
|
|
76
|
+
severity=Severity.ERROR,
|
|
77
|
+
suggestion="Use present-tense factual descriptions without temporal references",
|
|
78
|
+
)
|
|
@@ -75,9 +75,12 @@ class FilePlacementLinter:
|
|
|
75
75
|
# Load and validate config
|
|
76
76
|
if config_obj:
|
|
77
77
|
# Handle both wrapped and unwrapped config formats
|
|
78
|
-
# Wrapped: {"file-placement": {...}}
|
|
78
|
+
# Wrapped: {"file-placement": {...}} or {"file_placement": {...}}
|
|
79
79
|
# Unwrapped: {"directories": {...}, "global_deny": [...], ...}
|
|
80
|
-
|
|
80
|
+
# Try both hyphenated and underscored keys for backward compatibility
|
|
81
|
+
self.config = config_obj.get(
|
|
82
|
+
"file-placement", config_obj.get("file_placement", config_obj)
|
|
83
|
+
)
|
|
81
84
|
elif config_file:
|
|
82
85
|
self.config = self._components.config_loader.load_config_file(config_file)
|
|
83
86
|
else:
|
|
@@ -279,7 +282,9 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
|
|
|
279
282
|
|
|
280
283
|
@staticmethod
|
|
281
284
|
def _get_wrapped_config(context: BaseLintContext) -> dict[str, Any] | None:
|
|
282
|
-
"""Get config from wrapped format: {"file-placement": {...}}.
|
|
285
|
+
"""Get config from wrapped format: {"file-placement": {...}} or {"file_placement": {...}}.
|
|
286
|
+
|
|
287
|
+
Supports both hyphenated and underscored keys for backward compatibility.
|
|
283
288
|
|
|
284
289
|
Args:
|
|
285
290
|
context: Lint context with metadata
|
|
@@ -289,8 +294,12 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
|
|
|
289
294
|
"""
|
|
290
295
|
if not hasattr(context, "metadata"):
|
|
291
296
|
return None
|
|
297
|
+
# Try hyphenated format first (original format)
|
|
292
298
|
if "file-placement" in context.metadata:
|
|
293
299
|
return context.metadata["file-placement"]
|
|
300
|
+
# Try underscored format (normalized format)
|
|
301
|
+
if "file_placement" in context.metadata:
|
|
302
|
+
return context.metadata["file_placement"]
|
|
294
303
|
return None
|
|
295
304
|
|
|
296
305
|
@staticmethod
|
|
@@ -378,9 +387,11 @@ class FilePlacementRule(BaseLintRule): # thailint: ignore[srp.violation]
|
|
|
378
387
|
try:
|
|
379
388
|
config = self._parse_layout_file(layout_path)
|
|
380
389
|
|
|
381
|
-
# Unwrap file-placement key if present
|
|
390
|
+
# Unwrap file-placement key if present (try both formats for backward compatibility)
|
|
382
391
|
if "file-placement" in config:
|
|
383
392
|
return config["file-placement"]
|
|
393
|
+
if "file_placement" in config:
|
|
394
|
+
return config["file_placement"]
|
|
384
395
|
|
|
385
396
|
return config
|
|
386
397
|
except Exception:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Magic numbers linter package exports and convenience functions
|
|
3
|
+
|
|
4
|
+
Scope: Public API for magic numbers linter module
|
|
5
|
+
|
|
6
|
+
Overview: Provides the public interface for the magic numbers linter package. Exports main
|
|
7
|
+
MagicNumberRule class for use by the orchestrator and MagicNumberConfig for configuration.
|
|
8
|
+
Includes lint() convenience function that provides a simple API for running the magic numbers
|
|
9
|
+
linter on a file or directory without directly interacting with the orchestrator. This module
|
|
10
|
+
serves as the entry point for users of the magic numbers linter, hiding implementation details
|
|
11
|
+
and exposing only the essential components needed for linting operations.
|
|
12
|
+
|
|
13
|
+
Dependencies: .linter for MagicNumberRule, .config for MagicNumberConfig
|
|
14
|
+
|
|
15
|
+
Exports: MagicNumberRule class, MagicNumberConfig dataclass, lint() convenience function
|
|
16
|
+
|
|
17
|
+
Interfaces: lint(path, config) -> list[Violation] for simple linting operations
|
|
18
|
+
|
|
19
|
+
Implementation: Module-level exports with __all__ definition, convenience function wrapper
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .config import MagicNumberConfig
|
|
23
|
+
from .linter import MagicNumberRule
|
|
24
|
+
|
|
25
|
+
__all__ = ["MagicNumberRule", "MagicNumberConfig", "lint"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def lint(file_path: str, config: dict | None = None) -> list:
|
|
29
|
+
"""Convenience function for linting a file for magic numbers.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
file_path: Path to the file to lint
|
|
33
|
+
config: Optional configuration dictionary
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of violations found
|
|
37
|
+
"""
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
from src.orchestrator.core import FileLintContext
|
|
41
|
+
|
|
42
|
+
rule = MagicNumberRule()
|
|
43
|
+
context = FileLintContext(
|
|
44
|
+
path=Path(file_path),
|
|
45
|
+
lang="python",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return rule.check(context)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration schema for magic numbers linter
|
|
3
|
+
|
|
4
|
+
Scope: MagicNumberConfig dataclass with allowed_numbers and max_small_integer settings
|
|
5
|
+
|
|
6
|
+
Overview: Defines configuration schema for magic numbers linter. Provides MagicNumberConfig dataclass
|
|
7
|
+
with allowed_numbers set (default includes common acceptable numbers like -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000)
|
|
8
|
+
and max_small_integer threshold (default 10) for range() contexts. Supports per-file and per-directory
|
|
9
|
+
config overrides through from_dict class method. Validates that configuration values are appropriate
|
|
10
|
+
types. Integrates with orchestrator's configuration system to allow users to customize allowed numbers
|
|
11
|
+
via .thailint.yaml configuration files.
|
|
12
|
+
|
|
13
|
+
Dependencies: dataclasses for class definition, typing for type hints
|
|
14
|
+
|
|
15
|
+
Exports: MagicNumberConfig dataclass
|
|
16
|
+
|
|
17
|
+
Interfaces: MagicNumberConfig(allowed_numbers: set, max_small_integer: int, enabled: bool),
|
|
18
|
+
from_dict class method for loading configuration from dictionary
|
|
19
|
+
|
|
20
|
+
Implementation: Dataclass with validation and defaults, matches reference implementation patterns
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class MagicNumberConfig:
|
|
29
|
+
"""Configuration for magic numbers linter."""
|
|
30
|
+
|
|
31
|
+
enabled: bool = True
|
|
32
|
+
allowed_numbers: set[int | float] = field(
|
|
33
|
+
default_factory=lambda: {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000}
|
|
34
|
+
)
|
|
35
|
+
max_small_integer: int = 10
|
|
36
|
+
ignore: list[str] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
def __post_init__(self) -> None:
|
|
39
|
+
"""Validate configuration values."""
|
|
40
|
+
if self.max_small_integer <= 0:
|
|
41
|
+
raise ValueError(f"max_small_integer must be positive, got {self.max_small_integer}")
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "MagicNumberConfig":
|
|
45
|
+
"""Load configuration from dictionary with language-specific overrides.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
config: Dictionary containing configuration values
|
|
49
|
+
language: Programming language (python, typescript, javascript)
|
|
50
|
+
for language-specific settings
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
MagicNumberConfig instance with values from dictionary
|
|
54
|
+
"""
|
|
55
|
+
# Get language-specific config if available
|
|
56
|
+
if language and language in config:
|
|
57
|
+
lang_config = config[language]
|
|
58
|
+
allowed_numbers = set(
|
|
59
|
+
lang_config.get(
|
|
60
|
+
"allowed_numbers",
|
|
61
|
+
config.get("allowed_numbers", {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000}),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
max_small_integer = lang_config.get(
|
|
65
|
+
"max_small_integer", config.get("max_small_integer", 10)
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
allowed_numbers = set(
|
|
69
|
+
config.get("allowed_numbers", {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000})
|
|
70
|
+
)
|
|
71
|
+
max_small_integer = config.get("max_small_integer", 10)
|
|
72
|
+
|
|
73
|
+
ignore_patterns = config.get("ignore", [])
|
|
74
|
+
if not isinstance(ignore_patterns, list):
|
|
75
|
+
ignore_patterns = []
|
|
76
|
+
|
|
77
|
+
return cls(
|
|
78
|
+
enabled=config.get("enabled", True),
|
|
79
|
+
allowed_numbers=allowed_numbers,
|
|
80
|
+
max_small_integer=max_small_integer,
|
|
81
|
+
ignore=ignore_patterns,
|
|
82
|
+
)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Analyzes contexts to determine if numeric literals are acceptable
|
|
3
|
+
|
|
4
|
+
Scope: Context detection for magic number acceptable usage patterns
|
|
5
|
+
|
|
6
|
+
Overview: Provides ContextAnalyzer class that determines whether a numeric literal is in an acceptable
|
|
7
|
+
context where it should not be flagged as a magic number. Detects acceptable contexts including
|
|
8
|
+
constant definitions (UPPERCASE names), small integers in range() or enumerate() calls, test files,
|
|
9
|
+
and configuration contexts. Uses AST node analysis to inspect parent nodes and determine the usage
|
|
10
|
+
pattern of numeric literals. Helps reduce false positives by distinguishing between legitimate
|
|
11
|
+
numeric literals and true magic numbers that should be extracted to constants. Method count (10)
|
|
12
|
+
exceeds SRP limit (8) because refactoring for A-grade complexity requires extracting helper methods.
|
|
13
|
+
Class maintains single responsibility of context analysis - all methods support this core purpose.
|
|
14
|
+
|
|
15
|
+
Dependencies: ast module for AST node types, pathlib for Path handling
|
|
16
|
+
|
|
17
|
+
Exports: ContextAnalyzer class
|
|
18
|
+
|
|
19
|
+
Interfaces: ContextAnalyzer.is_acceptable_context(node, parent, file_path, config) -> bool,
|
|
20
|
+
various helper methods for specific context checks
|
|
21
|
+
|
|
22
|
+
Implementation: AST parent node inspection, pattern matching for acceptable contexts, configurable
|
|
23
|
+
max_small_integer threshold for range detection
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import ast
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ContextAnalyzer: # thailint: ignore[srp]
|
|
31
|
+
"""Analyzes contexts to determine if numeric literals are acceptable."""
|
|
32
|
+
|
|
33
|
+
def is_acceptable_context(
|
|
34
|
+
self,
|
|
35
|
+
node: ast.Constant,
|
|
36
|
+
parent: ast.AST | None,
|
|
37
|
+
file_path: Path | None,
|
|
38
|
+
config: dict,
|
|
39
|
+
) -> bool:
|
|
40
|
+
"""Check if a numeric literal is in an acceptable context.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
node: The numeric constant node
|
|
44
|
+
parent: The parent node in the AST
|
|
45
|
+
file_path: Path to the file being analyzed
|
|
46
|
+
config: Configuration with allowed_numbers and max_small_integer
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True if the context is acceptable and should not be flagged
|
|
50
|
+
"""
|
|
51
|
+
# File-level and definition checks
|
|
52
|
+
if self.is_test_file(file_path) or self.is_constant_definition(node, parent):
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
# Usage pattern checks
|
|
56
|
+
return self._is_acceptable_usage_pattern(node, parent, config)
|
|
57
|
+
|
|
58
|
+
def _is_acceptable_usage_pattern(
|
|
59
|
+
self, node: ast.Constant, parent: ast.AST | None, config: dict
|
|
60
|
+
) -> bool:
|
|
61
|
+
"""Check if numeric literal is in acceptable usage pattern.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
node: The numeric constant node
|
|
65
|
+
parent: The parent node in the AST
|
|
66
|
+
config: Configuration with max_small_integer threshold
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if usage pattern is acceptable
|
|
70
|
+
"""
|
|
71
|
+
if self.is_small_integer_in_range(node, parent, config):
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
if self.is_small_integer_in_enumerate(node, parent, config):
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
return self.is_string_repetition(node, parent)
|
|
78
|
+
|
|
79
|
+
def is_test_file(self, file_path: Path | None) -> bool:
|
|
80
|
+
"""Check if the file is a test file.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
file_path: Path to the file
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if the file is a test file (matches test_*.py pattern)
|
|
87
|
+
"""
|
|
88
|
+
if not file_path:
|
|
89
|
+
return False
|
|
90
|
+
return file_path.name.startswith("test_") or "_test.py" in file_path.name
|
|
91
|
+
|
|
92
|
+
def is_constant_definition(self, node: ast.Constant, parent: ast.AST | None) -> bool:
|
|
93
|
+
"""Check if the number is part of an UPPERCASE constant definition.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
node: The numeric constant node
|
|
97
|
+
parent: The parent node in the AST
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if this is a constant definition
|
|
101
|
+
"""
|
|
102
|
+
if not self._is_assignment_node(parent):
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
# Type narrowing: parent is ast.Assign after the check above
|
|
106
|
+
assert isinstance(parent, ast.Assign) # nosec B101
|
|
107
|
+
return self._has_constant_target(parent)
|
|
108
|
+
|
|
109
|
+
def _is_assignment_node(self, parent: ast.AST | None) -> bool:
|
|
110
|
+
"""Check if parent is an assignment node."""
|
|
111
|
+
return parent is not None and isinstance(parent, ast.Assign)
|
|
112
|
+
|
|
113
|
+
def _has_constant_target(self, parent: ast.Assign) -> bool:
|
|
114
|
+
"""Check if assignment has uppercase constant target."""
|
|
115
|
+
return any(
|
|
116
|
+
isinstance(target, ast.Name) and self._is_constant_name(target.id)
|
|
117
|
+
for target in parent.targets
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _is_constant_name(self, name: str) -> bool:
|
|
121
|
+
"""Check if a name follows constant naming convention.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
name: Variable name to check
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if the name is UPPERCASE (constant convention)
|
|
128
|
+
"""
|
|
129
|
+
return name.isupper() and len(name) > 1
|
|
130
|
+
|
|
131
|
+
def is_small_integer_in_range(
|
|
132
|
+
self, node: ast.Constant, parent: ast.AST | None, config: dict
|
|
133
|
+
) -> bool:
|
|
134
|
+
"""Check if this is a small integer used in range() call.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
node: The numeric constant node
|
|
138
|
+
parent: The parent node in the AST
|
|
139
|
+
config: Configuration with max_small_integer threshold
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if this is a small integer in range()
|
|
143
|
+
"""
|
|
144
|
+
if not isinstance(node.value, int):
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
max_small_int = config.get("max_small_integer", 10)
|
|
148
|
+
if not 0 <= node.value <= max_small_int:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
return self._is_in_range_call(parent)
|
|
152
|
+
|
|
153
|
+
def is_small_integer_in_enumerate(
|
|
154
|
+
self, node: ast.Constant, parent: ast.AST | None, config: dict
|
|
155
|
+
) -> bool:
|
|
156
|
+
"""Check if this is a small integer used in enumerate() call.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
node: The numeric constant node
|
|
160
|
+
parent: The parent node in the AST
|
|
161
|
+
config: Configuration with max_small_integer threshold
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if this is a small integer in enumerate()
|
|
165
|
+
"""
|
|
166
|
+
if not isinstance(node.value, int):
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
max_small_int = config.get("max_small_integer", 10)
|
|
170
|
+
if not 0 <= node.value <= max_small_int:
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
return self._is_in_enumerate_call(parent)
|
|
174
|
+
|
|
175
|
+
def _is_in_range_call(self, parent: ast.AST | None) -> bool:
|
|
176
|
+
"""Check if the parent is a range() call.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
parent: The parent node
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if parent is range() call
|
|
183
|
+
"""
|
|
184
|
+
return (
|
|
185
|
+
isinstance(parent, ast.Call)
|
|
186
|
+
and isinstance(parent.func, ast.Name)
|
|
187
|
+
and parent.func.id == "range"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def _is_in_enumerate_call(self, parent: ast.AST | None) -> bool:
|
|
191
|
+
"""Check if the parent is an enumerate() call.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
parent: The parent node
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
True if parent is enumerate() call
|
|
198
|
+
"""
|
|
199
|
+
return (
|
|
200
|
+
isinstance(parent, ast.Call)
|
|
201
|
+
and isinstance(parent.func, ast.Name)
|
|
202
|
+
and parent.func.id == "enumerate"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def is_string_repetition(self, node: ast.Constant, parent: ast.AST | None) -> bool:
|
|
206
|
+
"""Check if this number is used in string repetition (e.g., "-" * 40).
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
node: The numeric constant node
|
|
210
|
+
parent: The parent node in the AST
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
True if this is a string repetition pattern
|
|
214
|
+
"""
|
|
215
|
+
if not isinstance(node.value, int):
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
if not isinstance(parent, ast.BinOp):
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
if not isinstance(parent.op, ast.Mult):
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
# Check if either operand is a string constant
|
|
225
|
+
return self._has_string_operand(parent)
|
|
226
|
+
|
|
227
|
+
def _has_string_operand(self, binop: ast.BinOp) -> bool:
|
|
228
|
+
"""Check if binary operation has a string operand.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
binop: Binary operation node
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
True if either left or right operand is a string constant
|
|
235
|
+
"""
|
|
236
|
+
return self._is_string_constant(binop.left) or self._is_string_constant(binop.right)
|
|
237
|
+
|
|
238
|
+
def _is_string_constant(self, node: ast.AST) -> bool:
|
|
239
|
+
"""Check if a node is a string constant.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
node: AST node to check
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
True if node is a Constant with string value
|
|
246
|
+
"""
|
|
247
|
+
return isinstance(node, ast.Constant) and isinstance(node.value, str)
|