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.
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -1
- package/dist/java/index.d.ts +8 -0
- package/dist/java/index.d.ts.map +1 -0
- package/dist/java/index.js +7 -0
- package/dist/java/index.js.map +1 -0
- package/dist/java/java-analyzer.d.ts +142 -0
- package/dist/java/java-analyzer.d.ts.map +1 -0
- package/dist/java/java-analyzer.js +515 -0
- package/dist/java/java-analyzer.js.map +1 -0
- package/dist/php/index.d.ts +8 -0
- package/dist/php/index.d.ts.map +1 -0
- package/dist/php/index.js +7 -0
- package/dist/php/index.js.map +1 -0
- package/dist/php/php-analyzer.d.ts +149 -0
- package/dist/php/php-analyzer.d.ts.map +1 -0
- package/dist/php/php-analyzer.js +546 -0
- package/dist/php/php-analyzer.js.map +1 -0
- package/dist/python/index.d.ts +8 -0
- package/dist/python/index.d.ts.map +1 -0
- package/dist/python/index.js +7 -0
- package/dist/python/index.js.map +1 -0
- package/dist/python/python-analyzer.d.ts +156 -0
- package/dist/python/python-analyzer.d.ts.map +1 -0
- package/dist/python/python-analyzer.js +535 -0
- package/dist/python/python-analyzer.js.map +1 -0
- package/dist/typescript/index.d.ts +13 -0
- package/dist/typescript/index.d.ts.map +1 -0
- package/dist/typescript/index.js +12 -0
- package/dist/typescript/index.js.map +1 -0
- package/dist/typescript/typescript-analyzer.d.ts +194 -0
- package/dist/typescript/typescript-analyzer.d.ts.map +1 -0
- package/dist/typescript/typescript-analyzer.js +762 -0
- package/dist/typescript/typescript-analyzer.js.map +1 -0
- 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
|