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.
@@ -1,12 +1,11 @@
1
1
  """
2
- Purpose: Storage management for duplicate code blocks with cache and memory fallback
2
+ Purpose: Storage management for duplicate code blocks in SQLite
3
3
 
4
- Scope: Manages storage of code blocks in SQLite cache or in-memory dict
4
+ Scope: Manages storage of code blocks in SQLite for duplicate detection
5
5
 
6
- Overview: Provides unified storage interface for code blocks supporting both SQLite-backed caching
7
- and in-memory fallback when cache disabled. Handles block insertion, retrieval, and duplicate
8
- hash queries. Encapsulates Decision 6 (in-memory fallback) implementation. Separates storage
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 either SQLite cache or in-memory dict based on cache_enabled setting
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 cache or memory."""
26
+ """Manages storage of code blocks in SQLite."""
28
27
 
29
- def __init__(self, cache: DRYCache | None) -> None:
30
- """Initialize storage with optional cache.
28
+ def __init__(self, cache: DRYCache) -> None:
29
+ """Initialize storage with SQLite cache.
31
30
 
32
31
  Args:
33
- cache: SQLite cache instance (None for in-memory mode)
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 and cache.
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
- # Always add to memory for duplicate detection
46
- self._add_to_memory(blocks)
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 memory.
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
- # Always query from in-memory store for this run's files
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 memory.
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
- # Always query from in-memory store for this run's files
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)
@@ -1,45 +1,32 @@
1
1
  """
2
2
  Purpose: File analysis orchestration for duplicate detection
3
3
 
4
- Scope: Coordinates language-specific analyzers and cache checking
4
+ Scope: Coordinates language-specific analyzers
5
5
 
6
- Overview: Orchestrates file analysis by delegating to language-specific analyzers (Python, TypeScript)
7
- and checking cache freshness. Handles cache hits by loading from cache, and cache misses by
8
- analyzing files. Separates file analysis orchestration from main linter rule logic to maintain
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, DRYCache, DRYConfig, CodeBlock
10
+ Dependencies: PythonDuplicateAnalyzer, TypeScriptDuplicateAnalyzer, DRYConfig, CodeBlock
12
11
 
13
12
  Exports: FileAnalyzer class
14
13
 
15
- Interfaces: FileAnalyzer.analyze_or_load(file_path, content, language, config, cache)
14
+ Interfaces: FileAnalyzer.analyze(file_path, content, language, config)
16
15
 
17
- Implementation: Delegates to language-specific analyzers, checks cache freshness
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, DRYCache
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 with cache support."""
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 analyze_or_load( # pylint: disable=too-many-arguments,too-many-positional-arguments
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 or load from cache.
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, DRYCache
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
- cache = self._get_cache()
136
- blocks = self._file_analyzer.analyze_or_load(
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 memory if storage available."""
143
+ """Store blocks in SQLite if storage available."""
151
144
  if self._storage:
152
- self._storage.add_blocks_to_memory(file_path, blocks)
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(lines, start_line, end_line, 10, 5, is_within_class_body)
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 cache or in-memory fallback
4
+ Scope: Initializes DuplicateStorage with SQLite storage
5
5
 
6
- Overview: Handles storage initialization based on DRY configuration. Creates SQLite cache when
7
- cache_enabled is true, or triggers in-memory fallback when false (Decision 6). Separates
8
- initialization logic from main linter rule to maintain SRP compliance.
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, Path
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 cache if enabled, delegates to DuplicateStorage for storage management
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
- cache = None
42
- if config.cache_enabled:
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 5 # Default fallback
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
+ )