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.
Files changed (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {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)