thailint 0.2.0__py3-none-any.whl → 0.3.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.
@@ -0,0 +1,217 @@
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
+ try:
30
+ from tree_sitter import Node
31
+
32
+ TREE_SITTER_AVAILABLE = True
33
+ except ImportError:
34
+ TREE_SITTER_AVAILABLE = False
35
+ Node = Any # type: ignore
36
+
37
+
38
+ class TypeScriptMagicNumberAnalyzer(TypeScriptBaseAnalyzer): # thailint: ignore[srp]
39
+ """Analyzes TypeScript/JavaScript code for magic numbers using Tree-sitter.
40
+
41
+ Note: Method count (11) exceeds SRP limit (8) because refactoring for A-grade
42
+ complexity requires extracting helper methods. Class maintains single responsibility
43
+ of TypeScript magic number detection - all methods support this core purpose.
44
+ """
45
+
46
+ def find_numeric_literals(self, root_node: Node) -> list[tuple[Node, float | int, int]]:
47
+ """Find all numeric literal nodes in TypeScript/JavaScript AST.
48
+
49
+ Args:
50
+ root_node: Root tree-sitter node to search from
51
+
52
+ Returns:
53
+ List of (node, value, line_number) tuples for each numeric literal
54
+ """
55
+ if not TREE_SITTER_AVAILABLE or root_node is None:
56
+ return []
57
+
58
+ literals: list[tuple[Node, float | int, int]] = []
59
+ self._collect_numeric_literals(root_node, literals)
60
+ return literals
61
+
62
+ def _collect_numeric_literals(
63
+ self, node: Node, literals: list[tuple[Node, float | int, int]]
64
+ ) -> None:
65
+ """Recursively collect numeric literals from AST.
66
+
67
+ Args:
68
+ node: Current tree-sitter node
69
+ literals: List to accumulate found literals
70
+ """
71
+ if node.type == "number":
72
+ value = self._extract_numeric_value(node)
73
+ if value is not None:
74
+ line_number = node.start_point[0] + 1
75
+ literals.append((node, value, line_number))
76
+
77
+ for child in node.children:
78
+ self._collect_numeric_literals(child, literals)
79
+
80
+ def _extract_numeric_value(self, node: Node) -> float | int | None:
81
+ """Extract numeric value from number node.
82
+
83
+ Args:
84
+ node: Tree-sitter number node
85
+
86
+ Returns:
87
+ Numeric value (int or float) or None if parsing fails
88
+ """
89
+ text = self.extract_node_text(node)
90
+ try:
91
+ # Try int first
92
+ if "." not in text and "e" not in text.lower():
93
+ return int(text, 0) # Handles hex, octal, binary
94
+ # Otherwise float
95
+ return float(text)
96
+ except (ValueError, TypeError):
97
+ return None
98
+
99
+ def is_enum_context(self, node: Node) -> bool:
100
+ """Check if numeric literal is in enum definition.
101
+
102
+ Args:
103
+ node: Numeric literal node
104
+
105
+ Returns:
106
+ True if node is within enum_declaration
107
+ """
108
+ if not TREE_SITTER_AVAILABLE:
109
+ return False
110
+
111
+ current = node.parent
112
+ while current is not None:
113
+ if current.type == "enum_declaration":
114
+ return True
115
+ current = current.parent
116
+ return False
117
+
118
+ def is_constant_definition(self, node: Node, source_code: str) -> bool:
119
+ """Check if numeric literal is in UPPERCASE constant definition.
120
+
121
+ Args:
122
+ node: Numeric literal node
123
+ source_code: Full source code to extract variable names
124
+
125
+ Returns:
126
+ True if assigned to UPPERCASE constant variable
127
+ """
128
+ if not TREE_SITTER_AVAILABLE:
129
+ return False
130
+
131
+ # Find the declaration parent
132
+ parent = self._find_declaration_parent(node)
133
+ if parent is None:
134
+ return False
135
+
136
+ # Check if identifier is UPPERCASE constant
137
+ return self._has_uppercase_identifier(parent)
138
+
139
+ def _find_declaration_parent(self, node: Node) -> Node | None:
140
+ """Find the declaration parent node.
141
+
142
+ Args:
143
+ node: Starting node
144
+
145
+ Returns:
146
+ Declaration parent or None
147
+ """
148
+ parent = node.parent
149
+ if self._is_declaration_type(parent):
150
+ return parent
151
+
152
+ # Try grandparent for nested cases
153
+ if parent is not None:
154
+ grandparent = parent.parent
155
+ if self._is_declaration_type(grandparent):
156
+ return grandparent
157
+
158
+ return None
159
+
160
+ def _is_declaration_type(self, node: Node | None) -> bool:
161
+ """Check if node is a declaration type."""
162
+ if node is None:
163
+ return False
164
+ return node.type in ("variable_declarator", "lexical_declaration", "pair")
165
+
166
+ def _has_uppercase_identifier(self, parent_node: Node) -> bool:
167
+ """Check if declaration has UPPERCASE identifier.
168
+
169
+ Args:
170
+ parent_node: Declaration parent node
171
+
172
+ Returns:
173
+ True if identifier is UPPERCASE
174
+ """
175
+ identifier_node = self._find_identifier_in_declaration(parent_node)
176
+ if identifier_node is None:
177
+ return False
178
+
179
+ identifier_text = self.extract_node_text(identifier_node)
180
+ return self._is_uppercase_constant(identifier_text)
181
+
182
+ def _find_identifier_in_declaration(self, node: Node) -> Node | None:
183
+ """Find identifier node in variable declaration.
184
+
185
+ Args:
186
+ node: Variable declarator or lexical declaration node
187
+
188
+ Returns:
189
+ Identifier node or None
190
+ """
191
+ # Walk children looking for identifier
192
+ for child in node.children:
193
+ if child.type in ("identifier", "property_identifier"):
194
+ return child
195
+ # Recursively check children
196
+ result = self._find_identifier_in_declaration(child)
197
+ if result is not None:
198
+ return result
199
+ return None
200
+
201
+ def _is_uppercase_constant(self, name: str) -> bool:
202
+ """Check if identifier is UPPERCASE constant style.
203
+
204
+ Args:
205
+ name: Identifier name
206
+
207
+ Returns:
208
+ True if name is UPPERCASE with optional underscores
209
+ """
210
+ if not name:
211
+ return False
212
+ # Must be at least one letter and all letters must be uppercase
213
+ # Allow underscores and numbers
214
+ letters_only = "".join(c for c in name if c.isalpha())
215
+ if not letters_only:
216
+ return False
217
+ 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:
@@ -49,10 +52,10 @@ class NestingConfig:
49
52
  if language and language in config:
50
53
  lang_config = config[language]
51
54
  max_nesting_depth = lang_config.get(
52
- "max_nesting_depth", config.get("max_nesting_depth", 4)
55
+ "max_nesting_depth", config.get("max_nesting_depth", DEFAULT_MAX_NESTING_DEPTH)
53
56
  )
54
57
  else:
55
- max_nesting_depth = config.get("max_nesting_depth", 4)
58
+ max_nesting_depth = config.get("max_nesting_depth", DEFAULT_MAX_NESTING_DEPTH)
56
59
 
57
60
  return cls(
58
61
  max_nesting_depth=max_nesting_depth,
@@ -21,8 +21,8 @@ Implementation: Composition pattern with helper classes, AST-based analysis with
21
21
  import ast
22
22
  from typing import Any
23
23
 
24
- from src.core.base import BaseLintContext, BaseLintRule
25
- from src.core.linter_utils import has_file_content, load_linter_config
24
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
25
+ from src.core.linter_utils import load_linter_config
26
26
  from src.core.types import Violation
27
27
  from src.linter_config.ignore import IgnoreDirectiveParser
28
28
 
@@ -32,7 +32,7 @@ from .typescript_analyzer import TypeScriptNestingAnalyzer
32
32
  from .violation_builder import NestingViolationBuilder
33
33
 
34
34
 
35
- class NestingDepthRule(BaseLintRule):
35
+ class NestingDepthRule(MultiLanguageLintRule):
36
36
  """Detects excessive nesting depth in functions."""
37
37
 
38
38
  def __init__(self) -> None:
@@ -55,27 +55,16 @@ class NestingDepthRule(BaseLintRule):
55
55
  """Description of what this rule checks."""
56
56
  return "Functions should not have excessive nesting depth for better readability"
57
57
 
58
- def check(self, context: BaseLintContext) -> list[Violation]:
59
- """Check for excessive nesting depth violations.
58
+ def _load_config(self, context: BaseLintContext) -> NestingConfig:
59
+ """Load configuration from context.
60
60
 
61
61
  Args:
62
- context: Lint context with file information
62
+ context: Lint context
63
63
 
64
64
  Returns:
65
- List of violations found
65
+ NestingConfig instance
66
66
  """
67
- if not has_file_content(context):
68
- return []
69
-
70
- config = load_linter_config(context, "nesting", NestingConfig)
71
- if not config.enabled:
72
- return []
73
-
74
- if context.language == "python":
75
- return self._check_python(context, config)
76
- if context.language in ("typescript", "javascript"):
77
- return self._check_typescript(context, config)
78
- return []
67
+ return load_linter_config(context, "nesting", NestingConfig)
79
68
 
80
69
  def _process_python_functions(
81
70
  self, functions: list, analyzer: Any, config: NestingConfig, context: BaseLintContext
@@ -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 SRPConfig
25
+ from .config import DEFAULT_MAX_LOC_PER_CLASS, DEFAULT_MAX_METHODS_PER_CLASS, SRPConfig
26
26
  from .linter import SRPRule
27
27
  from .python_analyzer import PythonSRPAnalyzer
28
28
  from .typescript_analyzer import TypeScriptSRPAnalyzer
@@ -39,8 +39,8 @@ __all__ = [
39
39
  def lint(
40
40
  path: Path | str,
41
41
  config: dict[str, Any] | None = None,
42
- max_methods: int = 7,
43
- max_loc: int = 200,
42
+ max_methods: int = DEFAULT_MAX_METHODS_PER_CLASS,
43
+ max_loc: int = DEFAULT_MAX_LOC_PER_CLASS,
44
44
  ) -> list:
45
45
  """Lint a file or directory for SRP violations.
46
46
 
src/linters/srp/config.py CHANGED
@@ -23,13 +23,17 @@ Implementation: Dataclass with validation and defaults, heuristic-based SRP dete
23
23
  from dataclasses import dataclass, field
24
24
  from typing import Any
25
25
 
26
+ # Default SRP threshold constants
27
+ DEFAULT_MAX_METHODS_PER_CLASS = 7
28
+ DEFAULT_MAX_LOC_PER_CLASS = 200
29
+
26
30
 
27
31
  @dataclass
28
32
  class SRPConfig:
29
33
  """Configuration for SRP linter."""
30
34
 
31
- max_methods: int = 7 # Maximum methods per class
32
- max_loc: int = 200 # Maximum lines of code per class
35
+ max_methods: int = DEFAULT_MAX_METHODS_PER_CLASS # Maximum methods per class
36
+ max_loc: int = DEFAULT_MAX_LOC_PER_CLASS # Maximum lines of code per class
33
37
  enabled: bool = True
34
38
  check_keywords: bool = True
35
39
  keywords: list[str] = field(
@@ -58,11 +62,13 @@ class SRPConfig:
58
62
  # Get language-specific config if available
59
63
  if language and language in config:
60
64
  lang_config = config[language]
61
- max_methods = lang_config.get("max_methods", config.get("max_methods", 7))
62
- max_loc = lang_config.get("max_loc", config.get("max_loc", 200))
65
+ max_methods = lang_config.get(
66
+ "max_methods", config.get("max_methods", DEFAULT_MAX_METHODS_PER_CLASS)
67
+ )
68
+ max_loc = lang_config.get("max_loc", config.get("max_loc", DEFAULT_MAX_LOC_PER_CLASS))
63
69
  else:
64
- max_methods = config.get("max_methods", 7)
65
- max_loc = config.get("max_loc", 200)
70
+ max_methods = config.get("max_methods", DEFAULT_MAX_METHODS_PER_CLASS)
71
+ max_loc = config.get("max_loc", DEFAULT_MAX_LOC_PER_CLASS)
66
72
 
67
73
  return cls(
68
74
  max_methods=max_methods,
src/linters/srp/linter.py CHANGED
@@ -18,8 +18,8 @@ Interfaces: SRPRule.check(context) -> list[Violation], properties for rule metad
18
18
  Implementation: Composition pattern with helper classes, heuristic-based SRP analysis
19
19
  """
20
20
 
21
- from src.core.base import BaseLintContext, BaseLintRule
22
- from src.core.linter_utils import has_file_content, load_linter_config
21
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
22
+ from src.core.linter_utils import load_linter_config
23
23
  from src.core.types import Violation
24
24
  from src.linter_config.ignore import IgnoreDirectiveParser
25
25
 
@@ -29,7 +29,7 @@ from .metrics_evaluator import evaluate_metrics
29
29
  from .violation_builder import ViolationBuilder
30
30
 
31
31
 
32
- class SRPRule(BaseLintRule):
32
+ class SRPRule(MultiLanguageLintRule):
33
33
  """Detects Single Responsibility Principle violations in classes."""
34
34
 
35
35
  def __init__(self) -> None:
@@ -54,7 +54,9 @@ class SRPRule(BaseLintRule):
54
54
  return "Classes should have a single, well-defined responsibility"
55
55
 
56
56
  def check(self, context: BaseLintContext) -> list[Violation]:
57
- """Check for SRP violations.
57
+ """Check for SRP violations with custom ignore pattern handling.
58
+
59
+ Overrides parent to add file-level ignore pattern checking before dispatch.
58
60
 
59
61
  Args:
60
62
  context: Lint context with file information
@@ -62,39 +64,33 @@ class SRPRule(BaseLintRule):
62
64
  Returns:
63
65
  List of violations found
64
66
  """
65
- if not self._should_check_file(context):
66
- return []
67
+ from src.core.linter_utils import has_file_content
67
68
 
68
- config = load_linter_config(context, "srp", SRPConfig)
69
- if not self._is_linter_enabled(context, config):
69
+ if not has_file_content(context):
70
70
  return []
71
71
 
72
- return self._check_by_language(context, config)
73
-
74
- def _should_check_file(self, context: BaseLintContext) -> bool:
75
- """Check if file has content to analyze.
76
-
77
- Args:
78
- context: Lint context
72
+ config = self._load_config(context)
73
+ if not self._should_process_file(context, config):
74
+ return []
79
75
 
80
- Returns:
81
- True if file should be checked
82
- """
83
- return has_file_content(context)
76
+ # Standard language dispatch
77
+ return self._dispatch_by_language(context, config)
84
78
 
85
- def _is_linter_enabled(self, context: BaseLintContext, config: SRPConfig) -> bool:
86
- """Check if linter is enabled and file is not ignored.
79
+ def _should_process_file(self, context: BaseLintContext, config: SRPConfig) -> bool:
80
+ """Check if file should be processed.
87
81
 
88
82
  Args:
89
83
  context: Lint context
90
84
  config: SRP configuration
91
85
 
92
86
  Returns:
93
- True if linter should run on this file
87
+ True if file should be processed
94
88
  """
95
- return config.enabled and not self._is_file_ignored(context, config)
89
+ if not config.enabled:
90
+ return False
91
+ return not self._is_file_ignored(context, config)
96
92
 
97
- def _check_by_language(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
93
+ def _dispatch_by_language(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
98
94
  """Dispatch to language-specific checker.
99
95
 
100
96
  Args:
@@ -106,10 +102,23 @@ class SRPRule(BaseLintRule):
106
102
  """
107
103
  if context.language == "python":
108
104
  return self._check_python(context, config)
105
+
109
106
  if context.language in ("typescript", "javascript"):
110
107
  return self._check_typescript(context, config)
108
+
111
109
  return []
112
110
 
111
+ def _load_config(self, context: BaseLintContext) -> SRPConfig:
112
+ """Load configuration from context.
113
+
114
+ Args:
115
+ context: Lint context
116
+
117
+ Returns:
118
+ SRPConfig instance
119
+ """
120
+ return load_linter_config(context, "srp", SRPConfig)
121
+
113
122
  def _is_file_ignored(self, context: BaseLintContext, config: SRPConfig) -> bool:
114
123
  """Check if file matches ignore patterns.
115
124