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,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
- self.config = config_obj.get("file-placement", config_obj)
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)