performance-optimizer 2.5.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.
@@ -0,0 +1,184 @@
1
+ import os
2
+ import re
3
+ from pathlib import Path
4
+ from typing import List, Tuple
5
+ from core.issue import Issue, Severity, Layer
6
+
7
+ class SqlAnalyzer:
8
+ def __init__(self, path: str):
9
+ self.path = Path(path)
10
+ self.issues: List[Issue] = []
11
+
12
+ def analyze(self) -> List[Issue]:
13
+ # Scan dedicated .sql files
14
+ for f in self.path.rglob('*.sql'):
15
+ if any(x in str(f) for x in ['node_modules', 'dist/', '.git']):
16
+ continue
17
+ try:
18
+ src = f.read_text(encoding='utf-8', errors='ignore')
19
+ rel = str(f.relative_to(self.path)).replace('\\', '/')
20
+ lines = src.splitlines()
21
+ self._check_sql_file(src, lines, rel)
22
+ except Exception:
23
+ pass
24
+
25
+ # Scan .js and .ts files for inline SQL strings
26
+ for f in list(self.path.rglob('*.js')) + list(self.path.rglob('*.ts')):
27
+ if any(x in str(f) for x in ['node_modules', 'dist/', '.spec.', '.d.ts', '.git']):
28
+ continue
29
+ try:
30
+ src = f.read_text(encoding='utf-8', errors='ignore')
31
+ rel = str(f.relative_to(self.path)).replace('\\', '/')
32
+ lines = src.splitlines()
33
+ self._check_inline_sql(src, lines, rel)
34
+ except Exception:
35
+ pass
36
+
37
+ return self.issues
38
+
39
+ def _is_suppressed(self, lines: List[str], line_idx: int, rule_id: str) -> bool:
40
+ check_lines = []
41
+ if 0 <= line_idx < len(lines):
42
+ check_lines.append(lines[line_idx])
43
+ if 0 <= line_idx - 1 < len(lines):
44
+ check_lines.append(lines[line_idx - 1])
45
+ for cl in check_lines:
46
+ if f'perf-ignore {rule_id}' in cl or 'perf-ignore-all' in cl:
47
+ return True
48
+ return False
49
+
50
+ def _find_line(self, lines: List[str], regex_or_str) -> Tuple[int, str]:
51
+ for idx, line in enumerate(lines, 1):
52
+ if isinstance(regex_or_str, str) and regex_or_str.lower() in line.lower():
53
+ return idx, line.strip()
54
+ elif hasattr(regex_or_str, 'search') and regex_or_str.search(line):
55
+ return idx, line.strip()
56
+ return 1, (lines[0].strip() if lines else '')
57
+
58
+ def _check_sql_file(self, src: str, lines: List[str], rel: str):
59
+ # SQL001: SELECT * Usage
60
+ select_star_count = len(re.findall(r'SELECT\s+\*\s+FROM', src, re.IGNORECASE))
61
+ if select_star_count > 0:
62
+ line_no, snippet = self._find_line(lines, re.compile(r'SELECT\s+\*\s+FROM', re.IGNORECASE))
63
+ if not self._is_suppressed(lines, line_no - 1, 'SQL001'):
64
+ self.issues.append(Issue(
65
+ id='SQL001',
66
+ title=f'SELECT * Over-fetching ({select_star_count} occurrences)',
67
+ description='Fetching all table columns with SELECT * wastes database memory, network bandwidth, and prevents index-only query execution plans.',
68
+ fix='Explicitly enumerate the necessary columns: `SELECT id, name, status FROM table`.',
69
+ code_before='SELECT * FROM orders WHERE user_id = ?;',
70
+ code_after='SELECT id, total_amount, status, created_at FROM orders WHERE user_id = ?;',
71
+ file=rel,
72
+ line_number=line_no,
73
+ code_snippet=snippet,
74
+ category='Database I/O',
75
+ severity=Severity.HIGH,
76
+ layer=Layer.DATABASE,
77
+ impact=7,
78
+ effort=2,
79
+ occurrences=select_star_count,
80
+ perf_gain='Reduces row serialization size and network transfer'
81
+ ))
82
+
83
+ # SQL004: Missing indexes on foreign keys
84
+ table_defs = re.finditer(r'CREATE\s+TABLE\s+(\w+)\s*\(([^;]+)\)', src, re.IGNORECASE | re.DOTALL)
85
+ for tbl in table_defs:
86
+ tname = tbl.group(1)
87
+ tbody = tbl.group(2)
88
+ has_fk = re.search(r'(\w+_id|\w+_fk)\s+INT', tbody, re.IGNORECASE) or 'FOREIGN KEY' in tbody.upper()
89
+ has_idx = any(idx_kw in tbody.upper() for idx_kw in ['INDEX', 'KEY ', 'UNIQUE'])
90
+ if has_fk and not has_idx:
91
+ line_no, snippet = self._find_line(lines, re.compile(rf'CREATE\s+TABLE\s+{tname}', re.IGNORECASE))
92
+ if not self._is_suppressed(lines, line_no - 1, 'SQL004'):
93
+ self.issues.append(Issue(
94
+ id='SQL004',
95
+ title=f'Missing Foreign Key Index on Table `{tname}`',
96
+ description=f'Table `{tname}` defines relationship foreign keys without explicit B-Tree indexes. Queries using JOIN or WHERE on foreign keys will execute full table scans.',
97
+ fix=f'Add B-Tree indexes to foreign key columns: `INDEX idx_{tname.lower()}_fk (user_id)`.',
98
+ code_before=f'CREATE TABLE {tname} (\n id INT PRIMARY KEY,\n user_id INT -- No index: causes O(N) table scans\n);',
99
+ code_after=f'CREATE TABLE {tname} (\n id INT PRIMARY KEY,\n user_id INT,\n INDEX idx_{tname.lower()}_user (user_id)\n);',
100
+ file=rel,
101
+ line_number=line_no,
102
+ code_snippet=snippet,
103
+ category='Indexing & Optimization',
104
+ severity=Severity.CRITICAL,
105
+ layer=Layer.DATABASE,
106
+ impact=9,
107
+ effort=2,
108
+ occurrences=1,
109
+ perf_gain='Converts O(N) table scans into O(log N) B-Tree seeks'
110
+ ))
111
+
112
+ # SQL007: Deep OFFSET pagination antipattern
113
+ if re.search(r'OFFSET\s+[1-9]\d{3,}', src, re.IGNORECASE):
114
+ line_no, snippet = self._find_line(lines, re.compile(r'OFFSET\s+[1-9]\d{3,}', re.IGNORECASE))
115
+ if not self._is_suppressed(lines, line_no - 1, 'SQL007'):
116
+ self.issues.append(Issue(
117
+ id='SQL007',
118
+ title='Deep OFFSET Pagination Antipattern (O(N) Discard Waste)',
119
+ description='High OFFSET values force the database engine to fetch and discard thousands of rows before returning the requested slice. Response latency degrades proportionally with page depth.',
120
+ fix='Use keyset / cursor-based pagination: `WHERE id > :last_id ORDER BY id LIMIT 50`.',
121
+ code_before='SELECT id, name FROM orders ORDER BY id LIMIT 50 OFFSET 10000;',
122
+ code_after='SELECT id, name FROM orders WHERE id > :cursor ORDER BY id LIMIT 50;',
123
+ file=rel,
124
+ line_number=line_no,
125
+ code_snippet=snippet,
126
+ category='Query Performance',
127
+ severity=Severity.HIGH,
128
+ layer=Layer.DATABASE,
129
+ impact=8,
130
+ effort=3,
131
+ occurrences=1,
132
+ perf_gain='Constant O(1) query time regardless of pagination depth'
133
+ ))
134
+
135
+ def _check_inline_sql(self, src: str, lines: List[str], rel: str):
136
+ # SQL005: String concatenation in SQL queries
137
+ concat_matches = re.finditer(r'[`\'"](SELECT[^`\'"]+)[`\'"]\s*\+', src, re.IGNORECASE)
138
+ for m in concat_matches:
139
+ matched_text = m.group(0)
140
+ line_no, snippet = self._find_line(lines, matched_text[:20])
141
+ if not self._is_suppressed(lines, line_no - 1, 'SQL005'):
142
+ self.issues.append(Issue(
143
+ id='SQL005',
144
+ title='SQL Built via String Concatenation (Disables Execution Plan Cache)',
145
+ description='Dynamic string concatenation generates a unique SQL text for every argument, preventing the database from reusing compiled execution plans. Also opens SQL injection risk.',
146
+ fix='Use parameterized queries with `?` or `$1` placeholders so execution plans are cached.',
147
+ code_before='const sql = "SELECT * FROM users WHERE status = \'" + status + "\'";',
148
+ code_after='const sql = "SELECT id, name, status FROM users WHERE status = ?";\nawait db.query(sql, [status]);',
149
+ file=rel,
150
+ line_number=line_no,
151
+ code_snippet=snippet,
152
+ category='Execution Plan Cache',
153
+ severity=Severity.HIGH,
154
+ layer=Layer.DATABASE,
155
+ impact=8,
156
+ effort=2,
157
+ occurrences=1,
158
+ perf_gain='Enables query plan caching and eliminates SQL injection'
159
+ ))
160
+ break
161
+
162
+ # SQL006: LIKE with leading wildcard
163
+ leading_like = re.search(r"LIKE\s+['\"]%[^'\"]+['\"]", src, re.IGNORECASE)
164
+ if leading_like:
165
+ line_no, snippet = self._find_line(lines, re.compile(r"LIKE\s+['\"]%", re.IGNORECASE))
166
+ if not self._is_suppressed(lines, line_no - 1, 'SQL006'):
167
+ self.issues.append(Issue(
168
+ id='SQL006',
169
+ title='SQL LIKE Query with Leading Wildcard (%query)',
170
+ description='A leading wildcard pattern like `LIKE "%keyword"` prevents the database engine from using B-Tree indexes, triggering a full table scan across all rows.',
171
+ fix='Use trailing wildcard `LIKE "keyword%"` or implement Full-Text Search (FTS) / trigram indexing.',
172
+ code_before='SELECT id, title FROM articles WHERE title LIKE "%performance%";',
173
+ code_after='-- Use Full-Text index:\nSELECT id, title FROM articles WHERE MATCH(title) AGAINST(? IN NATURAL LANGUAGE MODE);',
174
+ file=rel,
175
+ line_number=line_no,
176
+ code_snippet=snippet,
177
+ category='Indexing & Search',
178
+ severity=Severity.MEDIUM,
179
+ layer=Layer.DATABASE,
180
+ impact=7,
181
+ effort=3,
182
+ occurrences=1,
183
+ perf_gain='Avoids full table scan on text searches'
184
+ ))
core/__init__.py ADDED
File without changes
core/issue.py ADDED
@@ -0,0 +1,67 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+ from typing import Optional
4
+
5
+ class Severity(Enum):
6
+ CRITICAL = 'critical'
7
+ HIGH = 'high'
8
+ MEDIUM = 'medium'
9
+ LOW = 'low'
10
+
11
+ class Layer(Enum):
12
+ FRONTEND = 'frontend'
13
+ BACKEND = 'backend'
14
+ DATABASE = 'database'
15
+ INFRA = 'infra'
16
+
17
+ @dataclass
18
+ class Issue:
19
+ id: str
20
+ title: str
21
+ description: str
22
+ fix: str
23
+ code_before: str
24
+ code_after: str
25
+ file: str
26
+ severity: Severity
27
+ layer: Layer
28
+ impact: int # 1-10
29
+ effort: int # 1-10
30
+ occurrences: int
31
+ perf_gain: str
32
+ line_number: Optional[int] = None
33
+ code_snippet: Optional[str] = None
34
+ category: str = 'Performance'
35
+ doc_url: Optional[str] = None
36
+
37
+ @property
38
+ def priority_score(self) -> float:
39
+ """Higher = fix first. Impact/Effort ratio."""
40
+ return round(self.impact / max(self.effort, 1), 2)
41
+
42
+ @property
43
+ def severity_order(self) -> int:
44
+ order = {Severity.CRITICAL: 0, Severity.HIGH: 1, Severity.MEDIUM: 2, Severity.LOW: 3}
45
+ return order.get(self.severity, 3)
46
+
47
+ def to_dict(self) -> dict:
48
+ return {
49
+ 'id': self.id,
50
+ 'title': self.title,
51
+ 'description': self.description,
52
+ 'fix': self.fix,
53
+ 'code_before': self.code_before,
54
+ 'code_after': self.code_after,
55
+ 'file': self.file,
56
+ 'line_number': self.line_number,
57
+ 'code_snippet': self.code_snippet,
58
+ 'category': self.category,
59
+ 'doc_url': self.doc_url,
60
+ 'severity': self.severity.value,
61
+ 'layer': self.layer.value,
62
+ 'impact': self.impact,
63
+ 'effort': self.effort,
64
+ 'occurrences': self.occurrences,
65
+ 'perf_gain': self.perf_gain,
66
+ 'priority_score': self.priority_score,
67
+ }
core/scorer.py ADDED
@@ -0,0 +1,94 @@
1
+ import math
2
+ from typing import List, Dict, Any
3
+ from core.issue import Issue, Severity, Layer
4
+
5
+ class Scorer:
6
+ def compute(self, issues: List[Issue]) -> Dict[str, Any]:
7
+ severity_weights = {
8
+ Severity.CRITICAL: 22,
9
+ Severity.HIGH: 12,
10
+ Severity.MEDIUM: 5,
11
+ Severity.LOW: 2
12
+ }
13
+
14
+ def compute_layer_score(layer: Layer) -> int:
15
+ sub = [i for i in issues if i.layer == layer]
16
+ if not sub:
17
+ return 100
18
+ # Diminishing returns penalty calculation
19
+ raw_penalty = sum(severity_weights.get(i.severity, 2) * min(max(i.occurrences, 1), 5) for i in sub)
20
+ # Smooth asymptotic curve: 100 / (1 + (penalty / 38)^1.15)
21
+ damped_score = 100.0 / (1.0 + (raw_penalty / 38.0) ** 1.15)
22
+ return max(5, min(100, round(damped_score)))
23
+
24
+ frontend_score = compute_layer_score(Layer.FRONTEND)
25
+ backend_score = compute_layer_score(Layer.BACKEND)
26
+ db_score = compute_layer_score(Layer.DATABASE)
27
+ infra_score = compute_layer_score(Layer.INFRA)
28
+
29
+ overall = round(
30
+ frontend_score * 0.40 +
31
+ backend_score * 0.30 +
32
+ db_score * 0.20 +
33
+ infra_score * 0.10
34
+ )
35
+
36
+ by_sev = {s.value: 0 for s in Severity}
37
+ by_layer = {l.value: 0 for l in Layer}
38
+ for i in issues:
39
+ by_sev[i.severity.value] += 1
40
+ by_layer[i.layer.value] += 1
41
+
42
+ # File breakdown
43
+ file_issues: Dict[str, List[Issue]] = {}
44
+ for i in issues:
45
+ file_issues.setdefault(i.file, []).append(i)
46
+
47
+ worst_files = sorted(file_issues.items(), key=lambda x: (
48
+ -sum(severity_weights.get(ii.severity, 2) for ii in x[1])
49
+ ))[:10]
50
+
51
+ file_scores = []
52
+ for fp, fi in worst_files:
53
+ file_raw = sum(severity_weights.get(ii.severity, 2) for ii in fi)
54
+ f_score = max(5, min(100, round(100.0 / (1.0 + (file_raw / 28.0) ** 1.15))))
55
+ file_scores.append({
56
+ 'file': fp,
57
+ 'issues': len(fi),
58
+ 'score': f_score,
59
+ 'critical': sum(1 for ii in fi if ii.severity == Severity.CRITICAL),
60
+ 'high': sum(1 for ii in fi if ii.severity == Severity.HIGH),
61
+ 'medium': sum(1 for ii in fi if ii.severity == Severity.MEDIUM),
62
+ 'low': sum(1 for ii in fi if ii.severity == Severity.LOW)
63
+ })
64
+
65
+ # Top issues sorted by priority_score (Impact/Effort) and severity
66
+ sorted_issues = sorted(issues, key=lambda i: (i.severity_order, -i.priority_score))
67
+ top3 = sorted_issues[:3]
68
+
69
+ # Projected impact calculations
70
+ has_dom_issues = any(i.id in ['ANG001', 'ANG003', 'ANG009', 'ANG010'] for i in issues)
71
+ has_db_loop = any(i.id in ['NODE004', 'SQL004', 'SQL003', 'NODE005'] for i in issues)
72
+ mem_leak_count = sum(1 for i in issues if i.id in ['ANG004', 'NODE009', 'NODE011'])
73
+ bundle_reduction = sum(45 if i.id == 'ANG013' else 25 if i.id == 'ANG012' else 0 for i in issues)
74
+
75
+ metrics = {
76
+ 'dom_render_waste_pct': 65 if has_dom_issues else 0,
77
+ 'db_query_reduction_pct': 85 if has_db_loop else 0,
78
+ 'memory_leak_count': mem_leak_count,
79
+ 'bundle_reduction_kb': bundle_reduction,
80
+ }
81
+
82
+ return {
83
+ 'overall': overall,
84
+ 'frontend': frontend_score,
85
+ 'backend': backend_score,
86
+ 'database': db_score,
87
+ 'infra': infra_score,
88
+ 'by_severity': by_sev,
89
+ 'by_layer': by_layer,
90
+ 'total_issues': len(issues),
91
+ 'top3': [i.to_dict() for i in top3],
92
+ 'worst_files': file_scores,
93
+ 'metrics': metrics,
94
+ }