GitRoot

Craft your forge, Build your project, Grow your community freely
  1// SPDX-FileCopyrightText: 2026 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package main
  6
  7import (
  8	"fmt"
  9	"go/ast"
 10	"go/token"
 11	"go/types"
 12	"os"
 13	"strings"
 14
 15	"golang.org/x/tools/go/packages"
 16)
 17
 18func Parse(targetDir string) *Generate {
 19	cfg := &packages.Config{
 20		Dir:   targetDir,
 21		Mode:  packages.NeedName | packages.NeedFiles | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo,
 22		Tests: false,
 23	}
 24
 25	pkgs, err := packages.Load(cfg, "./...")
 26	if err != nil {
 27		fmt.Printf("🛑 Can't load dir %s : %v\n", targetDir, err)
 28		os.Exit(1)
 29	}
 30
 31	return Transform(targetDir, pkgs)
 32}
 33
 34func Transform(targetDir string, pkgs []*packages.Package) *Generate {
 35
 36	generate := NewGeneration(targetDir)
 37
 38	for _, pkg := range pkgs {
 39		if len(pkg.Errors) > 0 {
 40			fmt.Printf("🛑 Pkg error %s : %v\n", pkg.PkgPath, pkg.Errors)
 41			continue
 42		}
 43
 44		fmt.Printf("🏁 Start pkg %s\n", pkg.PkgPath)
 45
 46		for _, fileAST := range pkg.Syntax {
 47			pos := pkg.Fset.Position(fileAST.Package)
 48			filename := pos.Filename
 49
 50			relativeSubPath := strings.TrimPrefix(filename, targetDir)
 51			found := false
 52			if relativeSubPath == filename {
 53				fmt.Printf("⏩ Skip (outside pkg) file %s\n", filename)
 54				found = true
 55			} else {
 56				for _, f := range fileToIgnore {
 57					if f == relativeSubPath {
 58						fmt.Printf("⏩ Skip (by conf) file %s\n", filename)
 59						found = true
 60						break
 61					}
 62				}
 63			}
 64			if found {
 65				continue
 66			}
 67
 68			fmt.Printf("🏁 Start file %s\n", filename)
 69
 70			currentFile := generate.getOrCreateFile(filename)
 71
 72			ast.Inspect(fileAST, func(n ast.Node) bool {
 73				switch x := n.(type) {
 74
 75				case *ast.TypeSpec:
 76					if _, ok := x.Type.(*ast.StructType); ok {
 77						if !ast.IsExported(x.Name.Name) {
 78							return true
 79						}
 80
 81						typeNameObj := pkg.TypesInfo.Defs[x.Name].(*types.TypeName)
 82						structObj := typeNameObj.Type().Underlying().(*types.Struct)
 83
 84						var fields []*types.Var
 85						for i := 0; i < structObj.NumFields(); i++ {
 86							field := structObj.Field(i)
 87							if field.Exported() {
 88								fields = append(fields, field)
 89							}
 90						}
 91
 92						generate.structLocations[x.Name.Name] = filename
 93
 94						currentFile.Structs = append(currentFile.Structs, Struct{
 95							TypeName: x.Name.Name,
 96							Doc:      x.Doc,
 97							Fields:   fields,
 98						})
 99					}
100				case *ast.GenDecl:
101					if x.Tok == token.TYPE {
102						for _, spec := range x.Specs {
103							typeSpec, ok := spec.(*ast.TypeSpec)
104							if !ok || !ast.IsExported(typeSpec.Name.Name) {
105								continue
106							}
107							if _, isStruct := typeSpec.Type.(*ast.StructType); !isStruct {
108								typeNameObj := pkg.TypesInfo.Defs[typeSpec.Name].(*types.TypeName)
109								underlying := typeNameObj.Type().Underlying().String()
110
111								if underlying == "string" || strings.HasPrefix(underlying, "int") {
112									generate.structLocations[typeSpec.Name.Name] = filename
113									currentFile.Enums = append(currentFile.Enums, Enum{
114										TypeName: typeSpec.Name.Name,
115										TypeType: underlying,
116										Values:   []string{},
117									})
118								}
119							}
120						}
121					}
122
123					if x.Tok == token.CONST {
124						for _, spec := range x.Specs {
125							valueSpec, ok := spec.(*ast.ValueSpec)
126							if !ok {
127								continue
128							}
129
130							if valueSpec.Type != nil {
131								if ident, ok := valueSpec.Type.(*ast.Ident); ok {
132									for i := range currentFile.Enums {
133										if currentFile.Enums[i].TypeName == ident.Name {
134											for _, name := range valueSpec.Names {
135												if ast.IsExported(name.Name) {
136													currentFile.Enums[i].Values = append(currentFile.Enums[i].Values, name.Name)
137												}
138											}
139										}
140									}
141								}
142							}
143						}
144					}
145				case *ast.FuncDecl:
146					if !ast.IsExported(x.Name.Name) && !strings.HasPrefix(x.Name.Name, "_") {
147						return true
148					}
149					if strings.HasSuffix(x.Name.Name, "FromString") {
150						return true
151					}
152
153					funcObj := pkg.TypesInfo.Defs[x.Name].(*types.Func)
154					sig := funcObj.Type().(*types.Signature)
155
156					wasmDir := WasmNone
157					if x.Doc != nil {
158						for _, comment := range x.Doc.List {
159							if strings.HasPrefix(comment.Text, "//go:wasmexport") {
160								wasmDir = WasmExport
161								break
162							} else if strings.HasPrefix(comment.Text, "//go:wasmimport") {
163								wasmDir = WasmImport
164								break
165							}
166						}
167					}
168
169					if x.Recv == nil {
170						currentFile.Funcs = append(currentFile.Funcs, Func{
171							Name: x.Name.Name,
172							Doc:  x.Doc,
173							Sig:  sig,
174							Wasm: wasmDir,
175						})
176					} else {
177						recvType := sig.Recv().Type()
178						if ptr, ok := recvType.(*types.Pointer); ok {
179							recvType = ptr.Elem()
180						}
181						structName := "Unknown"
182						if named, ok := recvType.(*types.Named); ok {
183							structName = named.Obj().Name()
184						}
185
186						currentFile.Methods = append(currentFile.Methods, Method{
187							StructName: structName,
188							Name:       x.Name.Name,
189							Doc:        x.Doc,
190							Sig:        sig,
191							Wasm:       wasmDir,
192						})
193					}
194				}
195				return true
196			})
197		}
198	}
199	return generate
200}