thailint 0.1.5__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 (91) hide show
  1. src/__init__.py +7 -2
  2. src/analyzers/__init__.py +23 -0
  3. src/analyzers/typescript_base.py +148 -0
  4. src/api.py +1 -1
  5. src/cli.py +1111 -144
  6. src/config.py +12 -33
  7. src/core/base.py +102 -5
  8. src/core/cli_utils.py +206 -0
  9. src/core/config_parser.py +126 -0
  10. src/core/linter_utils.py +168 -0
  11. src/core/registry.py +17 -92
  12. src/core/rule_discovery.py +132 -0
  13. src/core/violation_builder.py +122 -0
  14. src/linter_config/ignore.py +112 -40
  15. src/linter_config/loader.py +3 -13
  16. src/linters/dry/__init__.py +23 -0
  17. src/linters/dry/base_token_analyzer.py +76 -0
  18. src/linters/dry/block_filter.py +265 -0
  19. src/linters/dry/block_grouper.py +59 -0
  20. src/linters/dry/cache.py +172 -0
  21. src/linters/dry/cache_query.py +61 -0
  22. src/linters/dry/config.py +134 -0
  23. src/linters/dry/config_loader.py +44 -0
  24. src/linters/dry/deduplicator.py +120 -0
  25. src/linters/dry/duplicate_storage.py +63 -0
  26. src/linters/dry/file_analyzer.py +90 -0
  27. src/linters/dry/inline_ignore.py +140 -0
  28. src/linters/dry/linter.py +163 -0
  29. src/linters/dry/python_analyzer.py +668 -0
  30. src/linters/dry/storage_initializer.py +42 -0
  31. src/linters/dry/token_hasher.py +169 -0
  32. src/linters/dry/typescript_analyzer.py +592 -0
  33. src/linters/dry/violation_builder.py +74 -0
  34. src/linters/dry/violation_filter.py +94 -0
  35. src/linters/dry/violation_generator.py +174 -0
  36. src/linters/file_header/__init__.py +24 -0
  37. src/linters/file_header/atemporal_detector.py +87 -0
  38. src/linters/file_header/config.py +66 -0
  39. src/linters/file_header/field_validator.py +69 -0
  40. src/linters/file_header/linter.py +313 -0
  41. src/linters/file_header/python_parser.py +86 -0
  42. src/linters/file_header/violation_builder.py +78 -0
  43. src/linters/file_placement/config_loader.py +86 -0
  44. src/linters/file_placement/directory_matcher.py +80 -0
  45. src/linters/file_placement/linter.py +262 -471
  46. src/linters/file_placement/path_resolver.py +61 -0
  47. src/linters/file_placement/pattern_matcher.py +55 -0
  48. src/linters/file_placement/pattern_validator.py +106 -0
  49. src/linters/file_placement/rule_checker.py +229 -0
  50. src/linters/file_placement/violation_factory.py +177 -0
  51. src/linters/magic_numbers/__init__.py +48 -0
  52. src/linters/magic_numbers/config.py +82 -0
  53. src/linters/magic_numbers/context_analyzer.py +247 -0
  54. src/linters/magic_numbers/linter.py +516 -0
  55. src/linters/magic_numbers/python_analyzer.py +76 -0
  56. src/linters/magic_numbers/typescript_analyzer.py +218 -0
  57. src/linters/magic_numbers/violation_builder.py +98 -0
  58. src/linters/nesting/__init__.py +6 -2
  59. src/linters/nesting/config.py +17 -4
  60. src/linters/nesting/linter.py +81 -168
  61. src/linters/nesting/typescript_analyzer.py +39 -102
  62. src/linters/nesting/typescript_function_extractor.py +130 -0
  63. src/linters/nesting/violation_builder.py +139 -0
  64. src/linters/print_statements/__init__.py +53 -0
  65. src/linters/print_statements/config.py +83 -0
  66. src/linters/print_statements/linter.py +430 -0
  67. src/linters/print_statements/python_analyzer.py +155 -0
  68. src/linters/print_statements/typescript_analyzer.py +135 -0
  69. src/linters/print_statements/violation_builder.py +98 -0
  70. src/linters/srp/__init__.py +99 -0
  71. src/linters/srp/class_analyzer.py +113 -0
  72. src/linters/srp/config.py +82 -0
  73. src/linters/srp/heuristics.py +89 -0
  74. src/linters/srp/linter.py +234 -0
  75. src/linters/srp/metrics_evaluator.py +47 -0
  76. src/linters/srp/python_analyzer.py +72 -0
  77. src/linters/srp/typescript_analyzer.py +75 -0
  78. src/linters/srp/typescript_metrics_calculator.py +90 -0
  79. src/linters/srp/violation_builder.py +117 -0
  80. src/orchestrator/core.py +54 -9
  81. src/templates/thailint_config_template.yaml +158 -0
  82. src/utils/__init__.py +4 -0
  83. src/utils/project_root.py +203 -0
  84. thailint-0.5.0.dist-info/METADATA +1286 -0
  85. thailint-0.5.0.dist-info/RECORD +96 -0
  86. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
  87. src/.ai/layout.yaml +0 -48
  88. thailint-0.1.5.dist-info/METADATA +0 -629
  89. thailint-0.1.5.dist-info/RECORD +0 -28
  90. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
  91. {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,218 @@
1
+ """
2
+ Purpose: TypeScript/JavaScript magic number detection using Tree-sitter AST analysis
3
+
4
+ Scope: Tree-sitter based numeric literal detection for TypeScript and JavaScript code
5
+
6
+ Overview: Analyzes TypeScript and JavaScript code to detect numeric literals that should be
7
+ extracted to named constants. Uses Tree-sitter parser to traverse TypeScript AST and
8
+ identify numeric literal nodes with their line numbers and values. Detects acceptable
9
+ contexts such as enum definitions and UPPERCASE constant declarations to avoid false
10
+ positives. Supports both TypeScript and JavaScript files with shared detection logic.
11
+ Handles TypeScript-specific syntax including enums, const assertions, readonly properties,
12
+ arrow functions, async functions, and class methods.
13
+
14
+ Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, tree-sitter Node type
15
+
16
+ Exports: TypeScriptMagicNumberAnalyzer class with find_numeric_literals and context detection
17
+
18
+ Interfaces: find_numeric_literals(root_node) -> list[tuple], is_enum_context(node),
19
+ is_constant_definition(node)
20
+
21
+ Implementation: Tree-sitter node traversal with visitor pattern, context-aware filtering
22
+ for acceptable numeric literal locations
23
+ """
24
+
25
+ from typing import Any
26
+
27
+ from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
28
+
29
+ # dry: ignore-block - tree-sitter import pattern (common across TypeScript analyzers)
30
+ try:
31
+ from tree_sitter import Node
32
+
33
+ TREE_SITTER_AVAILABLE = True
34
+ except ImportError:
35
+ TREE_SITTER_AVAILABLE = False
36
+ Node = Any # type: ignore
37
+
38
+
39
+ class TypeScriptMagicNumberAnalyzer(TypeScriptBaseAnalyzer): # thailint: ignore[srp]
40
+ """Analyzes TypeScript/JavaScript code for magic numbers using Tree-sitter.
41
+
42
+ Note: Method count (11) exceeds SRP limit (8) because refactoring for A-grade
43
+ complexity requires extracting helper methods. Class maintains single responsibility
44
+ of TypeScript magic number detection - all methods support this core purpose.
45
+ """
46
+
47
+ def find_numeric_literals(self, root_node: Node) -> list[tuple[Node, float | int, int]]:
48
+ """Find all numeric literal nodes in TypeScript/JavaScript AST.
49
+
50
+ Args:
51
+ root_node: Root tree-sitter node to search from
52
+
53
+ Returns:
54
+ List of (node, value, line_number) tuples for each numeric literal
55
+ """
56
+ if not TREE_SITTER_AVAILABLE or root_node is None:
57
+ return []
58
+
59
+ literals: list[tuple[Node, float | int, int]] = []
60
+ self._collect_numeric_literals(root_node, literals)
61
+ return literals
62
+
63
+ def _collect_numeric_literals(
64
+ self, node: Node, literals: list[tuple[Node, float | int, int]]
65
+ ) -> None:
66
+ """Recursively collect numeric literals from AST.
67
+
68
+ Args:
69
+ node: Current tree-sitter node
70
+ literals: List to accumulate found literals
71
+ """
72
+ if node.type == "number":
73
+ value = self._extract_numeric_value(node)
74
+ if value is not None:
75
+ line_number = node.start_point[0] + 1
76
+ literals.append((node, value, line_number))
77
+
78
+ for child in node.children:
79
+ self._collect_numeric_literals(child, literals)
80
+
81
+ def _extract_numeric_value(self, node: Node) -> float | int | None:
82
+ """Extract numeric value from number node.
83
+
84
+ Args:
85
+ node: Tree-sitter number node
86
+
87
+ Returns:
88
+ Numeric value (int or float) or None if parsing fails
89
+ """
90
+ text = self.extract_node_text(node)
91
+ try:
92
+ # Try int first
93
+ if "." not in text and "e" not in text.lower():
94
+ return int(text, 0) # Handles hex, octal, binary
95
+ # Otherwise float
96
+ return float(text)
97
+ except (ValueError, TypeError):
98
+ return None
99
+
100
+ def is_enum_context(self, node: Node) -> bool:
101
+ """Check if numeric literal is in enum definition.
102
+
103
+ Args:
104
+ node: Numeric literal node
105
+
106
+ Returns:
107
+ True if node is within enum_declaration
108
+ """
109
+ if not TREE_SITTER_AVAILABLE:
110
+ return False
111
+
112
+ current = node.parent
113
+ while current is not None:
114
+ if current.type == "enum_declaration":
115
+ return True
116
+ current = current.parent
117
+ return False
118
+
119
+ def is_constant_definition(self, node: Node, source_code: str) -> bool:
120
+ """Check if numeric literal is in UPPERCASE constant definition.
121
+
122
+ Args:
123
+ node: Numeric literal node
124
+ source_code: Full source code to extract variable names
125
+
126
+ Returns:
127
+ True if assigned to UPPERCASE constant variable
128
+ """
129
+ if not TREE_SITTER_AVAILABLE:
130
+ return False
131
+
132
+ # Find the declaration parent
133
+ parent = self._find_declaration_parent(node)
134
+ if parent is None:
135
+ return False
136
+
137
+ # Check if identifier is UPPERCASE constant
138
+ return self._has_uppercase_identifier(parent)
139
+
140
+ def _find_declaration_parent(self, node: Node) -> Node | None:
141
+ """Find the declaration parent node.
142
+
143
+ Args:
144
+ node: Starting node
145
+
146
+ Returns:
147
+ Declaration parent or None
148
+ """
149
+ parent = node.parent
150
+ if self._is_declaration_type(parent):
151
+ return parent
152
+
153
+ # Try grandparent for nested cases
154
+ if parent is not None:
155
+ grandparent = parent.parent
156
+ if self._is_declaration_type(grandparent):
157
+ return grandparent
158
+
159
+ return None
160
+
161
+ def _is_declaration_type(self, node: Node | None) -> bool:
162
+ """Check if node is a declaration type."""
163
+ if node is None:
164
+ return False
165
+ return node.type in ("variable_declarator", "lexical_declaration", "pair")
166
+
167
+ def _has_uppercase_identifier(self, parent_node: Node) -> bool:
168
+ """Check if declaration has UPPERCASE identifier.
169
+
170
+ Args:
171
+ parent_node: Declaration parent node
172
+
173
+ Returns:
174
+ True if identifier is UPPERCASE
175
+ """
176
+ identifier_node = self._find_identifier_in_declaration(parent_node)
177
+ if identifier_node is None:
178
+ return False
179
+
180
+ identifier_text = self.extract_node_text(identifier_node)
181
+ return self._is_uppercase_constant(identifier_text)
182
+
183
+ def _find_identifier_in_declaration(self, node: Node) -> Node | None:
184
+ """Find identifier node in variable declaration.
185
+
186
+ Args:
187
+ node: Variable declarator or lexical declaration node
188
+
189
+ Returns:
190
+ Identifier node or None
191
+ """
192
+ # Walk children looking for identifier
193
+ for child in node.children:
194
+ if child.type in ("identifier", "property_identifier"):
195
+ return child
196
+ # Recursively check children
197
+ result = self._find_identifier_in_declaration(child)
198
+ if result is not None:
199
+ return result
200
+ return None
201
+
202
+ def _is_uppercase_constant(self, name: str) -> bool:
203
+ """Check if identifier is UPPERCASE constant style.
204
+
205
+ Args:
206
+ name: Identifier name
207
+
208
+ Returns:
209
+ True if name is UPPERCASE with optional underscores
210
+ """
211
+ if not name:
212
+ return False
213
+ # Must be at least one letter and all letters must be uppercase
214
+ # Allow underscores and numbers
215
+ letters_only = "".join(c for c in name if c.isalpha())
216
+ if not letters_only:
217
+ return False
218
+ return letters_only.isupper()
@@ -0,0 +1,98 @@
1
+ """
2
+ Purpose: Builds Violation objects for magic number detection
3
+
4
+ Scope: Violation message construction for magic numbers linter
5
+
6
+ Overview: Provides ViolationBuilder class that creates Violation objects for magic number detections.
7
+ Generates helpful, descriptive messages suggesting constant extraction for numeric literals.
8
+ Constructs complete Violation instances with rule_id, file_path, line number, column, message,
9
+ and suggestions. Formats messages to mention the specific numeric value and encourage using
10
+ named constants for better code maintainability and readability. Provides consistent violation
11
+ structure across all magic number detections.
12
+
13
+ Dependencies: src.core.types for Violation dataclass, pathlib for Path handling, ast for node types
14
+
15
+ Exports: ViolationBuilder class
16
+
17
+ Interfaces: ViolationBuilder.create_violation(node, value, line, file_path) -> Violation,
18
+ builds complete Violation object with all required fields
19
+
20
+ Implementation: Message template with value interpolation, structured violation construction
21
+ """
22
+
23
+ import ast
24
+ from pathlib import Path
25
+
26
+ from src.core.types import Violation
27
+
28
+
29
+ class ViolationBuilder:
30
+ """Builds violations for magic number detections."""
31
+
32
+ def __init__(self, rule_id: str) -> None:
33
+ """Initialize the violation builder.
34
+
35
+ Args:
36
+ rule_id: The rule ID to use in violations
37
+ """
38
+ self.rule_id = rule_id
39
+
40
+ def create_violation(
41
+ self,
42
+ node: ast.Constant,
43
+ value: int | float,
44
+ line: int,
45
+ file_path: Path | None,
46
+ ) -> Violation:
47
+ """Create a violation for a magic number.
48
+
49
+ Args:
50
+ node: The AST node containing the magic number
51
+ value: The numeric value
52
+ line: Line number where the violation occurs
53
+ file_path: Path to the file
54
+
55
+ Returns:
56
+ Violation object with details about the magic number
57
+ """
58
+ message = f"Magic number {value} should be a named constant"
59
+
60
+ suggestion = f"Extract {value} to a named constant (e.g., CONSTANT_NAME = {value})"
61
+
62
+ return Violation(
63
+ rule_id=self.rule_id,
64
+ file_path=str(file_path) if file_path else "",
65
+ line=line,
66
+ column=node.col_offset if hasattr(node, "col_offset") else 0,
67
+ message=message,
68
+ suggestion=suggestion,
69
+ )
70
+
71
+ def create_typescript_violation(
72
+ self,
73
+ value: int | float,
74
+ line: int,
75
+ file_path: Path | None,
76
+ ) -> Violation:
77
+ """Create a violation for a TypeScript magic number.
78
+
79
+ Args:
80
+ value: The numeric value
81
+ line: Line number where the violation occurs
82
+ file_path: Path to the file
83
+
84
+ Returns:
85
+ Violation object with details about the magic number
86
+ """
87
+ message = f"Magic number {value} should be a named constant"
88
+
89
+ suggestion = f"Extract {value} to a named constant (e.g., const CONSTANT_NAME = {value})"
90
+
91
+ return Violation(
92
+ rule_id=self.rule_id,
93
+ file_path=str(file_path) if file_path else "",
94
+ line=line,
95
+ column=0, # Tree-sitter nodes don't have easy column access
96
+ message=message,
97
+ suggestion=suggestion,
98
+ )
@@ -22,7 +22,7 @@ Implementation: Simple re-export pattern for package interface, convenience func
22
22
  from pathlib import Path
23
23
  from typing import Any
24
24
 
25
- from .config import NestingConfig
25
+ from .config import DEFAULT_MAX_NESTING_DEPTH, NestingConfig
26
26
  from .linter import NestingDepthRule
27
27
  from .python_analyzer import PythonNestingAnalyzer
28
28
  from .typescript_analyzer import TypeScriptNestingAnalyzer
@@ -36,7 +36,11 @@ __all__ = [
36
36
  ]
37
37
 
38
38
 
39
- def lint(path: Path | str, config: dict[str, Any] | None = None, max_depth: int = 4) -> list:
39
+ def lint(
40
+ path: Path | str,
41
+ config: dict[str, Any] | None = None,
42
+ max_depth: int = DEFAULT_MAX_NESTING_DEPTH,
43
+ ) -> list:
40
44
  """Lint a file or directory for nesting depth violations.
41
45
 
42
46
  Args:
@@ -21,12 +21,15 @@ Implementation: Dataclass with validation and defaults, matches reference implem
21
21
  from dataclasses import dataclass
22
22
  from typing import Any
23
23
 
24
+ # Default nesting threshold constant
25
+ DEFAULT_MAX_NESTING_DEPTH = 4
26
+
24
27
 
25
28
  @dataclass
26
29
  class NestingConfig:
27
30
  """Configuration for nesting depth linter."""
28
31
 
29
- max_nesting_depth: int = 4 # Default from reference implementation
32
+ max_nesting_depth: int = DEFAULT_MAX_NESTING_DEPTH # Default from reference implementation
30
33
  enabled: bool = True
31
34
 
32
35
  def __post_init__(self) -> None:
@@ -35,16 +38,26 @@ class NestingConfig:
35
38
  raise ValueError(f"max_nesting_depth must be positive, got {self.max_nesting_depth}")
36
39
 
37
40
  @classmethod
38
- def from_dict(cls, config: dict[str, Any]) -> "NestingConfig":
39
- """Load configuration from dictionary.
41
+ def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "NestingConfig":
42
+ """Load configuration from dictionary with language-specific overrides.
40
43
 
41
44
  Args:
42
45
  config: Dictionary containing configuration values
46
+ language: Programming language (python, typescript, javascript) for language-specific thresholds
43
47
 
44
48
  Returns:
45
49
  NestingConfig instance with values from dictionary
46
50
  """
51
+ # Get language-specific config if available
52
+ if language and language in config:
53
+ lang_config = config[language]
54
+ max_nesting_depth = lang_config.get(
55
+ "max_nesting_depth", config.get("max_nesting_depth", DEFAULT_MAX_NESTING_DEPTH)
56
+ )
57
+ else:
58
+ max_nesting_depth = config.get("max_nesting_depth", DEFAULT_MAX_NESTING_DEPTH)
59
+
47
60
  return cls(
48
- max_nesting_depth=config.get("max_nesting_depth", 4),
61
+ max_nesting_depth=max_nesting_depth,
49
62
  enabled=config.get("enabled", True),
50
63
  )