mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-10 16:30:01 +08:00
feat(goctl/rpc): support external proto imports with cross-package ty… (#5472)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,194 @@
|
||||
package parser
|
||||
|
||||
import "github.com/emicklei/proto"
|
||||
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/")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user