@goboost/scanner-typescript 1.2.1
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/package.json +19 -0
- package/src/controller-extractor.ts +364 -0
- package/src/dto-extractor.ts +363 -0
- package/src/guard-extractor.ts +206 -0
- package/src/index.ts +284 -0
- package/src/prisma-schema-extractor.ts +345 -0
- package/src/unsupported-detector.ts +253 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DTO extractor - finds DTO classes referenced by controller methods
|
|
3
|
+
* Extracts field names, TypeScript types, class-validator decorators, and nested DTO relationships
|
|
4
|
+
*/
|
|
5
|
+
import { Project, SourceFile, ClassDeclaration, PropertyDeclaration, Decorator, Node } from 'ts-morph';
|
|
6
|
+
|
|
7
|
+
interface DTODefinition {
|
|
8
|
+
name: string;
|
|
9
|
+
sourceFile: string;
|
|
10
|
+
extends: string | null;
|
|
11
|
+
fields: DTOField[];
|
|
12
|
+
isShared: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DTOField {
|
|
16
|
+
name: string;
|
|
17
|
+
tsType: string;
|
|
18
|
+
isOptional: boolean;
|
|
19
|
+
isArray: boolean;
|
|
20
|
+
nestedDto: string | null;
|
|
21
|
+
validationRules: ValidationRule[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ValidationRule {
|
|
25
|
+
decorator: string;
|
|
26
|
+
args: any[];
|
|
27
|
+
message: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const VALIDATION_DECORATORS = [
|
|
31
|
+
'IsString', 'IsNumber', 'IsEmail', 'IsInt', 'IsBoolean',
|
|
32
|
+
'Min', 'Max', 'MinLength', 'MaxLength', 'IsOptional',
|
|
33
|
+
'IsDate', 'IsArray', 'IsEnum', 'IsUUID', 'IsUrl',
|
|
34
|
+
'Matches', 'IsNotEmpty', 'IsDefined', 'IsPositive',
|
|
35
|
+
'IsNegative', 'IsDateString', 'IsISO8601', 'IsJSON',
|
|
36
|
+
'IsObject', 'IsNotEmptyObject', 'ValidateNested',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Map class-validator decorators to Go validate tags
|
|
40
|
+
const VALIDATOR_TO_GO_TAG: Record<string, string> = {
|
|
41
|
+
'IsString': '',
|
|
42
|
+
'IsNumber': '',
|
|
43
|
+
'IsEmail': 'email',
|
|
44
|
+
'IsInt': '',
|
|
45
|
+
'IsBoolean': '',
|
|
46
|
+
'Min': 'min',
|
|
47
|
+
'Max': 'max',
|
|
48
|
+
'MinLength': 'min',
|
|
49
|
+
'MaxLength': 'max',
|
|
50
|
+
'IsOptional': 'omitempty',
|
|
51
|
+
'IsDate': '',
|
|
52
|
+
'IsArray': '',
|
|
53
|
+
'IsEnum': 'oneof',
|
|
54
|
+
'IsUUID': 'uuid',
|
|
55
|
+
'IsUrl': 'url',
|
|
56
|
+
'Matches': 'regexp',
|
|
57
|
+
'IsNotEmpty': 'required',
|
|
58
|
+
'IsDefined': 'required',
|
|
59
|
+
'IsPositive': 'gt=0',
|
|
60
|
+
'IsDateString': '',
|
|
61
|
+
'IsJSON': '',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function extractDTOs(project: Project, referencedDTOs: Set<string>): Record<string, DTODefinition> {
|
|
65
|
+
const dtos: Record<string, DTODefinition> = {};
|
|
66
|
+
const processedDTOs = new Set<string>();
|
|
67
|
+
|
|
68
|
+
// First pass: collect all DTO classes
|
|
69
|
+
const allDTOClasses: { classDecl: ClassDeclaration; sourceFile: SourceFile }[] = [];
|
|
70
|
+
|
|
71
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
72
|
+
for (const classDecl of sourceFile.getClasses()) {
|
|
73
|
+
const name = classDecl.getName();
|
|
74
|
+
if (name && (isDTO(classDecl) || referencedDTOs.has(name))) {
|
|
75
|
+
allDTOClasses.push({ classDecl, sourceFile });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Second pass: extract DTO definitions
|
|
81
|
+
for (const { classDecl, sourceFile } of allDTOClasses) {
|
|
82
|
+
const name = classDecl.getName();
|
|
83
|
+
if (name && !processedDTOs.has(name)) {
|
|
84
|
+
const dto = extractDTO(classDecl, sourceFile, referencedDTOs);
|
|
85
|
+
dtos[name] = dto;
|
|
86
|
+
processedDTOs.add(name);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Mark shared DTOs (referenced by multiple controllers)
|
|
91
|
+
// This will be done by the caller who has knowledge of controller references
|
|
92
|
+
|
|
93
|
+
return dtos;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isDTO(classDecl: ClassDeclaration): boolean {
|
|
97
|
+
// Check if class has validation decorators on properties
|
|
98
|
+
for (const prop of classDecl.getProperties()) {
|
|
99
|
+
const decorators = prop.getDecorators();
|
|
100
|
+
if (decorators.some(d => VALIDATION_DECORATORS.includes(d.getName()))) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Also check if class name ends with 'Dto' or 'DTO'
|
|
106
|
+
const name = classDecl.getName() || '';
|
|
107
|
+
return name.endsWith('Dto') || name.endsWith('DTO');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractDTO(
|
|
111
|
+
classDecl: ClassDeclaration,
|
|
112
|
+
sourceFile: SourceFile,
|
|
113
|
+
referencedDTOs: Set<string>
|
|
114
|
+
): DTODefinition {
|
|
115
|
+
const name = classDecl.getName() || 'AnonymousDTO';
|
|
116
|
+
const fields = extractFields(classDecl, referencedDTOs);
|
|
117
|
+
const extendsClass = extractExtends(classDecl);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
name,
|
|
121
|
+
sourceFile: getRelativePath(sourceFile.getFilePath()),
|
|
122
|
+
extends: extendsClass,
|
|
123
|
+
fields,
|
|
124
|
+
isShared: false, // Will be determined by caller
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function extractExtends(classDecl: ClassDeclaration): string | null {
|
|
129
|
+
const ext = classDecl.getExtends();
|
|
130
|
+
if (ext) {
|
|
131
|
+
const text = ext.getText();
|
|
132
|
+
// Extract just the class name, not the full path
|
|
133
|
+
return text.split('.').pop() || text;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractFields(classDecl: ClassDeclaration, referencedDTOs: Set<string>): DTOField[] {
|
|
139
|
+
const fields: DTOField[] = [];
|
|
140
|
+
|
|
141
|
+
for (const prop of classDecl.getProperties()) {
|
|
142
|
+
const typeText = prop.getType().getText();
|
|
143
|
+
const isArray = typeText.includes('[]') || typeText.includes('Array<');
|
|
144
|
+
const baseType = extractBaseType(typeText);
|
|
145
|
+
|
|
146
|
+
// Check for unmappable custom types
|
|
147
|
+
let unmappableType: string | null = null;
|
|
148
|
+
if (!isPrimitiveType(baseType) && !isBuiltInType(baseType) && !referencedDTOs.has(baseType)) {
|
|
149
|
+
// Check if it's a known framework type
|
|
150
|
+
if (!isKnownType(baseType)) {
|
|
151
|
+
unmappableType = baseType;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const field: DTOField = {
|
|
156
|
+
name: prop.getName(),
|
|
157
|
+
tsType: typeText,
|
|
158
|
+
isOptional: prop.hasQuestionToken() || hasOptionalDecorator(prop),
|
|
159
|
+
isArray,
|
|
160
|
+
nestedDto: null,
|
|
161
|
+
validationRules: extractValidation(prop),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Check for nested DTO
|
|
165
|
+
if (!isPrimitiveType(baseType) && referencedDTOs.has(baseType)) {
|
|
166
|
+
field.nestedDto = baseType;
|
|
167
|
+
} else if (!isPrimitiveType(baseType) && !isBuiltInType(baseType)) {
|
|
168
|
+
// Potentially a nested DTO we haven't seen yet
|
|
169
|
+
field.nestedDto = baseType;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Add validation rule for unmappable types (will be flagged during generation)
|
|
173
|
+
if (unmappableType) {
|
|
174
|
+
field.validationRules.push({
|
|
175
|
+
decorator: 'UnmappableType',
|
|
176
|
+
args: [unmappableType],
|
|
177
|
+
message: `Custom TypeScript type '${unmappableType}' cannot be automatically mapped to Go`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fields.push(field);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return fields;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check if type is a known NestJS/framework type that can be mapped
|
|
188
|
+
function isKnownType(type: string): boolean {
|
|
189
|
+
const knownTypes = [
|
|
190
|
+
// NestJS common types
|
|
191
|
+
'Request', 'Response', 'Next', 'ExecutionContext',
|
|
192
|
+
// Common third-party types
|
|
193
|
+
'ObjectId', 'Buffer',
|
|
194
|
+
];
|
|
195
|
+
return knownTypes.includes(type);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function extractValidation(prop: PropertyDeclaration): ValidationRule[] {
|
|
199
|
+
const rules: ValidationRule[] = [];
|
|
200
|
+
const isOptional = prop.hasQuestionToken();
|
|
201
|
+
|
|
202
|
+
for (const decorator of prop.getDecorators()) {
|
|
203
|
+
const name = decorator.getName();
|
|
204
|
+
if (VALIDATION_DECORATORS.includes(name)) {
|
|
205
|
+
const rule: ValidationRule = {
|
|
206
|
+
decorator: name,
|
|
207
|
+
args: extractDecoratorArgs(decorator),
|
|
208
|
+
message: extractValidationMessage(decorator),
|
|
209
|
+
};
|
|
210
|
+
rules.push(rule);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Add required validation if not optional and no IsOptional decorator
|
|
215
|
+
if (!isOptional && !rules.some(r => r.decorator === 'IsOptional')) {
|
|
216
|
+
// Don't add required - Go validator uses required by default
|
|
217
|
+
// unless omitempty is present
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return rules;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function extractDecoratorArgs(decorator: Decorator): any[] {
|
|
224
|
+
const args: any[] = [];
|
|
225
|
+
const decoratorArgs = decorator.getArguments();
|
|
226
|
+
|
|
227
|
+
for (const arg of decoratorArgs) {
|
|
228
|
+
const argText = arg.getText();
|
|
229
|
+
|
|
230
|
+
// Try to parse as number
|
|
231
|
+
const num = parseFloat(argText);
|
|
232
|
+
if (!isNaN(num)) {
|
|
233
|
+
args.push(num);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Try to parse as string literal
|
|
238
|
+
if (Node.isStringLiteral(arg)) {
|
|
239
|
+
args.push(argText.replace(/['"]/g, ''));
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Try to parse as object (simplified)
|
|
244
|
+
if (argText.startsWith('{')) {
|
|
245
|
+
try {
|
|
246
|
+
args.push(JSON.parse(argText));
|
|
247
|
+
} catch {
|
|
248
|
+
args.push(argText);
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Default: keep as string
|
|
254
|
+
args.push(argText);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return args;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function extractValidationMessage(decorator: Decorator): string | null {
|
|
261
|
+
const args = decorator.getArguments();
|
|
262
|
+
if (args.length > 0) {
|
|
263
|
+
const lastArg = args[args.length - 1];
|
|
264
|
+
const argText = lastArg.getText();
|
|
265
|
+
// Check if it's an options object with message
|
|
266
|
+
const messageMatch = argText.match(/message:\s*['"]([^'"]+)['"]/);
|
|
267
|
+
if (messageMatch) {
|
|
268
|
+
return messageMatch[1];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function hasOptionalDecorator(prop: PropertyDeclaration): boolean {
|
|
275
|
+
return prop.getDecorators().some(d => d.getName() === 'IsOptional');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function extractBaseType(typeText: string): string {
|
|
279
|
+
// Remove array notation
|
|
280
|
+
let base = typeText.replace(/\[\]$/, '').replace(/Array</, '').replace(/>$/, '');
|
|
281
|
+
|
|
282
|
+
// Remove generics
|
|
283
|
+
const genericMatch = base.match(/^(\w+)</);
|
|
284
|
+
if (genericMatch) {
|
|
285
|
+
base = genericMatch[1];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Remove import paths
|
|
289
|
+
base = base.split('.').pop() || base;
|
|
290
|
+
|
|
291
|
+
return base.trim();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isPrimitiveType(type: string): boolean {
|
|
295
|
+
const primitives = [
|
|
296
|
+
'string', 'number', 'boolean', 'Date', 'any', 'unknown',
|
|
297
|
+
'null', 'undefined', 'void', 'object', 'Buffer'
|
|
298
|
+
];
|
|
299
|
+
return primitives.includes(type);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isBuiltInType(type: string): boolean {
|
|
303
|
+
const builtIns = [
|
|
304
|
+
'Promise', 'Observable', 'Array', 'Map', 'Set', 'Record',
|
|
305
|
+
'Partial', 'Required', 'Readonly', 'Pick', 'Omit', 'Exclude', 'Extract'
|
|
306
|
+
];
|
|
307
|
+
return builtIns.includes(type);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getRelativePath(filePath: string): string {
|
|
311
|
+
const parts = filePath.split(/[\\/]/);
|
|
312
|
+
const srcIndex = parts.indexOf('src');
|
|
313
|
+
if (srcIndex !== -1) {
|
|
314
|
+
return parts.slice(srcIndex).join('/');
|
|
315
|
+
}
|
|
316
|
+
return filePath;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Helper to collect all referenced DTOs from controllers
|
|
320
|
+
export function collectReferencedDTOs(controllers: any[]): Set<string> {
|
|
321
|
+
const referenced = new Set<string>();
|
|
322
|
+
|
|
323
|
+
for (const controller of controllers) {
|
|
324
|
+
for (const route of controller.routes) {
|
|
325
|
+
if (route.requestBody?.$ref) {
|
|
326
|
+
referenced.add(route.requestBody.$ref);
|
|
327
|
+
}
|
|
328
|
+
if (route.responseBody?.$ref) {
|
|
329
|
+
referenced.add(route.responseBody.$ref);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return referenced;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Mark DTOs that are shared across multiple controllers
|
|
338
|
+
export function markSharedDTOs(dtos: Record<string, DTODefinition>, controllers: any[]): void {
|
|
339
|
+
const referenceCounts: Record<string, number> = {};
|
|
340
|
+
|
|
341
|
+
for (const controller of controllers) {
|
|
342
|
+
const controllerDTOs = new Set<string>();
|
|
343
|
+
|
|
344
|
+
for (const route of controller.routes) {
|
|
345
|
+
if (route.requestBody?.$ref) {
|
|
346
|
+
controllerDTOs.add(route.requestBody.$ref);
|
|
347
|
+
}
|
|
348
|
+
if (route.responseBody?.$ref) {
|
|
349
|
+
controllerDTOs.add(route.responseBody.$ref);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const dtoName of controllerDTOs) {
|
|
354
|
+
referenceCounts[dtoName] = (referenceCounts[dtoName] || 0) + 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const [name, count] of Object.entries(referenceCounts)) {
|
|
359
|
+
if (count > 1 && dtos[name]) {
|
|
360
|
+
dtos[name].isShared = true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard extractor - finds @UseGuards references and classifies them
|
|
3
|
+
* Resolves guard class definitions, classifies as jwt/basic/custom
|
|
4
|
+
*/
|
|
5
|
+
import { Project, SourceFile, ClassDeclaration, Decorator, MethodDeclaration } from 'ts-morph';
|
|
6
|
+
|
|
7
|
+
interface GuardSpec {
|
|
8
|
+
name: string;
|
|
9
|
+
sourceFile: string;
|
|
10
|
+
type: 'jwt' | 'basic' | 'custom';
|
|
11
|
+
canGenerate: boolean;
|
|
12
|
+
failureReason: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function extractGuards(project: Project): Record<string, GuardSpec> {
|
|
16
|
+
const guards: Record<string, GuardSpec> = {};
|
|
17
|
+
|
|
18
|
+
// First pass: find all guard class definitions
|
|
19
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
20
|
+
for (const classDecl of sourceFile.getClasses()) {
|
|
21
|
+
if (isGuardClass(classDecl)) {
|
|
22
|
+
const guard = extractGuard(classDecl, sourceFile);
|
|
23
|
+
if (guard && !guards[guard.name]) {
|
|
24
|
+
guards[guard.name] = guard;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Second pass: find @UseGuards usages and ensure all guards are tracked
|
|
31
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
32
|
+
for (const classDecl of sourceFile.getClasses()) {
|
|
33
|
+
collectGuardsFromClass(classDecl, guards, sourceFile);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return guards;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isGuardClass(classDecl: ClassDeclaration): boolean {
|
|
41
|
+
// Check if class implements CanActivate
|
|
42
|
+
const implementsList = classDecl.getImplements();
|
|
43
|
+
for (const impl of implementsList) {
|
|
44
|
+
const text = impl.getText();
|
|
45
|
+
if (text.includes('CanActivate')) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check extends AuthGuard
|
|
51
|
+
const ext = classDecl.getExtends();
|
|
52
|
+
if (ext) {
|
|
53
|
+
const extText = ext.getText();
|
|
54
|
+
if (extText.includes('AuthGuard')) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check for @Injectable decorator with guard-like patterns
|
|
60
|
+
const hasInjectable = classDecl.getDecorators().some(d => d.getName() === 'Injectable');
|
|
61
|
+
const className = classDecl.getName() || '';
|
|
62
|
+
const isGuardByName = className.includes('Guard') || className.includes('AuthGuard');
|
|
63
|
+
|
|
64
|
+
return hasInjectable && isGuardByName;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractGuard(classDecl: ClassDeclaration, sourceFile: SourceFile): GuardSpec | null {
|
|
68
|
+
const name = classDecl.getName();
|
|
69
|
+
if (!name) return null;
|
|
70
|
+
|
|
71
|
+
const guardType = classifyGuard(classDecl);
|
|
72
|
+
const canGenerate = guardType !== 'custom';
|
|
73
|
+
const failureReason = canGenerate ? null : 'Custom guard logic cannot be automatically translated';
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
name,
|
|
77
|
+
sourceFile: getRelativePath(sourceFile.getFilePath()),
|
|
78
|
+
type: guardType,
|
|
79
|
+
canGenerate,
|
|
80
|
+
failureReason,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function classifyGuard(classDecl: ClassDeclaration): 'jwt' | 'basic' | 'custom' {
|
|
85
|
+
const name = classDecl.getName() || '';
|
|
86
|
+
const ext = classDecl.getExtends();
|
|
87
|
+
|
|
88
|
+
// Check by name patterns
|
|
89
|
+
const nameLower = name.toLowerCase();
|
|
90
|
+
if (nameLower.includes('jwt') || nameLower.includes('jwtauth')) {
|
|
91
|
+
return 'jwt';
|
|
92
|
+
}
|
|
93
|
+
if (nameLower.includes('basic') || nameLower.includes('basicauth')) {
|
|
94
|
+
return 'basic';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check by extends
|
|
98
|
+
if (ext) {
|
|
99
|
+
const extText = ext.getText();
|
|
100
|
+
if (extText.includes('JwtAuthGuard') || extText.includes("AuthGuard('jwt')")) {
|
|
101
|
+
return 'jwt';
|
|
102
|
+
}
|
|
103
|
+
if (extText.includes("AuthGuard('basic')") || extText.includes('BasicAuthGuard')) {
|
|
104
|
+
return 'basic';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check source file content for patterns
|
|
109
|
+
const sourceFile = classDecl.getSourceFile();
|
|
110
|
+
const text = sourceFile.getFullText();
|
|
111
|
+
|
|
112
|
+
// JWT patterns
|
|
113
|
+
if (
|
|
114
|
+
text.includes('JwtService') ||
|
|
115
|
+
text.includes('jwt.verify') ||
|
|
116
|
+
text.includes('extractToken') ||
|
|
117
|
+
text.includes('JwtPayload') ||
|
|
118
|
+
text.includes('fromAuthHeaderAsBearerToken')
|
|
119
|
+
) {
|
|
120
|
+
return 'jwt';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Basic auth patterns
|
|
124
|
+
if (
|
|
125
|
+
text.includes('BasicAuth') ||
|
|
126
|
+
text.includes('btoa') ||
|
|
127
|
+
text.includes('atob') ||
|
|
128
|
+
text.includes('fromAuthHeaderAsBasicAuth')
|
|
129
|
+
) {
|
|
130
|
+
return 'basic';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return 'custom';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function collectGuardsFromClass(
|
|
137
|
+
classDecl: ClassDeclaration,
|
|
138
|
+
guards: Record<string, GuardSpec>,
|
|
139
|
+
sourceFile: SourceFile
|
|
140
|
+
): void {
|
|
141
|
+
// Check class-level @UseGuards
|
|
142
|
+
const classGuards = classDecl.getDecorators().find(d => d.getName() === 'UseGuards');
|
|
143
|
+
if (classGuards) {
|
|
144
|
+
parseGuardsFromDecorator(classGuards, guards, sourceFile);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check method-level @UseGuards
|
|
148
|
+
for (const method of classDecl.getMethods()) {
|
|
149
|
+
const methodGuards = method.getDecorators().find(d => d.getName() === 'UseGuards');
|
|
150
|
+
if (methodGuards) {
|
|
151
|
+
parseGuardsFromDecorator(methodGuards, guards, sourceFile);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseGuardsFromDecorator(
|
|
157
|
+
decorator: Decorator,
|
|
158
|
+
guards: Record<string, GuardSpec>,
|
|
159
|
+
sourceFile: SourceFile
|
|
160
|
+
): void {
|
|
161
|
+
const args = decorator.getArguments();
|
|
162
|
+
for (const arg of args) {
|
|
163
|
+
let guardName = arg.getText().replace('()', '').trim();
|
|
164
|
+
|
|
165
|
+
// Handle new expressions or complex references
|
|
166
|
+
if (guardName.startsWith('new ')) {
|
|
167
|
+
guardName = guardName.replace('new ', '').replace(/\(.*\)/, '');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!guards[guardName]) {
|
|
171
|
+
// Try to classify the guard based on name
|
|
172
|
+
const guardType = classifyGuardByName(guardName);
|
|
173
|
+
const canGenerate = guardType !== 'custom';
|
|
174
|
+
|
|
175
|
+
guards[guardName] = {
|
|
176
|
+
name: guardName,
|
|
177
|
+
sourceFile: getRelativePath(sourceFile.getFilePath()),
|
|
178
|
+
type: guardType,
|
|
179
|
+
canGenerate,
|
|
180
|
+
failureReason: canGenerate ? null : 'Custom guard logic cannot be automatically translated',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function classifyGuardByName(name: string): 'jwt' | 'basic' | 'custom' {
|
|
187
|
+
const nameLower = name.toLowerCase();
|
|
188
|
+
|
|
189
|
+
if (nameLower.includes('jwt') || nameLower.includes('bearer')) {
|
|
190
|
+
return 'jwt';
|
|
191
|
+
}
|
|
192
|
+
if (nameLower.includes('basic')) {
|
|
193
|
+
return 'basic';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return 'custom';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getRelativePath(filePath: string): string {
|
|
200
|
+
const parts = filePath.split(/[\\/]/);
|
|
201
|
+
const srcIndex = parts.indexOf('src');
|
|
202
|
+
if (srcIndex !== -1) {
|
|
203
|
+
return parts.slice(srcIndex).join('/');
|
|
204
|
+
}
|
|
205
|
+
return filePath;
|
|
206
|
+
}
|