code-oracle 0.1.0__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 (40) hide show
  1. code_oracle/__init__.py +30 -0
  2. code_oracle/cli.py +795 -0
  3. code_oracle/config.py +145 -0
  4. code_oracle/dataset.py +5325 -0
  5. code_oracle/dead_code/__init__.py +32 -0
  6. code_oracle/dead_code/detector.py +379 -0
  7. code_oracle/dead_code/entrypoints.py +333 -0
  8. code_oracle/dead_code/models.py +255 -0
  9. code_oracle/dead_code/semantics.py +416 -0
  10. code_oracle/decision.py +906 -0
  11. code_oracle/engine.py +430 -0
  12. code_oracle/export_onnx.py +436 -0
  13. code_oracle/hook.py +531 -0
  14. code_oracle/indexer.py +894 -0
  15. code_oracle/languages/__init__.py +114 -0
  16. code_oracle/languages/common.py +127 -0
  17. code_oracle/languages/go.py +395 -0
  18. code_oracle/languages/python.py +336 -0
  19. code_oracle/languages/rust.py +474 -0
  20. code_oracle/languages/typescript.py +775 -0
  21. code_oracle/linearizer.py +166 -0
  22. code_oracle/locator.py +301 -0
  23. code_oracle/models.py +237 -0
  24. code_oracle/perf_lint/__init__.py +38 -0
  25. code_oracle/perf_lint/engine.py +234 -0
  26. code_oracle/perf_lint/models.py +229 -0
  27. code_oracle/perf_lint/rules/__init__.py +31 -0
  28. code_oracle/perf_lint/rules/async_blocking.py +143 -0
  29. code_oracle/perf_lint/rules/n_plus_one.py +232 -0
  30. code_oracle/perf_lint/rules/nested_loops.py +137 -0
  31. code_oracle/perf_lint/rules/unclosed_res.py +494 -0
  32. code_oracle/perf_lint/visitor.py +299 -0
  33. code_oracle/server.py +184 -0
  34. code_oracle/slicer.py +225 -0
  35. code_oracle/symbolic.py +459 -0
  36. code_oracle-0.1.0.dist-info/METADATA +225 -0
  37. code_oracle-0.1.0.dist-info/RECORD +40 -0
  38. code_oracle-0.1.0.dist-info/WHEEL +4 -0
  39. code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
  40. code_oracle-0.1.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,38 @@
1
+ """
2
+ Static Performance Anti-Patterns & Resource Leak Detector.
3
+ Detects nested loop complexity, N+1 queries, resource leaks, and blocking async calls.
4
+ """
5
+
6
+ from code_oracle.perf_lint.engine import (
7
+ PerfLintEngine,
8
+ lint_performance,
9
+ lint_performance_patterns,
10
+ )
11
+ from code_oracle.perf_lint.models import (
12
+ PerfDiagnostic,
13
+ PerfReport,
14
+ PerfRule,
15
+ Severity,
16
+ )
17
+ from code_oracle.perf_lint.rules import (
18
+ AsyncBlockingRule,
19
+ NPlusOneRule,
20
+ NestedLoopsRule,
21
+ UnclosedResourceRule,
22
+ )
23
+ from code_oracle.perf_lint.visitor import PerfLintVisitor
24
+
25
+ __all__ = [
26
+ "AsyncBlockingRule",
27
+ "NPlusOneRule",
28
+ "NestedLoopsRule",
29
+ "PerfDiagnostic",
30
+ "PerfLintEngine",
31
+ "PerfLintVisitor",
32
+ "PerfReport",
33
+ "PerfRule",
34
+ "Severity",
35
+ "UnclosedResourceRule",
36
+ "lint_performance",
37
+ "lint_performance_patterns",
38
+ ]
@@ -0,0 +1,234 @@
1
+ """
2
+ Orchestration Engine for Static Performance Anti-Patterns & Resource Leak Detector.
3
+ Coordinates workspace and file-level scans, applies diff patches in-memory,
4
+ filters diagnostics by severity thresholds, and benchmarks analysis latency.
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+ import time
10
+ from typing import Any, Dict, List, Optional, Set, Union
11
+
12
+ from code_oracle.indexer import IGNORE_DIRS
13
+ from code_oracle.languages import SUPPORTED_EXTENSIONS, detect_language
14
+ from code_oracle.locator import apply_patch
15
+ from code_oracle.perf_lint.models import PerfDiagnostic, PerfReport, Severity
16
+ from code_oracle.perf_lint.visitor import PerfLintVisitor
17
+
18
+
19
+ def _filter_dirs(dirs: List[str]) -> List[str]:
20
+ return [d for d in dirs if d not in IGNORE_DIRS and not d.startswith(".")]
21
+
22
+
23
+ def _walk_supported_files(directory: Path) -> Set[Path]:
24
+ """Discover all supported source code files within target directory."""
25
+ result: Set[Path] = set()
26
+ for root, dirs, files in os.walk(directory): # code-oracle: ignore-perf[PERF001]
27
+ dirs[:] = _filter_dirs(dirs)
28
+ for file in files:
29
+ p = Path(root) / file
30
+ if p.suffix.lower() in SUPPORTED_EXTENSIONS:
31
+ result.add(p)
32
+ return result
33
+
34
+
35
+ class PerfLintEngine:
36
+ """
37
+ Sub-50ms Static Performance Anti-Pattern & Resource Leak Engine.
38
+ Executes Tree-sitter AST visitor passes over Python, TypeScript, Go, and Rust files.
39
+ """
40
+
41
+ def __init__(self, workspace_root: Optional[Union[str, Path]] = None) -> None:
42
+ self.workspace_root = Path(workspace_root or Path.cwd()).resolve()
43
+
44
+ def lint_source(
45
+ self,
46
+ source: str,
47
+ file_path: str = "",
48
+ language: Optional[str] = None,
49
+ severity: str = "warn",
50
+ max_depth: Optional[int] = None,
51
+ ) -> List[PerfDiagnostic]:
52
+ """
53
+ Analyze in-memory source code string and return active diagnostics.
54
+ Filters findings by minimum severity threshold.
55
+ """
56
+ visitor = PerfLintVisitor(
57
+ source=source,
58
+ file_path=file_path,
59
+ language=language,
60
+ max_depth=max_depth,
61
+ )
62
+ diagnostics = visitor.run()
63
+
64
+ # Apply minimum severity filter
65
+ min_sev = Severity.from_str(severity)
66
+ if min_sev == Severity.ERROR:
67
+ diagnostics = [d for d in diagnostics if d.severity == Severity.ERROR]
68
+
69
+ return diagnostics
70
+
71
+ def lint_file(
72
+ self,
73
+ file_path: Union[str, Path],
74
+ severity: str = "warn",
75
+ max_depth: Optional[int] = None,
76
+ ) -> List[PerfDiagnostic]:
77
+ """Read and analyze a single source file from disk."""
78
+ target_path = Path(file_path)
79
+ if not target_path.is_absolute():
80
+ target_path = (self.workspace_root / target_path).resolve()
81
+
82
+ if not target_path.is_file():
83
+ return []
84
+
85
+ ext = target_path.suffix.lower()
86
+ if ext not in SUPPORTED_EXTENSIONS:
87
+ return []
88
+
89
+ try:
90
+ content = target_path.read_text(encoding="utf-8")
91
+ except Exception:
92
+ return []
93
+
94
+ try:
95
+ rel_path = str(target_path.relative_to(self.workspace_root)).replace("\\", "/")
96
+ except ValueError:
97
+ rel_path = str(target_path).replace("\\", "/")
98
+
99
+ return self.lint_source(
100
+ source=content,
101
+ file_path=rel_path,
102
+ severity=severity,
103
+ max_depth=max_depth,
104
+ )
105
+
106
+ def lint_workspace(
107
+ self,
108
+ paths: Optional[List[str]] = None,
109
+ severity: str = "warn",
110
+ max_depth: Optional[int] = None,
111
+ ) -> PerfReport:
112
+ """
113
+ Scan workspace files or specific file/directory targets for performance anti-patterns.
114
+ """
115
+ start_time = time.perf_counter()
116
+ target_files: Set[Path] = set()
117
+
118
+ if paths:
119
+ for p in paths:
120
+ raw_path = Path(p)
121
+ abs_path = raw_path if raw_path.is_absolute() else (self.workspace_root / raw_path).resolve()
122
+
123
+ if abs_path.is_file():
124
+ if abs_path.suffix.lower() in SUPPORTED_EXTENSIONS:
125
+ target_files.add(abs_path)
126
+ elif abs_path.is_dir():
127
+ target_files.update(_walk_supported_files(abs_path))
128
+ else:
129
+ target_files.update(_walk_supported_files(self.workspace_root))
130
+
131
+ all_diagnostics: List[PerfDiagnostic] = []
132
+ scanned_count = len(target_files)
133
+
134
+ for f_path in target_files:
135
+ try:
136
+ rel_path = str(f_path.relative_to(self.workspace_root)).replace("\\", "/")
137
+ except ValueError:
138
+ rel_path = str(f_path).replace("\\", "/")
139
+
140
+ try:
141
+ content = f_path.read_text(encoding="utf-8")
142
+ except Exception:
143
+ continue
144
+
145
+ diags = self.lint_source(
146
+ source=content,
147
+ file_path=rel_path,
148
+ severity=severity,
149
+ max_depth=max_depth,
150
+ )
151
+ all_diagnostics.extend(diags)
152
+
153
+ # Sort diagnostics by file path and line number
154
+ all_diagnostics.sort(key=lambda d: (d.file_path, d.lineno, d.col_offset))
155
+ elapsed_ms = (time.perf_counter() - start_time) * 1000.0
156
+
157
+ return PerfReport(
158
+ workspace_root=str(self.workspace_root),
159
+ diagnostics=all_diagnostics,
160
+ scanned_files_count=scanned_count,
161
+ latency_ms=elapsed_ms,
162
+ )
163
+
164
+
165
+ def lint_performance(
166
+ workspace_root: Optional[Union[str, Path]] = None,
167
+ paths: Optional[List[str]] = None,
168
+ severity: str = "warn",
169
+ max_depth: Optional[int] = None,
170
+ ) -> PerfReport:
171
+ """Convenience function to scan a workspace or path list for performance anti-patterns."""
172
+ engine = PerfLintEngine(workspace_root=workspace_root)
173
+ return engine.lint_workspace(paths=paths, severity=severity, max_depth=max_depth)
174
+
175
+
176
+ def lint_performance_patterns(
177
+ file_path: str,
178
+ patch_content: Optional[str] = None,
179
+ workspace_root: Optional[Union[str, Path]] = None,
180
+ severity: str = "warn",
181
+ max_depth: Optional[int] = None,
182
+ ) -> PerfReport:
183
+ """
184
+ FastMCP and patch-level performance evaluation endpoint.
185
+ Applies unified diff or replacement patch in-memory with zero side-effects.
186
+ """
187
+ start_time = time.perf_counter()
188
+ engine = PerfLintEngine(workspace_root=workspace_root)
189
+
190
+ target_path = Path(file_path)
191
+ if not target_path.is_absolute():
192
+ target_path = (engine.workspace_root / target_path).resolve()
193
+
194
+ try:
195
+ norm_path = str(target_path.relative_to(engine.workspace_root)).replace("\\", "/")
196
+ except ValueError:
197
+ norm_path = str(file_path).replace("\\", "/")
198
+
199
+ orig_content = ""
200
+ if target_path.is_file():
201
+ try:
202
+ orig_content = target_path.read_text(encoding="utf-8")
203
+ except Exception:
204
+ orig_content = ""
205
+
206
+ if patch_content is not None:
207
+ patched_content, _, _ = apply_patch(orig_content, patch_content)
208
+ content_to_lint = patched_content
209
+ else:
210
+ content_to_lint = orig_content
211
+
212
+ diags = engine.lint_source(
213
+ source=content_to_lint,
214
+ file_path=norm_path,
215
+ severity=severity,
216
+ max_depth=max_depth,
217
+ )
218
+ diags.sort(key=lambda d: (d.file_path, d.lineno, d.col_offset))
219
+ elapsed_ms = (time.perf_counter() - start_time) * 1000.0
220
+
221
+ return PerfReport(
222
+ workspace_root=str(engine.workspace_root),
223
+ diagnostics=diags,
224
+ scanned_files_count=1,
225
+ latency_ms=elapsed_ms,
226
+ )
227
+
228
+
229
+ __all__ = [
230
+ "PerfLintEngine",
231
+ "PerfLintVisitor",
232
+ "lint_performance",
233
+ "lint_performance_patterns",
234
+ ]
@@ -0,0 +1,229 @@
1
+ """
2
+ Data models for the Static Performance Anti-Patterns & Resource Leak Detector.
3
+ Defines representations for diagnostics, severity levels, rules, and reports.
4
+ """
5
+
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from typing import Any, Dict, List, Optional
9
+
10
+
11
+ class Severity(str, Enum):
12
+ """Diagnostic severity levels."""
13
+ WARN = "warn"
14
+ ERROR = "error"
15
+
16
+ @classmethod
17
+ def from_str(cls, value: str) -> "Severity":
18
+ """Convert string to Severity enum value safely."""
19
+ val = value.strip().lower()
20
+ if val in ("error", "err"):
21
+ return cls.ERROR
22
+ return cls.WARN
23
+
24
+
25
+ class PerfRule(str, Enum):
26
+ """Identifier codes for performance and leak linting rules."""
27
+ PERF001 = "PERF001"
28
+ PERF002 = "PERF002"
29
+ PERF003 = "PERF003"
30
+ PERF004 = "PERF004"
31
+
32
+
33
+ RULE_METADATA: Dict[str, Dict[str, Any]] = {
34
+ PerfRule.PERF001.value: {
35
+ "name": "Nested Loops Complexity",
36
+ "description": "Nested loop complexity escalation (O(N^2) warning, O(N^3) error)",
37
+ "default_severity": Severity.WARN,
38
+ },
39
+ PerfRule.PERF002.value: {
40
+ "name": "N+1 I/O in Loop",
41
+ "description": "N+1 database queries or network I/O calls inside iteration loop bodies",
42
+ "default_severity": Severity.WARN,
43
+ },
44
+ PerfRule.PERF003.value: {
45
+ "name": "Resource Leak / Unclosed Descriptor",
46
+ "description": "File or socket descriptor opened without scoped context manager or defer Close",
47
+ "default_severity": Severity.ERROR,
48
+ },
49
+ PerfRule.PERF004.value: {
50
+ "name": "Blocking Call in Async Context",
51
+ "description": "Blocking synchronous call inside asynchronous function",
52
+ "default_severity": Severity.ERROR,
53
+ },
54
+ }
55
+
56
+
57
+ @dataclass
58
+ class PerfDiagnostic:
59
+ """Represents a single performance anti-pattern or resource leak finding."""
60
+ rule_id: str
61
+ message: str
62
+ severity: Severity
63
+ file_path: str
64
+ lineno: int
65
+ end_lineno: int = 0
66
+ col_offset: int = 0
67
+ end_col_offset: int = 0
68
+ context_line: Optional[str] = None
69
+ rule_name: Optional[str] = None
70
+ suppressed: bool = False
71
+
72
+ def __post_init__(self) -> None:
73
+ if hasattr(self.rule_id, "value"):
74
+ self.rule_id = self.rule_id.value
75
+ if isinstance(self.severity, str):
76
+ self.severity = Severity.from_str(self.severity)
77
+ if not self.rule_name and self.rule_id in RULE_METADATA:
78
+ self.rule_name = RULE_METADATA[self.rule_id]["name"]
79
+ if self.end_lineno <= 0:
80
+ self.end_lineno = self.lineno
81
+
82
+ def to_dict(self) -> Dict[str, Any]:
83
+ """Serialize diagnostic to dictionary."""
84
+ return {
85
+ "rule_id": self.rule_id,
86
+ "rule_name": self.rule_name or self.rule_id,
87
+ "message": self.message,
88
+ "severity": self.severity.value,
89
+ "file_path": self.file_path,
90
+ "lineno": self.lineno,
91
+ "end_lineno": self.end_lineno,
92
+ "col_offset": self.col_offset,
93
+ "end_col_offset": self.end_col_offset,
94
+ "context_line": self.context_line,
95
+ "suppressed": self.suppressed,
96
+ }
97
+
98
+
99
+ @dataclass
100
+ class PerfReport:
101
+ """Complete static performance and resource leak evaluation report."""
102
+ workspace_root: str
103
+ diagnostics: List[PerfDiagnostic] = field(default_factory=list)
104
+ scanned_files_count: int = 0
105
+ latency_ms: float = 0.0
106
+ rules_checked: List[str] = field(
107
+ default_factory=lambda: [r.value for r in PerfRule]
108
+ )
109
+
110
+ @property
111
+ def total_diagnostics_count(self) -> int:
112
+ """Total number of active (non-suppressed) diagnostics."""
113
+ return len([d for d in self.diagnostics if not d.suppressed])
114
+
115
+ @property
116
+ def errors_count(self) -> int:
117
+ """Number of error-severity diagnostics."""
118
+ return len([
119
+ d for d in self.diagnostics
120
+ if not d.suppressed and d.severity == Severity.ERROR
121
+ ])
122
+
123
+ @property
124
+ def warnings_count(self) -> int:
125
+ """Number of warn-severity diagnostics."""
126
+ return len([
127
+ d for d in self.diagnostics
128
+ if not d.suppressed and d.severity == Severity.WARN
129
+ ])
130
+
131
+ def to_dict(self) -> Dict[str, Any]:
132
+ """Convert report to dictionary."""
133
+ active = [d for d in self.diagnostics if not d.suppressed]
134
+ return {
135
+ "workspace_root": self.workspace_root,
136
+ "total_diagnostics_count": self.total_diagnostics_count,
137
+ "errors_count": self.errors_count,
138
+ "warnings_count": self.warnings_count,
139
+ "scanned_files_count": self.scanned_files_count,
140
+ "latency_ms": round(self.latency_ms, 2),
141
+ "rules_checked": self.rules_checked,
142
+ "diagnostics": [d.to_dict() for d in active],
143
+ }
144
+
145
+ def format_table(self) -> str:
146
+ """Format report into clean, colored ASCII terminal table."""
147
+ active = [d for d in self.diagnostics if not d.suppressed]
148
+ if not active:
149
+ return (
150
+ f"\033[92m✔ No performance anti-patterns or resource leaks detected\033[0m "
151
+ f"across {self.scanned_files_count} files ({self.latency_ms:.2f} ms)."
152
+ )
153
+
154
+ headers = ["Rule", "Severity", "Location", "Message"]
155
+ rows: List[List[str]] = []
156
+ for d in active:
157
+ loc = f"{d.file_path}:{d.lineno}"
158
+ sev = d.severity.value.upper()
159
+ rows.append([d.rule_id, sev, loc, d.message])
160
+
161
+ col_widths = [len(h) for h in headers]
162
+ for row in rows:
163
+ for i, val in enumerate(row):
164
+ col_widths[i] = max(col_widths[i], len(val))
165
+
166
+ # Enforce maximum column width for message to prevent terminal overflow
167
+ if col_widths[3] > 80:
168
+ col_widths[3] = 80
169
+
170
+ def make_separator(char: str = "-") -> str:
171
+ parts = [char * (w + 2) for w in col_widths]
172
+ return f"+{'+'.join(parts)}+"
173
+
174
+ lines = [
175
+ make_separator("-"),
176
+ "| " + " | ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers)) + " |",
177
+ make_separator("="),
178
+ ]
179
+
180
+ for row in rows:
181
+ color = "\033[91m" if row[1] == "ERROR" else "\033[93m"
182
+ color_reset = "\033[0m"
183
+ msg = row[3]
184
+ if len(msg) > col_widths[3]:
185
+ msg = msg[: col_widths[3] - 3] + "..."
186
+ formatted_cells = [
187
+ row[0].ljust(col_widths[0]),
188
+ row[1].ljust(col_widths[1]),
189
+ row[2].ljust(col_widths[2]),
190
+ msg.ljust(col_widths[3]),
191
+ ]
192
+ lines.append(f"| {color}{' | '.join(formatted_cells)}{color_reset} |")
193
+
194
+ lines.append(make_separator("-"))
195
+
196
+ summary = (
197
+ f"\033[91m✖ Found {self.total_diagnostics_count} performance anti-patterns\033[0m "
198
+ f"({self.errors_count} errors, {self.warnings_count} warnings) "
199
+ f"across {self.scanned_files_count} files in {self.latency_ms:.2f} ms."
200
+ )
201
+ lines.append(summary)
202
+ return "\n".join(lines)
203
+
204
+ def format_text(self) -> str:
205
+ """Format report into concise text lines."""
206
+ active = [d for d in self.diagnostics if not d.suppressed]
207
+ if not active:
208
+ return (
209
+ f"✔ No performance anti-patterns or resource leaks detected "
210
+ f"across {self.scanned_files_count} files ({self.latency_ms:.2f} ms)."
211
+ )
212
+
213
+ lines = [
214
+ f"Performance Anti-Patterns & Resource Leaks ({self.total_diagnostics_count} issues: "
215
+ f"{self.errors_count} errors, {self.warnings_count} warnings):",
216
+ "--------------------------------------------------------------------------------",
217
+ ]
218
+ for d in active:
219
+ sev = d.severity.value.upper()
220
+ lines.append(
221
+ f" • {d.file_path}:{d.lineno} [{d.rule_id}] ({sev}): {d.message}"
222
+ )
223
+ if d.context_line:
224
+ lines.append(f" Line: {d.context_line}")
225
+ lines.append("--------------------------------------------------------------------------------")
226
+ lines.append(
227
+ f"Scan completed in {self.latency_ms:.2f} ms across {self.scanned_files_count} files."
228
+ )
229
+ return "\n".join(lines)
@@ -0,0 +1,31 @@
1
+ """
2
+ Performance and resource leak lint rules.
3
+ """
4
+
5
+ from code_oracle.perf_lint.rules.async_blocking import (
6
+ AsyncBlockingRule,
7
+ check_async_blocking,
8
+ )
9
+ from code_oracle.perf_lint.rules.n_plus_one import (
10
+ NPlusOneRule,
11
+ check_n_plus_one,
12
+ )
13
+ from code_oracle.perf_lint.rules.nested_loops import (
14
+ NestedLoopsRule,
15
+ check_nested_loop,
16
+ )
17
+ from code_oracle.perf_lint.rules.unclosed_res import (
18
+ UnclosedResourceRule,
19
+ check_unclosed_resource,
20
+ )
21
+
22
+ __all__ = [
23
+ "AsyncBlockingRule",
24
+ "NPlusOneRule",
25
+ "NestedLoopsRule",
26
+ "UnclosedResourceRule",
27
+ "check_async_blocking",
28
+ "check_n_plus_one",
29
+ "check_nested_loop",
30
+ "check_unclosed_resource",
31
+ ]
@@ -0,0 +1,143 @@
1
+ """
2
+ PERF004: Blocking Calls in Async Context Rule.
3
+ Detects synchronous blocking primitives like time.sleep or sync file I/O
4
+ inside async def / async function bodies across Python, TypeScript, and Rust.
5
+ """
6
+
7
+ from typing import List, Optional
8
+ from tree_sitter import Node
9
+
10
+ from code_oracle.perf_lint.models import PerfDiagnostic, PerfRule, Severity
11
+
12
+
13
+ class AsyncBlockingRule:
14
+ """Evaluates PERF004: Blocking Calls in Async Context."""
15
+
16
+ RULE_ID = PerfRule.PERF004.value
17
+
18
+ @staticmethod
19
+ def is_async_func_node(node: Node, language: str) -> bool:
20
+ """Determine if a function AST node is asynchronous."""
21
+ if language == "python":
22
+ return any(c.type == "async" for c in node.children)
23
+ elif language in ("typescript", "javascript"):
24
+ return any(c.type == "async" for c in node.children)
25
+ elif language == "rust":
26
+ for c in node.children:
27
+ if c.type == "function_modifiers":
28
+ return any(mc.type == "async" for mc in c.children)
29
+ return False
30
+ return False
31
+
32
+ @staticmethod
33
+ def is_blocking_call(callee_text: str, language: str) -> bool:
34
+ """Check if callee is a blocking synchronous primitive."""
35
+ clean = callee_text.strip()
36
+ clean_lower = clean.lower()
37
+
38
+ if language == "python":
39
+ # Exclude async constructors and configuration primitives
40
+ if clean in (
41
+ "httpx.AsyncClient",
42
+ "httpx.Timeout",
43
+ "httpx.Limits",
44
+ "httpx.AsyncHTTPTransport",
45
+ ) or clean.startswith("httpx.Async"):
46
+ return False
47
+
48
+ if clean in ("time.sleep", "sleep", "open", "urllib.request.urlopen", "os.system", "os.popen"):
49
+ return True
50
+ if clean.startswith(("requests.", "subprocess.", "urllib.", "urllib3.", "httpx.")):
51
+ return True
52
+ return False
53
+
54
+ elif language in ("typescript", "javascript"):
55
+ if "sync" in clean_lower and (
56
+ clean_lower.startswith("fs.")
57
+ or clean_lower.startswith("child_process.")
58
+ or clean_lower.startswith("crypto.")
59
+ or clean_lower.endswith("sync")
60
+ ):
61
+ return True
62
+ if clean in ("Atomics.wait", "crypto.pbkdf2Sync", "crypto.randomBytesSync"):
63
+ return True
64
+ return False
65
+
66
+ elif language == "rust":
67
+ if clean in ("std::thread::sleep", "thread::sleep"):
68
+ return True
69
+ if clean.startswith(("std::fs::", "fs::")):
70
+ return True
71
+ return False
72
+
73
+ return False
74
+
75
+ @staticmethod
76
+ def is_awaited(call_node: Node) -> bool:
77
+ """Check if call_node is directly enclosed in an await expression."""
78
+ curr = call_node.parent
79
+ while curr is not None and curr.type == "parenthesized_expression":
80
+ curr = curr.parent
81
+ return curr is not None and curr.type in ("await", "await_expression")
82
+
83
+ @classmethod
84
+ def check(
85
+ cls,
86
+ call_node: Node,
87
+ callee_text: str,
88
+ is_async_context: bool,
89
+ current_func_name: str,
90
+ language: str,
91
+ file_path: str,
92
+ lines: List[str],
93
+ ) -> Optional[PerfDiagnostic]:
94
+ """Evaluate if call is a blocking synchronous primitive in an async function."""
95
+ if not is_async_context or not cls.is_blocking_call(callee_text, language):
96
+ return None
97
+
98
+ # Awaited expressions are non-blocking (e.g. await sleep(1))
99
+ if cls.is_awaited(call_node):
100
+ return None
101
+
102
+ lineno = call_node.start_point.row + 1
103
+ end_lineno = call_node.end_point.row + 1
104
+ col = call_node.start_point.column
105
+ end_col = call_node.end_point.column
106
+ ctx = lines[lineno - 1].strip() if 1 <= lineno <= len(lines) else None
107
+
108
+ msg = (
109
+ f"Blocking synchronous call '{callee_text}' inside async "
110
+ f"function '{current_func_name}'"
111
+ )
112
+ return PerfDiagnostic(
113
+ rule_id=cls.RULE_ID,
114
+ message=msg,
115
+ severity=Severity.ERROR,
116
+ file_path=file_path,
117
+ lineno=lineno,
118
+ end_lineno=end_lineno,
119
+ col_offset=col,
120
+ end_col_offset=end_col,
121
+ context_line=ctx,
122
+ )
123
+
124
+
125
+ def check_async_blocking(
126
+ call_node: Node,
127
+ callee_text: str,
128
+ is_async_context: bool,
129
+ current_func_name: str,
130
+ language: str,
131
+ file_path: str,
132
+ lines: List[str],
133
+ ) -> Optional[PerfDiagnostic]:
134
+ """Convenience helper for PERF004 evaluation."""
135
+ return AsyncBlockingRule.check(
136
+ call_node=call_node,
137
+ callee_text=callee_text,
138
+ is_async_context=is_async_context,
139
+ current_func_name=current_func_name,
140
+ language=language,
141
+ file_path=file_path,
142
+ lines=lines,
143
+ )