EntityInfo contains relevant information about each entity in the package
(functions, types, interfaces)
ReferenceInfo contains information about references used by an entity
FieldInfo contains relevant information about each field in a struct
ImplementationInfo contains information about an implemented interface
ImportInfo contains information about an imported package
EntityExtractor defines an interface for extracting information from AST declarations
FunctionExtractor extracts information from function declarations
{
funcDecl := decl.(*ast.FuncDecl)
descriptionData := extractDescriptionData(funcDecl.Doc.Text())
return EntityInfo{
Name: funcDecl.Name.Name,
Type: "function",
Body: extractBody(fs, funcDecl),
Description: descriptionData.Description,
Example: descriptionData.Example,
Notes: descriptionData.Notes,
DeprecationNote: descriptionData.DeprecationNote,
Parameters: extractParameters(funcDecl.Type.Params),
Returns: extractParameters(funcDecl.Type.Results),
Package: pkgName,
PackageURL: url,
PackagePath: packagePath,
// Raw fields
DescriptionRaw: descriptionData.DescriptionRaw,
DeprecationNoteRaw: descriptionData.DeprecationNoteRaw,
}
}
MethodExtractor extracts information from method declarations
{
funcDecl := decl.(*ast.FuncDecl)
descriptionData := extractDescriptionData(funcDecl.Doc.Text())
return EntityInfo{
Name: funcDecl.Name.Name,
Type: "method",
Body: extractBody(fs, funcDecl),
Description: descriptionData.Description,
Example: descriptionData.Example,
Notes: descriptionData.Notes,
DeprecationNote: descriptionData.DeprecationNote,
Parameters: extractParameters(funcDecl.Type.Params),
Returns: extractParameters(funcDecl.Type.Results),
Package: pkgName,
PackageURL: url,
PackagePath: packagePath,
// Raw fields
DescriptionRaw: descriptionData.DescriptionRaw,
DeprecationNoteRaw: descriptionData.DeprecationNoteRaw,
}
}
StructExtractor extracts information from struct declarations
{
spec := decl.(*ast.GenDecl).Specs[0].(*ast.TypeSpec)
structType := spec.Type.(*ast.StructType)
descriptionData := extractDescriptionData(decl.(*ast.GenDecl).Doc.Text())
return EntityInfo{
Name: spec.Name.Name,
Type: "struct",
Description: descriptionData.Description,
Notes: descriptionData.Notes,
DeprecationNote: descriptionData.DeprecationNote,
Fields: extractFields(structType),
Package: pkgName,
PackageURL: url,
PackagePath: packagePath,
// Raw fields
DescriptionRaw: descriptionData.DescriptionRaw,
DeprecationNoteRaw: descriptionData.DeprecationNoteRaw,
}
}
InterfaceExtractor extracts information from interface declarations
{
spec := decl.(*ast.GenDecl).Specs[0].(*ast.TypeSpec)
interfaceType := spec.Type.(*ast.InterfaceType)
descriptionData := extractDescriptionData(decl.(*ast.GenDecl).Doc.Text())
return EntityInfo{
Name: spec.Name.Name,
Description: descriptionData.Description,
Notes: descriptionData.Notes,
DeprecationNote: descriptionData.DeprecationNote,
Type: "interface",
Methods: extractMethods(interfaceType),
Package: pkgName,
PackageURL: url,
PackagePath: packagePath,
// Raw fields
DescriptionRaw: descriptionData.DescriptionRaw,
DeprecationNoteRaw: descriptionData.DeprecationNoteRaw,
}
}
TypeExtractor extracts information from type declarations
{
spec := decl.(*ast.GenDecl).Specs[0].(*ast.TypeSpec)
typeExpr := formatExpr(spec.Type)
descriptionData := extractDescriptionData(decl.(*ast.GenDecl).Doc.Text())
return EntityInfo{
Name: spec.Name.Name,
Description: descriptionData.Description,
Notes: descriptionData.Notes,
DeprecationNote: descriptionData.DeprecationNote,
Type: "type",
Body: typeExpr,
Package: pkgName,
PackageURL: url,
PackagePath: packagePath,
// Raw fields
DescriptionRaw: descriptionData.DescriptionRaw,
DeprecationNoteRaw: descriptionData.DeprecationNoteRaw,
}
}
GetPackages returns a list of all package directories in the project
packages, err := parser.GetPackages()
if err != nil {
log.Fatalf("Error fetching packages: %v", err)
}
for _, pkg := range packages {
fmt.Printf("Package: %s\n", pkg)
}
{
cfg := &packages.Config{
Mode: packages.NeedFiles, // We only need the file paths
}
rootDir, err := filepath.Abs(".")
if err != nil {
return nil, err
}
pkgs, err := packages.Load(cfg, "./...")
if err != nil {
return nil, err
}
var packageDirs []string
for _, pkg := range pkgs {
if len(pkg.GoFiles) > 0 {
// To get the package directory, we take the directory of the first Go file
dir := filepath.Dir(pkg.GoFiles[0])
// if the directory is the root of the project, we skip it
if dir == rootDir {
continue
}
packageDirs = append(packageDirs, dir)
}
}
return packageDirs, nil
}
ParseEntitiesInPackage parses the entities in a given package and returns
a slice of EntityInfo
entities, err := parser.ParseEntitiesInPackage("/home/me/myproject/pkg/mypackage")
if err != nil {
log.Fatalf("Error parsing entities: %v", err)
}
for _, entity := range entities {
fmt.Printf("Name: %s\n", entity.Name)
fmt.Printf("Type: %s\n", entity.Type)
fmt.Printf("Description: %s\n", entity.Description)
fmt.Printf("Package: %s\n", entity.Package)
}
The package must be a full path to the package directory
{
var entities []EntityInfo
var imports []ImportInfo
var interfaces = make(map[string]EntityInfo)
var methodsByType = make(map[string][]EntityInfo)
var entityIndex = make(map[string]EntityInfo)
fs := token.NewFileSet()
pkgs, err := parser.ParseDir(fs, pkgPath, nil, parser.ParseComments)
if err != nil {
return nil, nil, err
}
var pkgName string
var pkg *ast.Package
for k, v := range pkgs {
pkgName = k
pkg = v
break
}
extractors := map[string]EntityExtractor{
"function": FunctionExtractor{},
"method": MethodExtractor{},
"struct": StructExtractor{},
"interface": InterfaceExtractor{},
"type": TypeExtractor{},
}
// Replace slashes with hyphens to ensure unique filenames
url := strings.ReplaceAll(relativePath, string(os.PathSeparator), "-")
for _, file := range pkg.Files {
// here we parse all imports
for _, imp := range file.Imports {
importPath := strings.Trim(imp.Path.Value, `"`)
var importName string
if imp.Name != nil {
if imp.Name.Name == "_" {
importName = "Anonymous Import"
} else {
importName = imp.Name.Name
}
} else {
importName = ""
}
importURL := strings.ReplaceAll(importPath, "/", "-")
doc := ""
comment := ""
if imp.Doc != nil {
doc = imp.Doc.Text()
}
if imp.Comment != nil {
comment = imp.Comment.Text()
}
imports = append(imports, ImportInfo{
Path: importPath,
URL: importURL,
Alias: importName,
Doc: doc,
Comment: comment,
})
}
// here we parse all entities types
for _, decl := range file.Decls {
switch decl := decl.(type) {
case *ast.FuncDecl:
if decl.Recv != nil {
receiverType := formatExpr(decl.Recv.List[0].Type)
method := extractors["method"].Extract(decl, fs, interfaces, pkgName, relativePath, url)
methodsByType[receiverType] = append(methodsByType[receiverType], method)
} else {
entity := extractors["function"].Extract(decl, fs, interfaces, pkgName, relativePath, url)
entities = append(entities, entity)
entityIndex[pkgName+"."+entity.Name] = entity
}
case *ast.GenDecl:
for _, spec := range decl.Specs {
switch spec := spec.(type) {
case *ast.TypeSpec:
var entityType string
switch spec.Type.(type) {
case *ast.StructType:
entityType = "struct"
case *ast.InterfaceType:
entityType = "interface"
if _, exists := interfaces[spec.Name.Name]; !exists {
ifaceInfo := extractors[entityType].Extract(decl, fs, interfaces, pkgName, relativePath, url)
ifaceInfo.Package = pkgName
interfaces[spec.Name.Name] = ifaceInfo
entities = append(entities, ifaceInfo)
entityIndex[pkgName+"."+ifaceInfo.Name] = ifaceInfo
}
default:
entityType = "type"
}
if entityType != "interface" {
entity := extractors[entityType].Extract(decl, fs, interfaces, pkgName, relativePath, url)
entities = append(entities, entity)
entityIndex[pkgName+"."+entity.Name] = entity
}
}
}
}
}
}
// Here we associate methods with structs, resolve interfaces
// implementations and find references for each entity
for i, entity := range entities {
references := findReferences(entity, entityIndex)
entity.References = references
// if the entity is a struct, we associate methods with it
if entity.Type == "struct" {
receiverName := entity.Name
if methods, ok := methodsByType[receiverName]; ok {
entity.Methods = append(entity.Methods, methods...)
} else if methods, ok := methodsByType["*"+receiverName]; ok {
entity.Methods = append(entity.Methods, methods...)
}
entity.Implements = findImplementedInterfaces(entity, interfaces)
// and here we find references for each method if any
for j, method := range entity.Methods {
methodReferences := findReferences(method, entityIndex)
entity.Methods[j].References = methodReferences
}
}
entities[i] = entity
}
return entities, imports, nil
}
findImplementedInterfaces checks which interfaces are implemented by a struct
{
var implemented []ImplementationInfo
for ifaceName, ifaceInfo := range interfaces {
if implementsInterface(entity, ifaceInfo) {
implemented = append(implemented, ImplementationInfo{
InterfaceName: ifaceName,
Package: ifaceInfo.Package,
})
}
}
return implemented
}
implementsInterface checks if a struct implements a given interface
{
methodSet := make(map[string]EntityInfo)
for _, method := range entity.Methods {
methodSet[method.Name] = method
}
for _, ifaceMethod := range iface.Methods {
if method, ok := methodSet[ifaceMethod.Name]; !ok {
return false
} else {
if !methodsMatch(ifaceMethod, method) {
return false
}
}
}
return true
}
methodsMatch checks if the parameters and return types of two methods match
{
if len(ifaceMethod.Parameters) != len(structMethod.Parameters) ||
len(ifaceMethod.Returns) != len(structMethod.Returns) {
return false
}
for i, param := range ifaceMethod.Parameters {
if param != structMethod.Parameters[i] {
return false
}
}
for i, ret := range ifaceMethod.Returns {
if ret != structMethod.Returns[i] {
return false
}
}
return true
}
ExampleDeprecationNote is an example of a deprecated function
This function is deprecated only for demonstration purposes
{}
extractMethods extracts methods from an interface declaration
{
var methods []EntityInfo
for _, field := range interfaceType.Methods.List {
if funcType, ok := field.Type.(*ast.FuncType); ok {
methodInfo := EntityInfo{
Name: field.Names[0].Name,
Parameters: extractParameters(funcType.Params),
Returns: extractParameters(funcType.Results),
}
methods = append(methods, methodInfo)
}
}
return methods
}
extractFields extracts fields from a struct
{
var fields []FieldInfo
for _, field := range structType.Fields.List {
typeStr := formatExpr(field.Type)
for _, name := range field.Names {
fieldInfo := FieldInfo{
Name: name.Name,
Type: typeStr,
Tag: extractTag(field),
}
fields = append(fields, fieldInfo)
}
}
return fields
}
extractTag extracts struct tags
{
if field.Tag != nil {
return strings.Trim(field.Tag.Value, "`")
}
return ""
}
DescriptionData contains different parts of a function's documentation comment
extractDescriptionData extracts the description and example code from a
function's documentation comment
{
lines := strings.Split(doc, "\n")
var descLines []string
var exampleLines []string
var notesLines []string
var deprecationNoteLines []string
var description string
var example string
var notes string
var deprecationNote string
isExample := false
isNotes := false
isDeprecationNote := false
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Example:") {
isExample = true
isNotes = false
isDeprecationNote = false
continue
}
if strings.HasPrefix(line, "Notes:") {
isNotes = true
isExample = false
isDeprecationNote = false
continue
}
if strings.HasPrefix(line, "Deprecated:") {
isDeprecationNote = true
isExample = false
isNotes = false
continue
}
if isExample {
exampleLines = append(exampleLines, line)
} else if isNotes {
notesLines = append(notesLines, line)
} else if isDeprecationNote {
deprecationNoteLines = append(deprecationNoteLines, line)
} else {
descLines = append(descLines, line)
}
}
// Description
descriptionRaw := strings.Join(descLines, "\n")
description = strings.Join(descLines, "</p>\n<p>")
description = "<p>" + description + "</p>"
description = strings.ReplaceAll(description, "\t", " ")
if description == "<p></p>" {
description = ""
}
// Example
example = strings.Join(exampleLines, "\n")
example = strings.TrimLeft(example, " \t")
example = strings.TrimLeft(example, "\n")
example = formatExample(example)
// Notes
notes = strings.Join(notesLines, "</p>\n<p>")
notes = "<p>" + notes + "</p>"
notes = strings.ReplaceAll(notes, "\t", " ")
if notes == "<p></p>" {
notes = ""
}
// Deprecation Note
deprecationNoteRaw := strings.Join(deprecationNoteLines, "\n")
deprecationNote = strings.Join(deprecationNoteLines, "</p>\n<p>")
deprecationNote = "<p>" + deprecationNote + "</p>"
deprecationNote = strings.ReplaceAll(deprecationNote, "\t", " ")
if deprecationNote == "<p></p>" {
deprecationNote = ""
}
return DescriptionData{
Description: description,
Example: example,
Notes: notes,
DeprecationNote: deprecationNote,
// Raw fields
DescriptionRaw: descriptionRaw,
DeprecationNoteRaw: deprecationNoteRaw,
}
}
formatExample formats the example code using the go/format package
{
src := []byte(example)
formattedSrc, err := format.Source(src)
if err != nil {
return example
}
return string(formattedSrc)
}
extractParameters extracts the parameters from a function or method declaration
{
var params []string
if fieldList != nil {
for _, param := range fieldList.List {
typeStr := formatExpr(param.Type)
for _, name := range param.Names {
params = append(params, name.Name+" "+typeStr)
}
if len(param.Names) == 0 {
params = append(params, typeStr)
}
}
}
return params
}
extractBody extracts the body of a function declaration
{
if fn.Body == nil {
return ""
}
start := fs.Position(fn.Body.Pos()).Offset
end := fs.Position(fn.Body.End()).Offset
fileContent, _ := os.ReadFile(fs.File(fn.Body.Pos()).Name())
body := string(fileContent[start:end])
// before returning we have to escape possible html snippets in it since
// those snippets are rendered by highlighting.js which has an issue with
// unescaped html snippets (yeah even if inside a Go string, what a pleasure)
return html.EscapeString(body)
}
formatExpr formats an expression using the go/format package
{
var out strings.Builder
if err := format.Node(&out, token.NewFileSet(), expr); err != nil {
return ""
}
return out.String()
}
findReferences finds references to other entities in an entity
{
var references []ReferenceInfo
// Check for parameters
for _, param := range entity.Parameters {
paramType := strings.Split(param, " ")[1]
if refEntity, found := entityIndex[entity.Package+"."+paramType]; found {
references = append(references, ReferenceInfo{
Name: paramType,
Package: refEntity.Package,
PackageURL: refEntity.PackageURL,
PackagePath: refEntity.PackagePath,
})
}
}
// Check for returns
for _, ret := range entity.Returns {
if refEntity, found := entityIndex[entity.Package+"."+ret]; found {
references = append(references, ReferenceInfo{
Name: ret,
Package: refEntity.Package,
PackageURL: refEntity.PackageURL,
PackagePath: refEntity.PackagePath,
})
}
}
// Check for fields
for _, field := range entity.Fields {
if refEntity, found := entityIndex[entity.Package+"."+field.Type]; found {
references = append(references, ReferenceInfo{
Name: field.Type,
Package: refEntity.Package,
PackageURL: refEntity.PackageURL,
PackagePath: refEntity.PackagePath,
})
}
}
return references
}
import "go/ast"
import "go/token"
import "path/filepath"
import "golang.org/x/tools/go/packages"
import "go/ast"
import "go/parser"
import "go/token"
import "os"
import "strings"
import "go/ast"
import "go/format"
import "go/token"
import "html"
import "os"
import "strings"