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
src/linters/cqs/types.py
ADDED
|
@@ -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
|