Information about each entity in the package
(functions, types, interfaces)
Information about references used by an entity
Information about each field in a struct
Information about an implemented interface
Information about an imported package
Define an interface for extracting information from AST declarations
Extract function details from a function declaration.
{
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,
}
}
Extract method details from a method declaration.
{
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,
}
}
Extract struct details from a struct declaration.
{
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,
}
}
Extract interface details from an interface declaration.
{
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,
}
}
Extract type details from a type declaration.
{
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,
}
}
Load and return a list of Go package directories from the current project.
Exclude the root directory and only include directories containing Go files.
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
}
Parse Go source files in the specified package directory, extracting function, method, type,
struct, and interface information. Also gather import details and associate methods with structs.
Resolve interface implementations and find references for each entity.
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
}
Check 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
}
Check 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
}
Check 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
{}
Extract 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
}
Extract 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
}
Extract a structs tags
{
if field.Tag != nil {
return strings.Trim(field.Tag.Value, "`")
}
return ""
}
Holds different parts of a function's documentation comment
Extract and format description data and example code from a
documentation comment string
{
lines := strings.Split(doc, "\n")
var descLines []string
var exampleLines []string
var notesLines []string
var deprecationNoteLines []string
var returnsLines []string
var description string
var example string
var notes string
var deprecationNote string
var returns string
isExample := false
isNotes := false
isDeprecationNote := false
isReturns := false
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Example:") {
isExample = true
isNotes = false
isDeprecationNote = false
isReturns = false
continue
}
if strings.HasPrefix(line, "Notes:") {
isNotes = true
isExample = false
isDeprecationNote = false
isReturns = false
continue
}
if strings.HasPrefix(line, "Deprecated:") {
isDeprecationNote = true
isExample = false
isNotes = false
isReturns = false
continue
}
if strings.HasPrefix(line, "Returns:") {
isReturns = true
isExample = false
isNotes = false
isDeprecationNote = false
continue
}
if isExample {
exampleLines = append(exampleLines, line)
} else if isNotes {
notesLines = append(notesLines, line)
} else if isDeprecationNote {
deprecationNoteLines = append(deprecationNoteLines, line)
} else if isReturns {
returnsLines = append(returnsLines, 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 = ""
}
// Returns
returns = strings.Join(returnsLines, "</p>\n<p>")
returns = "<p>" + returns + "</p>"
returns = strings.ReplaceAll(returns, "\t", " ")
if returns == "<p></p>" {
returns = ""
}
return DescriptionData{
Description: description,
Example: example,
Notes: notes,
DeprecationNote: deprecationNote,
Returns: returns,
// Raw fields
DescriptionRaw: descriptionRaw,
DeprecationNoteRaw: deprecationNoteRaw,
}
}
Format the example code snippet to be properly indented and aligned
using the go/format package.
{
src := []byte(example)
formattedSrc, err := format.Source(src)
if err != nil {
return example
}
return string(formattedSrc)
}
Extract 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
}
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)
}
Format an expression into a string representation
using the go/format package.
{
var out strings.Builder
if err := format.Node(&out, token.NewFileSet(), expr); err != nil {
return ""
}
return out.String()
}
Identify references to other entities within the given entity.
{
var references []ReferenceInfo
// Check for parameters
for _, param := range entity.Parameters {
parts := strings.Fields(param)
paramType := parts[len(parts)-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 {
parts := strings.Fields(ret)
retType := parts[len(parts)-1]
if refEntity, found := entityIndex[entity.Package+"."+retType]; found {
references = append(references, ReferenceInfo{
Name: retType,
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"