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.
analyzers/__init__.py ADDED
File without changes
@@ -0,0 +1,406 @@
1
+ import os
2
+ import re
3
+ from pathlib import Path
4
+ from typing import List, Tuple, Optional
5
+ from core.issue import Issue, Severity, Layer
6
+
7
+ class AngularAnalyzer:
8
+ def __init__(self, path: str):
9
+ self.path = Path(path)
10
+ self.issues: List[Issue] = []
11
+
12
+ def analyze(self) -> List[Issue]:
13
+ for f in self.path.rglob('*.ts'):
14
+ if any(x in str(f) for x in ['node_modules', '.spec.', 'dist/', '.d.ts', '.angular', '.git']):
15
+ continue
16
+ try:
17
+ src = f.read_text(encoding='utf-8', errors='ignore')
18
+ rel = str(f.relative_to(self.path)).replace('\\', '/')
19
+ lines = src.splitlines()
20
+
21
+ if '@Component' in src:
22
+ self._check_component(src, lines, rel)
23
+ if 'subscribe(' in src:
24
+ self._check_subscriptions(src, lines, rel)
25
+ if 'HttpClient' in src or 'http.get' in src.lower() or 'this.http.' in src:
26
+ self._check_http(src, lines, rel)
27
+ self._check_imports(src, lines, rel)
28
+ except Exception:
29
+ pass
30
+
31
+ for f in self.path.rglob('*.html'):
32
+ if any(x in str(f) for x in ['node_modules', 'dist/', '.angular', '.git']):
33
+ continue
34
+ try:
35
+ src = f.read_text(encoding='utf-8', errors='ignore')
36
+ if not self._is_angular_template(f, src):
37
+ continue
38
+ rel = str(f.relative_to(self.path)).replace('\\', '/')
39
+ lines = src.splitlines()
40
+ self._check_template(src, lines, rel)
41
+ except Exception:
42
+ pass
43
+
44
+ for f in self.path.rglob('*-routing.module.ts'):
45
+ if any(x in str(f) for x in ['node_modules', 'dist/', '.angular', '.git']):
46
+ continue
47
+ try:
48
+ src = f.read_text(encoding='utf-8', errors='ignore')
49
+ rel = str(f.relative_to(self.path)).replace('\\', '/')
50
+ lines = src.splitlines()
51
+ self._check_routing(src, lines, rel)
52
+ except Exception:
53
+ pass
54
+
55
+ return self.issues
56
+
57
+ def _is_angular_template(self, f: Path, src: str) -> bool:
58
+ # Standard Angular component template convention: *.component.html
59
+ if f.name.endswith('.component.html'):
60
+ return True
61
+ # Angular workspace indicators
62
+ if (self.path / 'angular.json').exists():
63
+ return True
64
+ # Associated component TS file exists in the same folder
65
+ stem = f.stem
66
+ if (f.parent / f"{stem}.ts").exists() or (f.parent / f"{stem}.component.ts").exists():
67
+ return True
68
+ # Definite Angular template syntax directives
69
+ angular_directives = [
70
+ '*ngIf=', '*ngFor=', '[ngClass]=', '[ngStyle]=',
71
+ '(click)=', '[(ngModel)]=', '<router-outlet',
72
+ '@defer', '@for ('
73
+ ]
74
+ return any(d in src for d in angular_directives)
75
+
76
+ def _is_suppressed(self, lines: List[str], line_idx: int, rule_id: str) -> bool:
77
+ """Checks for // perf-ignore [RULE_ID] on current or previous line."""
78
+ check_lines = []
79
+ if 0 <= line_idx < len(lines):
80
+ check_lines.append(lines[line_idx])
81
+ if 0 <= line_idx - 1 < len(lines):
82
+ check_lines.append(lines[line_idx - 1])
83
+ for cl in check_lines:
84
+ if f'perf-ignore {rule_id}' in cl or 'perf-ignore-all' in cl:
85
+ return True
86
+ return False
87
+
88
+ def _find_line(self, lines: List[str], regex_or_str: str) -> Tuple[int, str]:
89
+ for idx, line in enumerate(lines, 1):
90
+ if isinstance(regex_or_str, str) and regex_or_str in line:
91
+ return idx, line.strip()
92
+ elif hasattr(regex_or_str, 'search') and regex_or_str.search(line):
93
+ return idx, line.strip()
94
+ return 1, (lines[0].strip() if lines else '')
95
+
96
+ def _check_component(self, src: str, lines: List[str], rel: str):
97
+ # ANG001: Missing OnPush Change Detection
98
+ if '@Component' in src and 'ChangeDetectionStrategy.OnPush' not in src:
99
+ line_no, snippet = self._find_line(lines, '@Component')
100
+ if not self._is_suppressed(lines, line_no - 1, 'ANG001'):
101
+ self.issues.append(Issue(
102
+ id='ANG001',
103
+ title='Missing OnPush Change Detection Strategy',
104
+ description='Component uses default change detection. Angular dirty-checks the entire component subtree on every DOM event, timer, and HTTP response. OnPush restricts checks to @Input() reference changes or async pipe emissions.',
105
+ fix='Add `changeDetection: ChangeDetectionStrategy.OnPush` to @Component decorator.',
106
+ code_before='@Component({\n selector: "app-feature",\n templateUrl: "./feature.component.html"\n})\nexport class FeatureComponent {}',
107
+ code_after='@Component({\n selector: "app-feature",\n templateUrl: "./feature.component.html",\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class FeatureComponent {}',
108
+ file=rel,
109
+ line_number=line_no,
110
+ code_snippet=snippet,
111
+ category='DOM Re-renders',
112
+ doc_url='https://angular.dev/best-practices/runtime-performance#onpush-change-detection',
113
+ severity=Severity.HIGH,
114
+ layer=Layer.FRONTEND,
115
+ impact=9,
116
+ effort=3,
117
+ occurrences=1,
118
+ perf_gain='Up to 65% reduction in DOM re-renders for data-heavy UIs'
119
+ ))
120
+
121
+ # ANG002: Direct Input Mutation
122
+ input_props = re.findall(r'@Input\(\)\s+(?:public\s+|readonly\s+)?(\w+)', src)
123
+ for prop in input_props:
124
+ push_pattern = rf'this\.{prop}\.push\('
125
+ m = re.search(push_pattern, src)
126
+ if m:
127
+ line_no, snippet = self._find_line(lines, f'this.{prop}.push(')
128
+ if not self._is_suppressed(lines, line_no - 1, 'ANG002'):
129
+ self.issues.append(Issue(
130
+ id='ANG002',
131
+ title=f'Direct Mutation of @Input Property `{prop}`',
132
+ description=f'Mutating `this.{prop}.push(...)` alters internal state without changing object reference. With OnPush, Angular will fail to detect changes, leading to stale DOM updates.',
133
+ fix=f'Use immutable array spreading: `this.{prop} = [...this.{prop}, item];`',
134
+ code_before=f'this.{prop}.push(newItem); // Fails OnPush change detection',
135
+ code_after=f'this.{prop} = [...this.{prop}, newItem]; // Emits new object reference',
136
+ file=rel,
137
+ line_number=line_no,
138
+ code_snippet=snippet,
139
+ category='Change Detection',
140
+ doc_url='https://angular.dev/best-practices/runtime-performance',
141
+ severity=Severity.HIGH,
142
+ layer=Layer.FRONTEND,
143
+ impact=7,
144
+ effort=2,
145
+ occurrences=1,
146
+ perf_gain='Guarantees correct change propagation under OnPush'
147
+ ))
148
+ break
149
+
150
+ # ANG003: detectChanges inside loop
151
+ for idx, line in enumerate(lines, 1):
152
+ if 'detectChanges()' in line:
153
+ # Check previous 8 lines for loop declarations
154
+ prev_block = '\n'.join(lines[max(0, idx - 8):idx])
155
+ if any(loop_k in prev_block for loop_k in ['for (', 'for(', 'forEach(', '.map(']):
156
+ if not self._is_suppressed(lines, idx - 1, 'ANG003'):
157
+ self.issues.append(Issue(
158
+ id='ANG003',
159
+ title='detectChanges() Synchronously Called Inside Loop',
160
+ description='Triggering change detection inside a loop forces synchronous DOM calculation on every single item iteration, locking the browser UI thread.',
161
+ fix='Batch updates and call `markForCheck()` or trigger `detectChanges()` once after loop terminates.',
162
+ code_before='items.forEach(item => {\n this.updateItem(item);\n this.cdr.detectChanges(); // N synchronous renders!\n});',
163
+ code_after='items.forEach(item => this.updateItem(item));\nthis.cdr.markForCheck(); // Batched single cycle',
164
+ file=rel,
165
+ line_number=idx,
166
+ code_snippet=line.strip(),
167
+ category='Render Cycle',
168
+ severity=Severity.CRITICAL,
169
+ layer=Layer.FRONTEND,
170
+ impact=8,
171
+ effort=2,
172
+ occurrences=1,
173
+ perf_gain='Eliminates N redundant render cycles'
174
+ ))
175
+ break
176
+
177
+ def _check_subscriptions(self, src: str, lines: List[str], rel: str):
178
+ # ANG004: Observable subscription leak
179
+ has_take_until = 'takeUntil(' in src or 'takeUntilDestroyed(' in src or 'take(1)' in src or 'first()' in src
180
+ if not has_take_until and '@Component' in src:
181
+ for idx, line in enumerate(lines, 1):
182
+ if '.subscribe(' in line and not self._is_suppressed(lines, idx - 1, 'ANG004'):
183
+ self.issues.append(Issue(
184
+ id='ANG004',
185
+ title='Observable Subscription Memory Leak (Missing Teardown)',
186
+ description='Subscription created without takeUntilDestroyed, take(1), or unsubscribe. Retains component instance and view DOM tree in memory after route navigation.',
187
+ fix='Use Angular 16+ `takeUntilDestroyed(this.destroyRef)` or use the `| async` template pipe.',
188
+ code_before='ngOnInit() {\n this.service.data$.subscribe(d => this.data = d);\n}',
189
+ code_after='private destroyRef = inject(DestroyRef);\n\nngOnInit() {\n this.service.data$.pipe(\n takeUntilDestroyed(this.destroyRef)\n ).subscribe(d => this.data = d);\n}',
190
+ file=rel,
191
+ line_number=idx,
192
+ code_snippet=line.strip(),
193
+ category='Memory Leak',
194
+ doc_url='https://angular.dev/guide/signals/rxjs-interop',
195
+ severity=Severity.CRITICAL,
196
+ layer=Layer.FRONTEND,
197
+ impact=9,
198
+ effort=3,
199
+ occurrences=src.count('.subscribe('),
200
+ perf_gain='Prevents progressive memory growth across route transitions'
201
+ ))
202
+ break
203
+
204
+ # ANG005: Nested subscribe callback hell
205
+ for idx, line in enumerate(lines, 1):
206
+ if '.subscribe(' in line:
207
+ chunk = '\n'.join(lines[idx:min(len(lines), idx + 8)])
208
+ if '.subscribe(' in chunk:
209
+ if not self._is_suppressed(lines, idx - 1, 'ANG005'):
210
+ self.issues.append(Issue(
211
+ id='ANG005',
212
+ title='Nested subscribe() Callback Anti-pattern',
213
+ description='Subscribing inside a subscribe handler causes race conditions, unhandled rejections, and disables automatic request cancellation.',
214
+ fix='Flatten using RxJS higher-order mapping operators like `switchMap` or `concatMap`.',
215
+ code_before='this.route.params.subscribe(p => {\n this.api.getUser(p.id).subscribe(u => this.user = u);\n});',
216
+ code_after='this.route.params.pipe(\n switchMap(p => this.api.getUser(p.id))\n).subscribe(u => this.user = u);',
217
+ file=rel,
218
+ line_number=idx,
219
+ code_snippet=line.strip(),
220
+ category='Concurrency & Flow',
221
+ severity=Severity.HIGH,
222
+ layer=Layer.FRONTEND,
223
+ impact=7,
224
+ effort=3,
225
+ occurrences=1,
226
+ perf_gain='Eliminates race conditions and redundant network requests'
227
+ ))
228
+ break
229
+
230
+ def _check_http(self, src: str, lines: List[str], rel: str):
231
+ # ANG006: Unpaginated list fetch
232
+ has_get = 'http.get(' in src.lower() or 'this.http.get' in src.lower()
233
+ has_pagination = any(p in src.lower() for p in ['page', 'limit', 'offset', 'pagesize', 'skip', 'take'])
234
+ if has_get and not has_pagination and any(k in src.lower() for k in ['items', 'orders', 'users', 'list', 'all', 'records']):
235
+ line_no, snippet = self._find_line(lines, re.compile(r'http\.get', re.IGNORECASE))
236
+ if not self._is_suppressed(lines, line_no - 1, 'ANG006'):
237
+ self.issues.append(Issue(
238
+ id='ANG006',
239
+ title='Unpaginated HTTP Collection Request (Full Dataset Fetch)',
240
+ description='HTTP GET fetches entire data collections without pagination parameters. As tables grow, payload size balloons from KB to tens of MBs, blocking client CPU.',
241
+ fix='Add query parameters for page and limit; paginate on both server and client.',
242
+ code_before='this.http.get<Order[]>("/api/orders").subscribe(data => this.orders = data);',
243
+ code_after='this.http.get<Paged<Order>>("/api/orders", {\n params: { page: this.page, limit: 25 }\n}).subscribe(res => this.orders = res.items);',
244
+ file=rel,
245
+ line_number=line_no,
246
+ code_snippet=snippet,
247
+ category='Network & Bundle',
248
+ severity=Severity.CRITICAL,
249
+ layer=Layer.FRONTEND,
250
+ impact=10,
251
+ effort=4,
252
+ occurrences=1,
253
+ perf_gain='Reduces payload by 80%+ and cuts Time-to-Interactive'
254
+ ))
255
+
256
+ # ANG007: No debounce on user input
257
+ if ('valueChanges' in src or 'fromEvent' in src) and 'debounceTime' not in src:
258
+ line_no, snippet = self._find_line(lines, 'valueChanges')
259
+ if line_no == 1:
260
+ line_no, snippet = self._find_line(lines, 'fromEvent')
261
+ if not self._is_suppressed(lines, line_no - 1, 'ANG007'):
262
+ self.issues.append(Issue(
263
+ id='ANG007',
264
+ title='Missing debounceTime on Reactive Form / Input Stream',
265
+ description='Form `valueChanges` triggers downstream API requests or expensive filter calculations on every single keystroke.',
266
+ fix='Add `debounceTime(300)` and `distinctUntilChanged()` into the pipe operator.',
267
+ code_before='this.searchControl.valueChanges.pipe(\n switchMap(q => this.api.search(q))\n).subscribe();',
268
+ code_after='this.searchControl.valueChanges.pipe(\n debounceTime(300),\n distinctUntilChanged(),\n switchMap(q => this.api.search(q))\n).subscribe();',
269
+ file=rel,
270
+ line_number=line_no,
271
+ code_snippet=snippet,
272
+ category='Network Optimization',
273
+ severity=Severity.HIGH,
274
+ layer=Layer.FRONTEND,
275
+ impact=7,
276
+ effort=1,
277
+ occurrences=1,
278
+ perf_gain='Cuts redundant network requests by up to 85%'
279
+ ))
280
+
281
+ def _check_template(self, src: str, lines: List[str], rel: str):
282
+ # ANG009: *ngFor without trackBy or @for without track
283
+ ngfor_matches = len(re.findall(r'\*ngFor\s*=\s*["\']', src))
284
+ trackby_matches = len(re.findall(r'trackBy\s*:', src)) + len(re.findall(r'trackBy\s*=', src))
285
+ for_without_track = re.findall(r'@for\s*\([^;)]+\)', src) # missing track
286
+
287
+ if ngfor_matches > trackby_matches:
288
+ line_no, snippet = self._find_line(lines, re.compile(r'\*ngFor\s*=\s*["\']'))
289
+ if not self._is_suppressed(lines, line_no - 1, 'ANG009'):
290
+ self.issues.append(Issue(
291
+ id='ANG009',
292
+ title=f'*ngFor Loop Missing trackBy Identifier ({ngfor_matches - trackby_matches} un-tracked)',
293
+ description='Without trackBy, Angular destroys and recreates all DOM nodes in the list when data refreshes, causing UI stutter and input focus loss.',
294
+ fix='Use modern Angular `@for (item of items; track item.id)` or add `trackBy: trackById`.',
295
+ code_before='<div *ngFor="let item of items">{{ item.name }}</div>',
296
+ code_after='@for (item of items; track item.id) {\n <div>{{ item.name }}</div>\n}',
297
+ file=rel,
298
+ line_number=line_no,
299
+ code_snippet=snippet,
300
+ category='DOM Performance',
301
+ doc_url='https://angular.dev/guide/templates/control-flow#for-loop',
302
+ severity=Severity.CRITICAL,
303
+ layer=Layer.FRONTEND,
304
+ impact=8,
305
+ effort=1,
306
+ occurrences=ngfor_matches - trackby_matches,
307
+ perf_gain='Reduces DOM node re-creations by up to 99%'
308
+ ))
309
+
310
+ # ANG011: Method calls in template interpolation
311
+ method_calls = re.findall(r'{{\s*([a-zA-Z0-9_]+)\(', src)
312
+ if len(method_calls) >= 2:
313
+ line_no, snippet = self._find_line(lines, re.compile(r'{{\s*[a-zA-Z0-9_]+\('))
314
+ if not self._is_suppressed(lines, line_no - 1, 'ANG011'):
315
+ self.issues.append(Issue(
316
+ id='ANG011',
317
+ title=f'Method Calls in Template Interpolation ({len(method_calls)} detected)',
318
+ description='Invoking methods in template bindings evaluates the method on every single change detection tick (hundreds of times per second during interactions).',
319
+ fix='Replace method calls with pure Pipes or pre-computed signal/component properties.',
320
+ code_before='<div>{{ formatPrice(item.price) }}</div> <!-- Runs every cycle -->',
321
+ code_after='<div>{{ item.price | currency }}</div> <!-- Cached pure pipe -->',
322
+ file=rel,
323
+ line_number=line_no,
324
+ code_snippet=snippet,
325
+ category='DOM Re-renders',
326
+ severity=Severity.HIGH,
327
+ layer=Layer.FRONTEND,
328
+ impact=7,
329
+ effort=2,
330
+ occurrences=len(method_calls),
331
+ perf_gain='Eliminates repeated execution of heavy template methods'
332
+ ))
333
+
334
+ # ANG014: Missing @defer for heavy components
335
+ if '<app-' in src and '@defer' not in src and len(re.findall(r'<app-[\w-]+', src)) >= 3:
336
+ line_no, snippet = self._find_line(lines, re.compile(r'<app-[\w-]+'))
337
+ if not self._is_suppressed(lines, line_no - 1, 'ANG014'):
338
+ self.issues.append(Issue(
339
+ id='ANG014',
340
+ title='Heavy Subcomponents Rendered Without @defer (Lazy Viewport)',
341
+ description='Non-critical below-the-fold components are loaded synchronously in the initial bundle. Angular 17+ deferrable views delay JS download until visible.',
342
+ fix='Wrap heavy below-the-fold components in `@defer (on viewport) { ... }`.',
343
+ code_before='<app-heavy-chart [data]="chartData" />',
344
+ code_after='@defer (on viewport) {\n <app-heavy-chart [data]="chartData" />\n} @placeholder {\n <div class="chart-skeleton">Loading chart...</div>\n}',
345
+ file=rel,
346
+ line_number=line_no,
347
+ code_snippet=snippet,
348
+ category='Network & Bundle',
349
+ doc_url='https://angular.dev/guide/templates/defer',
350
+ severity=Severity.MEDIUM,
351
+ layer=Layer.FRONTEND,
352
+ impact=7,
353
+ effort=2,
354
+ occurrences=1,
355
+ perf_gain='Reduces initial JS chunk size and speeds up Largest Contentful Paint (LCP)'
356
+ ))
357
+
358
+ def _check_imports(self, src: str, lines: List[str], rel: str):
359
+ # ANG012: Full library wildcard imports
360
+ if 'import * as _' in src or "from 'lodash'" in src or "from 'rxjs/Rx'" in src:
361
+ line_no, snippet = self._find_line(lines, re.compile(r"(?:from 'lodash'|import \* as _|from 'rxjs/Rx')"))
362
+ if not self._is_suppressed(lines, line_no - 1, 'ANG012'):
363
+ self.issues.append(Issue(
364
+ id='ANG012',
365
+ title='Unoptimized Full Library Import (Bundle Bloat)',
366
+ description='Importing entire libraries like `lodash` or legacy RxJS bundles disables tree-shaking and adds 70KB+ unnecessary JS to client downloads.',
367
+ fix='Import individual functions: `import debounce from "lodash/debounce";` or use native JS.',
368
+ code_before='import _ from "lodash";\n_.cloneDeep(data);',
369
+ code_after='import cloneDeep from "lodash/cloneDeep";\ncloneDeep(data); // Or structuredClone(data)',
370
+ file=rel,
371
+ line_number=line_no,
372
+ code_snippet=snippet,
373
+ category='Bundle Optimization',
374
+ severity=Severity.MEDIUM,
375
+ layer=Layer.FRONTEND,
376
+ impact=6,
377
+ effort=2,
378
+ occurrences=1,
379
+ perf_gain='Saves 50KB-80KB in production bundle size'
380
+ ))
381
+
382
+ def _check_routing(self, src: str, lines: List[str], rel: str):
383
+ # ANG013: Eager routing without lazy loading
384
+ eager_routes = re.findall(r'component:\s*\w+Component', src)
385
+ lazy_routes = re.findall(r'loadChildren|loadComponent', src)
386
+ if len(eager_routes) >= 3 and len(lazy_routes) == 0:
387
+ line_no, snippet = self._find_line(lines, 'component:')
388
+ if not self._is_suppressed(lines, line_no - 1, 'ANG013'):
389
+ self.issues.append(Issue(
390
+ id='ANG013',
391
+ title=f'All {len(eager_routes)} Routes Loaded Eagerly at Startup',
392
+ description='All application feature modules load on initial page visit. Users download, parse, and compile code for routes they may never navigate to.',
393
+ fix='Convert route definitions to use `loadComponent` or `loadChildren` with dynamic `import()`.',
394
+ code_before='{ path: "admin", component: AdminComponent }',
395
+ code_after='{ path: "admin", loadComponent: () => import("./admin/admin.component").then(m => m.AdminComponent) }',
396
+ file=rel,
397
+ line_number=line_no,
398
+ code_snippet=snippet,
399
+ category='Bundle Optimization',
400
+ severity=Severity.HIGH,
401
+ layer=Layer.FRONTEND,
402
+ impact=8,
403
+ effort=3,
404
+ occurrences=len(eager_routes),
405
+ perf_gain='Cuts initial bundle by 40-60% via automatic code splitting'
406
+ ))
@@ -0,0 +1,122 @@
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 AwsAnalyzer:
8
+ def __init__(self, path: str):
9
+ self.path = Path(path)
10
+ self.issues: List[Issue] = []
11
+
12
+ def analyze(self) -> List[Issue]:
13
+ target_extensions = ['*.json', '*.yml', '*.yaml', '*.tf']
14
+ for ext in target_extensions:
15
+ for f in self.path.rglob(ext):
16
+ if any(x in str(f) for x in ['node_modules', 'dist/', '.git', 'package-lock.json']):
17
+ continue
18
+ try:
19
+ src = f.read_text(encoding='utf-8', errors='ignore')
20
+ rel = str(f.relative_to(self.path)).replace('\\', '/')
21
+ lines = src.splitlines()
22
+ self._check_aws_config(src, lines, rel)
23
+ except Exception:
24
+ pass
25
+ return self.issues
26
+
27
+ def _is_suppressed(self, lines: List[str], line_idx: int, rule_id: str) -> bool:
28
+ check_lines = []
29
+ if 0 <= line_idx < len(lines):
30
+ check_lines.append(lines[line_idx])
31
+ if 0 <= line_idx - 1 < len(lines):
32
+ check_lines.append(lines[line_idx - 1])
33
+ for cl in check_lines:
34
+ if f'perf-ignore {rule_id}' in cl or 'perf-ignore-all' in cl:
35
+ return True
36
+ return False
37
+
38
+ def _find_line(self, lines: List[str], regex_or_str) -> Tuple[int, str]:
39
+ for idx, line in enumerate(lines, 1):
40
+ if isinstance(regex_or_str, str) and regex_or_str.lower() in line.lower():
41
+ return idx, line.strip()
42
+ elif hasattr(regex_or_str, 'search') and regex_or_str.search(line):
43
+ return idx, line.strip()
44
+ return 1, (lines[0].strip() if lines else '')
45
+
46
+ def _check_aws_config(self, src: str, lines: List[str], rel: str):
47
+ sl = src.lower()
48
+
49
+ # AWS001: Lambda memory too low (<512MB)
50
+ mem_match = re.search(r'memorysize["\s:]+([0-9]+)', sl)
51
+ if mem_match:
52
+ val = int(mem_match.group(1))
53
+ if val < 512:
54
+ line_no, snippet = self._find_line(lines, re.compile(r'memorysize', re.IGNORECASE))
55
+ if not self._is_suppressed(lines, line_no - 1, 'AWS001'):
56
+ self.issues.append(Issue(
57
+ id='AWS001',
58
+ title=f'Lambda Memory Allocated Too Low ({val} MB)',
59
+ description=f'Lambda configured with only {val} MB memory. AWS CPU allocation scales proportionally with memory. Functions with <512MB memory suffer from throttled CPU, running 3-4x slower.',
60
+ fix='Increase MemorySize to 512 MB - 1024 MB. Faster completion often results in equal or lower net AWS cost.',
61
+ code_before=f'MemorySize: {val} # Under-allocated CPU',
62
+ code_after='MemorySize: 512 # Full vCPU core access; up to 65% faster completion',
63
+ file=rel,
64
+ line_number=line_no,
65
+ code_snippet=snippet,
66
+ category='Serverless Latency',
67
+ doc_url='https://docs.aws.amazon.com/lambda/latest/dg/configuration-function-common.html#configuration-memory-console',
68
+ severity=Severity.HIGH,
69
+ layer=Layer.INFRA,
70
+ impact=7,
71
+ effort=1,
72
+ occurrences=1,
73
+ perf_gain='Cuts function runtime duration by up to 60%'
74
+ ))
75
+
76
+ # AWS002: Static assets served without CloudFront CDN
77
+ has_s3 = 's3' in sl and any(w in sl for w in ['website', 'static', 'hosting', 'bucket'])
78
+ has_cdn = 'cloudfront' in sl or 'distribution' in sl
79
+ if has_s3 and not has_cdn:
80
+ line_no, snippet = self._find_line(lines, 's3')
81
+ if not self._is_suppressed(lines, line_no - 1, 'AWS002'):
82
+ self.issues.append(Issue(
83
+ id='AWS002',
84
+ title='Static S3 Hosting Without CloudFront Edge CDN',
85
+ description='Frontend static assets are served directly from an S3 bucket in a single region. Distant global users experience 200-500ms latency on every asset download.',
86
+ fix='Deploy an Amazon CloudFront distribution in front of S3 with gzip/brotli compression enabled.',
87
+ code_before='# Assets served from single S3 bucket region\n# Global users: 300ms+ roundtrip',
88
+ code_after='# CloudFront CDN distribution with edge caching\n# Global users: 15-30ms from local edge',
89
+ file=rel,
90
+ line_number=line_no,
91
+ code_snippet=snippet,
92
+ category='Edge & CDN',
93
+ severity=Severity.HIGH,
94
+ layer=Layer.INFRA,
95
+ impact=8,
96
+ effort=3,
97
+ occurrences=1,
98
+ perf_gain='Reduces static asset latency from ~350ms to 20ms globally'
99
+ ))
100
+
101
+ # AWS005: Lambda directly connecting to RDS without RDS Proxy
102
+ if ('lambda' in sl or 'serverless' in sl) and ('rds' in sl or 'postgres' in sl or 'mysql' in sl) and 'proxy' not in sl:
103
+ line_no, snippet = self._find_line(lines, re.compile(r'(?:rds|postgres|mysql)', re.IGNORECASE))
104
+ if not self._is_suppressed(lines, line_no - 1, 'AWS005'):
105
+ self.issues.append(Issue(
106
+ id='AWS005',
107
+ title='Serverless Lambda Connecting to RDS Without RDS Proxy',
108
+ description='Lambda functions executing concurrent requests spawn hundreds of unpooled database connections, quickly exhausting RDS connection limits and leading to connection refusal errors.',
109
+ fix='Place an Amazon RDS Proxy between Lambda and RDS to pool and multiplex database connections.',
110
+ code_before='DB_HOST: "my-rds-cluster.rds.amazonaws.com" # Direct unpooled connection',
111
+ code_after='DB_HOST: "my-proxy.proxy-xxx.rds.amazonaws.com" # Multiplexed pool via RDS Proxy',
112
+ file=rel,
113
+ line_number=line_no,
114
+ code_snippet=snippet,
115
+ category='Connection Pooling',
116
+ severity=Severity.HIGH,
117
+ layer=Layer.INFRA,
118
+ impact=9,
119
+ effort=4,
120
+ occurrences=1,
121
+ perf_gain='Prevents database connection exhaustion during traffic bursts'
122
+ ))