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.
- code_oracle/__init__.py +30 -0
- code_oracle/cli.py +795 -0
- code_oracle/config.py +145 -0
- code_oracle/dataset.py +5325 -0
- code_oracle/dead_code/__init__.py +32 -0
- code_oracle/dead_code/detector.py +379 -0
- code_oracle/dead_code/entrypoints.py +333 -0
- code_oracle/dead_code/models.py +255 -0
- code_oracle/dead_code/semantics.py +416 -0
- code_oracle/decision.py +906 -0
- code_oracle/engine.py +430 -0
- code_oracle/export_onnx.py +436 -0
- code_oracle/hook.py +531 -0
- code_oracle/indexer.py +894 -0
- code_oracle/languages/__init__.py +114 -0
- code_oracle/languages/common.py +127 -0
- code_oracle/languages/go.py +395 -0
- code_oracle/languages/python.py +336 -0
- code_oracle/languages/rust.py +474 -0
- code_oracle/languages/typescript.py +775 -0
- code_oracle/linearizer.py +166 -0
- code_oracle/locator.py +301 -0
- code_oracle/models.py +237 -0
- code_oracle/perf_lint/__init__.py +38 -0
- code_oracle/perf_lint/engine.py +234 -0
- code_oracle/perf_lint/models.py +229 -0
- code_oracle/perf_lint/rules/__init__.py +31 -0
- code_oracle/perf_lint/rules/async_blocking.py +143 -0
- code_oracle/perf_lint/rules/n_plus_one.py +232 -0
- code_oracle/perf_lint/rules/nested_loops.py +137 -0
- code_oracle/perf_lint/rules/unclosed_res.py +494 -0
- code_oracle/perf_lint/visitor.py +299 -0
- code_oracle/server.py +184 -0
- code_oracle/slicer.py +225 -0
- code_oracle/symbolic.py +459 -0
- code_oracle-0.1.0.dist-info/METADATA +225 -0
- code_oracle-0.1.0.dist-info/RECORD +40 -0
- code_oracle-0.1.0.dist-info/WHEEL +4 -0
- code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|