github.com/Big-big-orange/protoreflect@v0.0.0-20240408141420-285cedfdf6a4/desc/sourceinfo/cmd/protoc-gen-gosrcinfo/main.go (about)

     1  // Command protoc-gen-gosrcinfo is a protoc plugin. It emits Go code, into files
     2  // named "<file>.pb.srcinfo.go". These source files include source code info for
     3  // processed proto files and register that info with the srcinfo package.
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"compress/gzip"
     9  	"fmt"
    10  	"path"
    11  	"strings"
    12  
    13  	"github.com/jhump/gopoet"
    14  	"github.com/jhump/goprotoc/plugins"
    15  	"google.golang.org/protobuf/proto"
    16  	"google.golang.org/protobuf/types/pluginpb"
    17  
    18  	"github.com/Big-big-orange/protoreflect/desc"
    19  )
    20  
    21  func main() {
    22  	plugins.PluginMain(genSourceInfo)
    23  }
    24  
    25  func genSourceInfo(req *plugins.CodeGenRequest, resp *plugins.CodeGenResponse) error {
    26  	resp.SupportsFeatures(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)
    27  	args, err := parseArgs(req.Args)
    28  	if err != nil {
    29  		return err
    30  	}
    31  	names := plugins.GoNames{
    32  		ImportMap:      args.importMap,
    33  		ModuleRoot:     args.moduleRoot,
    34  		SourceRelative: args.sourceRelative,
    35  	}
    36  	if args.importPath != "" {
    37  		// if we're overriding import path, go ahead and query
    38  		// package for each file, which will cache the override name
    39  		// so all subsequent queries are consistent
    40  		for _, fd := range req.Files {
    41  			// Only use the override for files that don't otherwise have an
    42  			// entry in the specified import map
    43  			if _, ok := args.importMap[fd.GetName()]; !ok {
    44  				names.GoPackageForFileWithOverride(fd, args.importPath)
    45  			}
    46  		}
    47  	}
    48  	for _, fd := range req.Files {
    49  		if err := generateSourceInfo(fd, &names, resp, args); err != nil {
    50  			if fe, ok := err.(*gopoet.FormatError); ok {
    51  				if args.debug {
    52  					return fmt.Errorf("%s: error in generated Go code: %v:\n%s", fd.GetName(), err, fe.Unformatted)
    53  				} else {
    54  					return fmt.Errorf("%s: error in generated Go code: %v (use debug=true arg to show full source)", fd.GetName(), err)
    55  				}
    56  			} else {
    57  				return fmt.Errorf("%s: %v", fd.GetName(), err)
    58  			}
    59  		}
    60  	}
    61  	return nil
    62  }
    63  
    64  func generateSourceInfo(fd *desc.FileDescriptor, names *plugins.GoNames, resp *plugins.CodeGenResponse, args codeGenArgs) error {
    65  	si := fd.AsFileDescriptorProto().GetSourceCodeInfo()
    66  	if len(si.GetLocation()) == 0 {
    67  		return nil
    68  	}
    69  	pkg := names.GoPackageForFile(fd)
    70  	filename := names.OutputFilenameFor(fd, ".pb.srcinfo.go")
    71  	f := gopoet.NewGoFile(path.Base(filename), pkg.ImportPath, pkg.Name)
    72  
    73  	f.FileComment = "Code generated by protoc-gen-gosrcinfo. DO NOT EDIT.\n" +
    74  		"source: " + fd.GetName()
    75  
    76  	siBytes, err := proto.Marshal(si)
    77  	if err != nil {
    78  		return fmt.Errorf("failed to serialize source code info: %w", err)
    79  	}
    80  	// Source code info has lots of repeated sequences of bytes in the 'path' field
    81  	// of locations, and the comments tend to be text that is reasonably well
    82  	// compressed. So we can make binaries a little smaller by gzipping first.
    83  	var encodedBuf bytes.Buffer
    84  	zipWriter := gzip.NewWriter(&encodedBuf)
    85  	if _, err := zipWriter.Write(siBytes); err != nil {
    86  		return fmt.Errorf("failed to compress source code info: %w", err)
    87  	}
    88  	if err := zipWriter.Close(); err != nil {
    89  		return fmt.Errorf("failed to compress source code info: %w", err)
    90  	}
    91  
    92  	srcInfoPkg := f.RegisterImport("github.com/Big-big-orange/protoreflect/desc/sourceinfo", "sourceinfo")
    93  
    94  	var srcInfoVarBlock gopoet.CodeBlock
    95  	srcInfoVarBlock.Println("srcInfo := []byte{")
    96  	encodedBytes := encodedBuf.Bytes()
    97  	for len(encodedBytes) > 0 {
    98  		var chunk []byte
    99  		if len(encodedBytes) < 16 {
   100  			chunk = encodedBytes
   101  			encodedBytes = nil
   102  		} else {
   103  			chunk = encodedBytes[:16]
   104  			encodedBytes = encodedBytes[16:]
   105  		}
   106  		for _, b := range chunk {
   107  			srcInfoVarBlock.Printf(" 0x%02x,", b)
   108  		}
   109  		srcInfoVarBlock.Println("")
   110  	}
   111  	srcInfoVarBlock.Println("}")
   112  	f.AddElement(gopoet.NewFunc("init").
   113  		AddCode(&srcInfoVarBlock).
   114  		Printlnf("if err := %sRegisterEncodedSourceInfo(%q, srcInfo); err != nil {", srcInfoPkg, fd.GetName()).
   115  		Println("    panic(err)").
   116  		Println("}"))
   117  
   118  	out := resp.OutputFile(filename)
   119  	return gopoet.WriteGoFile(out, f)
   120  }
   121  
   122  func clean(name string) string {
   123  	name = strings.TrimSuffix(name, ".proto")
   124  	data := ([]byte)(name)
   125  	for i, b := range data {
   126  		if (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') {
   127  			continue
   128  		}
   129  		data[i] = '_'
   130  	}
   131  	return string(data)
   132  }
   133  
   134  type codeGenArgs struct {
   135  	debug          bool
   136  	importPath     string
   137  	importMap      map[string]string
   138  	moduleRoot     string
   139  	sourceRelative bool
   140  }
   141  
   142  func parseArgs(args []string) (codeGenArgs, error) {
   143  	var result codeGenArgs
   144  	for _, arg := range args {
   145  		vals := strings.SplitN(arg, "=", 2)
   146  		switch vals[0] {
   147  		case "debug":
   148  			val, err := boolVal(vals)
   149  			if err != nil {
   150  				return result, err
   151  			}
   152  			result.debug = val
   153  
   154  		case "import_path":
   155  			if len(vals) == 1 {
   156  				return result, fmt.Errorf("plugin option 'import_path' requires an argument")
   157  			}
   158  			result.importPath = vals[1]
   159  
   160  		case "module":
   161  			if len(vals) == 1 {
   162  				return result, fmt.Errorf("plugin option 'module' requires an argument")
   163  			}
   164  			result.moduleRoot = vals[1]
   165  
   166  		case "paths":
   167  			if len(vals) == 1 {
   168  				return result, fmt.Errorf("plugin option 'paths' requires an argument")
   169  			}
   170  			switch vals[1] {
   171  			case "import":
   172  				result.sourceRelative = false
   173  			case "source_relative":
   174  				result.sourceRelative = true
   175  			default:
   176  				return result, fmt.Errorf("plugin option 'paths' accepts 'import' or 'source_relative' as value, got %q", vals[1])
   177  			}
   178  
   179  		default:
   180  			if len(vals[0]) > 1 && vals[0][0] == 'M' {
   181  				if len(vals) == 1 {
   182  					return result, fmt.Errorf("plugin 'M' options require an argument: %s", vals[0])
   183  				}
   184  				if result.importMap == nil {
   185  					result.importMap = map[string]string{}
   186  				}
   187  				result.importMap[vals[0][1:]] = vals[1]
   188  				break
   189  			}
   190  
   191  			return result, fmt.Errorf("unknown plugin option: %s", vals[0])
   192  		}
   193  	}
   194  
   195  	if result.sourceRelative && result.moduleRoot != "" {
   196  		return result, fmt.Errorf("plugin option 'module' cannot be used with 'paths=source_relative'")
   197  	}
   198  
   199  	return result, nil
   200  }
   201  
   202  func boolVal(vals []string) (bool, error) {
   203  	if len(vals) == 1 {
   204  		// if no value, assume "true"
   205  		return true, nil
   206  	}
   207  	switch strings.ToLower(vals[1]) {
   208  	case "true", "on", "yes", "1":
   209  		return true, nil
   210  	case "false", "off", "no", "0":
   211  		return false, nil
   212  	default:
   213  		return false, fmt.Errorf("invalid boolean arg for option '%s': %s", vals[0], vals[1])
   214  	}
   215  }