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,82 @@
1
+ """
2
+ Purpose: Core data structures for CQS (Command-Query Separation) linter
3
+
4
+ Scope: Type definitions for INPUT operations, OUTPUT operations, and CQS patterns
5
+
6
+ Overview: Defines the fundamental data structures used by the CQS linter to represent
7
+ and analyze code patterns. InputOperation represents query-like operations that
8
+ assign call results to variables. OutputOperation represents command-like operations
9
+ that are statement-level calls without capturing return values. CQSPattern aggregates
10
+ these operations for a single function and provides methods to detect CQS violations
11
+ (functions that mix INPUTs and OUTPUTs).
12
+
13
+ Dependencies: dataclasses for structured data representation
14
+
15
+ Exports: InputOperation, OutputOperation, CQSPattern
16
+
17
+ Interfaces: CQSPattern.has_violation(), CQSPattern.get_full_name()
18
+
19
+ Implementation: Immutable dataclasses with computed methods for violation detection
20
+
21
+ Suppressions:
22
+ too-many-instance-attributes: CQSPattern requires 9 attributes to fully describe
23
+ a function's CQS analysis (name, location, file, inputs, outputs, flags)
24
+ """
25
+
26
+ from dataclasses import dataclass, field
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class InputOperation:
31
+ """Represents an INPUT (query) operation in code.
32
+
33
+ An INPUT operation is one where a function call result is captured and used,
34
+ typically through assignment: x = func().
35
+ """
36
+
37
+ line: int
38
+ column: int
39
+ expression: str # e.g., "fetch_data()"
40
+ target: str # e.g., "x" or "self.data"
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class OutputOperation:
45
+ """Represents an OUTPUT (command) operation in code.
46
+
47
+ An OUTPUT operation is a statement-level call where the return value is
48
+ discarded: func() as a standalone statement.
49
+ """
50
+
51
+ line: int
52
+ column: int
53
+ expression: str # e.g., "save_data(result)"
54
+
55
+
56
+ @dataclass
57
+ class CQSPattern: # pylint: disable=too-many-instance-attributes
58
+ """Represents a function's CQS analysis results.
59
+
60
+ Aggregates all INPUT and OUTPUT operations found within a function body
61
+ and provides methods to determine if the function violates CQS principles.
62
+ """
63
+
64
+ function_name: str
65
+ line: int
66
+ column: int
67
+ file_path: str
68
+ inputs: list[InputOperation] = field(default_factory=list)
69
+ outputs: list[OutputOperation] = field(default_factory=list)
70
+ is_method: bool = False
71
+ is_async: bool = False
72
+ class_name: str | None = None
73
+
74
+ def has_violation(self) -> bool:
75
+ """Return True if function mixes INPUTs and OUTPUTs (CQS violation)."""
76
+ return len(self.inputs) > 0 and len(self.outputs) > 0
77
+
78
+ def get_full_name(self) -> str:
79
+ """Return fully qualified name (ClassName.method or function)."""
80
+ if self.class_name:
81
+ return f"{self.class_name}.{self.function_name}"
82
+ return self.function_name
@@ -0,0 +1,61 @@
1
+ """
2
+ Purpose: Coordinator for TypeScript CQS analysis returning per-function CQSPattern objects
3
+
4
+ Scope: High-level analyzer that orchestrates TypeScriptFunctionAnalyzer for function-level detection
5
+
6
+ Overview: Provides TypeScriptCQSAnalyzer class that coordinates CQS pattern detection in TypeScript
7
+ code. Handles tree-sitter parsing with proper availability checking, returning empty results
8
+ when tree-sitter is unavailable rather than raising exceptions. Delegates to
9
+ TypeScriptFunctionAnalyzer to build CQSPattern objects for each function/method, which
10
+ contain INPUT and OUTPUT operations along with function metadata (name, class context,
11
+ async status).
12
+
13
+ Dependencies: TypeScriptBaseAnalyzer, TypeScriptFunctionAnalyzer, CQSConfig, CQSPattern
14
+
15
+ Exports: TypeScriptCQSAnalyzer
16
+
17
+ Interfaces: TypeScriptCQSAnalyzer.analyze(code, file_path, config) -> list[CQSPattern]
18
+
19
+ Implementation: Coordinates TypeScriptFunctionAnalyzer with tree-sitter availability checking
20
+ """
21
+
22
+ from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE, TypeScriptBaseAnalyzer
23
+
24
+ from .config import CQSConfig
25
+ from .types import CQSPattern
26
+ from .typescript_function_analyzer import TypeScriptFunctionAnalyzer
27
+
28
+
29
+ class TypeScriptCQSAnalyzer(TypeScriptBaseAnalyzer):
30
+ """Analyzes TypeScript code for CQS patterns, returning per-function results."""
31
+
32
+ def __init__(self) -> None:
33
+ """Initialize analyzer with function analyzer."""
34
+ super().__init__()
35
+ self._function_analyzer = TypeScriptFunctionAnalyzer()
36
+
37
+ def analyze(
38
+ self,
39
+ code: str,
40
+ file_path: str,
41
+ config: CQSConfig,
42
+ ) -> list[CQSPattern]:
43
+ """Analyze TypeScript code for CQS patterns in each function.
44
+
45
+ Args:
46
+ code: TypeScript source code to analyze
47
+ file_path: Path to the source file (for error context)
48
+ config: CQS configuration settings
49
+
50
+ Returns:
51
+ List of CQSPattern objects, one per function/method.
52
+ Returns empty list if tree-sitter is unavailable or parsing fails.
53
+ """
54
+ if not TREE_SITTER_AVAILABLE:
55
+ return []
56
+
57
+ root_node = self.parse_typescript(code)
58
+ if root_node is None:
59
+ return []
60
+
61
+ return self._function_analyzer.analyze(root_node, file_path, config)
@@ -0,0 +1,192 @@
1
+ """
2
+ Purpose: Tree-sitter based analyzer that builds CQSPattern objects for TypeScript functions
3
+
4
+ Scope: Per-function CQS analysis with config-driven filtering for TypeScript code
5
+
6
+ Overview: Provides TypeScriptFunctionAnalyzer class that traverses TypeScript AST to analyze
7
+ each function for CQS patterns. Builds CQSPattern objects containing INPUT and OUTPUT
8
+ operations for each function/method. Extends TypeScriptBaseAnalyzer for tree-sitter
9
+ utilities and delegates function extraction to TypeScriptFunctionExtractor. Applies
10
+ configuration filtering including ignore_methods for constructor exclusion and
11
+ detect_fluent_interface for return this patterns.
12
+
13
+ Dependencies: TypeScriptBaseAnalyzer, TypeScriptFunctionExtractor, TypeScriptInputDetector,
14
+ TypeScriptOutputDetector, CQSConfig, CQSPattern
15
+
16
+ Exports: TypeScriptFunctionAnalyzer
17
+
18
+ Interfaces: TypeScriptFunctionAnalyzer.analyze(root_node, file_path, config) -> list[CQSPattern]
19
+
20
+ Implementation: Tree-sitter function extraction with per-function INPUT/OUTPUT detection
21
+ """
22
+
23
+ from collections.abc import Callable
24
+
25
+ from src.analyzers.typescript_base import (
26
+ TREE_SITTER_AVAILABLE,
27
+ Node,
28
+ TypeScriptBaseAnalyzer,
29
+ )
30
+ from src.linters.nesting.typescript_function_extractor import TypeScriptFunctionExtractor
31
+
32
+ from .config import CQSConfig
33
+ from .types import CQSPattern, InputOperation, OutputOperation
34
+ from .typescript_input_detector import TypeScriptInputDetector
35
+ from .typescript_output_detector import TypeScriptOutputDetector
36
+
37
+
38
+ def _get_function_child_positions(func_node: Node) -> list[tuple[int, int]]:
39
+ """Get positions of child function nodes from a function_declaration."""
40
+ return [
41
+ (child.start_point[0], child.start_point[1])
42
+ for child in func_node.children
43
+ if child.type == "function"
44
+ ]
45
+
46
+
47
+ def _get_child_function_positions(functions: list[tuple[Node, str]]) -> set[tuple[int, int]]:
48
+ """Get positions of function nodes that are children of function_declarations."""
49
+ declaration_nodes = (
50
+ func_node for func_node, _ in functions if func_node.type == "function_declaration"
51
+ )
52
+ return {pos for node in declaration_nodes for pos in _get_function_child_positions(node)}
53
+
54
+
55
+ def _filter_duplicate_functions(
56
+ functions: list[tuple[Node, str]],
57
+ ) -> list[tuple[Node, str]]:
58
+ """Filter out duplicate function detections."""
59
+ declaration_positions = _get_child_function_positions(functions)
60
+ return [
61
+ (func_node, func_name)
62
+ for func_node, func_name in functions
63
+ if not (
64
+ func_node.type == "function"
65
+ and (func_node.start_point[0], func_node.start_point[1]) in declaration_positions
66
+ )
67
+ ]
68
+
69
+
70
+ def _find_function_body(func_node: Node) -> Node | None:
71
+ """Find the statement_block (body) of a function."""
72
+ return next(
73
+ (child for child in func_node.children if child.type == "statement_block"),
74
+ None,
75
+ )
76
+
77
+
78
+ def _ends_with_return_this(body_node: Node) -> bool:
79
+ """Check if function body ends with 'return this'."""
80
+ returns = [child for child in body_node.children if child.type == "return_statement"]
81
+ if not returns:
82
+ return False
83
+ return any(child.type == "this" for child in returns[-1].children)
84
+
85
+
86
+ def _is_async_function(func_node: Node, text: str) -> bool:
87
+ """Check if function is async."""
88
+ if any(child.type == "async" for child in func_node.children):
89
+ return True
90
+ return text.startswith("async ")
91
+
92
+
93
+ def _is_constructor_method(node: Node, get_text: Callable[[Node], str]) -> bool:
94
+ """Check if method is a constructor."""
95
+ for child in node.children:
96
+ if child.type == "property_identifier":
97
+ return get_text(child) == "constructor"
98
+ return False
99
+
100
+
101
+ def _get_class_type_identifier(class_node: Node, get_text: Callable[[Node], str]) -> str | None:
102
+ """Extract the type identifier from a class node."""
103
+ for child in class_node.children:
104
+ if child.type == "type_identifier":
105
+ return get_text(child)
106
+ return None
107
+
108
+
109
+ def _find_enclosing_class_name(func_node: Node, get_text: Callable[[Node], str]) -> str | None:
110
+ """Find enclosing class name for a method."""
111
+ current = func_node.parent
112
+ while current is not None:
113
+ if current.type in ("class_declaration", "class"):
114
+ return _get_class_type_identifier(current, get_text)
115
+ current = current.parent
116
+ return None
117
+
118
+
119
+ class TypeScriptFunctionAnalyzer(TypeScriptBaseAnalyzer):
120
+ """Analyzes TypeScript AST to build CQSPattern objects for each function."""
121
+
122
+ def __init__(self) -> None:
123
+ """Initialize analyzer with input/output detectors."""
124
+ super().__init__()
125
+ self._function_extractor = TypeScriptFunctionExtractor()
126
+ self._input_detector = TypeScriptInputDetector()
127
+ self._output_detector = TypeScriptOutputDetector()
128
+
129
+ def analyze(self, root_node: Node, file_path: str, config: CQSConfig) -> list[CQSPattern]:
130
+ """Analyze TypeScript AST and return CQSPattern for each function."""
131
+ if not TREE_SITTER_AVAILABLE or root_node is None:
132
+ return []
133
+
134
+ functions = self._function_extractor.collect_all_functions(root_node)
135
+ functions = _filter_duplicate_functions(functions)
136
+
137
+ return [
138
+ self._analyze_function(func_node, func_name, file_path)
139
+ for func_node, func_name in functions
140
+ if not self._should_skip(func_node, func_name, config)
141
+ ]
142
+
143
+ def _should_skip(self, func_node: Node, func_name: str, config: CQSConfig) -> bool:
144
+ """Check if function should be skipped (ignored or fluent interface)."""
145
+ return self._is_ignored_method(
146
+ func_node, func_name, config
147
+ ) or self._is_fluent_interface_function(func_node, config)
148
+
149
+ def _is_ignored_method(self, func_node: Node, func_name: str, config: CQSConfig) -> bool:
150
+ """Check if method should be ignored based on config."""
151
+ if func_name in config.ignore_methods:
152
+ return True
153
+ if func_node.type != "method_definition":
154
+ return False
155
+ if not _is_constructor_method(func_node, self.extract_node_text):
156
+ return False
157
+ return "constructor" in config.ignore_methods or "__init__" in config.ignore_methods
158
+
159
+ def _is_fluent_interface_function(self, func_node: Node, config: CQSConfig) -> bool:
160
+ """Check if function uses fluent interface pattern."""
161
+ if not config.detect_fluent_interface:
162
+ return False
163
+ body_node = _find_function_body(func_node)
164
+ return body_node is not None and _ends_with_return_this(body_node)
165
+
166
+ def _analyze_function(self, func_node: Node, func_name: str, file_path: str) -> CQSPattern:
167
+ """Analyze a single function for CQS patterns."""
168
+ body_node = _find_function_body(func_node)
169
+ inputs, outputs = self._detect_operations(body_node)
170
+
171
+ return CQSPattern(
172
+ function_name=func_name,
173
+ line=func_node.start_point[0] + 1,
174
+ column=func_node.start_point[1],
175
+ file_path=file_path,
176
+ inputs=inputs,
177
+ outputs=outputs,
178
+ is_method=func_node.type == "method_definition",
179
+ is_async=_is_async_function(func_node, self.extract_node_text(func_node)),
180
+ class_name=_find_enclosing_class_name(func_node, self.extract_node_text),
181
+ )
182
+
183
+ def _detect_operations(
184
+ self, body_node: Node | None
185
+ ) -> tuple[list[InputOperation], list[OutputOperation]]:
186
+ """Detect INPUT and OUTPUT operations in function body."""
187
+ if body_node is None:
188
+ return [], []
189
+ return (
190
+ self._input_detector.find_inputs(body_node),
191
+ self._output_detector.find_outputs(body_node),
192
+ )
@@ -0,0 +1,203 @@
1
+ """
2
+ Purpose: Tree-sitter based detector for INPUT (query) operations in TypeScript CQS analysis
3
+
4
+ Scope: Detects assignment patterns where function call results are captured in TypeScript code
5
+
6
+ Overview: Provides TypeScriptInputDetector class that uses tree-sitter AST traversal to find
7
+ INPUT operations in TypeScript code. INPUT operations are query-like assignments that
8
+ capture function call return values. Detects patterns including variable declarations
9
+ (const x = func(), let x = func()), destructuring (const { a, b } = func(),
10
+ const [a, b] = func()), await assignments (const x = await func()), and class field
11
+ assignments (this.x = func()). Uses tree-sitter node types lexical_declaration,
12
+ variable_declarator, assignment_expression, and call_expression for detection.
13
+
14
+ Dependencies: tree-sitter via TypeScriptBaseAnalyzer
15
+
16
+ Exports: TypeScriptInputDetector
17
+
18
+ Interfaces: TypeScriptInputDetector.find_inputs(root_node) -> list[InputOperation]
19
+
20
+ Implementation: Tree-sitter AST traversal with recursive node collection
21
+ """
22
+
23
+ from collections.abc import Callable
24
+ from typing import Any
25
+
26
+ from src.analyzers.typescript_base import (
27
+ TREE_SITTER_AVAILABLE,
28
+ Node,
29
+ TypeScriptBaseAnalyzer,
30
+ )
31
+
32
+ from .types import InputOperation
33
+
34
+ # Module-level helper functions for AST navigation
35
+
36
+
37
+ def _find_child_by_type(node: Node, types: set[str]) -> Node | None:
38
+ """Find first child matching any of the given types."""
39
+ return next((child for child in node.children if child.type in types), None)
40
+
41
+
42
+ def _find_children_after_token(node: Node, token: str) -> list[Node]:
43
+ """Get all children after a specific token."""
44
+ children = list(node.children)
45
+ for i, child in enumerate(children):
46
+ if child.type == token:
47
+ return children[i + 1 :]
48
+ return []
49
+
50
+
51
+ def _find_after_token(node: Node, token: str, exclude_types: set[str] | None = None) -> Node | None:
52
+ """Find first child after a specific token, excluding certain types."""
53
+ exclude = exclude_types or set()
54
+ remaining = _find_children_after_token(node, token)
55
+ return next((c for c in remaining if c.type not in exclude), None)
56
+
57
+
58
+ def _find_declarator_name(node: Node) -> Node | None:
59
+ """Find name/pattern node in variable declarator."""
60
+ return _find_child_by_type(node, {"identifier", "object_pattern", "array_pattern"})
61
+
62
+
63
+ def _find_declarator_value(node: Node) -> Node | None:
64
+ """Find value node in variable declarator (after =)."""
65
+ return _find_after_token(node, "=", {":", "type_annotation"})
66
+
67
+
68
+ def _find_assignment_left(node: Node) -> Node | None:
69
+ """Find left side of assignment expression."""
70
+ return _find_child_by_type(node, {"identifier", "member_expression", "subscript_expression"})
71
+
72
+
73
+ def _find_assignment_right(node: Node) -> Node | None:
74
+ """Find right side of assignment expression (after =)."""
75
+ return _find_after_token(node, "=")
76
+
77
+
78
+ def _extract_call_from_value(node: Node) -> Node | None:
79
+ """Extract call expression from value, handling await wrapper."""
80
+ if node.type == "call_expression":
81
+ return node
82
+ if node.type == "await_expression":
83
+ return next((c for c in node.children if c.type == "call_expression"), None)
84
+ return None
85
+
86
+
87
+ def _find_pattern_value(pair_node: Node) -> Any:
88
+ """Find value identifier in pair pattern (e.g., a: b -> returns 'b')."""
89
+ children_after_colon = _find_children_after_token(pair_node, ":")
90
+ return next((c for c in children_after_colon if c.type == "identifier"), None)
91
+
92
+
93
+ def _get_pattern_child_name(child: Node, get_text: Callable[[Node], str]) -> str | None:
94
+ """Extract name from an object pattern child node."""
95
+ if child.type == "shorthand_property_identifier_pattern":
96
+ return get_text(child)
97
+ if child.type == "pair_pattern":
98
+ value = _find_pattern_value(child)
99
+ return get_text(value) if value else None
100
+ return None
101
+
102
+
103
+ def _extract_object_pattern_names(node: Node, get_text: Callable[[Node], str]) -> str:
104
+ """Extract names from object destructuring pattern."""
105
+ names = [
106
+ name
107
+ for child in node.children
108
+ if (name := _get_pattern_child_name(child, get_text)) is not None
109
+ ]
110
+ return ", ".join(names) if names else get_text(node)
111
+
112
+
113
+ def _extract_array_pattern_names(node: Node, get_text: Callable[[Node], str]) -> str:
114
+ """Extract names from array destructuring pattern."""
115
+ names = [get_text(child) for child in node.children if child.type == "identifier"]
116
+ return ", ".join(names) if names else get_text(node)
117
+
118
+
119
+ def _extract_target_name(name_node: Node, get_text: Callable[[Node], str]) -> str:
120
+ """Extract string representation of assignment target."""
121
+ if name_node.type == "identifier":
122
+ return get_text(name_node)
123
+ if name_node.type == "object_pattern":
124
+ return _extract_object_pattern_names(name_node, get_text)
125
+ if name_node.type == "array_pattern":
126
+ return _extract_array_pattern_names(name_node, get_text)
127
+ return get_text(name_node)
128
+
129
+
130
+ class TypeScriptInputDetector(TypeScriptBaseAnalyzer):
131
+ """Detects INPUT (query) operations that capture function call results in TypeScript."""
132
+
133
+ def find_inputs(self, root_node: Node) -> list[InputOperation]:
134
+ """Find INPUT operations in TypeScript AST."""
135
+ if not TREE_SITTER_AVAILABLE or root_node is None:
136
+ return []
137
+ inputs: list[InputOperation] = []
138
+ self._find_inputs_recursive(root_node, inputs)
139
+ return inputs
140
+
141
+ def _find_inputs_recursive(self, node: Node, inputs: list[InputOperation]) -> None:
142
+ """Recursively find INPUT operations in AST."""
143
+ if node.type == "lexical_declaration":
144
+ self._check_lexical_declaration(node, inputs)
145
+ elif node.type == "assignment_expression":
146
+ self._check_assignment_expression(node, inputs)
147
+
148
+ for child in node.children:
149
+ self._find_inputs_recursive(child, inputs)
150
+
151
+ def _check_lexical_declaration(self, node: Node, inputs: list[InputOperation]) -> None:
152
+ """Check lexical declaration for INPUT patterns."""
153
+ for child in node.children:
154
+ if child.type == "variable_declarator":
155
+ self._check_variable_declarator(child, inputs)
156
+
157
+ def _check_variable_declarator(self, node: Node, inputs: list[InputOperation]) -> None:
158
+ """Check variable declarator for call expression assignment."""
159
+ name_node = _find_declarator_name(node)
160
+ value_node = _find_declarator_value(node)
161
+
162
+ if name_node is None or value_node is None:
163
+ return
164
+
165
+ call_node = _extract_call_from_value(value_node)
166
+ if call_node is None:
167
+ return
168
+
169
+ inputs.append(self._create_input_operation(node, name_node, call_node))
170
+
171
+ def _check_assignment_expression(self, node: Node, inputs: list[InputOperation]) -> None:
172
+ """Check assignment expression for INPUT pattern (this.x = func())."""
173
+ left_node = _find_assignment_left(node)
174
+ right_node = _find_assignment_right(node)
175
+
176
+ if left_node is None or right_node is None:
177
+ return
178
+
179
+ call_node = _extract_call_from_value(right_node)
180
+ if call_node is None:
181
+ return
182
+
183
+ target = self.extract_node_text(left_node)
184
+ expression = self.extract_node_text(call_node)
185
+ inputs.append(
186
+ InputOperation(
187
+ line=node.start_point[0] + 1,
188
+ column=node.start_point[1],
189
+ expression=expression,
190
+ target=target,
191
+ )
192
+ )
193
+
194
+ def _create_input_operation(
195
+ self, node: Node, name_node: Node, call_node: Node
196
+ ) -> InputOperation:
197
+ """Create an InputOperation from parsed nodes."""
198
+ return InputOperation(
199
+ line=node.start_point[0] + 1,
200
+ column=node.start_point[1],
201
+ expression=self.extract_node_text(call_node),
202
+ target=_extract_target_name(name_node, self.extract_node_text),
203
+ )
@@ -0,0 +1,117 @@
1
+ """
2
+ Purpose: Tree-sitter based detector for OUTPUT (command) operations in TypeScript CQS analysis
3
+
4
+ Scope: Detects statement-level calls where return values are discarded in TypeScript code
5
+
6
+ Overview: Provides TypeScriptOutputDetector class that uses tree-sitter AST traversal to find
7
+ OUTPUT operations in TypeScript code. OUTPUT operations are command-like statement-level
8
+ function calls that discard return values. Detects patterns including statement calls
9
+ (func();), async statement calls (await func();), method calls (obj.method();), and
10
+ chained method calls (obj.method().method2();). Only expression_statement nodes containing
11
+ call_expression or await_expression are detected as OUTPUT. Naturally excludes return
12
+ statements, conditionals, assignments, and other constructs that use call results.
13
+
14
+ Dependencies: tree-sitter via TypeScriptBaseAnalyzer
15
+
16
+ Exports: TypeScriptOutputDetector
17
+
18
+ Interfaces: TypeScriptOutputDetector.find_outputs(root_node) -> list[OutputOperation]
19
+
20
+ Implementation: Tree-sitter AST traversal targeting expression_statement nodes
21
+ """
22
+
23
+ from src.analyzers.typescript_base import (
24
+ TREE_SITTER_AVAILABLE,
25
+ Node,
26
+ TypeScriptBaseAnalyzer,
27
+ )
28
+
29
+ from .types import OutputOperation
30
+
31
+
32
+ class TypeScriptOutputDetector(TypeScriptBaseAnalyzer):
33
+ """Detects OUTPUT (command) operations that discard function call results in TypeScript."""
34
+
35
+ def find_outputs(self, root_node: Node) -> list[OutputOperation]:
36
+ """Find OUTPUT operations in TypeScript AST.
37
+
38
+ Args:
39
+ root_node: Tree-sitter AST root node to analyze
40
+
41
+ Returns:
42
+ List of detected OutputOperation objects
43
+ """
44
+ if not TREE_SITTER_AVAILABLE or root_node is None:
45
+ return []
46
+
47
+ outputs: list[OutputOperation] = []
48
+ self._find_outputs_recursive(root_node, outputs)
49
+ return outputs
50
+
51
+ def _find_outputs_recursive(self, node: Node, outputs: list[OutputOperation]) -> None:
52
+ """Recursively find OUTPUT operations in AST.
53
+
54
+ Only expression_statement containing call_expression or await_expression
55
+ with call_expression are OUTPUT operations.
56
+
57
+ Args:
58
+ node: Current tree-sitter node
59
+ outputs: List to accumulate OutputOperation objects
60
+ """
61
+ if node.type == "expression_statement":
62
+ self._check_expression_statement(node, outputs)
63
+
64
+ # Recurse into children
65
+ for child in node.children:
66
+ self._find_outputs_recursive(child, outputs)
67
+
68
+ def _check_expression_statement(self, node: Node, outputs: list[OutputOperation]) -> None:
69
+ """Check expression statement for OUTPUT pattern.
70
+
71
+ Handles: func();, await func();, obj.method();
72
+
73
+ Args:
74
+ node: expression_statement node
75
+ outputs: List to append outputs to
76
+ """
77
+ call_node = self._find_call_in_expression(node)
78
+ if call_node is None:
79
+ return
80
+
81
+ expression = self.extract_node_text(call_node)
82
+ line = node.start_point[0] + 1
83
+ column = node.start_point[1]
84
+
85
+ outputs.append(OutputOperation(line=line, column=column, expression=expression))
86
+
87
+ def _find_call_in_expression(self, node: Node) -> Node | None:
88
+ """Find call expression in expression statement.
89
+
90
+ Handles direct calls and await expressions containing calls.
91
+
92
+ Args:
93
+ node: expression_statement node
94
+
95
+ Returns:
96
+ call_expression node or None
97
+ """
98
+ for child in node.children:
99
+ if child.type == "call_expression":
100
+ return child
101
+ if child.type == "await_expression":
102
+ return self._find_call_in_await(child)
103
+ return None
104
+
105
+ def _find_call_in_await(self, node: Node) -> Node | None:
106
+ """Find call expression inside await expression.
107
+
108
+ Args:
109
+ node: await_expression node
110
+
111
+ Returns:
112
+ call_expression node or None
113
+ """
114
+ for child in node.children:
115
+ if child.type == "call_expression":
116
+ return child
117
+ return None