Path: blob/dev/pkg/js/devtools/bindgen/generator.go
2070 views
package generator12import (3"fmt"4"go/ast"5"go/importer"6"go/parser"7"go/token"8"go/types"9"log"10"os"11"strings"1213_ "embed"1415"github.com/pkg/errors"16"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"17)1819var (20//go:embed templates/js_class.tmpl21jsClassFile string22//go:embed templates/go_class.tmpl23goClassFile string24//go:embed templates/markdown_class.tmpl25markdownClassFile string26)2728// TemplateData contains the parameters for the JS code generator29type TemplateData struct {30PackageName string31PackagePath string32HasObjects bool33PackageFuncs map[string]string34PackageInterfaces map[string]string35PackageFuncsExtraNoType map[string]PackageFunctionExtra36PackageFuncsExtra map[string]PackageFuncExtra37PackageVars map[string]string38PackageVarsValues map[string]string39PackageTypes map[string]string40PackageTypesExtra map[string]PackageTypeExtra41PackageDefinedConstructor map[string]struct{}4243typesPackage *types.Package4445// NativeScripts contains the list of native scripts46// that should be included in the package.47NativeScripts []string48}4950// PackageTypeExtra contains extra information about a type51type PackageTypeExtra struct {52Fields map[string]string53}5455// PackageFuncExtra contains extra information about a function56type PackageFuncExtra struct {57Items map[string]PackageFunctionExtra58Doc string59}6061// PackageFunctionExtra contains extra information about a function62type PackageFunctionExtra struct {63Args []string64Name string65Returns []string66Doc string67}6869// newTemplateData creates a new template data structure70func newTemplateData(packagePrefix, pkgName string) *TemplateData {71return &TemplateData{72PackageName: pkgName,73PackagePath: packagePrefix + pkgName,74PackageFuncs: make(map[string]string),75PackageFuncsExtraNoType: make(map[string]PackageFunctionExtra),76PackageFuncsExtra: make(map[string]PackageFuncExtra),77PackageVars: make(map[string]string),78PackageVarsValues: make(map[string]string),79PackageTypes: make(map[string]string),80PackageInterfaces: make(map[string]string),81PackageTypesExtra: make(map[string]PackageTypeExtra),82PackageDefinedConstructor: make(map[string]struct{}),83}84}8586// GetLibraryModules takes a directory and returns subdirectories as modules87func GetLibraryModules(directory string) ([]string, error) {88dirs, err := os.ReadDir(directory)89if err != nil {90return nil, errors.Wrap(err, "could not read directory")91}92var modules []string93for _, dir := range dirs {94if dir.IsDir() {95modules = append(modules, dir.Name())96}97}98return modules, nil99}100101// CreateTemplateData creates a TemplateData structure from a directory102// of go source code.103func CreateTemplateData(directory string, packagePrefix string) (*TemplateData, error) {104fmt.Println(directory)105fset := token.NewFileSet()106107pkgs, err := parser.ParseDir(fset, directory, nil, parser.ParseComments)108if err != nil {109return nil, errors.Wrap(err, "could not parse directory")110}111if len(pkgs) != 1 {112return nil, fmt.Errorf("expected 1 package, got %d", len(pkgs))113}114115config := &types.Config{116Importer: importer.ForCompiler(fset, "source", nil),117}118var packageName string119var files []*ast.File120for k, v := range pkgs {121packageName = k122for _, f := range v.Files {123files = append(files, f)124}125break126}127128pkg, err := config.Check(packageName, fset, files, nil)129if err != nil {130return nil, errors.Wrap(err, "could not check package")131}132133if len(pkgs) == 0 {134return nil, errors.New("no packages found")135}136137var pkgName string138for k := range pkgs {139pkgName = k140break141}142143pkgMain := pkgs[pkgName]144145log.Printf("[create] [discover] Package: %s\n", pkgMain.Name)146data := newTemplateData(packagePrefix, pkgMain.Name)147data.typesPackage = pkg148data.gatherPackageData(pkgMain, data)149150for item, v := range data.PackageFuncsExtra {151if len(v.Items) == 0 {152delete(data.PackageFuncsExtra, item)153}154}155156// map types with corresponding constructors157for constructor := range data.PackageDefinedConstructor {158object:159for k := range data.PackageTypes {160if strings.Contains(constructor, k) {161data.PackageTypes[k] = constructor162break object163}164}165}166for k, v := range data.PackageTypes {167if k == v || v == "" {168data.HasObjects = true169data.PackageTypes[k] = ""170}171}172173return data, nil174}175176// InitNativeScripts initializes the native scripts array177// with all the exported functions from the runtime178func (d *TemplateData) InitNativeScripts() {179runtime := compiler.InternalGetGeneratorRuntime()180181exports := runtime.Get("exports")182if exports == nil {183return184}185exportsObj := exports.Export()186if exportsObj == nil {187return188}189for v := range exportsObj.(map[string]interface{}) {190d.NativeScripts = append(d.NativeScripts, v)191}192}193194// gatherPackageData gathers data about the package195func (d *TemplateData) gatherPackageData(astNode ast.Node, data *TemplateData) {196ast.Inspect(astNode, func(node ast.Node) bool {197switch node := node.(type) {198case *ast.FuncDecl:199extra := d.collectFuncDecl(node)200if extra.Name == "" {201return true202}203data.PackageFuncsExtraNoType[node.Name.Name] = extra204data.PackageFuncs[node.Name.Name] = node.Name.Name205case *ast.TypeSpec:206if !node.Name.IsExported() {207return true208}209if node.Type == nil {210return true211}212structDecl, ok := node.Type.(*ast.StructType)213if !ok {214return true215}216217packageTypes := PackageTypeExtra{218Fields: make(map[string]string),219}220for _, field := range structDecl.Fields.List {221fieldName := field.Names[0].Name222223var fieldTypeValue string224switch fieldType := field.Type.(type) {225case *ast.Ident: // Field type is a simple identifier226fieldTypeValue = fieldType.Name227case *ast.ArrayType:228switch fieldType.Elt.(type) {229case *ast.Ident:230fieldTypeValue = fmt.Sprintf("[]%s", fieldType.Elt.(*ast.Ident).Name)231case *ast.StarExpr:232fieldTypeValue = fmt.Sprintf("[]%s", d.handleStarExpr(fieldType.Elt.(*ast.StarExpr)))233}234case *ast.SelectorExpr: // Field type is a qualified identifier235fieldTypeValue = fmt.Sprintf("%s.%s", fieldType.X, fieldType.Sel)236}237packageTypes.Fields[fieldName] = fieldTypeValue238}239if len(packageTypes.Fields) == 0 {240return true241}242data.PackageTypesExtra[node.Name.Name] = packageTypes243case *ast.GenDecl:244identifyGenDecl(astNode, node, data)245}246return true247})248}249250func identifyGenDecl(node ast.Node, decl *ast.GenDecl, data *TemplateData) {251for _, spec := range decl.Specs {252switch spec := spec.(type) {253case *ast.ValueSpec:254if !spec.Names[0].IsExported() {255continue256}257if len(spec.Values) == 0 {258continue259}260data.PackageVars[spec.Names[0].Name] = spec.Names[0].Name261data.PackageVarsValues[spec.Names[0].Name] = spec.Values[0].(*ast.BasicLit).Value262case *ast.TypeSpec:263if !spec.Name.IsExported() {264continue265}266if spec.Type == nil {267continue268}269270switch spec.Type.(type) {271case *ast.InterfaceType:272data.PackageInterfaces[spec.Name.Name] = convertCommentsToJavascript(decl.Doc.Text())273274case *ast.StructType:275data.PackageFuncsExtra[spec.Name.Name] = PackageFuncExtra{276Items: make(map[string]PackageFunctionExtra),277Doc: convertCommentsToJavascript(decl.Doc.Text()),278}279280// Traverse the AST.281collectStructFuncsFromAST(node, spec, data)282data.PackageTypes[spec.Name.Name] = spec.Name.Name283}284}285}286}287288func collectStructFuncsFromAST(node ast.Node, spec *ast.TypeSpec, data *TemplateData) {289ast.Inspect(node, func(n ast.Node) bool {290if fn, isFunc := n.(*ast.FuncDecl); isFunc && fn.Name.IsExported() {291processFunc(fn, spec, data)292}293return true294})295}296297func processFunc(fn *ast.FuncDecl, spec *ast.TypeSpec, data *TemplateData) {298if fn.Recv == nil || len(fn.Recv.List) == 0 {299return300}301302if t, ok := fn.Recv.List[0].Type.(*ast.StarExpr); ok {303if ident, ok := t.X.(*ast.Ident); ok && spec.Name.Name == ident.Name {304processFunctionDetails(fn, ident, data)305}306}307}308309func processFunctionDetails(fn *ast.FuncDecl, ident *ast.Ident, data *TemplateData) {310extra := PackageFunctionExtra{311Name: fn.Name.Name,312Args: extractArgs(fn),313Doc: convertCommentsToJavascript(fn.Doc.Text()),314Returns: data.extractReturns(fn),315}316data.PackageFuncsExtra[ident.Name].Items[fn.Name.Name] = extra317}318319func extractArgs(fn *ast.FuncDecl) []string {320args := make([]string, 0)321for _, arg := range fn.Type.Params.List {322for _, name := range arg.Names {323args = append(args, name.Name)324}325}326return args327}328329func (d *TemplateData) extractReturns(fn *ast.FuncDecl) []string {330returns := make([]string, 0)331if fn.Type.Results == nil {332return returns333}334for _, ret := range fn.Type.Results.List {335returnType := d.extractReturnType(ret)336if returnType != "" {337returns = append(returns, returnType)338}339}340return returns341}342343func (d *TemplateData) extractReturnType(ret *ast.Field) string {344switch v := ret.Type.(type) {345case *ast.ArrayType:346if v, ok := v.Elt.(*ast.Ident); ok {347return fmt.Sprintf("[]%s", v.Name)348}349if v, ok := v.Elt.(*ast.StarExpr); ok {350return fmt.Sprintf("[]%s", d.handleStarExpr(v))351}352case *ast.Ident:353return v.Name354case *ast.StarExpr:355return d.handleStarExpr(v)356}357return ""358}359360func (d *TemplateData) handleStarExpr(v *ast.StarExpr) string {361switch vk := v.X.(type) {362case *ast.Ident:363return vk.Name364case *ast.SelectorExpr:365if vk.X != nil {366d.collectTypeFromExternal(d.typesPackage, vk.X.(*ast.Ident).Name, vk.Sel.Name)367}368return vk.Sel.Name369}370return ""371}372373func (d *TemplateData) collectTypeFromExternal(pkg *types.Package, pkgName, name string) {374if pkgName == "goja" {375// no need to attempt to collect types from goja ( this is metadata )376return377}378extra := PackageTypeExtra{379Fields: make(map[string]string),380}381382for _, importValue := range pkg.Imports() {383if importValue.Name() != pkgName {384continue385}386obj := importValue.Scope().Lookup(name)387if obj == nil || !obj.Exported() {388continue389}390typeName, ok := obj.(*types.TypeName)391if !ok {392continue393}394underlying, ok := typeName.Type().Underlying().(*types.Struct)395if !ok {396continue397}398for i := 0; i < underlying.NumFields(); i++ {399field := underlying.Field(i)400fieldType := field.Type().String()401402if val, ok := field.Type().Underlying().(*types.Pointer); ok {403fieldType = field.Name()404d.collectTypeFromExternal(pkg, pkgName, val.Elem().(*types.Named).Obj().Name())405}406if _, ok := field.Type().Underlying().(*types.Struct); ok {407fieldType = field.Name()408d.collectTypeFromExternal(pkg, pkgName, field.Name())409}410extra.Fields[field.Name()] = fieldType411}412if len(extra.Fields) > 0 {413d.PackageTypesExtra[name] = extra414}415}416}417418func (d *TemplateData) collectFuncDecl(decl *ast.FuncDecl) (extra PackageFunctionExtra) {419if decl.Recv != nil {420return421}422if !decl.Name.IsExported() {423return424}425extra.Name = decl.Name.Name426extra.Doc = convertCommentsToJavascript(decl.Doc.Text())427428isConstructor := false429430for _, arg := range decl.Type.Params.List {431p := exprToString(arg.Type)432if strings.Contains(p, "goja.ConstructorCall") {433isConstructor = true434}435for _, name := range arg.Names {436extra.Args = append(extra.Args, name.Name)437}438}439if isConstructor {440d.PackageDefinedConstructor[decl.Name.Name] = struct{}{}441}442443extra.Returns = d.extractReturns(decl)444return extra445}446447// convertCommentsToJavascript converts comments to javascript comments.448func convertCommentsToJavascript(comments string) string {449suffix := strings.Trim(strings.TrimSuffix(strings.ReplaceAll(comments, "\n", "\n// "), "// "), "\n")450return fmt.Sprintf("// %s", suffix)451}452453// exprToString converts an expression to a string454func exprToString(expr ast.Expr) string {455switch t := expr.(type) {456case *ast.Ident:457return t.Name458case *ast.SelectorExpr:459return exprToString(t.X) + "." + t.Sel.Name460case *ast.StarExpr:461return exprToString(t.X)462case *ast.ArrayType:463return "[]" + exprToString(t.Elt)464case *ast.InterfaceType:465return "interface{}"466// Add more cases to handle other types467default:468return fmt.Sprintf("%T", expr)469}470}471472473