thailint 0.2.0__py3-none-any.whl → 0.15.3__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.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +44 -27
- src/core/base.py +95 -5
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +36 -6
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +125 -22
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +142 -94
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +68 -21
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +20 -82
- src/linters/dry/file_analyzer.py +15 -50
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +182 -54
- src/linters/dry/python_analyzer.py +108 -336
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +129 -71
- src/linters/dry/typescript_analyzer.py +68 -380
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +9 -5
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +105 -0
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +140 -0
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +72 -0
- src/linters/file_header/linter.py +309 -0
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +42 -0
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +79 -0
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +74 -31
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +249 -0
- src/linters/magic_numbers/linter.py +462 -0
- src/linters/magic_numbers/python_analyzer.py +64 -0
- src/linters/magic_numbers/typescript_analyzer.py +215 -0
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +31 -34
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -11
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +78 -0
- src/linters/print_statements/linter.py +413 -0
- src/linters/print_statements/python_analyzer.py +153 -0
- src/linters/print_statements/typescript_analyzer.py +125 -0
- src/linters/print_statements/violation_builder.py +96 -0
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/config.py +12 -6
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +47 -39
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +264 -16
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +354 -0
- src/utils/project_root.py +138 -16
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1055
- thailint-0.2.0.dist-info/METADATA +0 -980
- thailint-0.2.0.dist-info/RECORD +0 -75
- thailint-0.2.0.dist-info/entry_points.txt +0 -4
- {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Python AST analysis for finding numeric literal nodes
|
|
3
|
+
|
|
4
|
+
Scope: Python magic number detection through AST traversal
|
|
5
|
+
|
|
6
|
+
Overview: Provides PythonMagicNumberAnalyzer class that traverses Python AST to find all numeric
|
|
7
|
+
literal nodes (integers and floats). Uses ast.NodeVisitor pattern to walk the syntax tree and
|
|
8
|
+
collect Constant nodes containing numeric values along with their parent nodes and line numbers.
|
|
9
|
+
Returns structured data about each numeric literal including the AST node, parent node, numeric
|
|
10
|
+
value, and source location. This analyzer handles Python-specific AST structure and provides
|
|
11
|
+
the foundation for magic number detection by identifying all candidates before context filtering.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for AST parsing and node types, analyzers.ast_utils
|
|
14
|
+
|
|
15
|
+
Exports: PythonMagicNumberAnalyzer class
|
|
16
|
+
|
|
17
|
+
Interfaces: PythonMagicNumberAnalyzer.find_numeric_literals(tree) -> list[tuple],
|
|
18
|
+
returns list of (node, parent, value, line_number) tuples
|
|
19
|
+
|
|
20
|
+
Implementation: AST NodeVisitor pattern with parent tracking, filters for numeric Constant nodes
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from src.analyzers.ast_utils import build_parent_map
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PythonMagicNumberAnalyzer(ast.NodeVisitor):
|
|
30
|
+
"""Analyzes Python AST to find numeric literals."""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
"""Initialize the analyzer."""
|
|
34
|
+
self.numeric_literals: list[tuple[ast.Constant, ast.AST | None, Any, int]] = []
|
|
35
|
+
self.parent_map: dict[ast.AST, ast.AST] = {}
|
|
36
|
+
|
|
37
|
+
def find_numeric_literals(
|
|
38
|
+
self, tree: ast.AST
|
|
39
|
+
) -> list[tuple[ast.Constant, ast.AST | None, Any, int]]:
|
|
40
|
+
"""Find all numeric literals in the AST.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
tree: The AST to analyze
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List of tuples (node, parent, value, line_number)
|
|
47
|
+
"""
|
|
48
|
+
self.numeric_literals = []
|
|
49
|
+
self.parent_map = build_parent_map(tree)
|
|
50
|
+
self.visit(tree)
|
|
51
|
+
return self.numeric_literals
|
|
52
|
+
|
|
53
|
+
def visit_Constant(self, node: ast.Constant) -> None:
|
|
54
|
+
"""Visit a Constant node and check if it's a numeric literal.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
node: The Constant node to check
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(node.value, (int, float)):
|
|
60
|
+
parent = self.parent_map.get(node)
|
|
61
|
+
line_number = node.lineno if hasattr(node, "lineno") else 0
|
|
62
|
+
self.numeric_literals.append((node, parent, node.value, line_number))
|
|
63
|
+
|
|
64
|
+
self.generic_visit(node)
|
|
@@ -0,0 +1,215 @@
|
|
|
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
|
+
Suppressions:
|
|
25
|
+
- srp: Analyzer implements tree-sitter traversal with context detection methods.
|
|
26
|
+
Methods support single responsibility of magic number detection in TypeScript.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from src.analyzers.typescript_base import (
|
|
30
|
+
TREE_SITTER_AVAILABLE,
|
|
31
|
+
Node,
|
|
32
|
+
TypeScriptBaseAnalyzer,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TypeScriptMagicNumberAnalyzer(TypeScriptBaseAnalyzer): # thailint: ignore[srp]
|
|
37
|
+
"""Analyzes TypeScript/JavaScript code for magic numbers using Tree-sitter.
|
|
38
|
+
|
|
39
|
+
Note: Method count (11) exceeds SRP limit (8) because refactoring for A-grade
|
|
40
|
+
complexity requires extracting helper methods. Class maintains single responsibility
|
|
41
|
+
of TypeScript magic number detection - all methods support this core purpose.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def find_numeric_literals(self, root_node: Node) -> list[tuple[Node, float | int, int]]:
|
|
45
|
+
"""Find all numeric literal nodes in TypeScript/JavaScript AST.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
root_node: Root tree-sitter node to search from
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of (node, value, line_number) tuples for each numeric literal
|
|
52
|
+
"""
|
|
53
|
+
if not TREE_SITTER_AVAILABLE or root_node is None:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
literals: list[tuple[Node, float | int, int]] = []
|
|
57
|
+
self._collect_numeric_literals(root_node, literals)
|
|
58
|
+
return literals
|
|
59
|
+
|
|
60
|
+
def _collect_numeric_literals(
|
|
61
|
+
self, node: Node, literals: list[tuple[Node, float | int, int]]
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Recursively collect numeric literals from AST.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
node: Current tree-sitter node
|
|
67
|
+
literals: List to accumulate found literals
|
|
68
|
+
"""
|
|
69
|
+
if node.type == "number":
|
|
70
|
+
value = self._extract_numeric_value(node)
|
|
71
|
+
if value is not None:
|
|
72
|
+
line_number = node.start_point[0] + 1
|
|
73
|
+
literals.append((node, value, line_number))
|
|
74
|
+
|
|
75
|
+
for child in node.children:
|
|
76
|
+
self._collect_numeric_literals(child, literals)
|
|
77
|
+
|
|
78
|
+
def _extract_numeric_value(self, node: Node) -> float | int | None:
|
|
79
|
+
"""Extract numeric value from number node.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
node: Tree-sitter number node
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Numeric value (int or float) or None if parsing fails
|
|
86
|
+
"""
|
|
87
|
+
text = self.extract_node_text(node)
|
|
88
|
+
try:
|
|
89
|
+
# Try int first
|
|
90
|
+
if "." not in text and "e" not in text.lower():
|
|
91
|
+
return int(text, 0) # Handles hex, octal, binary
|
|
92
|
+
# Otherwise float
|
|
93
|
+
return float(text)
|
|
94
|
+
except (ValueError, TypeError):
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def is_enum_context(self, node: Node) -> bool:
|
|
98
|
+
"""Check if numeric literal is in enum definition.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
node: Numeric literal node
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True if node is within enum_declaration
|
|
105
|
+
"""
|
|
106
|
+
if not TREE_SITTER_AVAILABLE:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
current = node.parent
|
|
110
|
+
while current is not None:
|
|
111
|
+
if current.type == "enum_declaration":
|
|
112
|
+
return True
|
|
113
|
+
current = current.parent
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def is_constant_definition(self, node: Node, source_code: str) -> bool:
|
|
117
|
+
"""Check if numeric literal is in UPPERCASE constant definition.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
node: Numeric literal node
|
|
121
|
+
source_code: Full source code to extract variable names
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if assigned to UPPERCASE constant variable
|
|
125
|
+
"""
|
|
126
|
+
if not TREE_SITTER_AVAILABLE:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
# Find the declaration parent
|
|
130
|
+
parent = self._find_declaration_parent(node)
|
|
131
|
+
if parent is None:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
# Check if identifier is UPPERCASE constant
|
|
135
|
+
return self._has_uppercase_identifier(parent)
|
|
136
|
+
|
|
137
|
+
def _find_declaration_parent(self, node: Node) -> Node | None:
|
|
138
|
+
"""Find the declaration parent node.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
node: Starting node
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Declaration parent or None
|
|
145
|
+
"""
|
|
146
|
+
parent = node.parent
|
|
147
|
+
if self._is_declaration_type(parent):
|
|
148
|
+
return parent
|
|
149
|
+
|
|
150
|
+
# Try grandparent for nested cases
|
|
151
|
+
if parent is not None:
|
|
152
|
+
grandparent = parent.parent
|
|
153
|
+
if self._is_declaration_type(grandparent):
|
|
154
|
+
return grandparent
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _is_declaration_type(self, node: Node | None) -> bool:
|
|
159
|
+
"""Check if node is a declaration type."""
|
|
160
|
+
if node is None:
|
|
161
|
+
return False
|
|
162
|
+
return node.type in ("variable_declarator", "lexical_declaration", "pair")
|
|
163
|
+
|
|
164
|
+
def _has_uppercase_identifier(self, parent_node: Node) -> bool:
|
|
165
|
+
"""Check if declaration has UPPERCASE identifier.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
parent_node: Declaration parent node
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
True if identifier is UPPERCASE
|
|
172
|
+
"""
|
|
173
|
+
identifier_node = self._find_identifier_in_declaration(parent_node)
|
|
174
|
+
if identifier_node is None:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
identifier_text = self.extract_node_text(identifier_node)
|
|
178
|
+
return self._is_uppercase_constant(identifier_text)
|
|
179
|
+
|
|
180
|
+
def _find_identifier_in_declaration(self, node: Node) -> Node | None:
|
|
181
|
+
"""Find identifier node in variable declaration.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
node: Variable declarator or lexical declaration node
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Identifier node or None
|
|
188
|
+
"""
|
|
189
|
+
# Walk children looking for identifier
|
|
190
|
+
for child in node.children:
|
|
191
|
+
if child.type in ("identifier", "property_identifier"):
|
|
192
|
+
return child
|
|
193
|
+
# Recursively check children
|
|
194
|
+
result = self._find_identifier_in_declaration(child)
|
|
195
|
+
if result is not None:
|
|
196
|
+
return result
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
def _is_uppercase_constant(self, name: str) -> bool:
|
|
200
|
+
"""Check if identifier is UPPERCASE constant style.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
name: Identifier name
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
True if name is UPPERCASE with optional underscores
|
|
207
|
+
"""
|
|
208
|
+
if not name:
|
|
209
|
+
return False
|
|
210
|
+
# Must be at least one letter and all letters must be uppercase
|
|
211
|
+
# Allow underscores and numbers
|
|
212
|
+
letters_only = "".join(c for c in name if c.isalpha())
|
|
213
|
+
if not letters_only:
|
|
214
|
+
return False
|
|
215
|
+
return letters_only.isupper()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: TypeScript-specific ignore directive checking for magic numbers linter
|
|
3
|
+
|
|
4
|
+
Scope: Ignore directive detection for TypeScript/JavaScript files
|
|
5
|
+
|
|
6
|
+
Overview: Provides ignore directive checking functionality specifically for TypeScript and JavaScript
|
|
7
|
+
files in the magic numbers linter. Handles both thailint-style and noqa-style ignore comments
|
|
8
|
+
using TypeScript comment syntax (// instead of #). Extracted from linter.py to reduce file
|
|
9
|
+
size and improve modularity.
|
|
10
|
+
|
|
11
|
+
Dependencies: IgnoreDirectiveParser from src.linter_config.ignore, Violation type, violation_utils
|
|
12
|
+
|
|
13
|
+
Exports: TypeScriptIgnoreChecker class
|
|
14
|
+
|
|
15
|
+
Interfaces: TypeScriptIgnoreChecker.should_ignore(violation, context) -> bool
|
|
16
|
+
|
|
17
|
+
Implementation: Comment parsing with TypeScript-specific syntax handling, uses shared utilities
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from src.core.base import BaseLintContext
|
|
21
|
+
from src.core.types import Violation
|
|
22
|
+
from src.core.violation_utils import get_violation_line, has_typescript_noqa
|
|
23
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TypeScriptIgnoreChecker:
|
|
27
|
+
"""Checks for TypeScript-style ignore directives in magic numbers linter."""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
"""Initialize with standard ignore parser."""
|
|
31
|
+
self._ignore_parser = get_ignore_parser()
|
|
32
|
+
|
|
33
|
+
def should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
34
|
+
"""Check if TypeScript violation should be ignored.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
violation: Violation to check
|
|
38
|
+
context: Lint context
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if should ignore
|
|
42
|
+
"""
|
|
43
|
+
if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
return self._check_typescript_ignore(violation, context)
|
|
47
|
+
|
|
48
|
+
def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
49
|
+
"""Check for TypeScript-style ignore directives.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
violation: Violation to check
|
|
53
|
+
context: Lint context
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if line has ignore directive
|
|
57
|
+
"""
|
|
58
|
+
line_text = get_violation_line(violation, context)
|
|
59
|
+
if line_text is None:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
return self._has_typescript_ignore_directive(line_text)
|
|
63
|
+
|
|
64
|
+
def _has_typescript_ignore_directive(self, line_text: str) -> bool:
|
|
65
|
+
"""Check if line has TypeScript-style ignore directive.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
line_text: Line text to check
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if has ignore directive
|
|
72
|
+
"""
|
|
73
|
+
if "// thailint: ignore[magic-numbers]" in line_text:
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
if "// thailint: ignore" in line_text:
|
|
77
|
+
after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
|
|
78
|
+
if "[" not in after_ignore:
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
return has_typescript_noqa(line_text)
|
|
@@ -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
|
+
)
|
|
@@ -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,138 @@
|
|
|
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
|
+
Suppressions:
|
|
23
|
+
- dry: MethodPropertyConfig includes extensive exclusion lists that share patterns with
|
|
24
|
+
other config classes. Lists are maintained separately for clear documentation.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
# Default action verb prefixes - methods starting with these are excluded
|
|
31
|
+
# These represent actions/transformations, not property access
|
|
32
|
+
DEFAULT_EXCLUDE_PREFIXES: tuple[str, ...] = (
|
|
33
|
+
"to_", # Transformation: to_dict, to_json, to_string
|
|
34
|
+
"dump_", # Serialization: dump_to_json, dump_to_apigw
|
|
35
|
+
"generate_", # Factory: generate_report, generate_html
|
|
36
|
+
"create_", # Factory: create_instance, create_config
|
|
37
|
+
"build_", # Construction: build_query, build_html
|
|
38
|
+
"make_", # Factory: make_request, make_connection
|
|
39
|
+
"render_", # Output: render_template, render_html
|
|
40
|
+
"compute_", # Calculation: compute_hash, compute_total
|
|
41
|
+
"calculate_", # Calculation: calculate_sum, calculate_average
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Default action verb names - exact method names that are excluded
|
|
45
|
+
# These are lifecycle hooks, display actions, and resource operations
|
|
46
|
+
DEFAULT_EXCLUDE_NAMES: frozenset[str] = frozenset(
|
|
47
|
+
{
|
|
48
|
+
"finalize", # Lifecycle hook
|
|
49
|
+
"serialize", # Transformation
|
|
50
|
+
"dump", # Serialization
|
|
51
|
+
"validate", # Validation action
|
|
52
|
+
"show", # Display action
|
|
53
|
+
"display", # Display action
|
|
54
|
+
"print", # Output action
|
|
55
|
+
"refresh", # Update action
|
|
56
|
+
"reset", # State action
|
|
57
|
+
"clear", # State action
|
|
58
|
+
"close", # Resource action
|
|
59
|
+
"open", # Resource action
|
|
60
|
+
"save", # Persistence action
|
|
61
|
+
"load", # Persistence action
|
|
62
|
+
"execute", # Action
|
|
63
|
+
"run", # Action
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _load_list_config(
|
|
69
|
+
config: dict[str, Any], key: str, override_key: str, default: tuple[str, ...]
|
|
70
|
+
) -> tuple[str, ...]:
|
|
71
|
+
"""Load a list config with extend/override semantics."""
|
|
72
|
+
if override_key in config and isinstance(config[override_key], list):
|
|
73
|
+
return tuple(config[override_key])
|
|
74
|
+
if key in config and isinstance(config[key], list):
|
|
75
|
+
return default + tuple(config[key])
|
|
76
|
+
return default
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _load_set_config(
|
|
80
|
+
config: dict[str, Any], key: str, override_key: str, default: frozenset[str]
|
|
81
|
+
) -> frozenset[str]:
|
|
82
|
+
"""Load a set config with extend/override semantics."""
|
|
83
|
+
if override_key in config and isinstance(config[override_key], list):
|
|
84
|
+
return frozenset(config[override_key])
|
|
85
|
+
if key in config and isinstance(config[key], list):
|
|
86
|
+
return default | frozenset(config[key])
|
|
87
|
+
return default
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class MethodPropertyConfig: # thailint: ignore[dry]
|
|
92
|
+
"""Configuration for method-should-be-property linter."""
|
|
93
|
+
|
|
94
|
+
enabled: bool = True
|
|
95
|
+
max_body_statements: int = 3
|
|
96
|
+
ignore: list[str] = field(default_factory=list)
|
|
97
|
+
ignore_methods: list[str] = field(default_factory=list)
|
|
98
|
+
|
|
99
|
+
# Action verb exclusions (extend defaults or override)
|
|
100
|
+
exclude_prefixes: tuple[str, ...] = DEFAULT_EXCLUDE_PREFIXES
|
|
101
|
+
exclude_names: frozenset[str] = DEFAULT_EXCLUDE_NAMES
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_dict(
|
|
105
|
+
cls, config: dict[str, Any] | None, language: str | None = None
|
|
106
|
+
) -> "MethodPropertyConfig":
|
|
107
|
+
"""Load configuration from dictionary.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
config: Dictionary containing configuration values, or None
|
|
111
|
+
language: Programming language (unused, for interface compatibility)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
MethodPropertyConfig instance with values from dictionary
|
|
115
|
+
"""
|
|
116
|
+
if config is None:
|
|
117
|
+
return cls()
|
|
118
|
+
|
|
119
|
+
ignore_patterns = config.get("ignore", [])
|
|
120
|
+
if not isinstance(ignore_patterns, list):
|
|
121
|
+
ignore_patterns = []
|
|
122
|
+
|
|
123
|
+
ignore_methods = config.get("ignore_methods", [])
|
|
124
|
+
if not isinstance(ignore_methods, list):
|
|
125
|
+
ignore_methods = []
|
|
126
|
+
|
|
127
|
+
return cls(
|
|
128
|
+
enabled=config.get("enabled", True),
|
|
129
|
+
max_body_statements=config.get("max_body_statements", 3),
|
|
130
|
+
ignore=ignore_patterns,
|
|
131
|
+
ignore_methods=ignore_methods,
|
|
132
|
+
exclude_prefixes=_load_list_config(
|
|
133
|
+
config, "exclude_prefixes", "exclude_prefixes_override", DEFAULT_EXCLUDE_PREFIXES
|
|
134
|
+
),
|
|
135
|
+
exclude_names=_load_set_config(
|
|
136
|
+
config, "exclude_names", "exclude_names_override", DEFAULT_EXCLUDE_NAMES
|
|
137
|
+
),
|
|
138
|
+
)
|