@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.
@@ -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
+ }