thailint 0.7.0__py3-none-any.whl → 0.9.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 (41) hide show
  1. src/cli.py +233 -1
  2. src/core/base.py +4 -0
  3. src/core/rule_discovery.py +110 -84
  4. src/core/violation_builder.py +75 -15
  5. src/linter_config/loader.py +45 -12
  6. src/linters/dry/block_filter.py +15 -8
  7. src/linters/dry/block_grouper.py +4 -0
  8. src/linters/dry/cache.py +3 -2
  9. src/linters/dry/cache_query.py +4 -0
  10. src/linters/dry/duplicate_storage.py +5 -4
  11. src/linters/dry/token_hasher.py +5 -1
  12. src/linters/dry/violation_builder.py +4 -0
  13. src/linters/dry/violation_filter.py +4 -0
  14. src/linters/dry/violation_generator.py +1 -1
  15. src/linters/file_header/bash_parser.py +4 -0
  16. src/linters/file_placement/directory_matcher.py +4 -0
  17. src/linters/file_placement/pattern_matcher.py +4 -0
  18. src/linters/file_placement/pattern_validator.py +4 -0
  19. src/linters/magic_numbers/context_analyzer.py +4 -0
  20. src/linters/magic_numbers/typescript_analyzer.py +4 -0
  21. src/linters/method_property/__init__.py +49 -0
  22. src/linters/method_property/config.py +135 -0
  23. src/linters/method_property/linter.py +419 -0
  24. src/linters/method_property/python_analyzer.py +472 -0
  25. src/linters/method_property/violation_builder.py +116 -0
  26. src/linters/nesting/python_analyzer.py +4 -0
  27. src/linters/nesting/typescript_function_extractor.py +4 -0
  28. src/linters/print_statements/typescript_analyzer.py +4 -0
  29. src/linters/srp/class_analyzer.py +4 -0
  30. src/linters/srp/python_analyzer.py +55 -20
  31. src/linters/srp/typescript_metrics_calculator.py +83 -47
  32. src/linters/srp/violation_builder.py +4 -0
  33. src/linters/stateless_class/__init__.py +25 -0
  34. src/linters/stateless_class/config.py +58 -0
  35. src/linters/stateless_class/linter.py +355 -0
  36. src/linters/stateless_class/python_analyzer.py +299 -0
  37. {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/METADATA +119 -3
  38. {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/RECORD +41 -32
  39. {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/WHEEL +0 -0
  40. {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/entry_points.txt +0 -0
  41. {thailint-0.7.0.dist-info → thailint-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -16,9 +16,9 @@ Overview: Loads linter configuration from .thailint.yaml or .thailint.json files
16
16
  Dependencies: PyYAML for YAML parsing with safe_load(), json (stdlib) for JSON parsing,
17
17
  pathlib for file path handling
18
18
 
19
- Exports: LinterConfigLoader class
19
+ Exports: load_config function, get_defaults function, LinterConfigLoader class (compat)
20
20
 
21
- Interfaces: load(config_path: Path) -> dict[str, Any] for loading config files,
21
+ Interfaces: load_config(config_path: Path) -> dict[str, Any] for loading config files,
22
22
  get_defaults() -> dict[str, Any] for default configuration structure
23
23
 
24
24
  Implementation: Extension-based format detection (.yaml/.yml vs .json), yaml.safe_load()
@@ -31,13 +31,51 @@ from typing import Any
31
31
  from src.core.config_parser import parse_config_file
32
32
 
33
33
 
34
+ def get_defaults() -> dict[str, Any]:
35
+ """Get default configuration.
36
+
37
+ Returns:
38
+ Default configuration with empty rules and ignore lists.
39
+ """
40
+ return {
41
+ "rules": {},
42
+ "ignore": [],
43
+ }
44
+
45
+
46
+ def load_config(config_path: Path) -> dict[str, Any]:
47
+ """Load configuration from file.
48
+
49
+ Args:
50
+ config_path: Path to YAML or JSON config file.
51
+
52
+ Returns:
53
+ Configuration dictionary.
54
+
55
+ Raises:
56
+ ConfigParseError: If file format is unsupported or parsing fails.
57
+ """
58
+ if not config_path.exists():
59
+ return get_defaults()
60
+
61
+ return parse_config_file(config_path)
62
+
63
+
64
+ # Legacy class wrapper for backward compatibility
34
65
  class LinterConfigLoader:
35
66
  """Load linter configuration from YAML or JSON files.
36
67
 
37
68
  Supports loading from .thailint.yaml, .thailint.json, or custom paths.
38
69
  Provides sensible defaults when config files don't exist.
70
+
71
+ Note: This class is a thin wrapper around module-level functions
72
+ for backward compatibility.
39
73
  """
40
74
 
75
+ def __init__(self) -> None:
76
+ """Initialize the loader."""
77
+ pass # No state needed
78
+
41
79
  def load(self, config_path: Path) -> dict[str, Any]:
42
80
  """Load configuration from file.
43
81
 
@@ -50,18 +88,13 @@ class LinterConfigLoader:
50
88
  Raises:
51
89
  ConfigParseError: If file format is unsupported or parsing fails.
52
90
  """
53
- if not config_path.exists():
54
- return self.get_defaults()
55
-
56
- return parse_config_file(config_path)
91
+ return load_config(config_path)
57
92
 
58
- def get_defaults(self) -> dict[str, Any]:
59
- """Get default configuration.
93
+ @property
94
+ def defaults(self) -> dict[str, Any]:
95
+ """Default configuration.
60
96
 
61
97
  Returns:
62
98
  Default configuration with empty rules and ignore lists.
63
99
  """
64
- return {
65
- "rules": {},
66
- "ignore": [],
67
- }
100
+ return get_defaults()
@@ -53,9 +53,10 @@ class BaseBlockFilter(ABC):
53
53
  """
54
54
  pass
55
55
 
56
+ @property
56
57
  @abstractmethod
57
- def get_name(self) -> str:
58
- """Get filter name for configuration and logging."""
58
+ def name(self) -> str:
59
+ """Filter name for configuration and logging."""
59
60
  pass
60
61
 
61
62
 
@@ -152,8 +153,9 @@ class KeywordArgumentFilter(BaseBlockFilter):
152
153
  return False
153
154
  return True
154
155
 
155
- def get_name(self) -> str:
156
- """Get filter name."""
156
+ @property
157
+ def name(self) -> str:
158
+ """Filter name."""
157
159
  return "keyword_argument_filter"
158
160
 
159
161
 
@@ -163,6 +165,10 @@ class ImportGroupFilter(BaseBlockFilter):
163
165
  Import organization often creates similar patterns that aren't meaningful duplication.
164
166
  """
165
167
 
168
+ def __init__(self) -> None:
169
+ """Initialize the import group filter."""
170
+ pass # Stateless filter for import blocks
171
+
166
172
  def should_filter(self, block: CodeBlock, file_content: str) -> bool:
167
173
  """Check if block is only import statements.
168
174
 
@@ -184,8 +190,9 @@ class ImportGroupFilter(BaseBlockFilter):
184
190
 
185
191
  return True
186
192
 
187
- def get_name(self) -> str:
188
- """Get filter name."""
193
+ @property
194
+ def name(self) -> str:
195
+ """Filter name."""
189
196
  return "import_group_filter"
190
197
 
191
198
 
@@ -204,7 +211,7 @@ class BlockFilterRegistry:
204
211
  filter_instance: Filter to register
205
212
  """
206
213
  self._filters.append(filter_instance)
207
- self._enabled_filters.add(filter_instance.get_name())
214
+ self._enabled_filters.add(filter_instance.name)
208
215
 
209
216
  def enable_filter(self, filter_name: str) -> None:
210
217
  """Enable a specific filter by name.
@@ -233,7 +240,7 @@ class BlockFilterRegistry:
233
240
  True if block should be filtered out
234
241
  """
235
242
  for filter_instance in self._filters:
236
- if filter_instance.get_name() not in self._enabled_filters:
243
+ if filter_instance.name not in self._enabled_filters:
237
244
  continue
238
245
 
239
246
  if filter_instance.should_filter(block, file_content):
@@ -26,6 +26,10 @@ from .cache import CodeBlock
26
26
  class BlockGrouper:
27
27
  """Groups blocks and violations by file path."""
28
28
 
29
+ def __init__(self) -> None:
30
+ """Initialize the block grouper."""
31
+ pass # Stateless grouper for code blocks
32
+
29
33
  def group_blocks_by_file(self, blocks: list[CodeBlock]) -> dict[Path, list[CodeBlock]]:
30
34
  """Group blocks by file path.
31
35
 
src/linters/dry/cache.py CHANGED
@@ -157,8 +157,9 @@ class DRYCache:
157
157
 
158
158
  return blocks
159
159
 
160
- def get_duplicate_hashes(self) -> list[int]:
161
- """Get all hash values that appear 2+ times.
160
+ @property
161
+ def duplicate_hashes(self) -> list[int]:
162
+ """Hash values that appear 2+ times.
162
163
 
163
164
  Returns:
164
165
  List of hash values with 2 or more occurrences
@@ -22,6 +22,10 @@ import sqlite3
22
22
  class CacheQueryService:
23
23
  """Handles cache database queries."""
24
24
 
25
+ def __init__(self) -> None:
26
+ """Initialize the cache query service."""
27
+ pass # Stateless query service for database operations
28
+
25
29
  def get_duplicate_hashes(self, db: sqlite3.Connection) -> list[int]:
26
30
  """Get all hash values that appear 2+ times.
27
31
 
@@ -11,7 +11,7 @@ Dependencies: DRYCache, CodeBlock, Path
11
11
 
12
12
  Exports: DuplicateStorage class
13
13
 
14
- Interfaces: DuplicateStorage.add_blocks(file_path, blocks), get_duplicate_hashes(),
14
+ Interfaces: DuplicateStorage.add_blocks(file_path, blocks), duplicate_hashes property,
15
15
  get_blocks_for_hash(hash_value)
16
16
 
17
17
  Implementation: Delegates to SQLite cache for all storage operations
@@ -43,13 +43,14 @@ class DuplicateStorage:
43
43
  if blocks:
44
44
  self._cache.add_blocks(file_path, blocks)
45
45
 
46
- def get_duplicate_hashes(self) -> list[int]:
47
- """Get all hash values with 2+ occurrences from SQLite.
46
+ @property
47
+ def duplicate_hashes(self) -> list[int]:
48
+ """Hash values with 2+ occurrences from SQLite.
48
49
 
49
50
  Returns:
50
51
  List of hash values that appear in multiple blocks
51
52
  """
52
- return self._cache.get_duplicate_hashes()
53
+ return self._cache.duplicate_hashes
53
54
 
54
55
  def get_blocks_for_hash(self, hash_value: int) -> list[CodeBlock]:
55
56
  """Get all blocks with given hash value from SQLite.
@@ -20,9 +20,13 @@ Implementation: Token-based normalization with rolling window algorithm, languag
20
20
  """
21
21
 
22
22
 
23
- class TokenHasher:
23
+ class TokenHasher: # thailint: ignore[srp] - Methods support single responsibility of code tokenization
24
24
  """Tokenize code and create rolling hashes for duplicate detection."""
25
25
 
26
+ def __init__(self) -> None:
27
+ """Initialize the token hasher."""
28
+ pass # Stateless hasher for code tokenization
29
+
26
30
  def tokenize(self, code: str) -> list[str]:
27
31
  """Tokenize code by stripping comments and normalizing whitespace.
28
32
 
@@ -27,6 +27,10 @@ from .cache import CodeBlock
27
27
  class DRYViolationBuilder:
28
28
  """Builds violation messages for duplicate code."""
29
29
 
30
+ def __init__(self) -> None:
31
+ """Initialize the DRY violation builder."""
32
+ pass # Stateless builder for duplicate code violations
33
+
30
34
  def build_violation(
31
35
  self, block: CodeBlock, all_duplicates: list[CodeBlock], rule_id: str
32
36
  ) -> Violation:
@@ -25,6 +25,10 @@ DEFAULT_FALLBACK_LINE_COUNT = 5
25
25
  class ViolationFilter:
26
26
  """Filters overlapping violations."""
27
27
 
28
+ def __init__(self) -> None:
29
+ """Initialize the violation filter."""
30
+ pass # Stateless filter for overlapping violations
31
+
28
32
  def filter_overlapping(self, sorted_violations: list[Violation]) -> list[Violation]:
29
33
  """Filter overlapping violations, keeping first occurrence.
30
34
 
@@ -55,7 +55,7 @@ class ViolationGenerator:
55
55
  Returns:
56
56
  List of violations filtered by ignore patterns and inline directives
57
57
  """
58
- duplicate_hashes = storage.get_duplicate_hashes()
58
+ duplicate_hashes = storage.duplicate_hashes
59
59
  violations = []
60
60
 
61
61
  for hash_value in duplicate_hashes:
@@ -24,6 +24,10 @@ from src.linters.file_header.base_parser import BaseHeaderParser
24
24
  class BashHeaderParser(BaseHeaderParser):
25
25
  """Extracts and parses Bash file headers from comment blocks."""
26
26
 
27
+ def __init__(self) -> None:
28
+ """Initialize the Bash header parser."""
29
+ pass # BaseHeaderParser has no __init__, but we need this for stateless-class
30
+
27
31
  def extract_header(self, code: str) -> str | None:
28
32
  """Extract comment header from Bash script."""
29
33
  if not code or not code.strip():
@@ -23,6 +23,10 @@ from typing import Any
23
23
  class DirectoryMatcher:
24
24
  """Finds matching directory rules based on path prefixes."""
25
25
 
26
+ def __init__(self) -> None:
27
+ """Initialize the directory matcher."""
28
+ pass # Stateless matcher for directory rules
29
+
26
30
  def find_matching_rule(
27
31
  self, path_str: str, directories: dict[str, Any]
28
32
  ) -> tuple[dict[str, Any] | None, str | None]:
@@ -23,6 +23,10 @@ import re
23
23
  class PatternMatcher:
24
24
  """Handles regex pattern matching for file paths."""
25
25
 
26
+ def __init__(self) -> None:
27
+ """Initialize the pattern matcher."""
28
+ pass # Stateless matcher for regex patterns
29
+
26
30
  def match_deny_patterns(
27
31
  self, path_str: str, deny_patterns: list[dict[str, str]]
28
32
  ) -> tuple[bool, str | None]:
@@ -24,6 +24,10 @@ from typing import Any
24
24
  class PatternValidator:
25
25
  """Validates regex patterns in file placement configuration."""
26
26
 
27
+ def __init__(self) -> None:
28
+ """Initialize the pattern validator."""
29
+ pass # Stateless validator for regex patterns
30
+
27
31
  def validate_config(self, config: dict[str, Any]) -> None:
28
32
  """Validate all regex patterns in configuration.
29
33
 
@@ -30,6 +30,10 @@ from pathlib import Path
30
30
  class ContextAnalyzer: # thailint: ignore[srp]
31
31
  """Analyzes contexts to determine if numeric literals are acceptable."""
32
32
 
33
+ def __init__(self) -> None:
34
+ """Initialize the context analyzer."""
35
+ pass # Stateless analyzer for context checking
36
+
33
37
  def is_acceptable_context(
34
38
  self,
35
39
  node: ast.Constant,
@@ -44,6 +44,10 @@ class TypeScriptMagicNumberAnalyzer(TypeScriptBaseAnalyzer): # thailint: ignore
44
44
  of TypeScript magic number detection - all methods support this core purpose.
45
45
  """
46
46
 
47
+ def __init__(self) -> None: # pylint: disable=useless-parent-delegation
48
+ """Initialize the TypeScript magic number analyzer."""
49
+ super().__init__() # Sets self.tree_sitter_available from base class
50
+
47
51
  def find_numeric_literals(self, root_node: Node) -> list[tuple[Node, float | int, int]]:
48
52
  """Find all numeric literal nodes in TypeScript/JavaScript AST.
49
53
 
@@ -0,0 +1,49 @@
1
+ """
2
+ Purpose: Package exports for method-should-be-property linter
3
+
4
+ Scope: Method property linter public API
5
+
6
+ Overview: Exports the MethodPropertyRule class and MethodPropertyConfig dataclass for use by
7
+ the orchestrator and external consumers. Provides a convenience lint() function for
8
+ standalone usage of the linter.
9
+
10
+ Dependencies: MethodPropertyRule from linter module, MethodPropertyConfig from config module
11
+
12
+ Exports: MethodPropertyRule, MethodPropertyConfig, lint function
13
+
14
+ Interfaces: lint(file_path, content, config) -> list[Violation] convenience function
15
+
16
+ Implementation: Simple re-exports from submodules with optional convenience wrapper
17
+ """
18
+
19
+ from .config import MethodPropertyConfig
20
+ from .linter import MethodPropertyRule
21
+
22
+ __all__ = ["MethodPropertyRule", "MethodPropertyConfig", "lint"]
23
+
24
+
25
+ def lint(
26
+ file_path: str,
27
+ content: str,
28
+ config: dict | None = None,
29
+ ) -> list:
30
+ """Lint a file for method-should-be-property violations.
31
+
32
+ Args:
33
+ file_path: Path to the file being linted
34
+ content: Content of the file
35
+ config: Optional configuration dictionary
36
+
37
+ Returns:
38
+ List of Violation objects
39
+ """
40
+ from unittest.mock import Mock
41
+
42
+ rule = MethodPropertyRule()
43
+ context = Mock()
44
+ context.file_path = file_path
45
+ context.file_content = content
46
+ context.language = "python"
47
+ context.config = config
48
+
49
+ return rule.check(context)
@@ -0,0 +1,135 @@
1
+ """
2
+ Purpose: Configuration schema for method-should-be-property linter
3
+
4
+ Scope: Method property linter configuration for Python files
5
+
6
+ Overview: Defines configuration schema for method-should-be-property linter. Provides
7
+ MethodPropertyConfig dataclass with enabled flag, max_body_statements threshold (default 3)
8
+ for determining when a method body is too complex to be a property candidate, and ignore
9
+ patterns list for excluding specific files or directories. Includes configurable action verb
10
+ exclusions (prefixes and names) with sensible defaults that can be extended or overridden.
11
+ Supports per-file and per-directory config overrides through from_dict class method.
12
+ Integrates with orchestrator's configuration system via .thailint.yaml.
13
+
14
+ Dependencies: dataclasses module for configuration structure, typing module for type hints
15
+
16
+ Exports: MethodPropertyConfig dataclass, DEFAULT_EXCLUDE_PREFIXES, DEFAULT_EXCLUDE_NAMES
17
+
18
+ Interfaces: from_dict(config, language) -> MethodPropertyConfig for configuration loading
19
+
20
+ Implementation: Dataclass with defaults matching Pythonic conventions and common use cases
21
+ """
22
+
23
+ from dataclasses import dataclass, field
24
+ from typing import Any
25
+
26
+ # Default action verb prefixes - methods starting with these are excluded
27
+ # These represent actions/transformations, not property access
28
+ DEFAULT_EXCLUDE_PREFIXES: tuple[str, ...] = (
29
+ "to_", # Transformation: to_dict, to_json, to_string
30
+ "dump_", # Serialization: dump_to_json, dump_to_apigw
31
+ "generate_", # Factory: generate_report, generate_html
32
+ "create_", # Factory: create_instance, create_config
33
+ "build_", # Construction: build_query, build_html
34
+ "make_", # Factory: make_request, make_connection
35
+ "render_", # Output: render_template, render_html
36
+ "compute_", # Calculation: compute_hash, compute_total
37
+ "calculate_", # Calculation: calculate_sum, calculate_average
38
+ )
39
+
40
+ # Default action verb names - exact method names that are excluded
41
+ # These are lifecycle hooks, display actions, and resource operations
42
+ DEFAULT_EXCLUDE_NAMES: frozenset[str] = frozenset(
43
+ {
44
+ "finalize", # Lifecycle hook
45
+ "serialize", # Transformation
46
+ "dump", # Serialization
47
+ "validate", # Validation action
48
+ "show", # Display action
49
+ "display", # Display action
50
+ "print", # Output action
51
+ "refresh", # Update action
52
+ "reset", # State action
53
+ "clear", # State action
54
+ "close", # Resource action
55
+ "open", # Resource action
56
+ "save", # Persistence action
57
+ "load", # Persistence action
58
+ "execute", # Action
59
+ "run", # Action
60
+ }
61
+ )
62
+
63
+
64
+ def _load_list_config(
65
+ config: dict[str, Any], key: str, override_key: str, default: tuple[str, ...]
66
+ ) -> tuple[str, ...]:
67
+ """Load a list config with extend/override semantics."""
68
+ if override_key in config and isinstance(config[override_key], list):
69
+ return tuple(config[override_key])
70
+ if key in config and isinstance(config[key], list):
71
+ return default + tuple(config[key])
72
+ return default
73
+
74
+
75
+ def _load_set_config(
76
+ config: dict[str, Any], key: str, override_key: str, default: frozenset[str]
77
+ ) -> frozenset[str]:
78
+ """Load a set config with extend/override semantics."""
79
+ if override_key in config and isinstance(config[override_key], list):
80
+ return frozenset(config[override_key])
81
+ if key in config and isinstance(config[key], list):
82
+ return default | frozenset(config[key])
83
+ return default
84
+
85
+
86
+ @dataclass
87
+ class MethodPropertyConfig: # thailint: ignore[dry]
88
+ """Configuration for method-should-be-property linter."""
89
+
90
+ enabled: bool = True
91
+ max_body_statements: int = 3
92
+ ignore: list[str] = field(default_factory=list)
93
+ ignore_methods: list[str] = field(default_factory=list)
94
+
95
+ # Action verb exclusions (extend defaults or override)
96
+ exclude_prefixes: tuple[str, ...] = DEFAULT_EXCLUDE_PREFIXES
97
+ exclude_names: frozenset[str] = DEFAULT_EXCLUDE_NAMES
98
+
99
+ # dry: ignore-block
100
+ @classmethod
101
+ def from_dict(
102
+ cls, config: dict[str, Any] | None, language: str | None = None
103
+ ) -> "MethodPropertyConfig":
104
+ """Load configuration from dictionary.
105
+
106
+ Args:
107
+ config: Dictionary containing configuration values, or None
108
+ language: Programming language (unused, for interface compatibility)
109
+
110
+ Returns:
111
+ MethodPropertyConfig instance with values from dictionary
112
+ """
113
+ if config is None:
114
+ return cls()
115
+
116
+ ignore_patterns = config.get("ignore", [])
117
+ if not isinstance(ignore_patterns, list):
118
+ ignore_patterns = []
119
+
120
+ ignore_methods = config.get("ignore_methods", [])
121
+ if not isinstance(ignore_methods, list):
122
+ ignore_methods = []
123
+
124
+ return cls(
125
+ enabled=config.get("enabled", True),
126
+ max_body_statements=config.get("max_body_statements", 3),
127
+ ignore=ignore_patterns,
128
+ ignore_methods=ignore_methods,
129
+ exclude_prefixes=_load_list_config(
130
+ config, "exclude_prefixes", "exclude_prefixes_override", DEFAULT_EXCLUDE_PREFIXES
131
+ ),
132
+ exclude_names=_load_set_config(
133
+ config, "exclude_names", "exclude_names_override", DEFAULT_EXCLUDE_NAMES
134
+ ),
135
+ )