Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/js/devtools/tsgen/parser.go
2070 views
1
package tsgen
2
3
import (
4
"errors"
5
"fmt"
6
"go/ast"
7
"go/parser"
8
"go/token"
9
"regexp"
10
"strings"
11
12
"github.com/projectdiscovery/gologger"
13
sliceutil "github.com/projectdiscovery/utils/slice"
14
"golang.org/x/tools/go/packages"
15
)
16
17
// EntityParser is responsible for parsing a go file and generating
18
// corresponding typescript entities.
19
type EntityParser struct {
20
syntax []*ast.File
21
structTypes map[string]Entity
22
imports map[string]*packages.Package
23
newObjects map[string]*Entity // new objects to create from external packages
24
vars []Entity
25
entities []Entity
26
}
27
28
// NewEntityParser creates a new EntityParser
29
func NewEntityParser(dir string) (*EntityParser, error) {
30
31
cfg := &packages.Config{
32
Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports |
33
packages.NeedTypes | packages.NeedSyntax | packages.NeedTypes |
34
packages.NeedModule | packages.NeedTypesInfo,
35
Tests: false,
36
Dir: dir,
37
ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
38
return parser.ParseFile(fset, filename, src, parser.ParseComments)
39
},
40
}
41
pkgs, err := packages.Load(cfg, ".")
42
if err != nil {
43
return nil, err
44
}
45
if len(pkgs) == 0 {
46
return nil, errors.New("no packages found")
47
}
48
pkg := pkgs[0]
49
50
return &EntityParser{
51
syntax: pkg.Syntax,
52
structTypes: map[string]Entity{},
53
imports: map[string]*packages.Package{},
54
newObjects: map[string]*Entity{},
55
}, nil
56
}
57
58
func (p *EntityParser) GetEntities() []Entity {
59
return p.entities
60
}
61
62
// Parse parses the given file and generates corresponding typescript entities
63
func (p *EntityParser) Parse() error {
64
p.extractVarsNConstants()
65
// extract all struct types from the AST
66
p.extractStructTypes()
67
// load all imported packages
68
if err := p.loadImportedPackages(); err != nil {
69
return err
70
}
71
72
for _, file := range p.syntax {
73
// Traverse the AST and find all relevant declarations
74
ast.Inspect(file, func(n ast.Node) bool {
75
// look for funtions and methods
76
// and generate entities for them
77
fn, ok := n.(*ast.FuncDecl)
78
if ok {
79
if !isExported(fn.Name.Name) {
80
return false
81
}
82
entity, err := p.extractFunctionFromNode(fn)
83
if err != nil {
84
gologger.Error().Msgf("Could not extract function %s: %s\n", fn.Name.Name, err)
85
return false
86
}
87
88
if entity.IsConstructor {
89
// add this to the list of entities
90
p.entities = append(p.entities, entity)
91
return false
92
}
93
94
// check if function has a receiver
95
if fn.Recv != nil {
96
// get the name of the receiver
97
receiverName := exprToString(fn.Recv.List[0].Type)
98
// check if the receiver is a struct
99
if _, ok := p.structTypes[receiverName]; ok {
100
// add the method to the class
101
method := Method{
102
Name: entity.Name,
103
Description: strings.ReplaceAll(entity.Description, "Function", "Method"),
104
Parameters: entity.Function.Parameters,
105
Returns: entity.Function.Returns,
106
CanFail: entity.Function.CanFail,
107
ReturnStmt: entity.Function.ReturnStmt,
108
}
109
110
// add this method to corresponding class
111
allMethods := p.structTypes[receiverName].Class.Methods
112
if allMethods == nil {
113
allMethods = []Method{}
114
}
115
entity = p.structTypes[receiverName]
116
entity.Class.Methods = append(allMethods, method)
117
p.structTypes[receiverName] = entity
118
return false
119
}
120
}
121
// add the function to the list of global entities
122
p.entities = append(p.entities, entity)
123
return false
124
}
125
126
return true
127
})
128
}
129
130
for _, file := range p.syntax {
131
ast.Inspect(file, func(n ast.Node) bool {
132
// logic here to extract all fields and methods from a struct
133
// and add them to the entities slice
134
// TODO: we only support structs and not type aliases
135
typeSpec, ok := n.(*ast.TypeSpec)
136
if ok {
137
if !isExported(typeSpec.Name.Name) {
138
return false
139
}
140
structType, ok := typeSpec.Type.(*ast.StructType)
141
if !ok {
142
// This is not a struct, so continue traversing the AST
143
return false
144
}
145
entity := Entity{
146
Name: typeSpec.Name.Name,
147
Type: "class",
148
Description: Ternary(strings.TrimSpace(typeSpec.Doc.Text()) != "", typeSpec.Doc.Text(), typeSpec.Name.Name+" Class"),
149
Class: Class{
150
Properties: p.extractClassProperties(structType),
151
},
152
}
153
// map struct name to entity and create a new entity if doesn't exist
154
if _, ok := p.structTypes[typeSpec.Name.Name]; ok {
155
entity.Class.Methods = p.structTypes[typeSpec.Name.Name].Class.Methods
156
entity.Description = p.structTypes[typeSpec.Name.Name].Description
157
p.structTypes[typeSpec.Name.Name] = entity
158
} else {
159
p.structTypes[typeSpec.Name.Name] = entity
160
}
161
return false
162
}
163
// Continue traversing the AST
164
return true
165
})
166
}
167
168
// add all struct types to the list of global entities
169
for k, v := range p.structTypes {
170
if v.Type == "class" && len(v.Class.Methods) > 0 {
171
p.entities = append(p.entities, v)
172
} else if v.Type == "class" && len(v.Class.Methods) == 0 {
173
if k == "Object" {
174
continue
175
}
176
entity := Entity{
177
Name: k,
178
Type: "interface",
179
Description: strings.TrimSpace(strings.ReplaceAll(v.Description, "Class", "interface")),
180
Object: Interface{
181
Properties: v.Class.Properties,
182
},
183
}
184
p.entities = append(p.entities, entity)
185
}
186
}
187
188
// handle external structs
189
for k := range p.newObjects {
190
// if k == "Object" {
191
// continue
192
// }
193
if err := p.scrapeAndCreate(k); err != nil {
194
return fmt.Errorf("could not scrape and create new object: %s", err)
195
}
196
}
197
198
interfaceList := map[string]struct{}{}
199
for _, v := range p.entities {
200
if v.Type == "interface" {
201
interfaceList[v.Name] = struct{}{}
202
}
203
}
204
205
// handle method return types
206
for index, v := range p.entities {
207
if len(v.Class.Methods) > 0 {
208
for i, method := range v.Class.Methods {
209
if !strings.Contains(method.Returns, "null") {
210
x := strings.TrimSpace(method.Returns)
211
if _, ok := interfaceList[x]; ok {
212
// non nullable interface return type detected
213
method.Returns = x + " | null"
214
method.ReturnStmt = "return null;"
215
p.entities[index].Class.Methods[i] = method
216
}
217
}
218
}
219
}
220
}
221
222
// handle constructors
223
for _, v := range p.entities {
224
if v.IsConstructor {
225
226
// correlate it with the class
227
foundStruct:
228
for i, class := range p.entities {
229
if class.Type != "class" {
230
continue foundStruct
231
}
232
if strings.Contains(v.Name, class.Name) {
233
// add constructor to the class
234
p.entities[i].Class.Constructor = v.Function
235
break foundStruct
236
}
237
}
238
}
239
}
240
241
filtered := []Entity{}
242
for _, v := range p.entities {
243
if !v.IsConstructor {
244
filtered = append(filtered, v)
245
}
246
}
247
248
// add all vars and constants
249
filtered = append(filtered, p.vars...)
250
251
p.entities = filtered
252
return nil
253
}
254
255
// extractPropertiesFromStruct extracts all properties from the given struct
256
func (p *EntityParser) extractClassProperties(node *ast.StructType) []Property {
257
var properties []Property
258
259
for _, field := range node.Fields.List {
260
// Skip unexported fields
261
if len(field.Names) > 0 && !field.Names[0].IsExported() {
262
continue
263
}
264
265
// Get the type of the field as a string
266
typeString := exprToString(field.Type)
267
268
// If the field is anonymous (embedded), use the type name as the field name
269
if len(field.Names) == 0 {
270
if ident, ok := field.Type.(*ast.Ident); ok {
271
properties = append(properties, Property{
272
Name: ident.Name,
273
Type: typeString,
274
Description: field.Doc.Text(),
275
})
276
}
277
continue
278
}
279
280
// Iterate through all names (for multiple names in a single declaration)
281
for _, fieldName := range field.Names {
282
// Only consider exported fields
283
if fieldName.IsExported() {
284
property := Property{
285
Name: fieldName.Name,
286
Type: typeString,
287
Description: field.Doc.Text(),
288
}
289
if strings.Contains(property.Type, ".") {
290
// external struct found
291
property.Type = p.handleExternalStruct(property.Type)
292
}
293
properties = append(properties, property)
294
}
295
}
296
}
297
return properties
298
}
299
300
var (
301
constructorRe = `(constructor\([^)]*\))`
302
constructorReCompiled = regexp.MustCompile(constructorRe)
303
)
304
305
// extractFunctionFromNode extracts a function from the given AST node
306
func (p *EntityParser) extractFunctionFromNode(fn *ast.FuncDecl) (Entity, error) {
307
entity := Entity{
308
Name: fn.Name.Name,
309
Type: "function",
310
Description: Ternary(strings.TrimSpace(fn.Doc.Text()) != "", fn.Doc.Text(), fn.Name.Name+" Function"),
311
Function: Function{
312
Parameters: p.extractParameters(fn),
313
Returns: p.extractReturnType(fn),
314
CanFail: checkCanFail(fn),
315
},
316
}
317
// check if it is a constructor
318
if strings.Contains(entity.Function.Returns, "Object") && len(entity.Function.Parameters) == 2 {
319
// this is a constructor defined that accepts something as input
320
// get constructor signature from comments
321
constructorSig := constructorReCompiled.FindString(entity.Description)
322
entity.IsConstructor = true
323
entity.Function = updateFuncWithConstructorSig(constructorSig, entity.Function)
324
return entity, nil
325
}
326
327
// fix/adjust return statement
328
if entity.Function.Returns == "void" {
329
entity.Function.ReturnStmt = "return;"
330
} else if strings.Contains(entity.Function.Returns, "null") {
331
entity.Function.ReturnStmt = "return null;"
332
} else if fn.Recv != nil && exprToString(fn.Recv.List[0].Type) == entity.Function.Returns {
333
entity.Function.ReturnStmt = "return this;"
334
} else {
335
entity.Function.ReturnStmt = "return " + TsDefaultValue(entity.Function.Returns) + ";"
336
}
337
return entity, nil
338
}
339
340
// extractReturnType extracts the return type from the given function
341
func (p *EntityParser) extractReturnType(fn *ast.FuncDecl) (out string) {
342
defer func() {
343
if out == "" {
344
out = "void"
345
}
346
if strings.Contains(out, "interface{}") {
347
out = strings.ReplaceAll(out, "interface{}", "any")
348
}
349
}()
350
var returns []string
351
if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 {
352
for _, result := range fn.Type.Results.List {
353
tmp := exprToString(result.Type)
354
if strings.Contains(tmp, ".") && !strings.HasPrefix(tmp, "goja.") {
355
tmp = p.handleExternalStruct(tmp) + " | null" // external object interfaces can always return null
356
}
357
returns = append(returns, tmp)
358
}
359
}
360
if len(returns) == 1 {
361
val := returns[0]
362
val = strings.TrimPrefix(val, "*")
363
if val == "error" {
364
out = "void"
365
} else {
366
out = val
367
}
368
return
369
}
370
if len(returns) > 1 {
371
// in goja we stick 2 only 2 values with one being error
372
for _, val := range returns {
373
val = strings.TrimPrefix(val, "*")
374
if val != "error" {
375
out = val
376
break
377
}
378
}
379
if sliceutil.Contains(returns, "error") {
380
// add | null to the return type
381
out = out + " | null"
382
return
383
}
384
}
385
return "void"
386
}
387
388
// example: Map[string][]string -> Record<string, string[]>
389
func convertMaptoRecord(input string) (out string) {
390
var key, value string
391
input = strings.TrimPrefix(input, "Map[")
392
key = input[:strings.Index(input, "]")]
393
value = input[strings.Index(input, "]")+1:]
394
return "Record<" + toTsTypes(key) + ", " + toTsTypes(value) + ">"
395
}
396
397
// extractParameters extracts all parameters from the given function
398
func (p *EntityParser) extractParameters(fn *ast.FuncDecl) []Parameter {
399
var parameters []Parameter
400
for _, param := range fn.Type.Params.List {
401
// get the parameter name
402
name := param.Names[0].Name
403
// get the parameter type
404
typ := exprToString(param.Type)
405
if strings.Contains(typ, ".") {
406
// replace with any
407
// we do not support or encourage passing external structs as parameters
408
typ = "any"
409
}
410
// add the parameter to the list of parameters
411
parameters = append(parameters, Parameter{
412
Name: name,
413
Type: toTsTypes(typ),
414
})
415
}
416
return parameters
417
}
418
419
// typeName is in format ssh.ClientConfig
420
// it first fetches all fields from the struct and creates a new object
421
// with that name and returns name of that object as type
422
func (p *EntityParser) handleExternalStruct(typeName string) string {
423
baseType := typeName[strings.LastIndex(typeName, ".")+1:]
424
p.newObjects[typeName] = &Entity{
425
Name: baseType,
426
Type: "interface",
427
Description: baseType + " Object",
428
}
429
// @tarunKoyalwar: scrape and create new object
430
// pkg := pkgMap[strings.Split(tmp, ".")[0]]
431
// if pkg == nil {
432
// for k := range pkgMap {
433
// fmt.Println(k)
434
// }
435
// panic("package not found")
436
// }
437
// props, err := extractFieldsFromType(pkg, tmp[strings.LastIndex(tmp, ".")+1:])
438
// if err != nil {
439
// panic(err)
440
// }
441
// // newObject := Entity{
442
// // Name: tmp[strings.LastIndex(tmp, ".")+1:],
443
// // Type: "interface",
444
// // Object: Object{
445
// // Properties: props,
446
// // },
447
// // }
448
// fmt.Println(props)
449
return baseType
450
}
451
452
// extractStructTypes extracts all struct types from the AST
453
func (p *EntityParser) extractStructTypes() {
454
for _, file := range p.syntax {
455
ast.Inspect(file, func(n ast.Node) bool {
456
// Check if the node is a type specification (which includes structs)
457
typeSpec, ok := n.(*ast.TypeSpec)
458
if ok {
459
// Check if the type specification is a struct type
460
_, ok := typeSpec.Type.(*ast.StructType)
461
if ok {
462
// Add the struct name to the list of struct names
463
p.structTypes[typeSpec.Name.Name] = Entity{
464
Name: typeSpec.Name.Name,
465
Description: typeSpec.Doc.Text(),
466
}
467
}
468
}
469
// Continue traversing the AST
470
return true
471
})
472
}
473
474
}
475
476
// extraGlobalConstant and vars
477
func (p *EntityParser) extractVarsNConstants() {
478
p.vars = []Entity{}
479
for _, file := range p.syntax {
480
ast.Inspect(file, func(n ast.Node) bool {
481
// Check if the node is a type specification (which includes structs)
482
gen, ok := n.(*ast.GenDecl)
483
if !ok {
484
return true
485
}
486
for _, v := range gen.Specs {
487
switch spec := v.(type) {
488
case *ast.ValueSpec:
489
if !spec.Names[0].IsExported() {
490
continue
491
}
492
if len(spec.Values) == 0 {
493
continue
494
}
495
// get comments or description
496
p.vars = append(p.vars, Entity{
497
Name: spec.Names[0].Name,
498
Type: "const",
499
Description: strings.TrimSpace(spec.Comment.Text()),
500
Value: spec.Values[0].(*ast.BasicLit).Value,
501
})
502
}
503
}
504
// Continue traversing the AST
505
return true
506
})
507
}
508
}
509
510
// loadImportedPackages loads all imported packages
511
func (p *EntityParser) loadImportedPackages() error {
512
// get all import statements
513
// iterate over all imports
514
for _, file := range p.syntax {
515
for _, imp := range file.Imports {
516
// get the package path
517
path := imp.Path.Value
518
// remove the quotes from the path
519
path = path[1 : len(path)-1]
520
// load the package
521
pkg, err := loadPackage(path)
522
if err != nil {
523
return err
524
}
525
importName := path[strings.LastIndex(path, "/")+1:]
526
if imp.Name != nil {
527
importName = imp.Name.Name
528
} else {
529
if !strings.HasSuffix(imp.Path.Value, pkg.Types.Name()+`"`) {
530
importName = pkg.Types.Name()
531
}
532
}
533
// add the package to the map
534
if _, ok := p.imports[importName]; !ok {
535
p.imports[importName] = pkg
536
}
537
}
538
}
539
return nil
540
}
541
542
// Load the package containing the type definition
543
// TODO: we don't support named imports yet
544
func loadPackage(pkgPath string) (*packages.Package, error) {
545
cfg := &packages.Config{Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo}
546
pkgs, err := packages.Load(cfg, pkgPath)
547
if err != nil {
548
return nil, err
549
}
550
if len(pkgs) == 0 {
551
return nil, errors.New("no packages found")
552
}
553
return pkgs[0], nil
554
}
555
556
// exprToString converts an expression to a string
557
func updateFuncWithConstructorSig(sig string, f Function) Function {
558
sig = strings.TrimSpace(sig)
559
f.Parameters = []Parameter{}
560
f.CanFail = true
561
f.ReturnStmt = ""
562
f.Returns = ""
563
if sig == "" {
564
return f
565
}
566
// example: constructor(public domain: string, public controller?: string)
567
// remove constructor( and )
568
sig = strings.TrimPrefix(sig, "constructor(")
569
sig = strings.TrimSuffix(sig, ")")
570
// split by comma
571
args := strings.Split(sig, ",")
572
for _, arg := range args {
573
arg = strings.TrimSpace(arg)
574
// check if it is optional
575
typeData := strings.Split(arg, ":")
576
if len(typeData) != 2 {
577
panic("invalid constructor signature")
578
}
579
f.Parameters = append(f.Parameters, Parameter{
580
Name: strings.TrimSpace(typeData[0]),
581
Type: strings.TrimSpace(typeData[1]),
582
})
583
}
584
return f
585
}
586
587