thailint 0.2.1__py3-none-any.whl → 0.3.1__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 +101 -0
- src/config.py +6 -2
- src/core/base.py +90 -5
- 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 +12 -1
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/violation_filter.py +4 -1
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +71 -0
- src/linters/magic_numbers/context_analyzer.py +247 -0
- src/linters/magic_numbers/linter.py +452 -0
- src/linters/magic_numbers/python_analyzer.py +76 -0
- src/linters/magic_numbers/typescript_analyzer.py +217 -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/srp/__init__.py +3 -3
- src/linters/srp/config.py +12 -6
- src/linters/srp/linter.py +33 -24
- {thailint-0.2.1.dist-info → thailint-0.3.1.dist-info}/METADATA +196 -42
- {thailint-0.2.1.dist-info → thailint-0.3.1.dist-info}/RECORD +30 -23
- {thailint-0.2.1.dist-info → thailint-0.3.1.dist-info}/LICENSE +0 -0
- {thailint-0.2.1.dist-info → thailint-0.3.1.dist-info}/WHEEL +0 -0
- {thailint-0.2.1.dist-info → thailint-0.3.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Purpose: Storage management for duplicate code blocks
|
|
2
|
+
Purpose: Storage management for duplicate code blocks in SQLite
|
|
3
3
|
|
|
4
|
-
Scope: Manages storage of code blocks in SQLite
|
|
4
|
+
Scope: Manages storage of code blocks in SQLite for duplicate detection
|
|
5
5
|
|
|
6
|
-
Overview: Provides
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
concerns from linting logic to maintain SRP compliance.
|
|
6
|
+
Overview: Provides storage interface for code blocks using SQLite (in-memory or tempfile mode).
|
|
7
|
+
Handles block insertion and duplicate hash queries. Delegates all storage operations to
|
|
8
|
+
DRYCache SQLite layer. Separates storage concerns from linting logic to maintain SRP compliance.
|
|
10
9
|
|
|
11
10
|
Dependencies: DRYCache, CodeBlock, Path
|
|
12
11
|
|
|
@@ -15,7 +14,7 @@ Exports: DuplicateStorage class
|
|
|
15
14
|
Interfaces: DuplicateStorage.add_blocks(file_path, blocks), get_duplicate_hashes(),
|
|
16
15
|
get_blocks_for_hash(hash_value)
|
|
17
16
|
|
|
18
|
-
Implementation: Delegates to
|
|
17
|
+
Implementation: Delegates to SQLite cache for all storage operations
|
|
19
18
|
"""
|
|
20
19
|
|
|
21
20
|
from pathlib import Path
|
|
@@ -24,82 +23,36 @@ from .cache import CodeBlock, DRYCache
|
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
class DuplicateStorage:
|
|
27
|
-
"""Manages storage of code blocks in
|
|
26
|
+
"""Manages storage of code blocks in SQLite."""
|
|
28
27
|
|
|
29
|
-
def __init__(self, cache: DRYCache
|
|
30
|
-
"""Initialize storage with
|
|
28
|
+
def __init__(self, cache: DRYCache) -> None:
|
|
29
|
+
"""Initialize storage with SQLite cache.
|
|
31
30
|
|
|
32
31
|
Args:
|
|
33
|
-
cache: SQLite cache instance (
|
|
32
|
+
cache: SQLite cache instance (in-memory or tempfile mode)
|
|
34
33
|
"""
|
|
35
34
|
self._cache = cache
|
|
36
|
-
self._memory_store: dict[int, list[CodeBlock]] = {}
|
|
37
35
|
|
|
38
36
|
def add_blocks(self, file_path: Path, blocks: list[CodeBlock]) -> None:
|
|
39
|
-
"""Add code blocks to storage
|
|
37
|
+
"""Add code blocks to SQLite storage.
|
|
40
38
|
|
|
41
39
|
Args:
|
|
42
40
|
file_path: Path to source file
|
|
43
41
|
blocks: List of code blocks to store
|
|
44
42
|
"""
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# Also persist to cache if available
|
|
49
|
-
if self._cache:
|
|
50
|
-
self._add_to_cache(file_path, blocks)
|
|
51
|
-
|
|
52
|
-
def add_blocks_to_memory(self, file_path: Path, blocks: list[CodeBlock]) -> None:
|
|
53
|
-
"""Add code blocks to in-memory storage only (for cache hits).
|
|
54
|
-
|
|
55
|
-
Args:
|
|
56
|
-
file_path: Path to source file (used for cache persistence check)
|
|
57
|
-
blocks: List of code blocks to store
|
|
58
|
-
"""
|
|
59
|
-
# Add to memory for duplicate detection this run
|
|
60
|
-
self._add_to_memory(blocks)
|
|
61
|
-
|
|
62
|
-
# Guard clauses - early returns for skip conditions
|
|
63
|
-
if not self._cache:
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
if not blocks:
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
# Update cache with new blocks if needed (for fresh analysis)
|
|
70
|
-
self._update_cache_if_fresh(file_path, blocks)
|
|
71
|
-
|
|
72
|
-
def _update_cache_if_fresh(self, file_path: Path, blocks: list[CodeBlock]) -> None:
|
|
73
|
-
"""Update cache if file analysis is fresh (not from cache).
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
file_path: Path to source file
|
|
77
|
-
blocks: List of code blocks to store
|
|
78
|
-
"""
|
|
79
|
-
if not self._cache:
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
try:
|
|
83
|
-
mtime = file_path.stat().st_mtime
|
|
84
|
-
except OSError:
|
|
85
|
-
# File doesn't exist, skip cache
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
# File was analyzed (not cached), so persist if not fresh
|
|
89
|
-
if not self._cache.is_fresh(file_path, mtime):
|
|
90
|
-
self._add_to_cache(file_path, blocks)
|
|
43
|
+
if blocks:
|
|
44
|
+
self._cache.add_blocks(file_path, blocks)
|
|
91
45
|
|
|
92
46
|
def get_duplicate_hashes(self) -> list[int]:
|
|
93
|
-
"""Get all hash values with 2+ occurrences from
|
|
47
|
+
"""Get all hash values with 2+ occurrences from SQLite.
|
|
94
48
|
|
|
95
49
|
Returns:
|
|
96
50
|
List of hash values that appear in multiple blocks
|
|
97
51
|
"""
|
|
98
|
-
|
|
99
|
-
return [h for h, blocks in self._memory_store.items() if len(blocks) >= 2]
|
|
52
|
+
return self._cache.get_duplicate_hashes()
|
|
100
53
|
|
|
101
54
|
def get_blocks_for_hash(self, hash_value: int) -> list[CodeBlock]:
|
|
102
|
-
"""Get all blocks with given hash value from
|
|
55
|
+
"""Get all blocks with given hash value from SQLite.
|
|
103
56
|
|
|
104
57
|
Args:
|
|
105
58
|
hash_value: Hash to search for
|
|
@@ -107,20 +60,4 @@ class DuplicateStorage:
|
|
|
107
60
|
Returns:
|
|
108
61
|
List of code blocks with this hash
|
|
109
62
|
"""
|
|
110
|
-
|
|
111
|
-
return self._memory_store.get(hash_value, [])
|
|
112
|
-
|
|
113
|
-
def _add_to_cache(self, file_path: Path, blocks: list[CodeBlock]) -> None:
|
|
114
|
-
"""Add blocks to SQLite cache."""
|
|
115
|
-
if not self._cache or not blocks:
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
mtime = file_path.stat().st_mtime
|
|
119
|
-
self._cache.save(file_path, mtime, blocks)
|
|
120
|
-
|
|
121
|
-
def _add_to_memory(self, blocks: list[CodeBlock]) -> None:
|
|
122
|
-
"""Add blocks to in-memory store."""
|
|
123
|
-
for block in blocks:
|
|
124
|
-
if block.hash_value not in self._memory_store:
|
|
125
|
-
self._memory_store[block.hash_value] = []
|
|
126
|
-
self._memory_store[block.hash_value].append(block)
|
|
63
|
+
return self._cache.find_duplicates_by_hash(hash_value)
|
src/linters/dry/file_analyzer.py
CHANGED
|
@@ -1,45 +1,32 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Purpose: File analysis orchestration for duplicate detection
|
|
3
3
|
|
|
4
|
-
Scope: Coordinates language-specific analyzers
|
|
4
|
+
Scope: Coordinates language-specific analyzers
|
|
5
5
|
|
|
6
|
-
Overview: Orchestrates file analysis by delegating to language-specific analyzers (Python, TypeScript)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
SRP compliance.
|
|
6
|
+
Overview: Orchestrates file analysis by delegating to language-specific analyzers (Python, TypeScript).
|
|
7
|
+
Analyzes files fresh every run - no cache loading. Separates file analysis orchestration from
|
|
8
|
+
main linter rule logic to maintain SRP compliance.
|
|
10
9
|
|
|
11
|
-
Dependencies: PythonDuplicateAnalyzer, TypeScriptDuplicateAnalyzer,
|
|
10
|
+
Dependencies: PythonDuplicateAnalyzer, TypeScriptDuplicateAnalyzer, DRYConfig, CodeBlock
|
|
12
11
|
|
|
13
12
|
Exports: FileAnalyzer class
|
|
14
13
|
|
|
15
|
-
Interfaces: FileAnalyzer.
|
|
14
|
+
Interfaces: FileAnalyzer.analyze(file_path, content, language, config)
|
|
16
15
|
|
|
17
|
-
Implementation: Delegates to language-specific analyzers,
|
|
16
|
+
Implementation: Delegates to language-specific analyzers, always performs fresh analysis
|
|
18
17
|
"""
|
|
19
18
|
|
|
20
|
-
from dataclasses import dataclass
|
|
21
19
|
from pathlib import Path
|
|
22
20
|
|
|
23
21
|
from .block_filter import BlockFilterRegistry, create_default_registry
|
|
24
|
-
from .cache import CodeBlock
|
|
22
|
+
from .cache import CodeBlock
|
|
25
23
|
from .config import DRYConfig
|
|
26
24
|
from .python_analyzer import PythonDuplicateAnalyzer
|
|
27
25
|
from .typescript_analyzer import TypeScriptDuplicateAnalyzer
|
|
28
26
|
|
|
29
27
|
|
|
30
|
-
@dataclass
|
|
31
|
-
class FileAnalysisContext:
|
|
32
|
-
"""Context for file analysis."""
|
|
33
|
-
|
|
34
|
-
file_path: Path
|
|
35
|
-
content: str
|
|
36
|
-
language: str
|
|
37
|
-
config: DRYConfig
|
|
38
|
-
cache: DRYCache | None
|
|
39
|
-
|
|
40
|
-
|
|
41
28
|
class FileAnalyzer:
|
|
42
|
-
"""Orchestrates file analysis
|
|
29
|
+
"""Orchestrates file analysis for duplicate detection."""
|
|
43
30
|
|
|
44
31
|
def __init__(self, config: DRYConfig | None = None) -> None:
|
|
45
32
|
"""Initialize with language-specific analyzers.
|
|
@@ -77,49 +64,25 @@ class FileAnalyzer:
|
|
|
77
64
|
|
|
78
65
|
return registry
|
|
79
66
|
|
|
80
|
-
def
|
|
67
|
+
def analyze(
|
|
81
68
|
self,
|
|
82
69
|
file_path: Path,
|
|
83
70
|
content: str,
|
|
84
71
|
language: str,
|
|
85
72
|
config: DRYConfig,
|
|
86
|
-
cache: DRYCache | None = None,
|
|
87
73
|
) -> list[CodeBlock]:
|
|
88
|
-
"""Analyze file
|
|
74
|
+
"""Analyze file for duplicate code blocks.
|
|
89
75
|
|
|
90
76
|
Args:
|
|
91
77
|
file_path: Path to file
|
|
92
78
|
content: File content
|
|
93
79
|
language: File language
|
|
94
80
|
config: DRY configuration
|
|
95
|
-
cache: Optional cache instance
|
|
96
81
|
|
|
97
82
|
Returns:
|
|
98
83
|
List of CodeBlock instances
|
|
99
84
|
"""
|
|
100
|
-
# Check if file is fresh in cache
|
|
101
|
-
if cache:
|
|
102
|
-
mtime = file_path.stat().st_mtime
|
|
103
|
-
if cache.is_fresh(file_path, mtime):
|
|
104
|
-
return cache.load(file_path)
|
|
105
|
-
|
|
106
85
|
# Analyze file based on language
|
|
107
|
-
return self._analyze_file(file_path, content, language, config)
|
|
108
|
-
|
|
109
|
-
def _analyze_file(
|
|
110
|
-
self, file_path: Path, content: str, language: str, config: DRYConfig
|
|
111
|
-
) -> list[CodeBlock]:
|
|
112
|
-
"""Analyze file based on language.
|
|
113
|
-
|
|
114
|
-
Args:
|
|
115
|
-
file_path: Path to file
|
|
116
|
-
content: File content
|
|
117
|
-
language: File language
|
|
118
|
-
config: DRY configuration
|
|
119
|
-
|
|
120
|
-
Returns:
|
|
121
|
-
List of CodeBlock instances
|
|
122
|
-
"""
|
|
123
86
|
if language == "python":
|
|
124
87
|
return self._python_analyzer.analyze(file_path, content, config)
|
|
125
88
|
if language in ("typescript", "javascript"):
|
src/linters/dry/linter.py
CHANGED
|
@@ -37,7 +37,7 @@ from .storage_initializer import StorageInitializer
|
|
|
37
37
|
from .violation_generator import ViolationGenerator
|
|
38
38
|
|
|
39
39
|
if TYPE_CHECKING:
|
|
40
|
-
from .cache import CodeBlock
|
|
40
|
+
from .cache import CodeBlock
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
@dataclass
|
|
@@ -132,24 +132,17 @@ class DRYRule(BaseLintRule):
|
|
|
132
132
|
return # Should never happen after initialization
|
|
133
133
|
|
|
134
134
|
file_path = Path(context.file_path)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
file_path, context.file_content, context.language, config, cache
|
|
135
|
+
blocks = self._file_analyzer.analyze(
|
|
136
|
+
file_path, context.file_content, context.language, config
|
|
138
137
|
)
|
|
139
138
|
|
|
140
139
|
if blocks:
|
|
141
140
|
self._store_blocks(file_path, blocks)
|
|
142
141
|
|
|
143
|
-
def _get_cache(self) -> DRYCache | None:
|
|
144
|
-
"""Get cache from storage if available."""
|
|
145
|
-
if not self._storage:
|
|
146
|
-
return None
|
|
147
|
-
return self._storage._cache # pylint: disable=protected-access
|
|
148
|
-
|
|
149
142
|
def _store_blocks(self, file_path: Path, blocks: list[CodeBlock]) -> None:
|
|
150
|
-
"""Store blocks in
|
|
143
|
+
"""Store blocks in SQLite if storage available."""
|
|
151
144
|
if self._storage:
|
|
152
|
-
self._storage.
|
|
145
|
+
self._storage.add_blocks(file_path, blocks)
|
|
153
146
|
|
|
154
147
|
def finalize(self) -> list[Violation]:
|
|
155
148
|
"""Generate violations after all files processed.
|
|
@@ -38,6 +38,10 @@ from .block_filter import BlockFilterRegistry, create_default_registry
|
|
|
38
38
|
from .cache import CodeBlock
|
|
39
39
|
from .config import DRYConfig
|
|
40
40
|
|
|
41
|
+
# AST context checking constants
|
|
42
|
+
AST_LOOKBACK_LINES = 10
|
|
43
|
+
AST_LOOKFORWARD_LINES = 5
|
|
44
|
+
|
|
41
45
|
# Type alias for AST nodes that have line number attributes
|
|
42
46
|
# All stmt and expr nodes have lineno and end_lineno after parsing
|
|
43
47
|
ASTWithLineNumbers = ast.stmt | ast.expr
|
|
@@ -514,4 +518,11 @@ class PythonDuplicateAnalyzer(BaseTokenAnalyzer): # thailint: ignore[srp.violat
|
|
|
514
518
|
return True
|
|
515
519
|
return False
|
|
516
520
|
|
|
517
|
-
return self._check_ast_context(
|
|
521
|
+
return self._check_ast_context(
|
|
522
|
+
lines,
|
|
523
|
+
start_line,
|
|
524
|
+
end_line,
|
|
525
|
+
AST_LOOKBACK_LINES,
|
|
526
|
+
AST_LOOKFORWARD_LINES,
|
|
527
|
+
is_within_class_body,
|
|
528
|
+
)
|
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Purpose: Storage initialization for DRY linter
|
|
3
3
|
|
|
4
|
-
Scope: Initializes DuplicateStorage with
|
|
4
|
+
Scope: Initializes DuplicateStorage with SQLite storage
|
|
5
5
|
|
|
6
|
-
Overview: Handles storage initialization based on DRY configuration. Creates SQLite
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
Overview: Handles storage initialization based on DRY configuration. Creates SQLite storage in
|
|
7
|
+
either memory or tempfile mode based on config.storage_mode. Separates initialization logic
|
|
8
|
+
from main linter rule to maintain SRP compliance.
|
|
9
9
|
|
|
10
|
-
Dependencies: BaseLintContext, DRYConfig, DRYCache, DuplicateStorage
|
|
10
|
+
Dependencies: BaseLintContext, DRYConfig, DRYCache, DuplicateStorage
|
|
11
11
|
|
|
12
12
|
Exports: StorageInitializer class
|
|
13
13
|
|
|
14
14
|
Interfaces: StorageInitializer.initialize(context, config) -> DuplicateStorage
|
|
15
15
|
|
|
16
|
-
Implementation: Creates
|
|
16
|
+
Implementation: Creates DRYCache with storage_mode, delegates to DuplicateStorage for management
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
19
|
from src.core.base import BaseLintContext
|
|
22
20
|
|
|
23
21
|
from .cache import DRYCache
|
|
@@ -36,16 +34,9 @@ class StorageInitializer:
|
|
|
36
34
|
config: DRY configuration
|
|
37
35
|
|
|
38
36
|
Returns:
|
|
39
|
-
DuplicateStorage instance
|
|
37
|
+
DuplicateStorage instance with SQLite storage
|
|
40
38
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Use SQLite cache
|
|
44
|
-
metadata = getattr(context, "metadata", {})
|
|
45
|
-
project_root = metadata.get("_project_root", Path.cwd())
|
|
46
|
-
cache_path = project_root / config.cache_path
|
|
47
|
-
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
-
cache = DRYCache(cache_path)
|
|
49
|
-
# else: cache = None triggers in-memory fallback in DuplicateStorage
|
|
39
|
+
# Create SQLite storage (in-memory or tempfile based on config)
|
|
40
|
+
cache = DRYCache(storage_mode=config.storage_mode)
|
|
50
41
|
|
|
51
42
|
return DuplicateStorage(cache)
|
|
@@ -18,6 +18,9 @@ Implementation: Iterates through sorted violations, keeps first of each overlapp
|
|
|
18
18
|
|
|
19
19
|
from src.core.types import Violation
|
|
20
20
|
|
|
21
|
+
# Default fallback for line count when parsing fails
|
|
22
|
+
DEFAULT_FALLBACK_LINE_COUNT = 5
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
class ViolationFilter:
|
|
23
26
|
"""Filters overlapping violations."""
|
|
@@ -88,4 +91,4 @@ class ViolationFilter:
|
|
|
88
91
|
end = message.index(" lines")
|
|
89
92
|
return int(message[start:end])
|
|
90
93
|
except (ValueError, IndexError):
|
|
91
|
-
return
|
|
94
|
+
return DEFAULT_FALLBACK_LINE_COUNT # Default fallback
|
|
@@ -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,71 @@
|
|
|
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 0, 1, 2, -1, 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(default_factory=lambda: {-1, 0, 1, 2, 10, 100, 1000})
|
|
33
|
+
max_small_integer: int = 10
|
|
34
|
+
|
|
35
|
+
def __post_init__(self) -> None:
|
|
36
|
+
"""Validate configuration values."""
|
|
37
|
+
if self.max_small_integer <= 0:
|
|
38
|
+
raise ValueError(f"max_small_integer must be positive, got {self.max_small_integer}")
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "MagicNumberConfig":
|
|
42
|
+
"""Load configuration from dictionary with language-specific overrides.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
config: Dictionary containing configuration values
|
|
46
|
+
language: Programming language (python, typescript, javascript)
|
|
47
|
+
for language-specific settings
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
MagicNumberConfig instance with values from dictionary
|
|
51
|
+
"""
|
|
52
|
+
# Get language-specific config if available
|
|
53
|
+
if language and language in config:
|
|
54
|
+
lang_config = config[language]
|
|
55
|
+
allowed_numbers = set(
|
|
56
|
+
lang_config.get(
|
|
57
|
+
"allowed_numbers", config.get("allowed_numbers", {-1, 0, 1, 2, 10, 100, 1000})
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
max_small_integer = lang_config.get(
|
|
61
|
+
"max_small_integer", config.get("max_small_integer", 10)
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
allowed_numbers = set(config.get("allowed_numbers", {-1, 0, 1, 2, 10, 100, 1000}))
|
|
65
|
+
max_small_integer = config.get("max_small_integer", 10)
|
|
66
|
+
|
|
67
|
+
return cls(
|
|
68
|
+
enabled=config.get("enabled", True),
|
|
69
|
+
allowed_numbers=allowed_numbers,
|
|
70
|
+
max_small_integer=max_small_integer,
|
|
71
|
+
)
|