Files
go-zero/tools/goctl/rpc/parser/import.go

195 lines
5.5 KiB
Go

package parser
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/emicklei/proto"
)
// Import embeds proto.Import
type Import struct {
*proto.Import
}
// ImportedProto holds the package information of a transitively imported proto file.
type ImportedProto struct {
// Src is the absolute path to the proto file.
Src string
// ProtoPackage is the value of the proto "package" declaration.
// It is the qualifier used in dotted type references, e.g. "ext" in "ext.ExtReq".
ProtoPackage string
// GoPackage is the value of the option go_package field, or the proto
// package name when go_package is absent.
GoPackage string
// PbPackage is the sanitized Go package name derived from GoPackage.
PbPackage string
}
// BuildProtoPackageMap returns a map from proto package name to ImportedProto,
// enabling O(1) lookup of Go package info given a proto type qualifier like "ext".
func BuildProtoPackageMap(importedProtos []ImportedProto) map[string]ImportedProto {
m := make(map[string]ImportedProto, len(importedProtos))
for _, imp := range importedProtos {
if imp.ProtoPackage != "" {
m[imp.ProtoPackage] = imp
}
}
return m
}
// ResolveImports returns the absolute paths of all transitively imported proto
// files reachable from src, excluding well-known types (google/*).
// It searches for imported files in protoPaths (equivalent to protoc -I flags).
// Files that cannot be found in protoPaths are silently skipped so that
// system-level or well-known protos do not cause errors.
func ResolveImports(src string, protoPaths []string) ([]string, error) {
absSrc, err := filepath.Abs(src)
if err != nil {
return nil, err
}
visited := make(map[string]bool)
visited[absSrc] = true // exclude the source itself from the result
var result []string
if err := collectImports(absSrc, protoPaths, visited, &result); err != nil {
return nil, err
}
return result, nil
}
// ParseImportedProtos resolves and parses all transitively imported proto
// files, returning their package information for use in code generation.
func ParseImportedProtos(src string, protoPaths []string) ([]ImportedProto, error) {
paths, err := ResolveImports(src, protoPaths)
if err != nil {
return nil, err
}
result := make([]ImportedProto, 0, len(paths))
for _, p := range paths {
goPackage, pbPackage, protoPackage, err := parseGoPackage(p)
if err != nil {
return nil, err
}
result = append(result, ImportedProto{
Src: p,
ProtoPackage: protoPackage,
GoPackage: goPackage,
PbPackage: pbPackage,
})
}
return result, nil
}
// collectImports recursively walks import declarations of src, appending newly
// discovered absolute proto file paths to result.
func collectImports(src string, protoPaths []string, visited map[string]bool, result *[]string) error {
importFilenames, err := parseImportFilenames(src)
if err != nil {
return err
}
for _, filename := range importFilenames {
if isWellKnownProto(filename) {
continue
}
abs, err := lookupProtoFile(filename, protoPaths)
if err != nil {
// Not found in the provided proto paths — may be a system-level proto.
// Skip rather than fail, mirroring protoc's own behaviour.
continue
}
if visited[abs] {
continue
}
visited[abs] = true
*result = append(*result, abs)
if err := collectImports(abs, protoPaths, visited, result); err != nil {
return err
}
}
return nil
}
// parseImportFilenames opens src and returns the Filename field of every
// import statement without performing any file-system lookups.
func parseImportFilenames(src string) ([]string, error) {
r, err := os.Open(src)
if err != nil {
return nil, err
}
defer r.Close()
p := proto.NewParser(r)
set, err := p.Parse()
if err != nil {
return nil, err
}
var imports []string
proto.Walk(set, proto.WithImport(func(i *proto.Import) {
imports = append(imports, i.Filename)
}))
return imports, nil
}
// parseGoPackage reads only the go_package option and package declaration from
// src, returning the derived GoPackage, PbPackage, and ProtoPackage without
// requiring a service definition (imported protos often have no service block).
func parseGoPackage(src string) (goPackage, pbPackage, protoPackage string, err error) {
r, err := os.Open(src)
if err != nil {
return "", "", "", err
}
defer r.Close()
p := proto.NewParser(r)
set, err := p.Parse()
if err != nil {
return "", "", "", err
}
var packageName string
proto.Walk(set,
proto.WithOption(func(opt *proto.Option) {
if opt.Name == "go_package" {
goPackage = opt.Constant.Source
}
}),
proto.WithPackage(func(pkg *proto.Package) {
packageName = pkg.Name
}),
)
if len(goPackage) == 0 {
goPackage = packageName
}
pbPackage = GoSanitized(filepath.Base(goPackage))
protoPackage = packageName
return goPackage, pbPackage, protoPackage, nil
}
// lookupProtoFile searches for filename inside each directory of protoPaths,
// returning its absolute path on the first match.
func lookupProtoFile(filename string, protoPaths []string) (string, error) {
for _, dir := range protoPaths {
candidate := filepath.Join(dir, filename)
if _, err := os.Stat(candidate); err == nil {
return filepath.Abs(candidate)
}
}
return "", fmt.Errorf("proto file %q not found in proto paths %v", filename, protoPaths)
}
// isWellKnownProto reports whether filename refers to a well-known type
// bundled with protoc (e.g. google/protobuf/timestamp.proto).
func isWellKnownProto(filename string) bool {
return strings.HasPrefix(filename, "google/")
}