thailint 0.5.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 +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- 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 +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- 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 +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- 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 +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- 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/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -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/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- 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/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- 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 +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Coordinate TypeScript stringly-typed pattern detection
|
|
3
|
+
|
|
4
|
+
Scope: Orchestrate detection of stringly-typed patterns in TypeScript and JavaScript files
|
|
5
|
+
|
|
6
|
+
Overview: Provides TypeScriptStringlyTypedAnalyzer class that coordinates detection of
|
|
7
|
+
stringly-typed patterns across TypeScript and JavaScript source files. Uses
|
|
8
|
+
TypeScriptCallTracker to find function calls with string arguments and
|
|
9
|
+
TypeScriptComparisonTracker to find scattered string comparisons. Returns
|
|
10
|
+
FunctionCallResult and ComparisonResult objects compatible with the Python analyzer
|
|
11
|
+
format for unified cross-file analysis. Handles tree-sitter parsing gracefully and
|
|
12
|
+
provides a single entry point for TypeScript analysis. Supports configuration options
|
|
13
|
+
for filtering and thresholds.
|
|
14
|
+
|
|
15
|
+
Dependencies: TypeScriptCallTracker, TypeScriptComparisonTracker, StringlyTypedConfig,
|
|
16
|
+
pathlib.Path, TypeScriptBaseAnalyzer
|
|
17
|
+
|
|
18
|
+
Exports: TypeScriptStringlyTypedAnalyzer class, FunctionCallResult (re-export from Python),
|
|
19
|
+
ComparisonResult (re-export from Python)
|
|
20
|
+
|
|
21
|
+
Interfaces: TypeScriptStringlyTypedAnalyzer.analyze_all(code, file_path) for optimized
|
|
22
|
+
single-parse analysis, plus individual analyze_function_calls and analyze_comparisons
|
|
23
|
+
|
|
24
|
+
Implementation: Facade pattern with single-parse optimization for performance
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
|
|
30
|
+
|
|
31
|
+
from ..config import StringlyTypedConfig
|
|
32
|
+
from ..python.analyzer import ComparisonResult, FunctionCallResult
|
|
33
|
+
from .call_tracker import TypeScriptCallTracker, TypeScriptFunctionCallPattern
|
|
34
|
+
from .comparison_tracker import TypeScriptComparisonPattern, TypeScriptComparisonTracker
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TypeScriptStringlyTypedAnalyzer:
|
|
38
|
+
"""Analyzes TypeScript/JavaScript code for stringly-typed patterns.
|
|
39
|
+
|
|
40
|
+
Coordinates detection of stringly-typed patterns including function calls
|
|
41
|
+
with string arguments ('process("active")', 'obj.setStatus("pending")') and
|
|
42
|
+
scattered string comparisons ('if (env === "production")').
|
|
43
|
+
Provides configuration-aware analysis with filtering support.
|
|
44
|
+
|
|
45
|
+
Uses single-parse optimization: parses code once and passes AST to both trackers.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, config: StringlyTypedConfig | None = None) -> None:
|
|
49
|
+
"""Initialize the analyzer with optional configuration.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
config: Configuration for stringly-typed detection. Uses defaults if None.
|
|
53
|
+
"""
|
|
54
|
+
self.config = config or StringlyTypedConfig()
|
|
55
|
+
self._call_tracker = TypeScriptCallTracker()
|
|
56
|
+
self._comparison_tracker = TypeScriptComparisonTracker()
|
|
57
|
+
self._base_analyzer = TypeScriptBaseAnalyzer()
|
|
58
|
+
|
|
59
|
+
def analyze_all(
|
|
60
|
+
self, code: str, file_path: Path
|
|
61
|
+
) -> tuple[list[FunctionCallResult], list[ComparisonResult]]:
|
|
62
|
+
"""Analyze TypeScript code for all stringly-typed patterns in single parse.
|
|
63
|
+
|
|
64
|
+
Optimized method that parses the code once and runs both call and comparison
|
|
65
|
+
detection on the same AST tree, avoiding duplicate parsing overhead.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
code: TypeScript source code to analyze
|
|
69
|
+
file_path: Path to the file being analyzed
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Tuple of (function_call_results, comparison_results)
|
|
73
|
+
"""
|
|
74
|
+
# Parse once
|
|
75
|
+
tree = self._base_analyzer.parse_typescript(code)
|
|
76
|
+
if tree is None:
|
|
77
|
+
return [], []
|
|
78
|
+
|
|
79
|
+
# Run both trackers with pre-parsed tree
|
|
80
|
+
call_patterns = self._call_tracker.find_patterns_from_tree(tree)
|
|
81
|
+
comp_patterns = self._comparison_tracker.find_patterns_from_tree(tree)
|
|
82
|
+
|
|
83
|
+
# Convert to result objects
|
|
84
|
+
calls = [self._convert_call_pattern(p, file_path) for p in call_patterns]
|
|
85
|
+
comps = [self._convert_comparison_pattern(p, file_path) for p in comp_patterns]
|
|
86
|
+
|
|
87
|
+
return calls, comps
|
|
88
|
+
|
|
89
|
+
def analyze_function_calls(self, code: str, file_path: Path) -> list[FunctionCallResult]:
|
|
90
|
+
"""Analyze TypeScript code for function calls with string arguments.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
code: TypeScript source code to analyze
|
|
94
|
+
file_path: Path to the file being analyzed
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of FunctionCallResult instances for each detected call
|
|
98
|
+
"""
|
|
99
|
+
call_patterns = self._call_tracker.find_patterns(code)
|
|
100
|
+
return [self._convert_call_pattern(pattern, file_path) for pattern in call_patterns]
|
|
101
|
+
|
|
102
|
+
def _convert_call_pattern(
|
|
103
|
+
self, pattern: TypeScriptFunctionCallPattern, file_path: Path
|
|
104
|
+
) -> FunctionCallResult:
|
|
105
|
+
"""Convert a TypeScriptFunctionCallPattern to FunctionCallResult.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
pattern: Detected function call pattern
|
|
109
|
+
file_path: Path to the file containing the call
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
FunctionCallResult representing the call
|
|
113
|
+
"""
|
|
114
|
+
return FunctionCallResult(
|
|
115
|
+
function_name=pattern.function_name,
|
|
116
|
+
param_index=pattern.param_index,
|
|
117
|
+
string_value=pattern.string_value,
|
|
118
|
+
file_path=file_path,
|
|
119
|
+
line_number=pattern.line_number,
|
|
120
|
+
column=pattern.column,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def analyze_comparisons(self, code: str, file_path: Path) -> list[ComparisonResult]:
|
|
124
|
+
"""Analyze TypeScript code for string comparisons.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
code: TypeScript source code to analyze
|
|
128
|
+
file_path: Path to the file being analyzed
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List of ComparisonResult instances for each detected comparison
|
|
132
|
+
"""
|
|
133
|
+
comparison_patterns = self._comparison_tracker.find_patterns(code)
|
|
134
|
+
return [
|
|
135
|
+
self._convert_comparison_pattern(pattern, file_path) for pattern in comparison_patterns
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
def _convert_comparison_pattern(
|
|
139
|
+
self, pattern: TypeScriptComparisonPattern, file_path: Path
|
|
140
|
+
) -> ComparisonResult:
|
|
141
|
+
"""Convert a TypeScriptComparisonPattern to ComparisonResult.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
pattern: Detected comparison pattern
|
|
145
|
+
file_path: Path to the file containing the comparison
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
ComparisonResult representing the comparison
|
|
149
|
+
"""
|
|
150
|
+
return ComparisonResult(
|
|
151
|
+
variable_name=pattern.variable_name,
|
|
152
|
+
compared_value=pattern.compared_value,
|
|
153
|
+
operator=pattern.operator,
|
|
154
|
+
file_path=file_path,
|
|
155
|
+
line_number=pattern.line_number,
|
|
156
|
+
column=pattern.column,
|
|
157
|
+
)
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Detect function calls with string literal arguments in TypeScript AST
|
|
3
|
+
|
|
4
|
+
Scope: Find function and method calls that consistently receive string arguments
|
|
5
|
+
|
|
6
|
+
Overview: Provides TypeScriptCallTracker class that uses tree-sitter to traverse TypeScript
|
|
7
|
+
AST and find function and method calls where string literals are passed as arguments.
|
|
8
|
+
Tracks the function name, parameter index, and string value to enable cross-file
|
|
9
|
+
aggregation. When a function is called with the same set of limited string values
|
|
10
|
+
across files, it suggests the parameter should be an enum. Handles both simple
|
|
11
|
+
function calls (foo("value")) and method calls (obj.method("value")), including
|
|
12
|
+
chained method calls.
|
|
13
|
+
|
|
14
|
+
Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, dataclasses for pattern structure,
|
|
15
|
+
src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
16
|
+
|
|
17
|
+
Exports: TypeScriptCallTracker class, TypeScriptFunctionCallPattern dataclass
|
|
18
|
+
|
|
19
|
+
Interfaces: TypeScriptCallTracker.find_patterns(code) -> list[TypeScriptFunctionCallPattern]
|
|
20
|
+
|
|
21
|
+
Implementation: Tree-sitter node traversal with call_expression node handling for string arguments
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
|
|
25
|
+
- srp: Tracker implements tree-sitter traversal with helper methods for call extraction.
|
|
26
|
+
Methods support single responsibility of function call pattern detection.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE, TypeScriptBaseAnalyzer
|
|
33
|
+
from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
34
|
+
|
|
35
|
+
if TREE_SITTER_AVAILABLE:
|
|
36
|
+
from tree_sitter import Node
|
|
37
|
+
else:
|
|
38
|
+
Node = Any # type: ignore[assignment,misc]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class TypeScriptFunctionCallPattern:
|
|
43
|
+
"""Represents a function call with a string literal argument.
|
|
44
|
+
|
|
45
|
+
Captures information about a function or method call where a string literal
|
|
46
|
+
is passed as an argument, enabling cross-file analysis to detect limited
|
|
47
|
+
value sets that should be enums.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
function_name: str
|
|
51
|
+
"""Fully qualified function name (e.g., 'process' or 'obj.method')."""
|
|
52
|
+
|
|
53
|
+
param_index: int
|
|
54
|
+
"""Index of the parameter receiving the string value (0-indexed)."""
|
|
55
|
+
|
|
56
|
+
string_value: str
|
|
57
|
+
"""The string literal value passed to the function."""
|
|
58
|
+
|
|
59
|
+
line_number: int
|
|
60
|
+
"""Line number where the call occurs (1-indexed)."""
|
|
61
|
+
|
|
62
|
+
column: int
|
|
63
|
+
"""Column number where the call starts (0-indexed)."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TypeScriptCallTracker(TypeScriptBaseAnalyzer): # thailint: ignore[srp]
|
|
67
|
+
"""Tracks function calls with string literal arguments in TypeScript.
|
|
68
|
+
|
|
69
|
+
Finds patterns like 'process("active")' and 'obj.setStatus("pending")' where
|
|
70
|
+
string literals are used for arguments that could be enums.
|
|
71
|
+
|
|
72
|
+
Note: Method count exceeds SRP limit because tree-sitter traversal requires
|
|
73
|
+
multiple helper methods for extracting function names, member expressions,
|
|
74
|
+
and argument handling. All methods support the single responsibility of
|
|
75
|
+
tracking function calls with string arguments.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self) -> None:
|
|
79
|
+
"""Initialize the tracker."""
|
|
80
|
+
super().__init__()
|
|
81
|
+
self.patterns: list[TypeScriptFunctionCallPattern] = []
|
|
82
|
+
|
|
83
|
+
def find_patterns(self, code: str) -> list[TypeScriptFunctionCallPattern]:
|
|
84
|
+
"""Find all function calls with string arguments in the code.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
code: TypeScript source code to analyze
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List of TypeScriptFunctionCallPattern instances for each detected call
|
|
91
|
+
"""
|
|
92
|
+
if not self.tree_sitter_available:
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
root = self.parse_typescript(code)
|
|
96
|
+
if root is None:
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
return self.find_patterns_from_tree(root)
|
|
100
|
+
|
|
101
|
+
def find_patterns_from_tree(self, tree: Node) -> list[TypeScriptFunctionCallPattern]:
|
|
102
|
+
"""Find all function calls with string arguments from a pre-parsed tree.
|
|
103
|
+
|
|
104
|
+
Optimized for single-parse workflows where the tree is shared between trackers.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
tree: Pre-parsed tree-sitter root node
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of TypeScriptFunctionCallPattern instances for each detected call
|
|
111
|
+
"""
|
|
112
|
+
self.patterns = []
|
|
113
|
+
self._traverse_tree(tree)
|
|
114
|
+
return self.patterns
|
|
115
|
+
|
|
116
|
+
def _traverse_tree(self, node: Node) -> None:
|
|
117
|
+
"""Recursively traverse tree looking for call expressions.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
node: Current tree-sitter node
|
|
121
|
+
"""
|
|
122
|
+
if node.type == "call_expression":
|
|
123
|
+
self._process_call_expression(node)
|
|
124
|
+
|
|
125
|
+
for child in node.children:
|
|
126
|
+
self._traverse_tree(child)
|
|
127
|
+
|
|
128
|
+
def _process_call_expression(self, node: Node) -> None:
|
|
129
|
+
"""Process a call expression node for string arguments.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
node: call_expression node
|
|
133
|
+
"""
|
|
134
|
+
function_name = self._extract_function_name(node)
|
|
135
|
+
if function_name is None:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
self._check_arguments(node, function_name)
|
|
139
|
+
|
|
140
|
+
def _extract_function_name(self, node: Node) -> str | None:
|
|
141
|
+
"""Extract the function name from a call expression.
|
|
142
|
+
|
|
143
|
+
Handles simple names (foo) and member access (obj.method).
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
node: call_expression node
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Function name string or None if not extractable
|
|
150
|
+
"""
|
|
151
|
+
# Find the function/method being called
|
|
152
|
+
func_node = self._find_function_node(node)
|
|
153
|
+
if func_node is None:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
if func_node.type == "identifier":
|
|
157
|
+
return self.extract_node_text(func_node)
|
|
158
|
+
|
|
159
|
+
if func_node.type == "member_expression":
|
|
160
|
+
return self._extract_member_expression_name(func_node)
|
|
161
|
+
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def _find_function_node(self, node: Node) -> Node | None:
|
|
165
|
+
"""Find the function/method node in a call expression.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
node: call_expression node
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The function or member_expression node, or None
|
|
172
|
+
"""
|
|
173
|
+
for child in node.children:
|
|
174
|
+
if child.type in ("identifier", "member_expression"):
|
|
175
|
+
return child
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
def _extract_member_expression_name(self, node: Node) -> str | None:
|
|
179
|
+
"""Extract function name from a member expression.
|
|
180
|
+
|
|
181
|
+
Builds qualified names like 'obj.method' or 'a.b.method'.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
node: member_expression node
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Qualified function name or None if too complex
|
|
188
|
+
"""
|
|
189
|
+
parts: list[str] = []
|
|
190
|
+
current: Node | None = node
|
|
191
|
+
|
|
192
|
+
for _ in range(MAX_ATTRIBUTE_CHAIN_DEPTH):
|
|
193
|
+
if current is None:
|
|
194
|
+
break
|
|
195
|
+
self._add_property_name(current, parts)
|
|
196
|
+
current = self._get_next_node(current, parts)
|
|
197
|
+
|
|
198
|
+
return ".".join(reversed(parts)) if parts else None
|
|
199
|
+
|
|
200
|
+
def _add_property_name(self, node: Node, parts: list[str]) -> None:
|
|
201
|
+
"""Add property name to parts list if found.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
node: member_expression node
|
|
205
|
+
parts: List to append property name to
|
|
206
|
+
"""
|
|
207
|
+
property_node = self._find_property_identifier(node)
|
|
208
|
+
if property_node:
|
|
209
|
+
parts.append(self.extract_node_text(property_node))
|
|
210
|
+
|
|
211
|
+
def _get_next_node(self, current: Node, parts: list[str]) -> Node | None:
|
|
212
|
+
"""Get the next node to process in member expression chain.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
current: Current member_expression node
|
|
216
|
+
parts: List of parts (modified if terminal node found)
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Next node to process or None to stop
|
|
220
|
+
"""
|
|
221
|
+
object_node = self._find_object_node(current)
|
|
222
|
+
if object_node is None:
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
if object_node.type == "identifier":
|
|
226
|
+
parts.append(self.extract_node_text(object_node))
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
if object_node.type == "member_expression":
|
|
230
|
+
return object_node
|
|
231
|
+
|
|
232
|
+
# Complex expression (call result, subscript, etc.)
|
|
233
|
+
parts.append("_")
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
def _find_property_identifier(self, node: Node) -> Node | None:
|
|
237
|
+
"""Find the property identifier in a member expression.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
node: member_expression node
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
property_identifier node or None
|
|
244
|
+
"""
|
|
245
|
+
for child in node.children:
|
|
246
|
+
if child.type == "property_identifier":
|
|
247
|
+
return child
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
def _find_object_node(self, node: Node) -> Node | None:
|
|
251
|
+
"""Find the object in a member expression.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
node: member_expression node
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Object node (identifier or member_expression) or None
|
|
258
|
+
"""
|
|
259
|
+
for child in node.children:
|
|
260
|
+
if child.type in ("identifier", "member_expression", "call_expression"):
|
|
261
|
+
return child
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
def _check_arguments(self, node: Node, function_name: str) -> None:
|
|
265
|
+
"""Check call arguments for string literals.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
node: call_expression node
|
|
269
|
+
function_name: Extracted function name
|
|
270
|
+
"""
|
|
271
|
+
args_node = self._find_arguments_node(node)
|
|
272
|
+
if args_node is None:
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
param_index = 0
|
|
276
|
+
for child in args_node.children:
|
|
277
|
+
string_value = self._extract_string_value(child)
|
|
278
|
+
if string_value is not None:
|
|
279
|
+
self._add_pattern(node, function_name, param_index, string_value)
|
|
280
|
+
# Only count actual arguments, not punctuation
|
|
281
|
+
if child.type not in ("(", ")", ","):
|
|
282
|
+
param_index += 1
|
|
283
|
+
|
|
284
|
+
def _find_arguments_node(self, node: Node) -> Node | None:
|
|
285
|
+
"""Find the arguments node in a call expression.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
node: call_expression node
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
arguments node or None
|
|
292
|
+
"""
|
|
293
|
+
for child in node.children:
|
|
294
|
+
if child.type == "arguments":
|
|
295
|
+
return child
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
def _extract_string_value(self, node: Node) -> str | None:
|
|
299
|
+
"""Extract string value from a node if it's a string literal.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
node: Potential string literal node
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
String value without quotes, or None if not a string
|
|
306
|
+
"""
|
|
307
|
+
if node.type != "string":
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
text = self.extract_node_text(node)
|
|
311
|
+
# Remove quotes (", ', or `)
|
|
312
|
+
if len(text) >= 2:
|
|
313
|
+
if text[0] in ('"', "'", "`") and text[-1] == text[0]:
|
|
314
|
+
return text[1:-1]
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
def _add_pattern(
|
|
318
|
+
self, node: Node, function_name: str, param_index: int, string_value: str
|
|
319
|
+
) -> None:
|
|
320
|
+
"""Create and add a function call pattern to results.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
node: call_expression node
|
|
324
|
+
function_name: Name of the function being called
|
|
325
|
+
param_index: Index of the string argument
|
|
326
|
+
string_value: The string literal value
|
|
327
|
+
"""
|
|
328
|
+
pattern = TypeScriptFunctionCallPattern(
|
|
329
|
+
function_name=function_name,
|
|
330
|
+
param_index=param_index,
|
|
331
|
+
string_value=string_value,
|
|
332
|
+
line_number=node.start_point[0] + 1, # 1-indexed
|
|
333
|
+
column=node.start_point[1],
|
|
334
|
+
)
|
|
335
|
+
self.patterns.append(pattern)
|