Skip to main content
Glama
go_analyzer.go12.4 kB
package main import ( "encoding/json" "fmt" "go/ast" "go/parser" "go/token" "os" "strings" ) type Location struct { StartLine int `json:"startLine"` StartColumn int `json:"startColumn"` EndLine int `json:"endLine"` EndColumn int `json:"endColumn"` } type ParameterInfo struct { Name string `json:"name"` Type *string `json:"type"` IsOptional bool `json:"isOptional"` DefaultValue *string `json:"defaultValue"` } type PropertyInfo struct { Name string `json:"name"` Type *string `json:"type"` Documentation *string `json:"documentation,omitempty"` Location Location `json:"location"` IsExported bool `json:"isExported"` } type MethodInfo struct { Name string `json:"name"` Documentation *string `json:"documentation,omitempty"` Location Location `json:"location"` IsExported bool `json:"isExported"` Parameters []ParameterInfo `json:"parameters"` ReturnType *string `json:"returnType"` ReceiverType *string `json:"receiverType,omitempty"` } type FunctionInfo struct { Name string `json:"name"` Documentation *string `json:"documentation,omitempty"` Location Location `json:"location"` IsExported bool `json:"isExported"` Parameters []ParameterInfo `json:"parameters"` ReturnType *string `json:"returnType"` } type StructInfo struct { Name string `json:"name"` Documentation *string `json:"documentation,omitempty"` Location Location `json:"location"` IsExported bool `json:"isExported"` Fields []PropertyInfo `json:"properties"` Methods []MethodInfo `json:"methods"` } type InterfaceInfo struct { Name string `json:"name"` Documentation *string `json:"documentation,omitempty"` Location Location `json:"location"` IsExported bool `json:"isExported"` Methods []MethodInfo `json:"methods"` } type ImportInfo struct { ImportedNames []string `json:"importedNames"` ModuleSpecifier string `json:"moduleSpecifier"` } type DocumentationInfo struct { HasDocumentation bool `json:"hasDocumentation"` DocumentedSymbols int `json:"documentedSymbols"` TotalSymbols int `json:"totalSymbols"` Coverage float64 `json:"coverage"` } type FileAnalysis struct { Path string `json:"path"` Classes []StructInfo `json:"classes"` Functions []FunctionInfo `json:"functions"` Interfaces []InterfaceInfo `json:"interfaces"` Enums []interface{} `json:"enums"` TypeAliases []interface{} `json:"typeAliases"` Imports []ImportInfo `json:"imports"` Exports []interface{} `json:"exports"` Documentation DocumentationInfo `json:"documentation"` } type Analyzer struct { fset *token.FileSet file *ast.File filePath string structs []StructInfo functions []FunctionInfo interfaces []InterfaceInfo imports []ImportInfo methods map[string][]MethodInfo totalSymbols int docedSymbols int } func NewAnalyzer(filePath string) *Analyzer { return &Analyzer{ fset: token.NewFileSet(), filePath: filePath, methods: make(map[string][]MethodInfo), } } func (a *Analyzer) getLocation(node ast.Node) Location { start := a.fset.Position(node.Pos()) end := a.fset.Position(node.End()) return Location{ StartLine: start.Line, StartColumn: start.Column, EndLine: end.Line, EndColumn: end.Column, } } func (a *Analyzer) getDocumentation(doc *ast.CommentGroup) *string { if doc == nil { return nil } text := doc.Text() if text == "" { return nil } trimmed := strings.TrimSpace(text) return &trimmed } func (a *Analyzer) isExported(name string) bool { if len(name) == 0 { return false } return ast.IsExported(name) } func (a *Analyzer) extractType(expr ast.Expr) *string { if expr == nil { return nil } var typeStr string switch t := expr.(type) { case *ast.Ident: typeStr = t.Name case *ast.StarExpr: if inner := a.extractType(t.X); inner != nil { typeStr = "*" + *inner } case *ast.ArrayType: if inner := a.extractType(t.Elt); inner != nil { typeStr = "[]" + *inner } case *ast.MapType: key := a.extractType(t.Key) val := a.extractType(t.Value) if key != nil && val != nil { typeStr = "map[" + *key + "]" + *val } case *ast.SelectorExpr: if x, ok := t.X.(*ast.Ident); ok { typeStr = x.Name + "." + t.Sel.Name } case *ast.InterfaceType: typeStr = "interface{}" case *ast.ChanType: if inner := a.extractType(t.Value); inner != nil { typeStr = "chan " + *inner } case *ast.FuncType: typeStr = "func" default: typeStr = "unknown" } return &typeStr } func (a *Analyzer) extractParameters(fields *ast.FieldList) []ParameterInfo { params := []ParameterInfo{} if fields == nil { return params } for _, field := range fields.List { fieldType := a.extractType(field.Type) if len(field.Names) == 0 { // Unnamed parameter params = append(params, ParameterInfo{ Name: "_", Type: fieldType, IsOptional: false, }) } else { for _, name := range field.Names { params = append(params, ParameterInfo{ Name: name.Name, Type: fieldType, IsOptional: false, }) } } } return params } func (a *Analyzer) extractReturnType(results *ast.FieldList) *string { if results == nil || len(results.List) == 0 { return nil } if len(results.List) == 1 && len(results.List[0].Names) == 0 { return a.extractType(results.List[0].Type) } // Multiple return values types := []string{} for _, field := range results.List { if t := a.extractType(field.Type); t != nil { types = append(types, *t) } } combined := "(" + strings.Join(types, ", ") + ")" return &combined } func (a *Analyzer) analyzeStruct(spec *ast.TypeSpec, doc *ast.CommentGroup) { structType, ok := spec.Type.(*ast.StructType) if !ok { return } structInfo := StructInfo{ Name: spec.Name.Name, Documentation: a.getDocumentation(doc), Location: a.getLocation(spec), IsExported: a.isExported(spec.Name.Name), Fields: []PropertyInfo{}, Methods: []MethodInfo{}, } a.totalSymbols++ if structInfo.Documentation != nil { a.docedSymbols++ } // Extract fields if structType.Fields != nil { for _, field := range structType.Fields.List { fieldType := a.extractType(field.Type) fieldDoc := a.getDocumentation(field.Doc) if len(field.Names) == 0 { // Embedded field if fieldType != nil { structInfo.Fields = append(structInfo.Fields, PropertyInfo{ Name: *fieldType, Type: fieldType, Documentation: fieldDoc, Location: a.getLocation(field), IsExported: true, }) } } else { for _, name := range field.Names { structInfo.Fields = append(structInfo.Fields, PropertyInfo{ Name: name.Name, Type: fieldType, Documentation: fieldDoc, Location: a.getLocation(field), IsExported: a.isExported(name.Name), }) } } } } a.structs = append(a.structs, structInfo) } func (a *Analyzer) analyzeInterface(spec *ast.TypeSpec, doc *ast.CommentGroup) { interfaceType, ok := spec.Type.(*ast.InterfaceType) if !ok { return } interfaceInfo := InterfaceInfo{ Name: spec.Name.Name, Documentation: a.getDocumentation(doc), Location: a.getLocation(spec), IsExported: a.isExported(spec.Name.Name), Methods: []MethodInfo{}, } a.totalSymbols++ if interfaceInfo.Documentation != nil { a.docedSymbols++ } // Extract methods if interfaceType.Methods != nil { for _, method := range interfaceType.Methods.List { if len(method.Names) == 0 { continue } funcType, ok := method.Type.(*ast.FuncType) if !ok { continue } for _, name := range method.Names { methodInfo := MethodInfo{ Name: name.Name, Documentation: a.getDocumentation(method.Doc), Location: a.getLocation(method), IsExported: a.isExported(name.Name), Parameters: a.extractParameters(funcType.Params), ReturnType: a.extractReturnType(funcType.Results), } a.totalSymbols++ if methodInfo.Documentation != nil { a.docedSymbols++ } interfaceInfo.Methods = append(interfaceInfo.Methods, methodInfo) } } } a.interfaces = append(a.interfaces, interfaceInfo) } func (a *Analyzer) analyzeFunction(decl *ast.FuncDecl) { if decl.Recv != nil { // This is a method, not a function a.analyzeMethod(decl) return } funcInfo := FunctionInfo{ Name: decl.Name.Name, Documentation: a.getDocumentation(decl.Doc), Location: a.getLocation(decl), IsExported: a.isExported(decl.Name.Name), Parameters: a.extractParameters(decl.Type.Params), ReturnType: a.extractReturnType(decl.Type.Results), } a.totalSymbols++ if funcInfo.Documentation != nil { a.docedSymbols++ } a.functions = append(a.functions, funcInfo) } func (a *Analyzer) analyzeMethod(decl *ast.FuncDecl) { if decl.Recv == nil || len(decl.Recv.List) == 0 { return } receiverType := a.extractType(decl.Recv.List[0].Type) if receiverType == nil { return } // Remove * from pointer receivers typeName := strings.TrimPrefix(*receiverType, "*") methodInfo := MethodInfo{ Name: decl.Name.Name, Documentation: a.getDocumentation(decl.Doc), Location: a.getLocation(decl), IsExported: a.isExported(decl.Name.Name), Parameters: a.extractParameters(decl.Type.Params), ReturnType: a.extractReturnType(decl.Type.Results), ReceiverType: &typeName, } a.totalSymbols++ if methodInfo.Documentation != nil { a.docedSymbols++ } a.methods[typeName] = append(a.methods[typeName], methodInfo) } func (a *Analyzer) analyzeImports(spec *ast.ImportSpec) { path := strings.Trim(spec.Path.Value, `"`) importInfo := ImportInfo{ ImportedNames: []string{}, ModuleSpecifier: path, } if spec.Name != nil { importInfo.ImportedNames = append(importInfo.ImportedNames, spec.Name.Name) } a.imports = append(a.imports, importInfo) } func (a *Analyzer) Analyze() (*FileAnalysis, error) { file, err := parser.ParseFile(a.fset, a.filePath, nil, parser.ParseComments) if err != nil { return nil, fmt.Errorf("failed to parse file: %w", err) } a.file = file // Walk the AST ast.Inspect(file, func(n ast.Node) bool { switch node := n.(type) { case *ast.GenDecl: for _, spec := range node.Specs { switch s := spec.(type) { case *ast.TypeSpec: if _, ok := s.Type.(*ast.StructType); ok { a.analyzeStruct(s, node.Doc) } else if _, ok := s.Type.(*ast.InterfaceType); ok { a.analyzeInterface(s, node.Doc) } case *ast.ImportSpec: a.analyzeImports(s) } } case *ast.FuncDecl: a.analyzeFunction(node) } return true }) // Attach methods to structs for i := range a.structs { if methods, ok := a.methods[a.structs[i].Name]; ok { a.structs[i].Methods = methods } } // Calculate coverage coverage := 0.0 if a.totalSymbols > 0 { coverage = float64(a.docedSymbols) / float64(a.totalSymbols) * 100 } return &FileAnalysis{ Path: a.filePath, Classes: a.structs, Functions: a.functions, Interfaces: a.interfaces, Enums: []interface{}{}, TypeAliases: []interface{}{}, Imports: a.imports, Exports: []interface{}{}, Documentation: DocumentationInfo{ HasDocumentation: a.docedSymbols > 0, DocumentedSymbols: a.docedSymbols, TotalSymbols: a.totalSymbols, Coverage: coverage, }, }, nil } func main() { if len(os.Args) < 2 { result := map[string]string{"error": "No file path provided"} json.NewEncoder(os.Stdout).Encode(result) os.Exit(1) } filePath := os.Args[1] analyzer := NewAnalyzer(filePath) result, err := analyzer.Analyze() if err != nil { errorResult := map[string]string{ "error": err.Error(), "path": filePath, } json.NewEncoder(os.Stdout).Encode(errorResult) os.Exit(1) } json.NewEncoder(os.Stdout).Encode(result) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ThaLoc0one/documentation-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server