bufbuild/protocompile

How can I re generate source code after compiled

iyaozhen opened this issue · 5 comments

I use protocompile.Compiler xxx.proto and delete it.

And I use protoreflect.MessageDescriptor generate json template.

for example:

message Result {
  required string url = 1;
  optional string title = 2;
  repeated string snippets = 3;
}

got json template

{
    "url": "",
    "title": "",
    "snippets": []
}

But user fill json depend on source code, like some comment、enum.

I try sourceinfo.GenerateSourceInfo, just got some infomation, not original text.

And thank you for provided this lib.

jhump commented

@iyaozhen, I'm not quite sure I understand the scenario. But maybe what you are looking for is to set the compiler's SourceInfoMode field to SourceInfoStandard (it defaults to SourceInfoNone).

I used SourceInfoMode, But How can i get original source code.

func Test_getMessageSource2(t *testing.T) {
	sourceCode := `
syntax = "proto2";
package pb3;

// some comment
message Result {
  required string url = 1;
  optional string title = 2;
  repeated string snippets = 3;
}
`

	ctx := context.Background()
	resolver := &protocompile.SourceResolver{
		Accessor: protocompile.SourceAccessorFromMap(map[string]string{"pb3.proto": sourceCode}),
	}
	compiler := protocompile.Compiler{
		Resolver:       resolver,
		Reporter:       nil,
		SourceInfoMode: protocompile.SourceInfoExtraComments,
		RetainASTs:     true,
	}

	linkFiles, err := compiler.Compile(ctx, "pb3.proto")
	if err != nil {
		t.Error(err)
		return
	}
	linkFile := linkFiles.FindFileByPath("pb3.proto")
	//messageDesc := linkFile.FindDescriptorByName("pb3.Result")
	linkerResult := linkFile.(linker.Result)

	sourceInfo := sourceinfo.GenerateSourceInfoWithExtraComments(linkerResult.AST(), nil)
	// What can I do ?
}

I want convert(rebuild) linkerResult to sourceCode.

Forgive my poor English.

jhump commented

Okay. This library wasn't really designed to go that direction, but there are a couple of possibilities:

  1. Set the compiler's RetainASTs field to true. This will keep the AST for the source code in the linker.Result. From the AST you can reconstruct the original source as seen below.
  2. Instead of keeping the original source code, you could also generate the source code from the descriptor. You can do this using a package in a different library: github.com/jhump/protoreflect/desc/protoprint. When generating source from the descriptor, it is possible that some comments may not be preserved (because not all comments in the file are captured in the descriptor's source info). And the formatting will, of course, differ from the original source. But this technique can be used even if you don't have an AST from a parsed file.

Example of recovering original source from AST:

result := fileDescriptor.(linker.Result)
file := result.AST()
items := file.Items()
current := items.First()
// loop through all comments and tokens in AST
for {
    itemInfo := file.ItemInfo(current)
    // print each item
    _, err := fmt.Print(itemInfo.LeadingWhitespace())
    if err != nil {
        panic(err)
    }
    _, err = fmt.Print(itemInfo.RawText())
    if err != nil {
        panic(err)
    }

    var ok bool
    current, ok = items.Next(current)
    if !ok {
        break // done
    }
}

Example of generating source code from descriptor:

fd, err := desc.WrapFile(fileDescriptor)
if err != nil {
    panic(err)
}
var printer protoprint.Printer
err = printer.PrintProtoFile(fd, os.Stdout)
if err != nil {
    panic(err)
}

Thanks very mush, let me try it.

I find a other way to resolve this problem. Need store idl file orginal content.

func findMessageSourceCodeByDesc(linkFiles linker.Files, filesContent map[string][]byte,
	messageDesc protoreflect.Descriptor) string {
	filePath := messageDesc.ParentFile().Path()
	linkFile := linkFiles.FindFileByPath(filePath)
	if linkFile == nil {
		return ""
	}
	sourceLocations := linkFile.SourceLocations()
	fileContent, ok := filesContent[filePath]
	if !ok {
		return ""
	}
	fileContentLines := strings.Split(string(fileContent), "\n")

	messageContentLines := make([]string, 0)
	sourceLocation := sourceLocations.ByDescriptor(messageDesc)
	leadingComments := sourceLocation.LeadingComments
	if len(leadingComments) > 0 {
		for _, comment := range strings.Split(leadingComments, "\n") {
			comment = strings.TrimSpace(comment)
			if len(comment) == 0 {
				continue
			}
			messageContentLines = append(messageContentLines, fmt.Sprintf("// %s", comment))
		}
	}
	messageContentLines = append(messageContentLines, fileContentLines[sourceLocation.StartLine:sourceLocation.
		EndLine+1]...)

	return strings.Join(messageContentLines, "\n")
}