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,201 @@
1
+ """
2
+ Purpose: AST visitor that builds CQSPattern objects for each function in Python code
3
+
4
+ Scope: Per-function CQS analysis with config-driven filtering
5
+
6
+ Overview: Provides FunctionAnalyzer class that traverses Python AST to analyze each function
7
+ for CQS patterns. Builds CQSPattern objects containing INPUT and OUTPUT operations for
8
+ each function/method. Applies configuration filtering including ignore_methods for
9
+ constructor exclusion, ignore_decorators for property-like methods, and detect_fluent_interface
10
+ for return self patterns. Tracks class context via stack for proper method detection.
11
+
12
+ Dependencies: ast module, InputDetector, OutputDetector, CQSConfig, CQSPattern
13
+
14
+ Exports: FunctionAnalyzer
15
+
16
+ Interfaces: FunctionAnalyzer.analyze(tree: ast.Module) -> list[CQSPattern]
17
+
18
+ Implementation: AST NodeVisitor with class context tracking and config-based filtering
19
+
20
+ Suppressions:
21
+ - N802: visit_ClassDef, visit_FunctionDef, visit_AsyncFunctionDef follow Python AST
22
+ visitor naming convention (camelCase required by ast.NodeVisitor)
23
+ - invalid-name: AST visitor methods follow required camelCase naming convention
24
+ """
25
+
26
+ import ast
27
+ from collections.abc import Sequence
28
+
29
+ from .config import CQSConfig
30
+ from .input_detector import InputDetector
31
+ from .output_detector import OutputDetector
32
+ from .types import CQSPattern, InputOperation, OutputOperation
33
+
34
+
35
+ def _get_name_from_decorator(dec: ast.expr) -> str | None:
36
+ """Extract decorator name from a single decorator expression."""
37
+ if isinstance(dec, ast.Name):
38
+ return dec.id
39
+ if isinstance(dec, ast.Attribute):
40
+ return dec.attr
41
+ if isinstance(dec, ast.Call):
42
+ return _get_name_from_call_decorator(dec.func)
43
+ return None
44
+
45
+
46
+ def _get_name_from_call_decorator(func: ast.expr) -> str | None:
47
+ """Extract name from Call decorator's func attribute."""
48
+ if isinstance(func, ast.Name):
49
+ return func.id
50
+ if isinstance(func, ast.Attribute):
51
+ return func.attr
52
+ return None
53
+
54
+
55
+ def _get_decorator_names(decorators: list[ast.expr]) -> list[str]:
56
+ """Extract decorator names from decorator list."""
57
+ names = [_get_name_from_decorator(dec) for dec in decorators]
58
+ return [name for name in names if name is not None]
59
+
60
+
61
+ def _has_return_self(body: Sequence[ast.stmt]) -> bool:
62
+ """Check if function body ends with 'return self'."""
63
+ if not body:
64
+ return False
65
+
66
+ last_stmt = body[-1]
67
+ if not isinstance(last_stmt, ast.Return):
68
+ return False
69
+ if last_stmt.value is None:
70
+ return False
71
+ return isinstance(last_stmt.value, ast.Name) and last_stmt.value.id == "self"
72
+
73
+
74
+ def _is_fluent_interface(node: ast.FunctionDef | ast.AsyncFunctionDef, config: CQSConfig) -> bool:
75
+ """Check if function uses fluent interface pattern (return self)."""
76
+ if not config.detect_fluent_interface:
77
+ return False
78
+ return _has_return_self(node.body)
79
+
80
+
81
+ def _is_function_definition(stmt: ast.stmt) -> bool:
82
+ """Check if statement is a function definition."""
83
+ return isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef))
84
+
85
+
86
+ def _detect_inputs_in_body(
87
+ body: Sequence[ast.stmt], input_detector: InputDetector
88
+ ) -> list[InputOperation]:
89
+ """Detect INPUT operations in function body statements."""
90
+ inputs: list[InputOperation] = []
91
+ stmts = [stmt for stmt in body if not _is_function_definition(stmt)]
92
+ for stmt in stmts:
93
+ mini_module = ast.Module(body=[stmt], type_ignores=[])
94
+ stmt_inputs = input_detector.find_inputs(mini_module)
95
+ inputs.extend(stmt_inputs)
96
+ return inputs
97
+
98
+
99
+ def _detect_outputs_in_body(
100
+ body: Sequence[ast.stmt], output_detector: OutputDetector
101
+ ) -> list[OutputOperation]:
102
+ """Detect OUTPUT operations in function body statements."""
103
+ outputs: list[OutputOperation] = []
104
+ stmts = [stmt for stmt in body if not _is_function_definition(stmt)]
105
+ for stmt in stmts:
106
+ mini_module = ast.Module(body=[stmt], type_ignores=[])
107
+ stmt_outputs = output_detector.find_outputs(mini_module)
108
+ outputs.extend(stmt_outputs)
109
+ return outputs
110
+
111
+
112
+ class FunctionAnalyzer(ast.NodeVisitor):
113
+ """Analyzes Python AST to build CQSPattern objects for each function."""
114
+
115
+ def __init__(self, file_path: str, config: CQSConfig) -> None:
116
+ """Initialize the analyzer."""
117
+ self._file_path = file_path
118
+ self._config = config
119
+ self._input_detector = InputDetector()
120
+ self._output_detector = OutputDetector()
121
+ self._patterns: list[CQSPattern] = []
122
+ self._class_stack: list[str] = []
123
+
124
+ def analyze(self, tree: ast.Module) -> list[CQSPattern]:
125
+ """Analyze AST and return CQSPattern for each function."""
126
+ self._patterns = []
127
+ self._class_stack = []
128
+ self.visit(tree)
129
+ return list(self._patterns)
130
+
131
+ def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: N802 # pylint: disable=invalid-name
132
+ """Visit class definition to track class context."""
133
+ self._class_stack.append(node.name)
134
+ self.generic_visit(node)
135
+ self._class_stack.pop()
136
+
137
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802 # pylint: disable=invalid-name
138
+ """Visit function definition to analyze for CQS patterns."""
139
+ self._analyze_function(node, is_async=False)
140
+
141
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: # noqa: N802 # pylint: disable=invalid-name
142
+ """Visit async function definition to analyze for CQS patterns."""
143
+ self._analyze_function(node, is_async=True)
144
+
145
+ def _analyze_function(
146
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef, is_async: bool
147
+ ) -> None:
148
+ """Analyze a function/method for CQS patterns."""
149
+ # Check if function should be ignored
150
+ if self._should_ignore_function(node):
151
+ self.generic_visit(node)
152
+ return
153
+
154
+ # Check for fluent interface pattern
155
+ if _is_fluent_interface(node, self._config):
156
+ self.generic_visit(node)
157
+ return
158
+
159
+ # Detect INPUTs and OUTPUTs in function body only (not nested functions)
160
+ inputs = _detect_inputs_in_body(node.body, self._input_detector)
161
+ outputs = _detect_outputs_in_body(node.body, self._output_detector)
162
+
163
+ # Build pattern
164
+ pattern = self._build_pattern(node, is_async, inputs, outputs)
165
+ self._patterns.append(pattern)
166
+
167
+ # Continue visiting nested functions
168
+ self.generic_visit(node)
169
+
170
+ def _build_pattern(
171
+ self,
172
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
173
+ is_async: bool,
174
+ inputs: list[InputOperation],
175
+ outputs: list[OutputOperation],
176
+ ) -> CQSPattern:
177
+ """Build CQSPattern from function node and detected operations."""
178
+ is_method = len(self._class_stack) > 0
179
+ class_name = self._class_stack[-1] if self._class_stack else None
180
+
181
+ return CQSPattern(
182
+ function_name=node.name,
183
+ line=node.lineno,
184
+ column=node.col_offset,
185
+ file_path=self._file_path,
186
+ inputs=inputs,
187
+ outputs=outputs,
188
+ is_method=is_method,
189
+ is_async=is_async,
190
+ class_name=class_name,
191
+ )
192
+
193
+ def _should_ignore_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
194
+ """Check if function should be ignored based on config."""
195
+ # Check ignore_methods (e.g., __init__, __new__)
196
+ if node.name in self._config.ignore_methods:
197
+ return True
198
+
199
+ # Check ignore_decorators (e.g., @property, @cached_property)
200
+ decorator_names = _get_decorator_names(node.decorator_list)
201
+ return any(name in self._config.ignore_decorators for name in decorator_names)
@@ -0,0 +1,139 @@
1
+ """
2
+ Purpose: AST-based detector for INPUT (query) operations in CQS analysis
3
+
4
+ Scope: Detects assignment patterns where function call results are captured
5
+
6
+ Overview: Provides InputDetector class that uses AST traversal to find INPUT operations
7
+ which are query-like assignments that capture function call return values. Detects
8
+ patterns including simple assignments (x = func()), tuple unpacking (x, y = func()),
9
+ async assignments (x = await func()), attribute assignments (self.x = func()),
10
+ subscript assignments (cache[key] = func()), annotated assignments (result: int = func()),
11
+ and walrus operator patterns ((x := func())). Excludes non-call assignments like
12
+ literals, variable copies, and expression results.
13
+
14
+ Dependencies: ast module for Python AST traversal
15
+
16
+ Exports: InputDetector
17
+
18
+ Interfaces: InputDetector.find_inputs(tree: ast.AST) -> list[InputOperation]
19
+
20
+ Implementation: AST NodeVisitor pattern with visit_Assign, visit_AnnAssign, visit_NamedExpr
21
+
22
+ Suppressions:
23
+ - N802: visit_Assign, visit_AnnAssign, visit_NamedExpr follow Python AST visitor
24
+ naming convention (camelCase required by ast.NodeVisitor)
25
+ - invalid-name: visit_Assign, visit_AnnAssign, visit_NamedExpr follow Python AST visitor
26
+ naming convention (camelCase required by ast.NodeVisitor)
27
+ """
28
+
29
+ import ast
30
+
31
+ from .types import InputOperation
32
+
33
+
34
+ def _is_call_expression(node: ast.expr) -> bool:
35
+ """Check if expression is a Call or Await(Call)."""
36
+ if isinstance(node, ast.Call):
37
+ return True
38
+ if isinstance(node, ast.Await) and isinstance(node.value, ast.Call):
39
+ return True
40
+ return False
41
+
42
+
43
+ def _unwrap_await(node: ast.expr) -> ast.expr:
44
+ """Unwrap Await to get inner expression."""
45
+ if isinstance(node, ast.Await):
46
+ return node.value
47
+ return node
48
+
49
+
50
+ def _extract_target_name(target: ast.expr) -> str:
51
+ """Extract string representation of assignment target."""
52
+ if isinstance(target, ast.Name):
53
+ return target.id
54
+ if isinstance(target, ast.Tuple):
55
+ return ", ".join(_extract_target_name(elt) for elt in target.elts)
56
+ return ast.unparse(target)
57
+
58
+
59
+ class InputDetector(ast.NodeVisitor):
60
+ """Detects INPUT (query) operations that capture function call results."""
61
+
62
+ def __init__(self) -> None:
63
+ """Initialize the detector."""
64
+ self._inputs: list[InputOperation] = []
65
+
66
+ def find_inputs(self, tree: ast.AST) -> list[InputOperation]:
67
+ """Find INPUT operations in AST.
68
+
69
+ Args:
70
+ tree: Python AST to analyze
71
+
72
+ Returns:
73
+ List of detected InputOperation objects
74
+ """
75
+ self._inputs = []
76
+ self.visit(tree)
77
+ return list(self._inputs)
78
+
79
+ def visit_Assign(self, node: ast.Assign) -> None: # noqa: N802 # pylint: disable=invalid-name
80
+ """Visit assignment to check for INPUT pattern.
81
+
82
+ Detects: x = func(), x, y = func(), self.x = func(), x[key] = func()
83
+
84
+ Args:
85
+ node: AST Assign node to analyze
86
+ """
87
+ if _is_call_expression(node.value):
88
+ call_node = _unwrap_await(node.value)
89
+ for target in node.targets:
90
+ self._inputs.append(
91
+ InputOperation(
92
+ line=node.lineno,
93
+ column=node.col_offset,
94
+ expression=ast.unparse(call_node),
95
+ target=_extract_target_name(target),
96
+ )
97
+ )
98
+ self.generic_visit(node)
99
+
100
+ def visit_AnnAssign(self, node: ast.AnnAssign) -> None: # noqa: N802 # pylint: disable=invalid-name
101
+ """Visit annotated assignment to check for INPUT pattern.
102
+
103
+ Detects: result: int = func()
104
+
105
+ Args:
106
+ node: AST AnnAssign node to analyze
107
+ """
108
+ if node.value is not None and node.target is not None:
109
+ if _is_call_expression(node.value):
110
+ call_node = _unwrap_await(node.value)
111
+ self._inputs.append(
112
+ InputOperation(
113
+ line=node.lineno,
114
+ column=node.col_offset,
115
+ expression=ast.unparse(call_node),
116
+ target=_extract_target_name(node.target),
117
+ )
118
+ )
119
+ self.generic_visit(node)
120
+
121
+ def visit_NamedExpr(self, node: ast.NamedExpr) -> None: # noqa: N802 # pylint: disable=invalid-name
122
+ """Visit named expression (walrus operator) to check for INPUT pattern.
123
+
124
+ Detects: (x := func())
125
+
126
+ Args:
127
+ node: AST NamedExpr node to analyze
128
+ """
129
+ if _is_call_expression(node.value):
130
+ call_node = _unwrap_await(node.value)
131
+ self._inputs.append(
132
+ InputOperation(
133
+ line=node.lineno,
134
+ column=node.col_offset,
135
+ expression=ast.unparse(call_node),
136
+ target=node.target.id,
137
+ )
138
+ )
139
+ self.generic_visit(node)
@@ -0,0 +1,159 @@
1
+ """
2
+ Purpose: Main CQS linter rule implementing MultiLanguageLintRule interface
3
+
4
+ Scope: Entry point for CQS (Command-Query Separation) violation detection in Python and TypeScript
5
+
6
+ Overview: Provides CQSRule class that implements the MultiLanguageLintRule interface for
7
+ detecting functions that violate Command-Query Separation by mixing queries (INPUTs)
8
+ with commands (OUTPUTs). Supports both Python (via AST) and TypeScript (via tree-sitter).
9
+ Returns violations for functions that mix operations according to min_operations threshold.
10
+ Supports file path ignore patterns via glob matching.
11
+
12
+ Dependencies: MultiLanguageLintRule, PythonCQSAnalyzer, TypeScriptCQSAnalyzer, CQSConfig,
13
+ build_cqs_violation
14
+
15
+ Exports: CQSRule
16
+
17
+ Interfaces: check(context: BaseLintContext) -> list[Violation]
18
+
19
+ Implementation: Multi-language analysis with config-driven filtering and min_operations threshold
20
+ """
21
+
22
+ from fnmatch import fnmatch
23
+
24
+ from src.core.base import BaseLintContext, MultiLanguageLintRule
25
+ from src.core.linter_utils import load_linter_config
26
+ from src.core.types import Violation
27
+
28
+ from .config import CQSConfig
29
+ from .python_analyzer import PythonCQSAnalyzer
30
+ from .types import CQSPattern
31
+ from .typescript_cqs_analyzer import TypeScriptCQSAnalyzer
32
+ from .violation_builder import build_cqs_violation
33
+
34
+
35
+ class CQSRule(MultiLanguageLintRule):
36
+ """Detects CQS (Command-Query Separation) violations in Python and TypeScript code."""
37
+
38
+ def __init__(self, config: CQSConfig | None = None) -> None:
39
+ """Initialize the CQS rule.
40
+
41
+ Args:
42
+ config: Optional configuration override for testing
43
+ """
44
+ super().__init__()
45
+ self._config_override = config
46
+ self._python_analyzer = PythonCQSAnalyzer()
47
+ self._typescript_analyzer = TypeScriptCQSAnalyzer()
48
+
49
+ @property
50
+ def rule_id(self) -> str:
51
+ """Unique identifier for this rule."""
52
+ return "cqs"
53
+
54
+ @property
55
+ def rule_name(self) -> str:
56
+ """Human-readable name for this rule."""
57
+ return "Command-Query Separation"
58
+
59
+ @property
60
+ def description(self) -> str:
61
+ """Description of what this rule checks."""
62
+ return (
63
+ "Detects functions that violate Command-Query Separation by mixing "
64
+ "queries (functions that return values) with commands (functions that "
65
+ "perform side effects). Functions should either query state and return "
66
+ "a value, or command a change without returning data."
67
+ )
68
+
69
+ def _load_config(self, context: BaseLintContext) -> CQSConfig:
70
+ """Load configuration from context.
71
+
72
+ Args:
73
+ context: Lint context
74
+
75
+ Returns:
76
+ CQSConfig object
77
+ """
78
+ if self._config_override is not None:
79
+ return self._config_override
80
+ return load_linter_config(context, "cqs", CQSConfig)
81
+
82
+ def _check_python(self, context: BaseLintContext, config: CQSConfig) -> list[Violation]:
83
+ """Check Python code for CQS violations.
84
+
85
+ Args:
86
+ context: Lint context with Python file information
87
+ config: Loaded configuration
88
+
89
+ Returns:
90
+ List of violations found in Python code
91
+ """
92
+ file_path = str(context.file_path) if context.file_path else "unknown"
93
+
94
+ if self._matches_ignore_pattern(file_path, config):
95
+ return []
96
+
97
+ patterns = self._python_analyzer.analyze(context.file_content or "", file_path, config)
98
+ return self._patterns_to_violations(patterns, config)
99
+
100
+ def _check_typescript(self, context: BaseLintContext, config: CQSConfig) -> list[Violation]:
101
+ """Check TypeScript/JavaScript code for CQS violations.
102
+
103
+ Args:
104
+ context: Lint context with TypeScript/JavaScript file information
105
+ config: Loaded configuration
106
+
107
+ Returns:
108
+ List of violations found in TypeScript/JavaScript code
109
+ """
110
+ file_path = str(context.file_path) if context.file_path else "unknown"
111
+
112
+ if self._matches_ignore_pattern(file_path, config):
113
+ return []
114
+
115
+ patterns = self._typescript_analyzer.analyze(context.file_content or "", file_path, config)
116
+ return self._patterns_to_violations(patterns, config)
117
+
118
+ def _patterns_to_violations(
119
+ self, patterns: list[CQSPattern], config: CQSConfig
120
+ ) -> list[Violation]:
121
+ """Convert CQSPatterns to Violations, applying thresholds.
122
+
123
+ Args:
124
+ patterns: List of CQSPattern objects
125
+ config: CQS configuration
126
+
127
+ Returns:
128
+ List of Violation objects
129
+ """
130
+ violating_patterns = [p for p in patterns if self._is_violation(p, config)]
131
+ return [build_cqs_violation(p) for p in violating_patterns]
132
+
133
+ def _matches_ignore_pattern(self, file_path: str, config: CQSConfig) -> bool:
134
+ """Check if file path matches any ignore pattern.
135
+
136
+ Args:
137
+ file_path: Path to check
138
+ config: CQS configuration
139
+
140
+ Returns:
141
+ True if path matches an ignore pattern
142
+ """
143
+ return any(fnmatch(file_path, pattern) for pattern in config.ignore_patterns)
144
+
145
+ def _is_violation(self, pattern: CQSPattern, config: CQSConfig) -> bool:
146
+ """Check if pattern represents a violation based on config.
147
+
148
+ Args:
149
+ pattern: CQSPattern to check
150
+ config: CQS configuration
151
+
152
+ Returns:
153
+ True if pattern is a violation
154
+ """
155
+ if not pattern.has_violation():
156
+ return False
157
+
158
+ min_ops = config.min_operations
159
+ return len(pattern.inputs) >= min_ops and len(pattern.outputs) >= min_ops
@@ -0,0 +1,84 @@
1
+ """
2
+ Purpose: AST-based detector for OUTPUT (command) operations in CQS analysis
3
+
4
+ Scope: Detects statement-level calls where return values are discarded
5
+
6
+ Overview: Provides OutputDetector class that uses AST traversal to find OUTPUT operations
7
+ which are command-like statement-level function calls that discard return values.
8
+ Detects patterns including statement calls (func()), async statements (await func()),
9
+ method calls (obj.method()), and chained calls (obj.method().method2()). Only ast.Expr
10
+ nodes containing Call or Await(Call) are detected as OUTPUT. All other constructs
11
+ (return, if, while, for, with, assert, raise, yield, assignments, comprehensions)
12
+ are naturally excluded because they use different AST node types.
13
+
14
+ Dependencies: ast module for Python AST traversal
15
+
16
+ Exports: OutputDetector
17
+
18
+ Interfaces: OutputDetector.find_outputs(tree: ast.AST) -> list[OutputOperation]
19
+
20
+ Implementation: AST NodeVisitor pattern with visit_Expr to detect statement-level calls
21
+
22
+ Suppressions:
23
+ - N802: visit_Expr follows Python AST visitor naming convention
24
+ (camelCase required by ast.NodeVisitor)
25
+ - invalid-name: visit_Expr follows Python AST visitor naming convention
26
+ (camelCase required by ast.NodeVisitor)
27
+ """
28
+
29
+ import ast
30
+
31
+ from .types import OutputOperation
32
+
33
+
34
+ def _extract_call_expression(node: ast.expr) -> ast.Call | None:
35
+ """Extract Call from expression, unwrapping Await if present."""
36
+ if isinstance(node, ast.Call):
37
+ return node
38
+ if isinstance(node, ast.Await) and isinstance(node.value, ast.Call):
39
+ return node.value
40
+ return None
41
+
42
+
43
+ class OutputDetector(ast.NodeVisitor):
44
+ """Detects OUTPUT (command) operations that discard function call results."""
45
+
46
+ def __init__(self) -> None:
47
+ """Initialize the detector."""
48
+ self._outputs: list[OutputOperation] = []
49
+
50
+ def find_outputs(self, tree: ast.AST) -> list[OutputOperation]:
51
+ """Find OUTPUT operations in AST.
52
+
53
+ Args:
54
+ tree: Python AST to analyze
55
+
56
+ Returns:
57
+ List of detected OutputOperation objects
58
+ """
59
+ self._outputs = []
60
+ self.visit(tree)
61
+ return list(self._outputs)
62
+
63
+ def visit_Expr(self, node: ast.Expr) -> None: # noqa: N802 # pylint: disable=invalid-name
64
+ """Visit expression statement to check for OUTPUT pattern.
65
+
66
+ Only statement-level expressions (ast.Expr) are OUTPUT. This naturally
67
+ excludes return statements, conditionals, assignments, comprehensions,
68
+ and other constructs that use the call result.
69
+
70
+ Detects: func(), await func(), obj.method(), obj.method().method2()
71
+
72
+ Args:
73
+ node: AST Expr node to analyze
74
+ """
75
+ call_node = _extract_call_expression(node.value)
76
+ if call_node is not None:
77
+ self._outputs.append(
78
+ OutputOperation(
79
+ line=node.lineno,
80
+ column=node.col_offset,
81
+ expression=ast.unparse(call_node),
82
+ )
83
+ )
84
+ self.generic_visit(node)
@@ -0,0 +1,54 @@
1
+ """
2
+ Purpose: Coordinator for Python CQS analysis returning per-function CQSPattern objects
3
+
4
+ Scope: High-level analyzer that orchestrates FunctionAnalyzer for function-level detection
5
+
6
+ Overview: Provides PythonCQSAnalyzer class that coordinates CQS pattern detection in Python
7
+ code. Handles AST parsing with proper SyntaxError handling, returning empty results for
8
+ unparseable code rather than raising exceptions. Delegates to FunctionAnalyzer to build
9
+ CQSPattern objects for each function/method, which contain INPUT and OUTPUT operations
10
+ along with function metadata (name, class context, async status).
11
+
12
+ Dependencies: ast module, FunctionAnalyzer, CQSConfig, CQSPattern
13
+
14
+ Exports: PythonCQSAnalyzer
15
+
16
+ Interfaces: PythonCQSAnalyzer.analyze(code, file_path, config) -> list[CQSPattern]
17
+
18
+ Implementation: Coordinates FunctionAnalyzer with error handling for AST parsing failures
19
+ """
20
+
21
+ import ast
22
+
23
+ from .config import CQSConfig
24
+ from .function_analyzer import FunctionAnalyzer
25
+ from .types import CQSPattern
26
+
27
+
28
+ class PythonCQSAnalyzer:
29
+ """Analyzes Python code for CQS patterns, returning per-function results."""
30
+
31
+ def analyze(
32
+ self,
33
+ code: str,
34
+ file_path: str,
35
+ config: CQSConfig,
36
+ ) -> list[CQSPattern]:
37
+ """Analyze Python code for CQS patterns in each function.
38
+
39
+ Args:
40
+ code: Python source code to analyze
41
+ file_path: Path to the source file (for error context)
42
+ config: CQS configuration settings
43
+
44
+ Returns:
45
+ List of CQSPattern objects, one per function/method.
46
+ Returns empty list if code cannot be parsed due to SyntaxError.
47
+ """
48
+ try:
49
+ tree = ast.parse(code, filename=file_path)
50
+ except SyntaxError:
51
+ return []
52
+
53
+ analyzer = FunctionAnalyzer(file_path, config)
54
+ return analyzer.analyze(tree)