driftdetect-core 0.8.1 → 0.8.2

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 (37) hide show
  1. package/dist/index.d.ts +8 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +24 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/java/index.d.ts +8 -0
  6. package/dist/java/index.d.ts.map +1 -0
  7. package/dist/java/index.js +7 -0
  8. package/dist/java/index.js.map +1 -0
  9. package/dist/java/java-analyzer.d.ts +142 -0
  10. package/dist/java/java-analyzer.d.ts.map +1 -0
  11. package/dist/java/java-analyzer.js +515 -0
  12. package/dist/java/java-analyzer.js.map +1 -0
  13. package/dist/php/index.d.ts +8 -0
  14. package/dist/php/index.d.ts.map +1 -0
  15. package/dist/php/index.js +7 -0
  16. package/dist/php/index.js.map +1 -0
  17. package/dist/php/php-analyzer.d.ts +149 -0
  18. package/dist/php/php-analyzer.d.ts.map +1 -0
  19. package/dist/php/php-analyzer.js +546 -0
  20. package/dist/php/php-analyzer.js.map +1 -0
  21. package/dist/python/index.d.ts +8 -0
  22. package/dist/python/index.d.ts.map +1 -0
  23. package/dist/python/index.js +7 -0
  24. package/dist/python/index.js.map +1 -0
  25. package/dist/python/python-analyzer.d.ts +156 -0
  26. package/dist/python/python-analyzer.d.ts.map +1 -0
  27. package/dist/python/python-analyzer.js +535 -0
  28. package/dist/python/python-analyzer.js.map +1 -0
  29. package/dist/typescript/index.d.ts +13 -0
  30. package/dist/typescript/index.d.ts.map +1 -0
  31. package/dist/typescript/index.js +12 -0
  32. package/dist/typescript/index.js.map +1 -0
  33. package/dist/typescript/typescript-analyzer.d.ts +194 -0
  34. package/dist/typescript/typescript-analyzer.d.ts.map +1 -0
  35. package/dist/typescript/typescript-analyzer.js +762 -0
  36. package/dist/typescript/typescript-analyzer.js.map +1 -0
  37. package/package.json +1 -1
@@ -0,0 +1,149 @@
1
+ /**
2
+ * PHP Analyzer
3
+ *
4
+ * Main analyzer for PHP projects. Provides comprehensive analysis of:
5
+ * - HTTP routes (Laravel, Symfony, Slim, Lumen)
6
+ * - Classes and methods
7
+ * - Error handling patterns
8
+ * - Data access patterns (Eloquent, Doctrine, PDO)
9
+ * - Traits and interfaces
10
+ */
11
+ import type { FunctionExtraction, ClassExtraction, CallExtraction, ImportExtraction } from '../call-graph/types.js';
12
+ export interface PhpAnalyzerConfig {
13
+ rootDir: string;
14
+ verbose?: boolean | undefined;
15
+ includePatterns?: string[] | undefined;
16
+ excludePatterns?: string[] | undefined;
17
+ }
18
+ export interface PhpAnalysisResult {
19
+ projectInfo: {
20
+ name: string | null;
21
+ version: string | null;
22
+ files: number;
23
+ framework: string | null;
24
+ };
25
+ detectedFrameworks: string[];
26
+ stats: PhpAnalysisStats;
27
+ functions: FunctionExtraction[];
28
+ classes: ClassExtraction[];
29
+ calls: CallExtraction[];
30
+ imports: ImportExtraction[];
31
+ }
32
+ export interface PhpAnalysisStats {
33
+ fileCount: number;
34
+ classCount: number;
35
+ traitCount: number;
36
+ interfaceCount: number;
37
+ functionCount: number;
38
+ methodCount: number;
39
+ linesOfCode: number;
40
+ testFileCount: number;
41
+ analysisTimeMs: number;
42
+ }
43
+ export interface PhpRoute {
44
+ method: string;
45
+ path: string;
46
+ handler: string;
47
+ framework: string;
48
+ file: string;
49
+ line: number;
50
+ middleware: string[];
51
+ }
52
+ export interface PhpRoutesResult {
53
+ routes: PhpRoute[];
54
+ byFramework: Record<string, number>;
55
+ }
56
+ export interface PhpErrorPattern {
57
+ type: 'try-catch' | 'throw' | 'custom-exception' | 'error-handler';
58
+ file: string;
59
+ line: number;
60
+ context: string;
61
+ }
62
+ export interface PhpErrorHandlingResult {
63
+ stats: {
64
+ tryCatchBlocks: number;
65
+ throwStatements: number;
66
+ customExceptions: number;
67
+ errorHandlers: number;
68
+ };
69
+ patterns: PhpErrorPattern[];
70
+ issues: PhpErrorIssue[];
71
+ }
72
+ export interface PhpErrorIssue {
73
+ type: string;
74
+ file: string;
75
+ line: number;
76
+ message: string;
77
+ suggestion?: string | undefined;
78
+ }
79
+ export interface PhpDataAccessResult {
80
+ accessPoints: PhpDataAccessPoint[];
81
+ byFramework: Record<string, number>;
82
+ byOperation: Record<string, number>;
83
+ models: string[];
84
+ }
85
+ export interface PhpDataAccessPoint {
86
+ model: string;
87
+ operation: string;
88
+ framework: string;
89
+ file: string;
90
+ line: number;
91
+ isRawSql: boolean;
92
+ }
93
+ export interface PhpTraitsResult {
94
+ traits: PhpTrait[];
95
+ usages: PhpTraitUsage[];
96
+ }
97
+ export interface PhpTrait {
98
+ name: string;
99
+ file: string;
100
+ line: number;
101
+ methods: string[];
102
+ }
103
+ export interface PhpTraitUsage {
104
+ trait: string;
105
+ usedIn: string;
106
+ file: string;
107
+ line: number;
108
+ }
109
+ export declare class PhpAnalyzer {
110
+ private config;
111
+ private extractor;
112
+ constructor(config: PhpAnalyzerConfig);
113
+ /**
114
+ * Full project analysis
115
+ */
116
+ analyze(): Promise<PhpAnalysisResult>;
117
+ /**
118
+ * Analyze HTTP routes
119
+ */
120
+ analyzeRoutes(): Promise<PhpRoutesResult>;
121
+ /**
122
+ * Analyze error handling patterns
123
+ */
124
+ analyzeErrorHandling(): Promise<PhpErrorHandlingResult>;
125
+ /**
126
+ * Analyze data access patterns
127
+ */
128
+ analyzeDataAccess(): Promise<PhpDataAccessResult>;
129
+ /**
130
+ * Analyze traits
131
+ */
132
+ analyzeTraits(): Promise<PhpTraitsResult>;
133
+ private findFiles;
134
+ private parseProjectInfo;
135
+ private detectFramework;
136
+ private isTestFile;
137
+ private extractRoutes;
138
+ private extractLaravelHandler;
139
+ private extractLaravelMiddleware;
140
+ private extractSymfonyMethods;
141
+ private extractNextMethod;
142
+ private extractDataAccess;
143
+ private normalizeOperation;
144
+ }
145
+ /**
146
+ * Factory function
147
+ */
148
+ export declare function createPhpAnalyzer(config: PhpAnalyzerConfig): PhpAnalyzer;
149
+ //# sourceMappingURL=php-analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"php-analyzer.d.ts","sourceRoot":"","sources":["../../src/php/php-analyzer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,546 @@
1
+ /**
2
+ * PHP Analyzer
3
+ *
4
+ * Main analyzer for PHP projects. Provides comprehensive analysis of:
5
+ * - HTTP routes (Laravel, Symfony, Slim, Lumen)
6
+ * - Classes and methods
7
+ * - Error handling patterns
8
+ * - Data access patterns (Eloquent, Doctrine, PDO)
9
+ * - Traits and interfaces
10
+ */
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { createPhpHybridExtractor } from '../call-graph/extractors/php-hybrid-extractor.js';
14
+ // ============================================================================
15
+ // Default Configuration
16
+ // ============================================================================
17
+ const DEFAULT_CONFIG = {
18
+ verbose: false,
19
+ includePatterns: ['**/*.php'],
20
+ excludePatterns: ['**/vendor/**', '**/node_modules/**', '**/.git/**'],
21
+ };
22
+ // ============================================================================
23
+ // PHP Analyzer Implementation
24
+ // ============================================================================
25
+ export class PhpAnalyzer {
26
+ config;
27
+ extractor;
28
+ constructor(config) {
29
+ this.config = { ...DEFAULT_CONFIG, ...config };
30
+ this.extractor = createPhpHybridExtractor();
31
+ }
32
+ /**
33
+ * Full project analysis
34
+ */
35
+ async analyze() {
36
+ const startTime = Date.now();
37
+ const files = await this.findFiles();
38
+ const projectInfo = await this.parseProjectInfo();
39
+ const allFunctions = [];
40
+ const allClasses = [];
41
+ const allCalls = [];
42
+ const allImports = [];
43
+ const detectedFrameworks = new Set();
44
+ let linesOfCode = 0;
45
+ let testFileCount = 0;
46
+ let traitCount = 0;
47
+ let interfaceCount = 0;
48
+ let methodCount = 0;
49
+ for (const file of files) {
50
+ const source = await fs.promises.readFile(file, 'utf-8');
51
+ linesOfCode += source.split('\n').length;
52
+ const isTestFile = this.isTestFile(file);
53
+ if (isTestFile)
54
+ testFileCount++;
55
+ const result = this.extractor.extract(source, file);
56
+ // Detect frameworks from imports/use statements
57
+ for (const imp of result.imports) {
58
+ const framework = this.detectFramework(imp.source);
59
+ if (framework)
60
+ detectedFrameworks.add(framework);
61
+ }
62
+ // Count traits and interfaces
63
+ traitCount += (source.match(/\btrait\s+\w+/g) || []).length;
64
+ interfaceCount += (source.match(/\binterface\s+\w+/g) || []).length;
65
+ // Count methods
66
+ methodCount += result.functions.filter(f => f.isMethod).length;
67
+ allFunctions.push(...result.functions);
68
+ allClasses.push(...result.classes);
69
+ allCalls.push(...result.calls);
70
+ allImports.push(...result.imports);
71
+ }
72
+ const analysisTimeMs = Date.now() - startTime;
73
+ return {
74
+ projectInfo: {
75
+ name: projectInfo.name,
76
+ version: projectInfo.version,
77
+ files: files.length,
78
+ framework: projectInfo.framework,
79
+ },
80
+ detectedFrameworks: Array.from(detectedFrameworks),
81
+ stats: {
82
+ fileCount: files.length,
83
+ classCount: allClasses.length,
84
+ traitCount,
85
+ interfaceCount,
86
+ functionCount: allFunctions.filter(f => !f.isMethod).length,
87
+ methodCount,
88
+ linesOfCode,
89
+ testFileCount,
90
+ analysisTimeMs,
91
+ },
92
+ functions: allFunctions,
93
+ classes: allClasses,
94
+ calls: allCalls,
95
+ imports: allImports,
96
+ };
97
+ }
98
+ /**
99
+ * Analyze HTTP routes
100
+ */
101
+ async analyzeRoutes() {
102
+ const files = await this.findFiles();
103
+ const routes = [];
104
+ for (const file of files) {
105
+ const source = await fs.promises.readFile(file, 'utf-8');
106
+ const fileRoutes = this.extractRoutes(source, file);
107
+ routes.push(...fileRoutes);
108
+ }
109
+ const byFramework = {};
110
+ for (const route of routes) {
111
+ byFramework[route.framework] = (byFramework[route.framework] || 0) + 1;
112
+ }
113
+ return { routes, byFramework };
114
+ }
115
+ /**
116
+ * Analyze error handling patterns
117
+ */
118
+ async analyzeErrorHandling() {
119
+ const files = await this.findFiles();
120
+ let tryCatchBlocks = 0;
121
+ let throwStatements = 0;
122
+ let customExceptions = 0;
123
+ let errorHandlers = 0;
124
+ const patterns = [];
125
+ const issues = [];
126
+ for (const file of files) {
127
+ const source = await fs.promises.readFile(file, 'utf-8');
128
+ const lines = source.split('\n');
129
+ for (let i = 0; i < lines.length; i++) {
130
+ const line = lines[i];
131
+ const lineNum = i + 1;
132
+ // Try-catch blocks
133
+ if (/\btry\s*\{/.test(line)) {
134
+ tryCatchBlocks++;
135
+ patterns.push({ type: 'try-catch', file, line: lineNum, context: line.trim() });
136
+ }
137
+ // Throw statements
138
+ if (/\bthrow\s+new\s+/.test(line)) {
139
+ throwStatements++;
140
+ patterns.push({ type: 'throw', file, line: lineNum, context: line.trim() });
141
+ }
142
+ // Custom exception classes
143
+ if (/class\s+\w+.*extends\s+.*Exception/.test(line)) {
144
+ customExceptions++;
145
+ patterns.push({ type: 'custom-exception', file, line: lineNum, context: line.trim() });
146
+ }
147
+ // Laravel exception handlers
148
+ if (/function\s+render\s*\(.*\$exception/.test(line) || /function\s+report\s*\(.*Throwable/.test(line)) {
149
+ errorHandlers++;
150
+ patterns.push({ type: 'error-handler', file, line: lineNum, context: line.trim() });
151
+ }
152
+ // Empty catch blocks
153
+ if (/catch\s*\([^)]+\)\s*\{\s*\}/.test(line)) {
154
+ issues.push({
155
+ type: 'empty-catch',
156
+ file,
157
+ line: lineNum,
158
+ message: 'Empty catch block swallows exceptions silently',
159
+ suggestion: 'Log the exception or handle it appropriately',
160
+ });
161
+ }
162
+ // Catching generic Exception without re-throwing
163
+ if (/catch\s*\(\s*\\?Exception\s+\$/.test(line)) {
164
+ issues.push({
165
+ type: 'generic-catch',
166
+ file,
167
+ line: lineNum,
168
+ message: 'Catching generic Exception may hide specific errors',
169
+ suggestion: 'Catch specific exception types when possible',
170
+ });
171
+ }
172
+ }
173
+ }
174
+ return {
175
+ stats: { tryCatchBlocks, throwStatements, customExceptions, errorHandlers },
176
+ patterns,
177
+ issues,
178
+ };
179
+ }
180
+ /**
181
+ * Analyze data access patterns
182
+ */
183
+ async analyzeDataAccess() {
184
+ const files = await this.findFiles();
185
+ const accessPoints = [];
186
+ const models = new Set();
187
+ for (const file of files) {
188
+ const source = await fs.promises.readFile(file, 'utf-8');
189
+ const fileAccessPoints = this.extractDataAccess(source, file);
190
+ accessPoints.push(...fileAccessPoints);
191
+ for (const ap of fileAccessPoints) {
192
+ if (ap.model && ap.model !== 'unknown' && ap.model !== 'raw') {
193
+ models.add(ap.model);
194
+ }
195
+ }
196
+ }
197
+ const byFramework = {};
198
+ const byOperation = {};
199
+ for (const ap of accessPoints) {
200
+ byFramework[ap.framework] = (byFramework[ap.framework] || 0) + 1;
201
+ byOperation[ap.operation] = (byOperation[ap.operation] || 0) + 1;
202
+ }
203
+ return { accessPoints, byFramework, byOperation, models: Array.from(models) };
204
+ }
205
+ /**
206
+ * Analyze traits
207
+ */
208
+ async analyzeTraits() {
209
+ const files = await this.findFiles();
210
+ const traits = [];
211
+ const usages = [];
212
+ for (const file of files) {
213
+ const source = await fs.promises.readFile(file, 'utf-8');
214
+ const lines = source.split('\n');
215
+ let currentClass = null;
216
+ for (let i = 0; i < lines.length; i++) {
217
+ const line = lines[i];
218
+ const lineNum = i + 1;
219
+ // Track current class
220
+ const classMatch = line.match(/class\s+(\w+)/);
221
+ if (classMatch) {
222
+ currentClass = classMatch[1];
223
+ }
224
+ // Trait definitions
225
+ const traitMatch = line.match(/trait\s+(\w+)/);
226
+ if (traitMatch) {
227
+ const methods = [];
228
+ // Look for methods in trait
229
+ for (let j = i + 1; j < lines.length; j++) {
230
+ if (/^\s*\}/.test(lines[j]))
231
+ break;
232
+ const methodMatch = lines[j].match(/function\s+(\w+)/);
233
+ if (methodMatch)
234
+ methods.push(methodMatch[1]);
235
+ }
236
+ traits.push({
237
+ name: traitMatch[1],
238
+ file,
239
+ line: lineNum,
240
+ methods,
241
+ });
242
+ }
243
+ // Trait usages
244
+ const useMatch = line.match(/use\s+(\w+(?:\s*,\s*\w+)*)\s*;/);
245
+ if (useMatch && currentClass && !/^use\s+[A-Z].*\\/.test(line)) {
246
+ const traitNames = useMatch[1].split(',').map(t => t.trim());
247
+ for (const traitName of traitNames) {
248
+ usages.push({
249
+ trait: traitName,
250
+ usedIn: currentClass,
251
+ file,
252
+ line: lineNum,
253
+ });
254
+ }
255
+ }
256
+ }
257
+ }
258
+ return { traits, usages };
259
+ }
260
+ // ============================================================================
261
+ // Private Helper Methods
262
+ // ============================================================================
263
+ async findFiles() {
264
+ const results = [];
265
+ const excludePatterns = this.config.excludePatterns ?? ['vendor', 'node_modules', '.git'];
266
+ const walk = async (dir) => {
267
+ let entries;
268
+ try {
269
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
270
+ }
271
+ catch {
272
+ return;
273
+ }
274
+ for (const entry of entries) {
275
+ const fullPath = path.join(dir, entry.name);
276
+ const relativePath = path.relative(this.config.rootDir, fullPath);
277
+ const shouldExclude = excludePatterns.some((pattern) => {
278
+ if (pattern.includes('*')) {
279
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
280
+ return regex.test(relativePath);
281
+ }
282
+ return relativePath.includes(pattern);
283
+ });
284
+ if (shouldExclude)
285
+ continue;
286
+ if (entry.isDirectory()) {
287
+ await walk(fullPath);
288
+ }
289
+ else if (entry.isFile() && entry.name.endsWith('.php')) {
290
+ results.push(fullPath);
291
+ }
292
+ }
293
+ };
294
+ await walk(this.config.rootDir);
295
+ return results;
296
+ }
297
+ async parseProjectInfo() {
298
+ const composerPath = path.join(this.config.rootDir, 'composer.json');
299
+ try {
300
+ const content = await fs.promises.readFile(composerPath, 'utf-8');
301
+ const pkg = JSON.parse(content);
302
+ // Detect framework from dependencies
303
+ let framework = null;
304
+ const deps = { ...pkg.require, ...pkg['require-dev'] };
305
+ if (deps['laravel/framework'])
306
+ framework = 'laravel';
307
+ else if (deps['symfony/framework-bundle'])
308
+ framework = 'symfony';
309
+ else if (deps['slim/slim'])
310
+ framework = 'slim';
311
+ else if (deps['laravel/lumen-framework'])
312
+ framework = 'lumen';
313
+ return {
314
+ name: pkg.name ?? null,
315
+ version: pkg.version ?? null,
316
+ framework,
317
+ };
318
+ }
319
+ catch {
320
+ return { name: null, version: null, framework: null };
321
+ }
322
+ }
323
+ detectFramework(importSource) {
324
+ const frameworks = {
325
+ 'Illuminate': 'laravel',
326
+ 'Laravel': 'laravel',
327
+ 'Symfony': 'symfony',
328
+ 'Slim': 'slim',
329
+ 'Doctrine': 'doctrine',
330
+ 'Eloquent': 'eloquent',
331
+ 'PHPUnit': 'phpunit',
332
+ 'Pest': 'pest',
333
+ };
334
+ for (const [prefix, name] of Object.entries(frameworks)) {
335
+ if (importSource.includes(prefix))
336
+ return name;
337
+ }
338
+ return null;
339
+ }
340
+ isTestFile(file) {
341
+ const testPatterns = [
342
+ /Test\.php$/,
343
+ /Tests\.php$/,
344
+ /\/tests?\//i,
345
+ /Spec\.php$/,
346
+ ];
347
+ return testPatterns.some((p) => p.test(file));
348
+ }
349
+ extractRoutes(source, file) {
350
+ const routes = [];
351
+ const lines = source.split('\n');
352
+ // Laravel patterns: Route::get('/path', [Controller::class, 'method'])
353
+ const laravelPattern = /Route::(get|post|put|delete|patch|options|any)\s*\(\s*['"]([^'"]+)['"]/gi;
354
+ // Symfony patterns: #[Route('/path', methods: ['GET'])]
355
+ const symfonyPattern = /#\[Route\s*\(\s*['"]([^'"]+)['"]/gi;
356
+ // Slim patterns: $app->get('/path', function)
357
+ const slimPattern = /\$app->(get|post|put|delete|patch|options)\s*\(\s*['"]([^'"]+)['"]/gi;
358
+ for (let i = 0; i < lines.length; i++) {
359
+ const line = lines[i];
360
+ const lineNum = i + 1;
361
+ let match;
362
+ // Laravel
363
+ while ((match = laravelPattern.exec(line)) !== null) {
364
+ routes.push({
365
+ method: match[1].toUpperCase(),
366
+ path: match[2],
367
+ handler: this.extractLaravelHandler(line),
368
+ framework: 'laravel',
369
+ file,
370
+ line: lineNum,
371
+ middleware: this.extractLaravelMiddleware(lines, i),
372
+ });
373
+ }
374
+ laravelPattern.lastIndex = 0;
375
+ // Symfony
376
+ while ((match = symfonyPattern.exec(line)) !== null) {
377
+ const methods = this.extractSymfonyMethods(line);
378
+ for (const method of methods) {
379
+ routes.push({
380
+ method,
381
+ path: match[1],
382
+ handler: this.extractNextMethod(lines, i),
383
+ framework: 'symfony',
384
+ file,
385
+ line: lineNum,
386
+ middleware: [],
387
+ });
388
+ }
389
+ }
390
+ symfonyPattern.lastIndex = 0;
391
+ // Slim
392
+ while ((match = slimPattern.exec(line)) !== null) {
393
+ routes.push({
394
+ method: match[1].toUpperCase(),
395
+ path: match[2],
396
+ handler: 'closure',
397
+ framework: 'slim',
398
+ file,
399
+ line: lineNum,
400
+ middleware: [],
401
+ });
402
+ }
403
+ slimPattern.lastIndex = 0;
404
+ }
405
+ return routes;
406
+ }
407
+ extractLaravelHandler(line) {
408
+ // [Controller::class, 'method']
409
+ const match = line.match(/\[\s*(\w+)::class\s*,\s*['"](\w+)['"]\s*\]/);
410
+ if (match)
411
+ return `${match[1]}@${match[2]}`;
412
+ // 'Controller@method'
413
+ const stringMatch = line.match(/['"](\w+@\w+)['"]/);
414
+ if (stringMatch)
415
+ return stringMatch[1];
416
+ return 'closure';
417
+ }
418
+ extractLaravelMiddleware(lines, routeLine) {
419
+ const middleware = [];
420
+ // Look for ->middleware() on same or next line
421
+ for (let i = routeLine; i < Math.min(routeLine + 3, lines.length); i++) {
422
+ const match = lines[i].match(/->middleware\s*\(\s*\[?([^\])]+)\]?\s*\)/);
423
+ if (match) {
424
+ const mws = match[1].split(',').map(m => m.trim().replace(/['"]/g, ''));
425
+ middleware.push(...mws);
426
+ }
427
+ }
428
+ return middleware;
429
+ }
430
+ extractSymfonyMethods(line) {
431
+ const methodsMatch = line.match(/methods:\s*\[([^\]]+)\]/);
432
+ if (methodsMatch) {
433
+ return methodsMatch[1]
434
+ .split(',')
435
+ .map(m => m.trim().replace(/['"]/g, '').toUpperCase())
436
+ .filter(m => m);
437
+ }
438
+ return ['GET'];
439
+ }
440
+ extractNextMethod(lines, annotationLine) {
441
+ for (let i = annotationLine + 1; i < Math.min(annotationLine + 10, lines.length); i++) {
442
+ const match = lines[i].match(/function\s+(\w+)/);
443
+ if (match)
444
+ return match[1];
445
+ }
446
+ return 'unknown';
447
+ }
448
+ extractDataAccess(source, file) {
449
+ const accessPoints = [];
450
+ const lines = source.split('\n');
451
+ // Laravel Eloquent: Model::where(), $model->save()
452
+ const eloquentStaticPattern = /(\w+)::(where|find|findOrFail|all|first|firstOrFail|create|insert|update|delete|destroy)/gi;
453
+ const eloquentInstancePattern = /\$\w+->(save|update|delete|refresh|push)/gi;
454
+ // Laravel Query Builder: DB::table('users')
455
+ const dbPattern = /DB::table\s*\(\s*['"](\w+)['"]\s*\)/gi;
456
+ // Doctrine: $em->getRepository(), $em->persist()
457
+ const doctrinePattern = /\$\w+->(getRepository|persist|remove|flush|find)/gi;
458
+ // PDO: $pdo->query(), $stmt->execute()
459
+ const pdoPattern = /\$(?:pdo|db|conn|stmt)->(query|execute|prepare)/gi;
460
+ for (let i = 0; i < lines.length; i++) {
461
+ const line = lines[i];
462
+ const lineNum = i + 1;
463
+ let match;
464
+ // Eloquent static
465
+ while ((match = eloquentStaticPattern.exec(line)) !== null) {
466
+ accessPoints.push({
467
+ model: match[1],
468
+ operation: this.normalizeOperation(match[2]),
469
+ framework: 'eloquent',
470
+ file,
471
+ line: lineNum,
472
+ isRawSql: false,
473
+ });
474
+ }
475
+ eloquentStaticPattern.lastIndex = 0;
476
+ // Eloquent instance
477
+ while ((match = eloquentInstancePattern.exec(line)) !== null) {
478
+ accessPoints.push({
479
+ model: 'unknown',
480
+ operation: this.normalizeOperation(match[1]),
481
+ framework: 'eloquent',
482
+ file,
483
+ line: lineNum,
484
+ isRawSql: false,
485
+ });
486
+ }
487
+ eloquentInstancePattern.lastIndex = 0;
488
+ // DB facade
489
+ while ((match = dbPattern.exec(line)) !== null) {
490
+ accessPoints.push({
491
+ model: match[1],
492
+ operation: 'query',
493
+ framework: 'eloquent',
494
+ file,
495
+ line: lineNum,
496
+ isRawSql: false,
497
+ });
498
+ }
499
+ dbPattern.lastIndex = 0;
500
+ // Doctrine
501
+ while ((match = doctrinePattern.exec(line)) !== null) {
502
+ accessPoints.push({
503
+ model: 'unknown',
504
+ operation: this.normalizeOperation(match[1]),
505
+ framework: 'doctrine',
506
+ file,
507
+ line: lineNum,
508
+ isRawSql: false,
509
+ });
510
+ }
511
+ doctrinePattern.lastIndex = 0;
512
+ // PDO
513
+ if (pdoPattern.test(line)) {
514
+ accessPoints.push({
515
+ model: 'raw',
516
+ operation: 'query',
517
+ framework: 'pdo',
518
+ file,
519
+ line: lineNum,
520
+ isRawSql: true,
521
+ });
522
+ }
523
+ pdoPattern.lastIndex = 0;
524
+ }
525
+ return accessPoints;
526
+ }
527
+ normalizeOperation(op) {
528
+ const opLower = op.toLowerCase();
529
+ if (['where', 'find', 'findorfail', 'all', 'first', 'firstorfail', 'get', 'getrepository', 'query'].includes(opLower))
530
+ return 'read';
531
+ if (['create', 'insert', 'save', 'persist', 'push'].includes(opLower))
532
+ return 'write';
533
+ if (['update', 'refresh'].includes(opLower))
534
+ return 'update';
535
+ if (['delete', 'destroy', 'remove'].includes(opLower))
536
+ return 'delete';
537
+ return 'unknown';
538
+ }
539
+ }
540
+ /**
541
+ * Factory function
542
+ */
543
+ export function createPhpAnalyzer(config) {
544
+ return new PhpAnalyzer(config);
545
+ }
546
+ //# sourceMappingURL=php-analyzer.js.map