442 lines
10 KiB
Go
442 lines
10 KiB
Go
|
|
package internal
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"go/ast"
|
||
|
|
"go/token"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"golang.org/x/tools/go/packages"
|
||
|
|
)
|
||
|
|
|
||
|
|
// PDKSymbols contains all exported symbols parsed from extism/go-pdk.
|
||
|
|
type PDKSymbols struct {
|
||
|
|
Types []PDKType
|
||
|
|
Consts []PDKConst
|
||
|
|
Functions []PDKFunc
|
||
|
|
}
|
||
|
|
|
||
|
|
// PDKType represents an exported type from extism/go-pdk.
|
||
|
|
type PDKType struct {
|
||
|
|
Name string
|
||
|
|
Underlying string // The underlying type (e.g., "int" for LogLevel)
|
||
|
|
IsAlias bool // True if it's a type alias (type X = Y)
|
||
|
|
Doc string // Documentation comment
|
||
|
|
Methods []PDKFunc // Methods on this type
|
||
|
|
Fields []PDKField // Struct fields (if it's a struct type)
|
||
|
|
}
|
||
|
|
|
||
|
|
// PDKField represents a struct field.
|
||
|
|
type PDKField struct {
|
||
|
|
Name string
|
||
|
|
Type string
|
||
|
|
Tag string // Struct tag (e.g., `json:"name"`)
|
||
|
|
}
|
||
|
|
|
||
|
|
// PDKConst represents an exported constant from extism/go-pdk.
|
||
|
|
type PDKConst struct {
|
||
|
|
Name string
|
||
|
|
Type string // The type name (may be empty for untyped consts)
|
||
|
|
Value string // The value expression
|
||
|
|
Doc string
|
||
|
|
}
|
||
|
|
|
||
|
|
// PDKFunc represents an exported function from extism/go-pdk.
|
||
|
|
type PDKFunc struct {
|
||
|
|
Name string
|
||
|
|
Doc string
|
||
|
|
Receiver string // Empty for package-level functions
|
||
|
|
Params []PDKParam
|
||
|
|
Returns []PDKReturn
|
||
|
|
IsVariadic bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// PDKParam represents a function parameter.
|
||
|
|
type PDKParam struct {
|
||
|
|
Name string
|
||
|
|
Type string
|
||
|
|
}
|
||
|
|
|
||
|
|
// PDKReturn represents a function return value.
|
||
|
|
type PDKReturn struct {
|
||
|
|
Name string // May be empty for unnamed returns
|
||
|
|
Type string
|
||
|
|
}
|
||
|
|
|
||
|
|
// ParseExtismPDK parses the extism/go-pdk package and extracts all exported symbols.
|
||
|
|
func ParseExtismPDK() (*PDKSymbols, error) {
|
||
|
|
// Load both packages with syntax trees in one call
|
||
|
|
cfg := &packages.Config{
|
||
|
|
Mode: packages.NeedName | packages.NeedSyntax | packages.NeedFiles,
|
||
|
|
}
|
||
|
|
pkgs, err := packages.Load(cfg,
|
||
|
|
"github.com/extism/go-pdk",
|
||
|
|
"github.com/extism/go-pdk/internal/memory",
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("loading extism/go-pdk: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find both packages
|
||
|
|
var pdkPkg, memoryPkg *packages.Package
|
||
|
|
for _, pkg := range pkgs {
|
||
|
|
if len(pkg.Errors) > 0 {
|
||
|
|
return nil, fmt.Errorf("loading %s: %v", pkg.PkgPath, pkg.Errors[0])
|
||
|
|
}
|
||
|
|
switch pkg.Name {
|
||
|
|
case "pdk":
|
||
|
|
pdkPkg = pkg
|
||
|
|
case "memory":
|
||
|
|
memoryPkg = pkg
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if pdkPkg == nil {
|
||
|
|
return nil, fmt.Errorf("package github.com/extism/go-pdk not found")
|
||
|
|
}
|
||
|
|
if memoryPkg == nil {
|
||
|
|
return nil, fmt.Errorf("package github.com/extism/go-pdk/internal/memory not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
symbols := &PDKSymbols{}
|
||
|
|
seenTypes := make(map[string]bool)
|
||
|
|
|
||
|
|
// Extract Memory type from internal/memory package first
|
||
|
|
extractMemorySymbols(memoryPkg.Syntax, symbols, seenTypes)
|
||
|
|
|
||
|
|
// First pass: collect types from pdk package (skip if already found in internal packages)
|
||
|
|
for _, file := range pdkPkg.Syntax {
|
||
|
|
for _, decl := range file.Decls {
|
||
|
|
if genDecl, ok := decl.(*ast.GenDecl); ok {
|
||
|
|
for _, spec := range genDecl.Specs {
|
||
|
|
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
|
||
|
|
if !typeSpec.Name.IsExported() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
// Skip if we already have this type (from internal packages)
|
||
|
|
if seenTypes[typeSpec.Name.Name] {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
seenTypes[typeSpec.Name.Name] = true
|
||
|
|
pdkType := extractType(typeSpec, genDecl.Doc)
|
||
|
|
symbols.Types = append(symbols.Types, pdkType)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build typeMap from the final slice (after all types are added)
|
||
|
|
typeMap := make(map[string]*PDKType)
|
||
|
|
for i := range symbols.Types {
|
||
|
|
typeMap[symbols.Types[i].Name] = &symbols.Types[i]
|
||
|
|
}
|
||
|
|
|
||
|
|
// Second pass: collect functions and methods from pdk package
|
||
|
|
for _, file := range pdkPkg.Syntax {
|
||
|
|
for _, decl := range file.Decls {
|
||
|
|
switch d := decl.(type) {
|
||
|
|
case *ast.GenDecl:
|
||
|
|
if d.Tok == token.CONST {
|
||
|
|
consts := extractConsts(d)
|
||
|
|
symbols.Consts = append(symbols.Consts, consts...)
|
||
|
|
}
|
||
|
|
case *ast.FuncDecl:
|
||
|
|
if !d.Name.IsExported() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
fn := extractFunc(d)
|
||
|
|
if fn.Receiver != "" {
|
||
|
|
// It's a method, associate with type
|
||
|
|
typeName := fn.Receiver
|
||
|
|
if strings.HasPrefix(typeName, "*") {
|
||
|
|
typeName = typeName[1:]
|
||
|
|
}
|
||
|
|
if t, ok := typeMap[typeName]; ok {
|
||
|
|
t.Methods = append(t.Methods, fn)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
symbols.Functions = append(symbols.Functions, fn)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sort for consistent output
|
||
|
|
sort.Slice(symbols.Types, func(i, j int) bool {
|
||
|
|
return symbols.Types[i].Name < symbols.Types[j].Name
|
||
|
|
})
|
||
|
|
sort.Slice(symbols.Consts, func(i, j int) bool {
|
||
|
|
return symbols.Consts[i].Name < symbols.Consts[j].Name
|
||
|
|
})
|
||
|
|
sort.Slice(symbols.Functions, func(i, j int) bool {
|
||
|
|
return symbols.Functions[i].Name < symbols.Functions[j].Name
|
||
|
|
})
|
||
|
|
|
||
|
|
return symbols, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func extractType(spec *ast.TypeSpec, doc *ast.CommentGroup) PDKType {
|
||
|
|
t := PDKType{
|
||
|
|
Name: spec.Name.Name,
|
||
|
|
Doc: extractDoc(doc),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if it's an alias (type X = Y)
|
||
|
|
t.IsAlias = spec.Assign.IsValid()
|
||
|
|
|
||
|
|
// Extract underlying type
|
||
|
|
t.Underlying = typeString(spec.Type)
|
||
|
|
|
||
|
|
// Extract struct fields if it's a struct type
|
||
|
|
if structType, ok := spec.Type.(*ast.StructType); ok {
|
||
|
|
t.Fields = extractStructFields(structType)
|
||
|
|
}
|
||
|
|
|
||
|
|
return t
|
||
|
|
}
|
||
|
|
|
||
|
|
func extractStructFields(st *ast.StructType) []PDKField {
|
||
|
|
var fields []PDKField
|
||
|
|
if st.Fields == nil {
|
||
|
|
return fields
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, field := range st.Fields.List {
|
||
|
|
fieldType := typeString(field.Type)
|
||
|
|
tag := ""
|
||
|
|
if field.Tag != nil {
|
||
|
|
tag = field.Tag.Value
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(field.Names) == 0 {
|
||
|
|
// Embedded field
|
||
|
|
fields = append(fields, PDKField{
|
||
|
|
Name: fieldType, // Use type name as field name for embedded
|
||
|
|
Type: fieldType,
|
||
|
|
Tag: tag,
|
||
|
|
})
|
||
|
|
} else {
|
||
|
|
for _, name := range field.Names {
|
||
|
|
// Skip unexported fields
|
||
|
|
if !name.IsExported() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
fields = append(fields, PDKField{
|
||
|
|
Name: name.Name,
|
||
|
|
Type: fieldType,
|
||
|
|
Tag: tag,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return fields
|
||
|
|
}
|
||
|
|
|
||
|
|
func extractConsts(decl *ast.GenDecl) []PDKConst {
|
||
|
|
var consts []PDKConst
|
||
|
|
var currentType string // For iota-style const blocks
|
||
|
|
|
||
|
|
for i, spec := range decl.Specs {
|
||
|
|
valSpec, ok := spec.(*ast.ValueSpec)
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update type if specified
|
||
|
|
if valSpec.Type != nil {
|
||
|
|
currentType = typeString(valSpec.Type)
|
||
|
|
}
|
||
|
|
|
||
|
|
for j, name := range valSpec.Names {
|
||
|
|
if !name.IsExported() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
c := PDKConst{
|
||
|
|
Name: name.Name,
|
||
|
|
Type: currentType,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extract value
|
||
|
|
if j < len(valSpec.Values) {
|
||
|
|
c.Value = exprString(valSpec.Values[j])
|
||
|
|
} else if i == 0 && j == 0 {
|
||
|
|
// First const with no value - likely iota
|
||
|
|
c.Value = "iota"
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extract doc
|
||
|
|
if valSpec.Doc != nil {
|
||
|
|
c.Doc = extractDoc(valSpec.Doc)
|
||
|
|
} else if i == 0 && decl.Doc != nil {
|
||
|
|
c.Doc = extractDoc(decl.Doc)
|
||
|
|
}
|
||
|
|
|
||
|
|
consts = append(consts, c)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return consts
|
||
|
|
}
|
||
|
|
|
||
|
|
func extractFunc(decl *ast.FuncDecl) PDKFunc {
|
||
|
|
fn := PDKFunc{
|
||
|
|
Name: decl.Name.Name,
|
||
|
|
Doc: extractDoc(decl.Doc),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extract receiver
|
||
|
|
if decl.Recv != nil && len(decl.Recv.List) > 0 {
|
||
|
|
fn.Receiver = typeString(decl.Recv.List[0].Type)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extract parameters
|
||
|
|
if decl.Type.Params != nil {
|
||
|
|
for _, field := range decl.Type.Params.List {
|
||
|
|
paramType := typeString(field.Type)
|
||
|
|
|
||
|
|
// Check for variadic
|
||
|
|
if _, ok := field.Type.(*ast.Ellipsis); ok {
|
||
|
|
fn.IsVariadic = true
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(field.Names) == 0 {
|
||
|
|
// Unnamed parameter
|
||
|
|
fn.Params = append(fn.Params, PDKParam{Type: paramType})
|
||
|
|
} else {
|
||
|
|
for _, name := range field.Names {
|
||
|
|
fn.Params = append(fn.Params, PDKParam{
|
||
|
|
Name: name.Name,
|
||
|
|
Type: paramType,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extract returns
|
||
|
|
if decl.Type.Results != nil {
|
||
|
|
for _, field := range decl.Type.Results.List {
|
||
|
|
retType := typeString(field.Type)
|
||
|
|
|
||
|
|
if len(field.Names) == 0 {
|
||
|
|
// Unnamed return
|
||
|
|
fn.Returns = append(fn.Returns, PDKReturn{Type: retType})
|
||
|
|
} else {
|
||
|
|
for _, name := range field.Names {
|
||
|
|
fn.Returns = append(fn.Returns, PDKReturn{
|
||
|
|
Name: name.Name,
|
||
|
|
Type: retType,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return fn
|
||
|
|
}
|
||
|
|
|
||
|
|
func extractDoc(doc *ast.CommentGroup) string {
|
||
|
|
if doc == nil {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
return strings.TrimSpace(doc.Text())
|
||
|
|
}
|
||
|
|
|
||
|
|
func typeString(expr ast.Expr) string {
|
||
|
|
switch t := expr.(type) {
|
||
|
|
case *ast.Ident:
|
||
|
|
return t.Name
|
||
|
|
case *ast.StarExpr:
|
||
|
|
return "*" + typeString(t.X)
|
||
|
|
case *ast.SelectorExpr:
|
||
|
|
return typeString(t.X) + "." + t.Sel.Name
|
||
|
|
case *ast.ArrayType:
|
||
|
|
if t.Len == nil {
|
||
|
|
return "[]" + typeString(t.Elt)
|
||
|
|
}
|
||
|
|
return fmt.Sprintf("[%s]%s", exprString(t.Len), typeString(t.Elt))
|
||
|
|
case *ast.MapType:
|
||
|
|
return fmt.Sprintf("map[%s]%s", typeString(t.Key), typeString(t.Value))
|
||
|
|
case *ast.InterfaceType:
|
||
|
|
return "any" // Simplified
|
||
|
|
case *ast.Ellipsis:
|
||
|
|
return "..." + typeString(t.Elt)
|
||
|
|
case *ast.StructType:
|
||
|
|
return "struct{}" // Simplified for anonymous structs
|
||
|
|
case *ast.FuncType:
|
||
|
|
return "func()" // Simplified
|
||
|
|
default:
|
||
|
|
return fmt.Sprintf("%T", expr)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func exprString(expr ast.Expr) string {
|
||
|
|
switch e := expr.(type) {
|
||
|
|
case *ast.Ident:
|
||
|
|
return e.Name
|
||
|
|
case *ast.BasicLit:
|
||
|
|
return e.Value
|
||
|
|
case *ast.BinaryExpr:
|
||
|
|
return exprString(e.X) + " " + e.Op.String() + " " + exprString(e.Y)
|
||
|
|
case *ast.UnaryExpr:
|
||
|
|
return e.Op.String() + exprString(e.X)
|
||
|
|
case *ast.CallExpr:
|
||
|
|
return typeString(e.Fun) + "(...)"
|
||
|
|
default:
|
||
|
|
return fmt.Sprintf("%T", expr)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// extractMemorySymbols extracts the Memory type and its methods from already-parsed syntax trees.
|
||
|
|
// This is needed because Memory is defined in internal/memory but re-exported by the pdk package.
|
||
|
|
func extractMemorySymbols(files []*ast.File, symbols *PDKSymbols, seenTypes map[string]bool) {
|
||
|
|
// Collect the Memory type
|
||
|
|
for _, file := range files {
|
||
|
|
for _, decl := range file.Decls {
|
||
|
|
if genDecl, ok := decl.(*ast.GenDecl); ok {
|
||
|
|
for _, spec := range genDecl.Specs {
|
||
|
|
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
|
||
|
|
// Only interested in Memory type
|
||
|
|
if typeSpec.Name.Name == "Memory" {
|
||
|
|
pdkType := extractType(typeSpec, genDecl.Doc)
|
||
|
|
symbols.Types = append(symbols.Types, pdkType)
|
||
|
|
seenTypes["Memory"] = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build local type map for method association
|
||
|
|
localTypeMap := make(map[string]*PDKType)
|
||
|
|
for i := range symbols.Types {
|
||
|
|
localTypeMap[symbols.Types[i].Name] = &symbols.Types[i]
|
||
|
|
}
|
||
|
|
|
||
|
|
// Collect methods for Memory
|
||
|
|
for _, file := range files {
|
||
|
|
for _, decl := range file.Decls {
|
||
|
|
if funcDecl, ok := decl.(*ast.FuncDecl); ok {
|
||
|
|
if !funcDecl.Name.IsExported() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
fn := extractFunc(funcDecl)
|
||
|
|
if fn.Receiver != "" {
|
||
|
|
typeName := fn.Receiver
|
||
|
|
if strings.HasPrefix(typeName, "*") {
|
||
|
|
typeName = typeName[1:]
|
||
|
|
}
|
||
|
|
if typeName == "Memory" {
|
||
|
|
if t, ok := localTypeMap["Memory"]; ok {
|
||
|
|
t.Methods = append(t.Methods, fn)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|