From 680ce254a3a8a6cd26d2ea43f92bdb6ce71d7ebc Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Tue, 21 Jul 2026 04:07:10 +0300 Subject: [PATCH 1/4] feat(compilers/protobuf): add Protocol Buffers and gRPC compiler Add a compiler that lowers .proto schemas into the spec-agnostic IR, the protobuf/gRPC counterpart to the OpenAPI compiler. It parses a single root .proto with bufbuild/protocompile, resolves well-known-type imports from the parser's bundle, and treats any other unresolved import as a diagnostic (compilers do no file I/O). Coverage spans proto2, proto3, and the 2023 edition: - messages, nested messages, and enums hoist into the flat type registry with IDs derived from their fully-qualified names; field numbers become wire IDs. - presence lowers to Property.Presence (implicit / explicit / required) with proto2 required, proto3 implicit vs optional, and editions features resolved by the parser; protobuf has no null, so nullability stays off. - oneof becomes a WireTagged, exclusive Union reached through one synthetic Flatten wrapper property; synthetic proto3-optional oneofs stay presence markers, not unions. - repeated and map fields hoist List and MapT container nodes; packed vs expanded is recorded on List.Encoding; sint/fixed/zigzag scalar variants lower to Encoding over the base primitive. - enums carry open/closed semantics and allow_alias duplicate values; reserved ranges and names, and custom options, are preserved verbatim in Extensions. - well-known types map faithfully: Timestamp/Duration to date-time primitives, Any/Struct/Value to the schemaless Any node, wrappers to a nullable primitive (or an External under policy), FieldMask/Empty to External. - extension fields attach to the message they extend with Property.ExtensionOf; extension ranges become Model.ExtensionRanges. - services become operation groups; each rpc is an Operation with a gRPC RPCBinding, streaming direction, and idempotency level; an Empty request or response lowers to an absent payload. --- compilers/protobuf/diag.go | 43 ++++ compilers/protobuf/doc.go | 10 + compilers/protobuf/extensions.go | 278 ++++++++++++++++++++++ compilers/protobuf/ids.go | 33 +++ compilers/protobuf/load.go | 152 ++++++++++++ compilers/protobuf/lower.go | 181 ++++++++++++++ compilers/protobuf/naming.go | 94 ++++++++ compilers/protobuf/options.go | 35 +++ compilers/protobuf/protobuf.go | 77 ++++++ compilers/protobuf/service.go | 267 +++++++++++++++++++++ compilers/protobuf/types.go | 397 +++++++++++++++++++++++++++++++ compilers/protobuf/wkt.go | 127 ++++++++++ go.mod | 2 + go.sum | 4 + 14 files changed, 1700 insertions(+) create mode 100644 compilers/protobuf/diag.go create mode 100644 compilers/protobuf/doc.go create mode 100644 compilers/protobuf/extensions.go create mode 100644 compilers/protobuf/ids.go create mode 100644 compilers/protobuf/load.go create mode 100644 compilers/protobuf/lower.go create mode 100644 compilers/protobuf/naming.go create mode 100644 compilers/protobuf/options.go create mode 100644 compilers/protobuf/protobuf.go create mode 100644 compilers/protobuf/service.go create mode 100644 compilers/protobuf/types.go create mode 100644 compilers/protobuf/wkt.go diff --git a/compilers/protobuf/diag.go b/compilers/protobuf/diag.go new file mode 100644 index 0000000..6a6c1e8 --- /dev/null +++ b/compilers/protobuf/diag.go @@ -0,0 +1,43 @@ +package protobuf + +import ( + "fmt" + + "github.com/dexpace/morphic/ir" +) + +// Stable diagnostic codes emitted by the protobuf compiler. Codes are stable +// strings so CI can allowlist them (ir-design §13). +const ( + // codeCompile reports a hard failure to parse or link the .proto source; the + // document cannot be lowered. + codeCompile = "protobuf/compile-error" + // codeUnresolvedImport reports an import the compiler could not resolve — + // any import other than a bundled well-known type, since the compiler + // receives a single root file and does no file I/O. + codeUnresolvedImport = "protobuf/unresolved-import" + // codeWarning reports a non-fatal finding surfaced by the parser. + codeWarning = "protobuf/warning" + // codeReserved reports reserved field numbers/names preserved verbatim in + // Extensions and guarded by the validate pass (ir-design §14 protobuf row). + codeReserved = "protobuf/reserved" + // codeCustomOptionDefinition reports an extension that defines a custom option + // (extend google.protobuf.*Options) rather than a data field; its identity is + // preserved but it contributes no model property. + codeCustomOptionDefinition = "protobuf/custom-option-definition" + // codeDegradedConstruct reports a construct preserved raw because the IR has no + // structural home for it. + codeDegradedConstruct = "protobuf/degraded-construct" +) + +// diagf builds an ir.Diagnostic with a formatted message. It is the single +// constructor for compiler diagnostics so severity, code, and provenance are +// always populated. +func diagf(sev ir.Severity, code string, prov ir.Provenance, format string, args ...any) ir.Diagnostic { + return ir.Diagnostic{ + Severity: sev, + Code: code, + Message: fmt.Sprintf(format, args...), + Provenance: prov, + } +} diff --git a/compilers/protobuf/doc.go b/compilers/protobuf/doc.go new file mode 100644 index 0000000..3bcb180 --- /dev/null +++ b/compilers/protobuf/doc.go @@ -0,0 +1,10 @@ +// Package protobuf lowers Protocol Buffers .proto schemas (proto2, proto3, and +// editions, including gRPC service definitions) into the Morphic IR. It +// implements compilers.Compiler. Parsing is delegated to +// github.com/bufbuild/protocompile, a pure-Go .proto compiler that produces +// fully linked descriptors with resolved edition features and bundled +// well-known-type imports. This package owns identity (fully-qualified-name +// derived IDs), the mapping of protobuf's presence/oneof/encoding semantics onto +// the IR, and lossless preservation of constructs the IR does not model +// structurally (reserved ranges, custom options, file options). +package protobuf diff --git a/compilers/protobuf/extensions.go b/compilers/protobuf/extensions.go new file mode 100644 index 0000000..92501f4 --- /dev/null +++ b/compilers/protobuf/extensions.go @@ -0,0 +1,278 @@ +package protobuf + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "sort" + + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/dexpace/morphic/ir" +) + +// maxOptionDepth bounds recursion through nested custom-option message values +// (styleguide bounded-recursion rule); deeply nested options degrade gracefully. +const maxOptionDepth = 32 + +// deprecationOf returns a Deprecation when the descriptor carries the standard +// `deprecated` option, else nil. +func deprecationOf(d protoreflect.Descriptor) *ir.Deprecation { + if optionBool(d, "deprecated") { + return &ir.Deprecation{} + } + return nil +} + +// optionBool reads a boolean standard option by name, returning false when the +// descriptor has no options or the option is unset. +func optionBool(d protoreflect.Descriptor, name protoreflect.Name) bool { + m := optionsMessage(d) + if m == nil { + return false + } + fd := m.Descriptor().Fields().ByName(name) + if fd == nil || fd.Kind() != protoreflect.BoolKind { + return false + } + return m.Get(fd).Bool() +} + +// optionsMessage returns the descriptor's options as a reflective message, or +// nil when no options are set. +func optionsMessage(d protoreflect.Descriptor) protoreflect.Message { + opts := d.Options() + if opts == nil { + return nil + } + m := opts.ProtoReflect() + if !m.IsValid() { + return nil + } + return m +} + +// customOptions renders a descriptor's custom (extension) options into +// Extensions, keyed by the option's fully-qualified name and namespaced under +// "protobuf:option:". Standard options the compiler models elsewhere are +// excluded; ordering is deterministic. +func (l *lowerer) customOptions(d protoreflect.Descriptor) ir.Extensions { + m := optionsMessage(d) + if m == nil { + return nil + } + type entry struct { + key string + raw ir.RawValue + } + var entries []entry + m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + if !fd.IsExtension() { + return true // standard options are modeled by dedicated fields + } + if raw, ok := renderValue(fd, v, 0); ok { + entries = append(entries, entry{"protobuf:option:" + string(fd.FullName()), raw}) + } + return true + }) + if len(entries) == 0 { + return nil + } + sort.Slice(entries, func(i, j int) bool { return entries[i].key < entries[j].key }) + ext := make(ir.Extensions, len(entries)) + for _, e := range entries { + ext[e.key] = e.raw + } + return ext +} + +// renderValue renders one option field value (scalar, message, list, or map) +// into deterministic JSON. +func renderValue(fd protoreflect.FieldDescriptor, v protoreflect.Value, depth int) (ir.RawValue, bool) { + if depth > maxOptionDepth { + return nil, false + } + switch { + case fd.IsList(): + return renderList(fd, v.List(), depth) + case fd.IsMap(): + return renderMap(fd, v.Map(), depth) + default: + return renderScalar(fd, v, depth) + } +} + +// renderList renders a repeated option value into a JSON array. +func renderList(fd protoreflect.FieldDescriptor, list protoreflect.List, depth int) (ir.RawValue, bool) { + parts := make([]json.RawMessage, 0, list.Len()) + for i := range list.Len() { + if raw, ok := renderScalar(fd, list.Get(i), depth); ok { + parts = append(parts, raw) + } + } + b, err := json.Marshal(parts) + if err != nil { + return nil, false + } + return ir.RawValue(b), true +} + +// renderMap renders a map option value into a JSON object with key-sorted +// entries for determinism. +func renderMap(fd protoreflect.FieldDescriptor, mp protoreflect.Map, depth int) (ir.RawValue, bool) { + type entry struct { + key string + raw json.RawMessage + } + var entries []entry + valField := fd.MapValue() + mp.Range(func(mk protoreflect.MapKey, v protoreflect.Value) bool { + if raw, ok := renderScalar(valField, v, depth); ok { + entries = append(entries, entry{mk.String(), raw}) + } + return true + }) + sort.Slice(entries, func(i, j int) bool { return entries[i].key < entries[j].key }) + return objectRaw(len(entries), func(i int) (string, json.RawMessage) { + return entries[i].key, entries[i].raw + }), true +} + +// renderScalar renders a singular option value into JSON: nested messages +// recurse, enums render by member name, bytes as base64, and other scalars as +// their JSON form. +func renderScalar(fd protoreflect.FieldDescriptor, v protoreflect.Value, depth int) (ir.RawValue, bool) { + switch fd.Kind() { + case protoreflect.MessageKind, protoreflect.GroupKind: + return renderMessage(v.Message(), depth+1) + case protoreflect.EnumKind: + if ev := fd.Enum().Values().ByNumber(v.Enum()); ev != nil { + return jsonRaw(string(ev.Name())) + } + return jsonRaw(int64(v.Enum())) + case protoreflect.BytesKind: + return jsonRaw(base64.StdEncoding.EncodeToString(v.Bytes())) + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind, + protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + return jsonRaw(v.Uint()) + default: + return jsonRaw(v.Interface()) + } +} + +// renderMessage renders a message option value into a JSON object with its set +// fields ordered by field number for determinism. +func renderMessage(m protoreflect.Message, depth int) (ir.RawValue, bool) { + if depth > maxOptionDepth { + return nil, false + } + type entry struct { + num int32 + key string + raw json.RawMessage + } + var entries []entry + m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + if raw, ok := renderValue(fd, v, depth); ok { + entries = append(entries, entry{int32(fd.Number()), string(fd.Name()), raw}) + } + return true + }) + sort.Slice(entries, func(i, j int) bool { return entries[i].num < entries[j].num }) + return objectRaw(len(entries), func(i int) (string, json.RawMessage) { + return entries[i].key, entries[i].raw + }), true +} + +// objectRaw assembles a JSON object from n key/value pairs supplied by at. +func objectRaw(n int, at func(int) (string, json.RawMessage)) ir.RawValue { + var b bytes.Buffer + b.WriteByte('{') + for i := range n { + if i > 0 { + b.WriteByte(',') + } + key, raw := at(i) + k, _ := json.Marshal(key) + b.Write(k) + b.WriteByte(':') + b.Write(raw) + } + b.WriteByte('}') + return ir.RawValue(b.Bytes()) +} + +// jsonRaw marshals a Go value into a RawValue. +func jsonRaw(v any) (ir.RawValue, bool) { + b, err := json.Marshal(v) + if err != nil { + return nil, false + } + return ir.RawValue(b), true +} + +// halfOpenRanges converts protobuf field ranges ([start, end)) into inclusive +// IR wire-ID ranges. +func halfOpenRanges(ranges protoreflect.FieldRanges) []ir.WireIDRange { + if ranges.Len() == 0 { + return nil + } + out := make([]ir.WireIDRange, 0, ranges.Len()) + for i := range ranges.Len() { + r := ranges.Get(i) + out = append(out, ir.WireIDRange{From: int(r[0]), To: int(r[1]) - 1}) + } + return out +} + +// inclusiveEnumRanges converts protobuf enum ranges ([start, end]) into +// inclusive IR wire-ID ranges. +func inclusiveEnumRanges(ranges protoreflect.EnumRanges) []ir.WireIDRange { + if ranges.Len() == 0 { + return nil + } + out := make([]ir.WireIDRange, 0, ranges.Len()) + for i := range ranges.Len() { + r := ranges.Get(i) + out = append(out, ir.WireIDRange{From: int(r[0]), To: int(r[1])}) + } + return out +} + +// nameList copies a protobuf reserved-name list into a plain string slice. +func nameList(names protoreflect.Names) []string { + if names.Len() == 0 { + return nil + } + out := make([]string, 0, names.Len()) + for i := range names.Len() { + out = append(out, string(names.Get(i))) + } + return out +} + +// reservedRaw renders reserved ranges and names into deterministic JSON, or nil +// when both are empty. +func reservedRaw(ranges []ir.WireIDRange, names []string) ir.RawValue { + if len(ranges) == 0 && len(names) == 0 { + return nil + } + payload := struct { + Ranges []ir.WireIDRange `json:"ranges,omitempty"` + Names []string `json:"names,omitempty"` + }{Ranges: ranges, Names: names} + b, err := json.Marshal(payload) + if err != nil { + return nil + } + return ir.RawValue(b) +} + +// mergeRaw sets key to raw in ext, allocating the map on first use. +func mergeRaw(ext ir.Extensions, key string, raw ir.RawValue) ir.Extensions { + if ext == nil { + ext = ir.Extensions{} + } + ext[key] = raw + return ext +} diff --git a/compilers/protobuf/ids.go b/compilers/protobuf/ids.go new file mode 100644 index 0000000..2e46e7e --- /dev/null +++ b/compilers/protobuf/ids.go @@ -0,0 +1,33 @@ +package protobuf + +import "github.com/dexpace/morphic/ir" + +// IDs are derived from a descriptor's fully-qualified proto name — the stable +// structural identity protobuf assigns every declaration, independent of any +// display/renaming (ir-design §3.1). No other code constructs IDs. + +// namedTypeID returns the stable ID of a message or enum by its fully-qualified +// name, e.g. "t/protobuf/example.v1.User". +func namedTypeID(fullName string) ir.TypeID { return ir.TypeID("t/protobuf/" + fullName) } + +// anonTypeID returns the stable ID of a hoisted anonymous type (a container or a +// oneof union) at scope, e.g. "t/anon/protobuf/example.v1.User.tags/list". +func anonTypeID(scope string) ir.TypeID { return ir.TypeID("t/anon/protobuf/" + scope) } + +// primTypeID returns the interned ID of primitive kind k. +func primTypeID(k ir.PrimKind) ir.TypeID { return ir.TypeID("t/prim/" + string(k)) } + +// anyTypeID is the shared ID of the schemaless Any node the well-known dynamic +// types (Any, Struct, Value, …) resolve to. +const anyTypeID ir.TypeID = "t/protobuf/any" + +// propID returns the stable ID of a field by its fully-qualified name, +// e.g. "p/protobuf/example.v1.User.id". +func propID(fullName string) ir.PropID { return ir.PropID("p/protobuf/" + fullName) } + +// opID returns the stable ID of an rpc method by its fully-qualified name. +func opID(fullName string) ir.OpID { return ir.OpID("op/protobuf/" + fullName) } + +// serviceID returns the stable ID of the document service for scope (the proto +// package, or the source path when the file declares no package). +func serviceID(scope string) ir.ServiceID { return ir.ServiceID("s/protobuf/" + scope) } diff --git a/compilers/protobuf/load.go b/compilers/protobuf/load.go new file mode 100644 index 0000000..b2be9cf --- /dev/null +++ b/compilers/protobuf/load.go @@ -0,0 +1,152 @@ +package protobuf + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "io/fs" + + "github.com/bufbuild/protocompile" + "github.com/bufbuild/protocompile/reporter" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/dexpace/morphic/compilers" + "github.com/dexpace/morphic/ir" +) + +// errParse marks a hard failure to parse the source — an I/O- or +// programmer-level fault distinct from a spec problem reported as a diagnostic. +var errParse = errors.New("parse source") + +// loaded is the successful output of the load phase: one fully linked root file +// descriptor plus the identity metadata the rest of the compiler needs. A nil +// *loaded with error-severity diagnostics means the source is a spec problem the +// compiler refuses to lower (a parse error or an unresolvable import). +type loaded struct { + File protoreflect.FileDescriptor // linked, feature-resolved root file + Format compilers.SourceFormat // "protobuf" + syntax digit + Source ir.SourceInfo // format tag, path, content hash +} + +// load parses, links, and feature-resolves one .proto source. Well-known-type +// imports resolve from the parser's bundle; any other import is unresolvable +// because the compiler holds only the root bytes and does no file I/O. Spec +// problems become ir.Diagnostic values; the Go error return is reserved for the +// programmer error of a parser panic. +// +//nolint:unparam // srcIndex varies once Compile drives a multi-source loop +func load(ctx context.Context, srcIndex int, src compilers.Source, _ Options) (*loaded, []ir.Diagnostic, error) { + var diags []ir.Diagnostic + rep := reporter.NewReporter( + func(err reporter.ErrorWithPos) error { + diags = append(diags, parseDiag(srcIndex, err)) + return err // stop at the first hard error; it is already recorded + }, + func(warn reporter.ErrorWithPos) { + diags = append(diags, diagf(ir.SeverityWarning, codeWarning, posOf(srcIndex, warn), "%s", warn.Error())) + }, + ) + + files, err := compileRoot(ctx, src, rep) + if err != nil { + if len(diags) == 0 { // reporter never fired: a resolution or internal error + diags = append(diags, diagf(ir.SeverityError, importOrCompileCode(err), + ir.Provenance{Source: srcIndex, Pointer: src.Path}, "%s", err.Error())) + } + return nil, diags, nil // refuse to lower, do not abort the batch + } + if len(files) == 0 { + return nil, diags, nil + } + + root := files[0] + return &loaded{ + File: root, + Format: compilers.SourceFormat{Name: "protobuf", Version: syntaxDigit(root)}, + Source: ir.SourceInfo{ + Format: "protobuf@" + syntaxDigit(root), + Path: src.Path, + Hash: sourceHash(src.Data), + }, + }, diags, nil +} + +// compileRoot links the single root file against the bundled well-known types. +// It converts a parser panic on degenerate input into an errParse error so the +// compiler upholds the no-panics-escape invariant. +func compileRoot(ctx context.Context, src compilers.Source, rep reporter.Reporter) (fds []protoreflect.FileDescriptor, err error) { + defer func() { + if r := recover(); r != nil { + fds, err = nil, fmt.Errorf("parser panicked (%v): %w", r, errParse) + } + }() + resolver := protocompile.WithStandardImports(&protocompile.SourceResolver{ + Accessor: func(path string) (io.ReadCloser, error) { + if path == src.Path { + return io.NopCloser(bytes.NewReader(src.Data)), nil + } + return nil, fs.ErrNotExist // any non-root, non-WKT import is unresolvable + }, + }) + c := protocompile.Compiler{ + Resolver: resolver, + SourceInfoMode: protocompile.SourceInfoStandard, // comments → Docs, positions → provenance + Reporter: rep, + } + compiled, err := c.Compile(ctx, src.Path) + if err != nil { + return nil, fmt.Errorf("compile %q: %w", src.Path, err) + } + out := make([]protoreflect.FileDescriptor, len(compiled)) + for i, f := range compiled { + out[i] = f + } + return out, nil +} + +// parseDiag converts one reporter error into a diagnostic, classifying an +// unresolved import distinctly from a general compile error. +func parseDiag(srcIndex int, err reporter.ErrorWithPos) ir.Diagnostic { + return diagf(ir.SeverityError, importOrCompileCode(err), posOf(srcIndex, err), "%s", err.Error()) +} + +// importOrCompileCode selects the unresolved-import code when the error is an +// import-resolution failure, else the general compile-error code. +func importOrCompileCode(err error) string { + if errors.Is(err, fs.ErrNotExist) { + return codeUnresolvedImport + } + return codeCompile +} + +// posOf builds line:col provenance from a positioned parser error. +func posOf(srcIndex int, err reporter.ErrorWithPos) ir.Provenance { + p := err.GetPosition() + return ir.Provenance{Source: srcIndex, Pointer: fmt.Sprintf("%d:%d", p.Line, p.Col)} +} + +// syntaxDigit maps a file's syntax to the version digit the compiler reports: +// "2", "3", or the edition string for editions files. +func syntaxDigit(fd protoreflect.FileDescriptor) string { + switch fd.Syntax() { + case protoreflect.Proto2: + return "2" + case protoreflect.Proto3: + return "3" + case protoreflect.Editions: + return "2023" + default: + return "3" + } +} + +// sourceHash returns the lowercase hex SHA-256 of the raw source bytes, used as +// the SourceInfo content hash for caching and golden-snapshot identity. +func sourceHash(data []byte) string { + sum := sha256.Sum256(data) + return hex.EncodeToString(sum[:]) +} diff --git a/compilers/protobuf/lower.go b/compilers/protobuf/lower.go new file mode 100644 index 0000000..53bb163 --- /dev/null +++ b/compilers/protobuf/lower.go @@ -0,0 +1,181 @@ +package protobuf + +import ( + "strings" + + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/dexpace/morphic/ir" +) + +// maxCommentLines caps how many leading-comment lines a Docs description keeps, +// bounding work on pathologically commented sources (styleguide bounded-loops). +const maxCommentLines = 4096 + +// lowerer is the single mutable context of one Compile call: a local, never a +// package global. It threads the IR document under construction, the interning +// guard, accumulated diagnostics, and the resolved options through the walk. +type lowerer struct { + srcIndex int + file protoreflect.FileDescriptor + source ir.SourceInfo + out *ir.Document + opts Options + diags []ir.Diagnostic +} + +// newLowerer allocates a lowerer over one loaded file, with an empty IR document +// and type registry ready for lowering. +// +//nolint:unparam // srcIndex varies once Compile drives a multi-source loop +func newLowerer(srcIndex int, ld *loaded, opts Options) *lowerer { + return &lowerer{ + srcIndex: srcIndex, + file: ld.File, + source: ld.Source, + out: &ir.Document{Types: ir.TypeRegistry{}}, + opts: opts, + } +} + +// lowerTypes hoists every message and enum declared in the file, recursing into +// nested declarations. Interning by ID makes each one lower exactly once, so a +// type unreferenced by any service still lands in the registry (as component +// schemas do in the OpenAPI compiler). +func (l *lowerer) lowerTypes() { + msgs := l.file.Messages() + for i := range msgs.Len() { + l.hoistMessage(msgs.Get(i)) + } + enums := l.file.Enums() + for i := range enums.Len() { + l.enumRef(enums.Get(i)) + } +} + +// hoistMessage interns md and recurses into its nested messages and enums. Map +// entry messages are synthetic (they model map storage) and never hoisted; +// their fields are read directly through the map field. +func (l *lowerer) hoistMessage(md protoreflect.MessageDescriptor) { + if md.IsMapEntry() { + return + } + l.messageRef(md) + nested := md.Messages() + for i := range nested.Len() { + l.hoistMessage(nested.Get(i)) + } + enums := md.Enums() + for i := range enums.Len() { + l.enumRef(enums.Get(i)) + } +} + +// messageRef interns the Model for md and returns a reference to it. The Model is +// registered before its fields are lowered, so a self- or cyclic reference +// reached during the walk resolves to the interned ID instead of recursing. +func (l *lowerer) messageRef(md protoreflect.MessageDescriptor) ir.TypeRef { + id := namedTypeID(string(md.FullName())) + if _, ok := l.out.Types[id]; !ok { + m := &ir.Model{TypeCommon: l.namedCommon(id, md)} + l.out.Types[id] = m + l.fillModel(m, md) + } + return ir.TypeRef{Target: id} +} + +// enumRef interns the Enum for ed and returns a reference to it. +func (l *lowerer) enumRef(ed protoreflect.EnumDescriptor) ir.TypeRef { + id := namedTypeID(string(ed.FullName())) + if _, ok := l.out.Types[id]; !ok { + e := &ir.Enum{TypeCommon: l.namedCommon(id, ed)} + l.out.Types[id] = e + l.fillEnum(e, ed) + } + return ir.TypeRef{Target: id} +} + +// primRef interns the primitive of kind k under its shared ID on first use and +// returns a reference to it. Primitives are leaves and never carry provenance +// pointers. +func (l *lowerer) primRef(k ir.PrimKind) ir.TypeRef { + id := primTypeID(k) + if _, ok := l.out.Types[id]; !ok { + l.out.Types[id] = &ir.Primitive{ + TypeCommon: ir.TypeCommon{ID: id, Provenance: ir.Provenance{Source: l.srcIndex}}, + Prim: k, + } + } + return ir.TypeRef{Target: id} +} + +// anyRef interns the shared schemaless Any node and returns a reference to it. +func (l *lowerer) anyRef() ir.TypeRef { + if _, ok := l.out.Types[anyTypeID]; !ok { + l.out.Types[anyTypeID] = &ir.Any{ + TypeCommon: ir.TypeCommon{ + ID: anyTypeID, + Name: ir.Naming{Hint: "any"}, + Anonymous: true, + Provenance: ir.Provenance{Source: l.srcIndex}, + }, + } + } + return ir.TypeRef{Target: anyTypeID} +} + +// namedCommon builds the TypeCommon shared by a hoisted message or enum: its +// stable ID, source/canonical name, declared package namespace, docs, and +// deprecation. +func (l *lowerer) namedCommon(id ir.TypeID, d protoreflect.Descriptor) ir.TypeCommon { + name := string(d.Name()) + common := ir.TypeCommon{ + ID: id, + Name: ir.Naming{Source: name, Canonical: canonicalWords(name)}, + Namespace: packageWords(string(l.file.Package())), + Docs: l.docsFor(d), + Provenance: ir.Provenance{Source: l.srcIndex, Pointer: string(d.FullName())}, + } + if dep := deprecationOf(d); dep != nil { + common.Deprecation = dep + } + return common +} + +// anonCommon builds the TypeCommon of a hoisted anonymous type (a container or a +// oneof union), carrying only a context-derived naming hint. +func (l *lowerer) anonCommon(id ir.TypeID, pointer, hint string) ir.TypeCommon { + return ir.TypeCommon{ + ID: id, + Name: ir.Naming{Hint: hint}, + Anonymous: true, + Provenance: ir.Provenance{Source: l.srcIndex, Pointer: pointer}, + } +} + +// docsFor builds Docs from a declaration's leading source comment. +func (l *lowerer) docsFor(d protoreflect.Descriptor) ir.Docs { + loc := l.file.SourceLocations().ByDescriptor(d) + desc := cleanComment(loc.LeadingComments) + if desc == "" { + return ir.Docs{} + } + return ir.Docs{Description: desc} +} + +// cleanComment normalizes a proto leading comment block into a plain paragraph: +// it strips the single leading space protoc records on each line and trims +// surrounding blank lines, holding no Markdown opinion. +func cleanComment(raw string) string { + if raw == "" { + return "" + } + lines := strings.Split(raw, "\n") + if len(lines) > maxCommentLines { + lines = lines[:maxCommentLines] + } + for i, line := range lines { + lines[i] = strings.TrimPrefix(line, " ") + } + return strings.TrimSpace(strings.Join(lines, "\n")) +} diff --git a/compilers/protobuf/naming.go b/compilers/protobuf/naming.go new file mode 100644 index 0000000..10505b0 --- /dev/null +++ b/compilers/protobuf/naming.go @@ -0,0 +1,94 @@ +package protobuf + +import ( + "strings" + "unicode" + + "google.golang.org/protobuf/reflect/protoreflect" +) + +// canonicalWords renders name as a neutral lower_snake word sequence: it splits +// on _/-/space/dot and on camel-case and letter/digit boundaries, lowercases, +// and joins with "_". It holds no acronym opinion beyond boundary detection; +// casing policy is an emitter concern (ir-design §3.2). +func canonicalWords(name string) string { + var words []string + var cur []rune + flush := func() { + if len(cur) > 0 { + words = append(words, strings.ToLower(string(cur))) + cur = cur[:0] + } + } + runes := []rune(name) + for i, r := range runes { + if r == '_' || r == '-' || r == ' ' || r == '.' { + flush() + continue + } + if len(cur) > 0 && wordBoundary(cur[len(cur)-1], r, runes, i) { + flush() + } + cur = append(cur, r) + } + flush() + return strings.Join(words, "_") +} + +// wordBoundary reports whether a new word starts at runes[i] given the previous +// accumulated rune prev. +func wordBoundary(prev, r rune, runes []rune, i int) bool { + switch { + case unicode.IsUpper(r) && (unicode.IsLower(prev) || unicode.IsDigit(prev)): + return true // lower/digit -> Upper: "userID" -> user|ID + case unicode.IsUpper(prev) && unicode.IsUpper(r) && i+1 < len(runes) && unicode.IsLower(runes[i+1]): + return true // acronym tail: "HTTPServer" -> HTTP|Server + case unicode.IsLetter(prev) && unicode.IsDigit(r), unicode.IsDigit(prev) && unicode.IsLetter(r): + return true // letter<->digit: "APIKey2" -> ...Key|2 + default: + return false + } +} + +// packageWords splits a proto package path ("example.v1") into its segments, +// used as a type's or service's Namespace. An empty package yields nil. +func packageWords(pkg string) []string { + if pkg == "" { + return nil + } + return strings.Split(pkg, ".") +} + +// jsonNameDefault reproduces protobuf's auto-derived JSON name for a field name: +// underscores are dropped and the following letter is upper-cased, leaving all +// other characters untouched ("created_at" → "createdAt"). It is the baseline an +// explicit json_name option is compared against so the IR never stores a +// compiler-derived camelCase as a wire name. +func jsonNameDefault(protoName string) string { + var b strings.Builder + b.Grow(len(protoName)) + upperNext := false + for _, r := range protoName { + if r == '_' { + upperNext = true + continue + } + if upperNext { + b.WriteRune(unicode.ToUpper(r)) + upperNext = false + continue + } + b.WriteRune(r) + } + return b.String() +} + +// explicitWireName returns a field's serialized name only when its json_name was +// explicitly overridden — i.e. it differs from the auto-derived default. An +// auto-derived camelCase is left to the emitter, which owns casing. +func explicitWireName(fd protoreflect.FieldDescriptor) string { + if name := string(fd.Name()); fd.JSONName() != jsonNameDefault(name) { + return fd.JSONName() + } + return "" +} diff --git a/compilers/protobuf/options.go b/compilers/protobuf/options.go new file mode 100644 index 0000000..07a16b7 --- /dev/null +++ b/compilers/protobuf/options.go @@ -0,0 +1,35 @@ +package protobuf + +// WrapperPolicy selects how the google.protobuf wrapper types (Int32Value, +// StringValue, …) lower into the IR. It is the injectable-policy seam +// (architecture principle 6): the mapping is a modeling choice, not a source +// fact, and can be switched. +type WrapperPolicy string + +// Wrapper policies. +const ( + // WrapperNullablePrimitive lowers each wrapper to its underlying primitive + // with a nullable reference (the default). This matches the wrappers' purpose + // — giving a scalar explicit presence — and their proto-JSON form, where a + // wrapper serializes as the bare value or null. + WrapperNullablePrimitive WrapperPolicy = "nullable-primitive" + // WrapperExternal lowers each wrapper to an External referencing the + // well-known runtime type, preserving the box as a distinct type. + WrapperExternal WrapperPolicy = "external" +) + +// Options configures the protobuf compiler. It is the concrete type this +// compiler expects in compilers.Options.FormatOptions; the zero value is valid +// and normalized by withDefaults. +type Options struct { + // Wrappers selects how google.protobuf wrapper types lower. + Wrappers WrapperPolicy `json:"wrappers,omitempty"` +} + +// withDefaults returns a copy of o with unset fields filled from the defaults. +func (o Options) withDefaults() Options { + if o.Wrappers == "" { + o.Wrappers = WrapperNullablePrimitive + } + return o +} diff --git a/compilers/protobuf/protobuf.go b/compilers/protobuf/protobuf.go new file mode 100644 index 0000000..bf974a2 --- /dev/null +++ b/compilers/protobuf/protobuf.go @@ -0,0 +1,77 @@ +package protobuf + +import ( + "context" + "fmt" + + "github.com/dexpace/morphic/compilers" + "github.com/dexpace/morphic/ir" +) + +// Compiler lowers Protocol Buffers .proto schemas into the IR. +type Compiler struct{} + +// New returns the protobuf compiler. +func New() *Compiler { return &Compiler{} } + +// Formats reports the protobuf dialects this compiler accepts. proto2, proto3, +// and the 2023 edition share one lowering; the parser resolves edition features +// into the same descriptor surface the proto2/proto3 paths produce. +func (*Compiler) Formats() []compilers.SourceFormat { + return []compilers.SourceFormat{ + {Name: "protobuf", Version: "2"}, + {Name: "protobuf", Version: "3"}, + {Name: "protobuf", Version: "2023"}, + } +} + +// Compile implements compilers.Compiler. It accepts exactly one root .proto +// source; imports other than bundled well-known types are unresolved (the +// compiler does no file I/O) and reported as diagnostics. +func (c *Compiler) Compile(ctx context.Context, sources []compilers.Source, opts compilers.Options) (*ir.Document, []ir.Diagnostic, error) { + if len(sources) != 1 { + return nil, nil, fmt.Errorf("protobuf: expected exactly one source, got %d", len(sources)) + } + formatOpts, err := optionsFrom(opts) // nil FormatOptions → defaults; wrong type → error + if err != nil { + return nil, nil, err + } + loadedFile, diags, err := load(ctx, 0, sources[0], formatOpts) + if err != nil || loadedFile == nil { + return nil, diags, err + } + l := newLowerer(0, loadedFile, formatOpts) + out := l.run() // types → extensions → service/operations → meta; assembles Document + //nolint:gocritic // deliberate concat: load diagnostics precede lowering diagnostics + out.Diagnostics = append(diags, l.diags...) + return out, out.Diagnostics, nil +} + +// run drives the lowering pipeline over one linked file. Order matters: all +// declared messages and enums are hoisted first so field, extension, and rpc +// references find interned IDs; then extension fields are attached to the +// messages they extend; then the service walk; then file-level metadata. It +// assembles and returns the Document. +func (l *lowerer) run() *ir.Document { + l.lowerTypes() + l.lowerExtensions() + l.out.Services = []ir.Service{l.lowerService()} + l.lowerMeta() + l.out.IRVersion = ir.IRVersion + l.out.Sources = []ir.SourceInfo{l.source} + return l.out +} + +// optionsFrom resolves the compiler-specific options: a nil FormatOptions gets +// defaults, a protobuf.Options value is normalized, and any other type is a +// programmer error. +func optionsFrom(opts compilers.Options) (Options, error) { + switch fo := opts.FormatOptions.(type) { + case nil: + return Options{}.withDefaults(), nil + case Options: + return fo.withDefaults(), nil + default: + return Options{}, fmt.Errorf("protobuf: FormatOptions must be protobuf.Options, got %T", opts.FormatOptions) + } +} diff --git a/compilers/protobuf/service.go b/compilers/protobuf/service.go new file mode 100644 index 0000000..a3fa2c9 --- /dev/null +++ b/compilers/protobuf/service.go @@ -0,0 +1,267 @@ +package protobuf + +import ( + "encoding/json" + "strings" + + "github.com/bufbuild/protocompile/protoutil" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/dexpace/morphic/ir" +) + +// lowerService lowers the file into the single document service, one +// OperationGroup per proto service (ir-design §7.1). A file with no services +// still yields the service so the document always carries one. +func (l *lowerer) lowerService() ir.Service { + pkg := string(l.file.Package()) + svc := ir.Service{ + ID: serviceID(l.serviceScope()), + Name: ir.Naming{Source: pkg, Canonical: canonicalWords(pkg)}, + Namespace: packageWords(pkg), + Provenance: ir.Provenance{Source: l.srcIndex, Pointer: l.file.Path()}, + } + services := l.file.Services() + for i := range services.Len() { + svc.Groups = append(svc.Groups, l.lowerOperationGroup(services.Get(i))) + } + return svc +} + +// serviceScope is the identity scope of the document service: the proto package, +// or the source path when the file declares no package. +func (l *lowerer) serviceScope() string { + if pkg := string(l.file.Package()); pkg != "" { + return pkg + } + return l.file.Path() +} + +// lowerOperationGroup lowers one proto service into an OperationGroup of rpc +// operations. +func (l *lowerer) lowerOperationGroup(sd protoreflect.ServiceDescriptor) ir.OperationGroup { + name := string(sd.Name()) + g := ir.OperationGroup{ + Name: ir.Naming{Source: name, Canonical: canonicalWords(name)}, + Docs: l.docsFor(sd), + } + if ext := l.customOptions(sd); len(ext) > 0 { + g.Extensions = ext + } + methods := sd.Methods() + for i := range methods.Len() { + g.Operations = append(g.Operations, l.lowerMethod(sd, methods.Get(i))) + } + return g +} + +// lowerMethod lowers one rpc into an Operation with a gRPC RPCBinding. The +// request/response messages carry the payloads; streaming modifiers and the +// idempotency level lower onto the neutral core. +func (l *lowerer) lowerMethod(sd protoreflect.ServiceDescriptor, md protoreflect.MethodDescriptor) ir.Operation { + name := string(md.Name()) + idem, level := methodIdempotency(md) + op := ir.Operation{ + ID: opID(string(md.FullName())), + Name: ir.Naming{Source: name, Canonical: canonicalWords(name)}, + Docs: l.docsFor(md), + Idempotency: idem, + Provenance: ir.Provenance{Source: l.srcIndex, Pointer: string(md.FullName())}, + } + inRef, inEmpty := l.rpcMessage(md.Input()) + outRef, outEmpty := l.rpcMessage(md.Output()) + rpc := &ir.RPCBinding{ + System: "grpc", + FullMethod: "/" + string(sd.FullName()) + "/" + name, + IdempotencyLevel: level, + } + if !inEmpty { + in := inRef + op.Request = &ir.Payload{Contents: []ir.Content{{Type: in}}} + rpc.InputType = &in + } + op.Responses = []ir.Response{rpcResponse(outRef, outEmpty)} + applyStreaming(&op, md) + op.Bindings = ir.OpBindings{RPC: rpc} + if dep := deprecationOf(md); dep != nil { + op.Deprecation = dep + } + if ext := l.customOptions(md); len(ext) > 0 { + op.Extensions = ext + } + return op +} + +// rpcMessage resolves an rpc request/response message type, reporting whether it +// is google.protobuf.Empty (which lowers to an absent payload, not a type). +func (l *lowerer) rpcMessage(md protoreflect.MessageDescriptor) (ir.TypeRef, bool) { + if string(md.FullName()) == "google.protobuf.Empty" { + return ir.TypeRef{}, true + } + return l.messageOrWKT(md), false +} + +// rpcResponse builds the single response of an rpc: empty Conditions (RPC has no +// status codes) and a payload unless the response message is Empty. +func rpcResponse(ref ir.TypeRef, empty bool) ir.Response { + resp := ir.Response{Name: ir.Naming{Hint: "response"}} + if !empty { + resp.Payload = &ir.Payload{Contents: []ir.Content{{Type: ref}}} + } + return resp +} + +// applyStreaming records the operation's streaming direction from the rpc's +// client/server stream modifiers. +func applyStreaming(op *ir.Operation, md protoreflect.MethodDescriptor) { + cs, ss := md.IsStreamingClient(), md.IsStreamingServer() + switch { + case cs && ss: + op.Streaming = ir.StreamingBidi + op.RequestStream = &ir.StreamDetail{} + op.ResponseStream = &ir.StreamDetail{} + case cs: + op.Streaming = ir.StreamingClient + op.RequestStream = &ir.StreamDetail{} + case ss: + op.Streaming = ir.StreamingServer + op.ResponseStream = &ir.StreamDetail{} + } +} + +// methodIdempotency maps the rpc idempotency_level option onto the IR +// idempotency classification and its raw level string. +func methodIdempotency(md protoreflect.MethodDescriptor) (ir.Idempotency, string) { + m := optionsMessage(md) + if m == nil { + return ir.Idempotency{}, "" + } + fd := m.Descriptor().Fields().ByName("idempotency_level") + if fd == nil || fd.Kind() != protoreflect.EnumKind { + return ir.Idempotency{}, "" + } + switch m.Get(fd).Enum() { + case 1: // NO_SIDE_EFFECTS + return ir.Idempotency{Kind: ir.IdempotencySafe}, "NO_SIDE_EFFECTS" + case 2: // IDEMPOTENT + return ir.Idempotency{Kind: ir.IdempotencyIdempotent}, "IDEMPOTENT" + default: + return ir.Idempotency{}, "" + } +} + +// lowerExtensions attaches every proto2 extension field to the message it +// extends, walking file-level and message-nested extend blocks. +func (l *lowerer) lowerExtensions() { + l.lowerExtensionSet(l.file.Extensions()) + msgs := l.file.Messages() + for i := range msgs.Len() { + l.lowerMessageExtensions(msgs.Get(i)) + } +} + +// lowerMessageExtensions attaches extend blocks nested in a message and recurses. +func (l *lowerer) lowerMessageExtensions(md protoreflect.MessageDescriptor) { + l.lowerExtensionSet(md.Extensions()) + nested := md.Messages() + for i := range nested.Len() { + l.lowerMessageExtensions(nested.Get(i)) + } +} + +// lowerExtensionSet lowers each extension descriptor in a set. +func (l *lowerer) lowerExtensionSet(exts protoreflect.ExtensionDescriptors) { + for i := range exts.Len() { + l.lowerExtensionField(exts.Get(i)) + } +} + +// lowerExtensionField attaches one extension field to the Model it extends, +// recording its declaring scope in Property.ExtensionOf. An extension that +// targets a well-known options message defines a custom option and is preserved +// at document level instead of contributing a data property. +func (l *lowerer) lowerExtensionField(ext protoreflect.FieldDescriptor) { + extended := ext.ContainingMessage() + model, ok := l.out.Types[namedTypeID(string(extended.FullName()))].(*ir.Model) + if !ok { + l.recordOptionDefinition(ext, extended) + return + } + prop := l.lowerField(ext) + prop.ExtensionOf = extensionScope(ext) + model.Properties = append(model.Properties, prop) +} + +// recordOptionDefinition preserves a custom-option-defining extension at document +// level and emits an info diagnostic; it contributes no model property. +func (l *lowerer) recordOptionDefinition(ext protoreflect.FieldDescriptor, extended protoreflect.MessageDescriptor) { + def := map[string]any{ + "extends": string(extended.FullName()), + "number": int32(ext.Number()), + "name": string(ext.FullName()), + } + if raw, err := json.Marshal(def); err == nil { + l.out.Extensions = mergeRaw(l.out.Extensions, + "protobuf:custom-option:"+string(ext.FullName()), ir.RawValue(raw)) + } + l.diags = append(l.diags, diagf(ir.SeverityInfo, codeCustomOptionDefinition, + ir.Provenance{Source: l.srcIndex, Pointer: string(ext.FullName())}, + "extension %s defines a custom option on %s", ext.FullName(), extended.FullName())) +} + +// extensionScope is the fully-qualified declaring scope of an extension field: +// its full name with the field name removed. +func extensionScope(ext protoreflect.FieldDescriptor) string { + full := string(ext.FullName()) + if i := strings.LastIndex(full, "."); i >= 0 { + return full[:i] + } + return full +} + +// lowerMeta records file-level metadata: the package as the document name and +// the file's syntax, edition, and standard options preserved in Extensions +// (ir-design §12, per-file metadata keyed under Document.Extensions). +func (l *lowerer) lowerMeta() { + l.out.Name = string(l.file.Package()) + if raw := l.fileOptionsRaw(); raw != nil { + l.out.Extensions = mergeRaw(l.out.Extensions, "protobuf:file", raw) + } +} + +// fileOptionsRaw renders the file's syntax, edition, and set standard options +// into deterministic JSON. +func (l *lowerer) fileOptionsRaw() ir.RawValue { + fields := map[string]any{"syntax": l.file.Syntax().String()} + fdp := protoutil.ProtoFromFileDescriptor(l.file) + if l.file.Syntax() == protoreflect.Editions { + fields["edition"] = fdp.GetEdition().String() + } + if o := fdp.GetOptions(); o != nil { + putStr(fields, "goPackage", o.GetGoPackage()) + putStr(fields, "javaPackage", o.GetJavaPackage()) + putStr(fields, "javaOuterClassname", o.GetJavaOuterClassname()) + putStr(fields, "csharpNamespace", o.GetCsharpNamespace()) + putStr(fields, "objcClassPrefix", o.GetObjcClassPrefix()) + putStr(fields, "phpNamespace", o.GetPhpNamespace()) + putStr(fields, "rubyPackage", o.GetRubyPackage()) + if o.GetJavaMultipleFiles() { + fields["javaMultipleFiles"] = true + } + if o.GetDeprecated() { + fields["deprecated"] = true + } + } + b, err := json.Marshal(fields) + if err != nil { + return nil + } + return ir.RawValue(b) +} + +// putStr sets key to v in fields when v is non-empty. +func putStr(fields map[string]any, key, v string) { + if v != "" { + fields[key] = v + } +} diff --git a/compilers/protobuf/types.go b/compilers/protobuf/types.go new file mode 100644 index 0000000..f62a9c0 --- /dev/null +++ b/compilers/protobuf/types.go @@ -0,0 +1,397 @@ +package protobuf + +import ( + "math" + "strconv" + + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/dexpace/morphic/ir" +) + +// fillModel populates a registered Model from its message descriptor: metadata +// (extension ranges, reserved names, custom options) and then its properties. +func (l *lowerer) fillModel(m *ir.Model, md protoreflect.MessageDescriptor) { + l.applyMessageMeta(m, md) + m.Properties = l.lowerFields(md) +} + +// applyMessageMeta attaches a message's extension ranges and its reserved and +// custom-option metadata (the latter two preserved in Extensions). +func (l *lowerer) applyMessageMeta(m *ir.Model, md protoreflect.MessageDescriptor) { + if ranges := halfOpenRanges(md.ExtensionRanges()); len(ranges) > 0 { + m.ExtensionRanges = ranges + } + ext := l.customOptions(md) + reserved := halfOpenRanges(md.ReservedRanges()) + names := nameList(md.ReservedNames()) + if raw := reservedRaw(reserved, names); raw != nil { + ext = mergeRaw(ext, "protobuf:reserved", raw) + l.diags = append(l.diags, diagf(ir.SeverityInfo, codeReserved, m.Provenance, + "reserved field numbers/names for %s preserved in Extensions", md.FullName())) + } + if len(ext) > 0 { + m.Extensions = ext + } +} + +// lowerFields lowers a message's fields in source order. Each non-synthetic +// oneof collapses into one synthetic wrapper property (a WireTagged Union) +// emitted at the position of its first member; synthetic oneofs (proto3 +// optional) are presence markers and lower as ordinary fields. +func (l *lowerer) lowerFields(md protoreflect.MessageDescriptor) []ir.Property { + fields := md.Fields() + emitted := make(map[protoreflect.Name]bool) + props := make([]ir.Property, 0, fields.Len()) + for i := range fields.Len() { + fd := fields.Get(i) + if oo := fd.ContainingOneof(); oo != nil && !oo.IsSynthetic() { + if !emitted[oo.Name()] { + emitted[oo.Name()] = true + props = append(props, l.lowerOneof(oo)) + } + continue + } + props = append(props, l.lowerField(fd)) + } + return props +} + +// lowerField lowers one ordinary (non-oneof) field into a Property, carrying its +// wire ID, presence discipline, type, default, and metadata. +func (l *lowerer) lowerField(fd protoreflect.FieldDescriptor) ir.Property { + name := string(fd.Name()) + num := int(fd.Number()) + p := ir.Property{ + ID: propID(string(fd.FullName())), + Name: ir.Naming{Source: name, Canonical: canonicalWords(name)}, + WireID: &num, + Docs: l.docsFor(fd), + Provenance: ir.Provenance{Source: l.srcIndex, Pointer: string(fd.FullName())}, + } + p.WireName = explicitWireName(fd) + l.applyFieldType(&p, fd) + if fd.HasDefault() { + p.Default = l.lowerDefault(fd) + } + if dep := deprecationOf(fd); dep != nil { + p.Deprecation = dep + } + if ext := l.customOptions(fd); len(ext) > 0 { + p.Extensions = ext + } + return p +} + +// applyFieldType sets a property's Type, Encoding, Required, and Presence from +// the field's cardinality and kind. Repeated and map fields hoist container +// nodes; singular fields resolve their presence discipline (ir-design §5.1). +func (l *lowerer) applyFieldType(p *ir.Property, fd protoreflect.FieldDescriptor) { + switch { + case fd.IsMap(): + p.Type = l.mapRef(fd) + case fd.IsList(): + p.Type = l.listRef(fd) + default: + ref, enc := l.leafType(fd) + p.Type = ref + p.Encoding = enc + if fd.Cardinality() == protoreflect.Required { + p.Required = true + p.Presence = ir.PresenceRequired + return + } + if fd.HasPresence() { + p.Presence = ir.PresenceExplicit + return + } + p.Presence = ir.PresenceImplicit + } +} + +// lowerOneof builds the synthetic wrapper property for a real oneof: a Flatten +// property whose type is a hoisted WireTagged, Exclusive Union of the members. +func (l *lowerer) lowerOneof(oo protoreflect.OneofDescriptor) ir.Property { + name := string(oo.Name()) + return ir.Property{ + ID: propID(string(oo.FullName())), + Name: ir.Naming{Source: name, Canonical: canonicalWords(name)}, + Type: l.unionRef(oo), + Flatten: true, // oneof members are top-level wire fields + Presence: ir.PresenceExplicit, // which member (if any) is set is observable + Docs: l.docsFor(oo), + Provenance: ir.Provenance{Source: l.srcIndex, Pointer: string(oo.FullName())}, + } +} + +// unionRef interns the Union for a oneof and returns a reference to it. The Union +// is registered before its variants are lowered so a variant referencing the +// enclosing message resolves to an interned ID. +func (l *lowerer) unionRef(oo protoreflect.OneofDescriptor) ir.TypeRef { + scope := string(oo.FullName()) + id := anonTypeID(scope + "/oneof") + if _, ok := l.out.Types[id]; !ok { + u := &ir.Union{ + TypeCommon: l.anonCommon(id, scope, canonicalWords(string(oo.Name()))), + Exclusive: true, + WireTagged: true, + } + l.out.Types[id] = u + u.Variants = l.oneofVariants(oo) + } + return ir.TypeRef{Target: id} +} + +// oneofVariants lowers a oneof's member fields into union variants, preserving +// each member's field number as the variant wire ID. +func (l *lowerer) oneofVariants(oo protoreflect.OneofDescriptor) []ir.Variant { + fields := oo.Fields() + vars := make([]ir.Variant, 0, fields.Len()) + for i := range fields.Len() { + fd := fields.Get(i) + name := string(fd.Name()) + num := int(fd.Number()) + v := ir.Variant{ + Name: ir.Naming{Source: name, Canonical: canonicalWords(name)}, + Type: l.elementRef(fd), + WireID: &num, + Docs: l.docsFor(fd), + } + v.WireName = explicitWireName(fd) + if dep := deprecationOf(fd); dep != nil { + v.Deprecation = dep + } + vars = append(vars, v) + } + return vars +} + +// listRef interns the List node for a repeated field. The container encoding +// (packed vs expanded) is recorded for packable element kinds and stacks with +// any element-level encoding carried by the element type. +func (l *lowerer) listRef(fd protoreflect.FieldDescriptor) ir.TypeRef { + id := anonTypeID(string(fd.FullName()) + "/list") + if _, ok := l.out.Types[id]; !ok { + list := &ir.List{ + TypeCommon: l.anonCommon(id, string(fd.FullName()), canonicalWords(string(fd.Name()))), + Elem: l.elementRef(fd), + Encoding: listEncoding(fd), + } + l.out.Types[id] = list + } + return ir.TypeRef{Target: id} +} + +// mapRef interns the MapT node for a map field, resolving key and value element +// types (each wrapped in a Scalar when it carries a wire encoding). +func (l *lowerer) mapRef(fd protoreflect.FieldDescriptor) ir.TypeRef { + id := anonTypeID(string(fd.FullName()) + "/map") + if _, ok := l.out.Types[id]; !ok { + l.out.Types[id] = &ir.MapT{ + TypeCommon: l.anonCommon(id, string(fd.FullName()), canonicalWords(string(fd.Name()))), + Key: l.elementRef(fd.MapKey()), + Value: l.elementRef(fd.MapValue()), + } + } + return ir.TypeRef{Target: id} +} + +// elementRef resolves the element/key/value type of a nested position (list +// element, map entry, or oneof variant). A packable scalar carrying a wire +// encoding (sint/fixed) is wrapped in a hoisted Scalar so the encoding survives +// where a bare TypeRef has no encoding slot; every other leaf resolves directly. +func (l *lowerer) elementRef(fd protoreflect.FieldDescriptor) ir.TypeRef { + if prim, enc, ok := scalarPrim(fd.Kind()); ok && enc != "" { + return l.scalarWrap(fd, l.primRef(prim), &ir.Encoding{Name: enc}) + } + ref, _ := l.leafType(fd) + return ref +} + +// scalarWrap interns a Scalar node that restricts base with a wire encoding, +// used for encoded scalars in nested positions. +func (l *lowerer) scalarWrap(fd protoreflect.FieldDescriptor, base ir.TypeRef, enc *ir.Encoding) ir.TypeRef { + id := anonTypeID(string(fd.FullName()) + "/elem") + if _, ok := l.out.Types[id]; !ok { + b := base + l.out.Types[id] = &ir.Scalar{ + TypeCommon: l.anonCommon(id, string(fd.FullName()), canonicalWords(string(fd.Name()))), + Base: &b, + Encoding: enc, + } + } + return ir.TypeRef{Target: id} +} + +// leafType resolves a field's scalar/message/enum leaf into a TypeRef plus any +// property-level wire encoding (zigzag/fixed for scalars, delimited for groups). +func (l *lowerer) leafType(fd protoreflect.FieldDescriptor) (ir.TypeRef, *ir.Encoding) { + if prim, enc, ok := scalarPrim(fd.Kind()); ok { + ref := l.primRef(prim) + if enc != "" { + return ref, &ir.Encoding{Name: enc} + } + return ref, nil + } + switch fd.Kind() { + case protoreflect.EnumKind: + return l.enumRef(fd.Enum()), nil + case protoreflect.GroupKind: + return l.messageOrWKT(fd.Message()), &ir.Encoding{Name: "delimited"} + case protoreflect.MessageKind: + return l.messageOrWKT(fd.Message()), nil + default: + return l.anyRef(), nil + } +} + +// scalarPrim maps a protobuf scalar kind to its IR primitive and the name of its +// wire encoding when the kind is an encoded variant of that primitive (sint* → +// zigzag, fixed*/sfixed* → fixed). It reports false for non-scalar kinds. +func scalarPrim(k protoreflect.Kind) (ir.PrimKind, string, bool) { + switch k { + case protoreflect.BoolKind: + return ir.PrimBool, "", true + case protoreflect.StringKind: + return ir.PrimString, "", true + case protoreflect.BytesKind: + return ir.PrimBytes, "", true + case protoreflect.Int32Kind: + return ir.PrimInt32, "", true + case protoreflect.Sint32Kind: + return ir.PrimInt32, "zigzag", true + case protoreflect.Sfixed32Kind: + return ir.PrimInt32, "fixed", true + case protoreflect.Uint32Kind: + return ir.PrimUint32, "", true + case protoreflect.Fixed32Kind: + return ir.PrimUint32, "fixed", true + case protoreflect.Int64Kind: + return ir.PrimInt64, "", true + case protoreflect.Sint64Kind: + return ir.PrimInt64, "zigzag", true + case protoreflect.Sfixed64Kind: + return ir.PrimInt64, "fixed", true + case protoreflect.Uint64Kind: + return ir.PrimUint64, "", true + case protoreflect.Fixed64Kind: + return ir.PrimUint64, "fixed", true + case protoreflect.FloatKind: + return ir.PrimFloat32, "", true + case protoreflect.DoubleKind: + return ir.PrimFloat64, "", true + default: + return "", "", false + } +} + +// listEncoding reports the container-level encoding of a repeated field: packed +// or expanded for packable element kinds, nil for strings, bytes, and messages +// (which are never packed). +func listEncoding(fd protoreflect.FieldDescriptor) *ir.Encoding { + if !packable(fd.Kind()) { + return nil + } + if fd.IsPacked() { + return &ir.Encoding{Name: "packed"} + } + return &ir.Encoding{Name: "expanded"} +} + +// packable reports whether a repeated element kind is subject to packed +// encoding (numeric, bool, and enum kinds are; length-delimited kinds are not). +func packable(k protoreflect.Kind) bool { + switch k { + case protoreflect.StringKind, protoreflect.BytesKind, + protoreflect.MessageKind, protoreflect.GroupKind: + return false + default: + return true + } +} + +// fillEnum populates a registered Enum from its descriptor. Proto enums are +// int32-valued; openness follows the resolved closed/open semantics, and +// duplicate member values (allow_alias) survive as distinct members. +func (l *lowerer) fillEnum(e *ir.Enum, ed protoreflect.EnumDescriptor) { + e.ValueType = ir.PrimInt32 + e.Closed = ed.IsClosed() + vals := ed.Values() + e.Members = make([]ir.EnumMember, 0, vals.Len()) + for i := range vals.Len() { + v := vals.Get(i) + name := string(v.Name()) + mem := ir.EnumMember{ + Name: ir.Naming{Source: name, Canonical: canonicalWords(name)}, + Value: ir.Value{Kind: ir.ValueNumber, Num: ir.BigVal(strconv.FormatInt(int64(v.Number()), 10))}, + Docs: l.docsFor(v), + } + if dep := deprecationOf(v); dep != nil { + mem.Deprecation = dep + } + if ext := l.customOptions(v); len(ext) > 0 { + mem.Extensions = ext + } + e.Members = append(e.Members, mem) + } + l.applyEnumMeta(e, ed) +} + +// applyEnumMeta attaches an enum's reserved and custom-option metadata, +// preserved in Extensions (enum reserved ranges are inclusive). +func (l *lowerer) applyEnumMeta(e *ir.Enum, ed protoreflect.EnumDescriptor) { + ext := l.customOptions(ed) + if raw := reservedRaw(inclusiveEnumRanges(ed.ReservedRanges()), nameList(ed.ReservedNames())); raw != nil { + ext = mergeRaw(ext, "protobuf:reserved", raw) + l.diags = append(l.diags, diagf(ir.SeverityInfo, codeReserved, e.Provenance, + "reserved enum numbers/names for %s preserved in Extensions", ed.FullName())) + } + if len(ext) > 0 { + e.Extensions = ext + } +} + +// lowerDefault lowers a proto2 field default into the Values channel. Numeric +// defaults are carried as BigVal decimal strings, never float64 (ir-design §6). +func (l *lowerer) lowerDefault(fd protoreflect.FieldDescriptor) *ir.Value { + d := fd.Default() + switch fd.Kind() { + case protoreflect.BoolKind: + return &ir.Value{Kind: ir.ValueBool, Bool: d.Bool()} + case protoreflect.StringKind: + return &ir.Value{Kind: ir.ValueString, Str: d.String()} + case protoreflect.BytesKind: + return &ir.Value{Kind: ir.ValueBytes, Bytes: d.Bytes()} + case protoreflect.EnumKind: + ev := fd.DefaultEnumValue() + return &ir.Value{Kind: ir.ValueRefKind, Ref: &ir.ValueRef{ + Type: namedTypeID(string(fd.Enum().FullName())), Member: string(ev.Name())}} + case protoreflect.FloatKind, protoreflect.DoubleKind: + return l.floatDefault(fd, d.Float()) + default: + return l.numericDefault(fd, d) + } +} + +// floatDefault lowers a floating default, degrading non-finite defaults +// (inf/nan, which proto permits) to a diagnostic rather than an invalid BigVal. +func (l *lowerer) floatDefault(fd protoreflect.FieldDescriptor, f float64) *ir.Value { + if math.IsInf(f, 0) || math.IsNaN(f) { + l.diags = append(l.diags, diagf(ir.SeverityInfo, codeDegradedConstruct, + ir.Provenance{Source: l.srcIndex, Pointer: string(fd.FullName())}, + "non-finite default for %s dropped (not representable as a decimal)", fd.FullName())) + return nil + } + return &ir.Value{Kind: ir.ValueNumber, Num: ir.BigVal(strconv.FormatFloat(f, 'g', -1, 64))} +} + +// numericDefault lowers an integral field default (signed or unsigned). +func (l *lowerer) numericDefault(fd protoreflect.FieldDescriptor, d protoreflect.Value) *ir.Value { + switch fd.Kind() { + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind, + protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + return &ir.Value{Kind: ir.ValueNumber, Num: ir.BigVal(strconv.FormatUint(d.Uint(), 10))} + default: + return &ir.Value{Kind: ir.ValueNumber, Num: ir.BigVal(strconv.FormatInt(d.Int(), 10))} + } +} diff --git a/compilers/protobuf/wkt.go b/compilers/protobuf/wkt.go new file mode 100644 index 0000000..ec9f259 --- /dev/null +++ b/compilers/protobuf/wkt.go @@ -0,0 +1,127 @@ +package protobuf + +import ( + "strings" + + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/dexpace/morphic/ir" +) + +// Go import paths of the well-known runtime packages, recorded on External nodes +// so an emitter can resolve them to library types. +const ( + pkgKnown = "google.golang.org/protobuf/types/known" + pkgWrappers = pkgKnown + "/wrapperspb" + pkgEmpty = pkgKnown + "/emptypb" + pkgFieldMask = pkgKnown + "/fieldmaskpb" +) + +// messageOrWKT resolves a message-typed reference. Well-known types map to the +// faithful IR node (a date/time primitive, Any, a nullable primitive, or an +// External); every other well-known type from google/protobuf preserves as an +// External so its internals are never hoisted; user messages hoist normally. +func (l *lowerer) messageOrWKT(md protoreflect.MessageDescriptor) ir.TypeRef { + fn := string(md.FullName()) + if ref, ok := l.wellKnown(fn); ok { + return ref + } + if isWellKnownFile(md.ParentFile()) { + return l.externalRef(fn, pkgKnown) + } + return l.messageRef(md) +} + +// wellKnown maps a recognized google.protobuf type to its IR lowering, reporting +// false for types with no special mapping. +func (l *lowerer) wellKnown(fn string) (ir.TypeRef, bool) { + switch fn { + case "google.protobuf.Timestamp": + return l.primRef(ir.PrimDatetime), true + case "google.protobuf.Duration": + return l.primRef(ir.PrimDuration), true + case "google.protobuf.Any", + "google.protobuf.Struct", + "google.protobuf.Value", + "google.protobuf.ListValue", + "google.protobuf.NullValue": + return l.anyRef(), true + case "google.protobuf.Empty": + return l.externalRef(fn, pkgEmpty), true + case "google.protobuf.FieldMask": + return l.externalRef(fn, pkgFieldMask), true + } + if prim, ok := wrapperPrim(fn); ok { + return l.wrapperRef(fn, prim), true + } + return ir.TypeRef{}, false +} + +// wrapperRef lowers a google.protobuf wrapper per the configured policy: a +// nullable primitive (default) or an External box. +func (l *lowerer) wrapperRef(fn string, prim ir.PrimKind) ir.TypeRef { + if l.opts.Wrappers == WrapperExternal { + return l.externalRef(fn, pkgWrappers) + } + ref := l.primRef(prim) + ref.Nullable = true + return ref +} + +// wrapperPrim maps a google.protobuf wrapper type to its underlying primitive. +func wrapperPrim(fn string) (ir.PrimKind, bool) { + switch fn { + case "google.protobuf.DoubleValue": + return ir.PrimFloat64, true + case "google.protobuf.FloatValue": + return ir.PrimFloat32, true + case "google.protobuf.Int64Value": + return ir.PrimInt64, true + case "google.protobuf.UInt64Value": + return ir.PrimUint64, true + case "google.protobuf.Int32Value": + return ir.PrimInt32, true + case "google.protobuf.UInt32Value": + return ir.PrimUint32, true + case "google.protobuf.BoolValue": + return ir.PrimBool, true + case "google.protobuf.StringValue": + return ir.PrimString, true + case "google.protobuf.BytesValue": + return ir.PrimBytes, true + default: + return "", false + } +} + +// externalRef interns an External node for a well-known library type identified +// by its full proto name, and returns a reference to it. +func (l *lowerer) externalRef(identity, pkg string) ir.TypeRef { + id := ir.TypeID("t/protobuf/external/" + identity) + if _, ok := l.out.Types[id]; !ok { + l.out.Types[id] = &ir.External{ + TypeCommon: ir.TypeCommon{ + ID: id, + Name: ir.Naming{Source: identity, Canonical: canonicalWords(lastSegment(identity))}, + Provenance: ir.Provenance{Source: l.srcIndex, Pointer: identity}, + }, + Identity: identity, + Package: pkg, + } + } + return ir.TypeRef{Target: id} +} + +// isWellKnownFile reports whether a file descriptor is one of the bundled +// google/protobuf well-known-type definitions. +func isWellKnownFile(fd protoreflect.FileDescriptor) bool { + return strings.HasPrefix(fd.Path(), "google/protobuf/") +} + +// lastSegment returns the final dot-separated segment of a qualified name. +func lastSegment(name string) string { + if i := strings.LastIndex(name, "."); i >= 0 { + return name[i+1:] + } + return name +} diff --git a/go.mod b/go.mod index 6c2c24a..99dd4ee 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module github.com/dexpace/morphic go 1.26.3 require ( + github.com/bufbuild/protocompile v0.14.1 github.com/google/go-cmp v0.7.0 github.com/speakeasy-api/openapi v1.24.0 github.com/stretchr/testify v1.11.1 + google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 53d0ec2..36bbd60 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -25,6 +27,8 @@ golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 30ef6673d80da9d3fe0fee9d02bc3354af267b23 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Tue, 21 Jul 2026 04:07:20 +0300 Subject: [PATCH 2/4] test(compilers/protobuf): add conformance corpus, goldens, and round-trip Add the test suite for the protobuf compiler, mirroring the OpenAPI compiler: - a conformance corpus of one minimal .proto per protobuf/gRPC capability from the cross-spec matrix (messages, nested types, open/closed enums, oneof, proto3 optional, maps, packed/expanded repeated, scalar wire encodings, proto2 presence and defaults, extensions, reserved, packages, well-known types, all four streaming modes with idempotency, deprecation, doc comments, custom options, and a 2023-edition file), each with a focused assertion and a byte-exact golden IR snapshot; - a full gRPC library service golden snapshot; - a JSON round-trip property test (compile, marshal, unmarshal, deep-equal, and re-marshal for determinism); - registry, option-policy, unresolved-import, and multi-source-rejection tests. Extend the architecture import-graph test so compilers/protobuf may import ir, compilers, protocompile, and the protobuf runtime only. --- compilers/protobuf/compile_test.go | 144 +++ compilers/protobuf/conformance_test.go | 427 +++++++ compilers/protobuf/golden_test.go | 31 + internal/archtest/arch_test.go | 15 +- .../conformance/protobuf/comments.golden.json | 104 ++ testdata/conformance/protobuf/comments.proto | 9 + .../protobuf/custom-options.golden.json | 136 ++ .../conformance/protobuf/custom-options.proto | 18 + .../conformance/protobuf/defaults.golden.json | 337 +++++ testdata/conformance/protobuf/defaults.proto | 16 + .../protobuf/deprecation.golden.json | 154 +++ .../conformance/protobuf/deprecation.proto | 13 + .../conformance/protobuf/editions.golden.json | 141 ++ testdata/conformance/protobuf/editions.proto | 8 + .../protobuf/enum-closed.golden.json | 167 +++ .../conformance/protobuf/enum-closed.proto | 15 + .../protobuf/enum-open.golden.json | 167 +++ testdata/conformance/protobuf/enum-open.proto | 14 + .../protobuf/extensions.golden.json | 178 +++ .../conformance/protobuf/extensions.proto | 13 + testdata/conformance/protobuf/map.golden.json | 194 +++ testdata/conformance/protobuf/map.proto | 8 + .../conformance/protobuf/messages.golden.json | 369 ++++++ testdata/conformance/protobuf/messages.proto | 18 + .../conformance/protobuf/nested.golden.json | 230 ++++ testdata/conformance/protobuf/nested.proto | 15 + .../conformance/protobuf/oneof.golden.json | 193 +++ testdata/conformance/protobuf/oneof.proto | 12 + .../conformance/protobuf/package.golden.json | 104 ++ testdata/conformance/protobuf/package.proto | 7 + .../conformance/protobuf/presence.golden.json | 173 +++ testdata/conformance/protobuf/presence.proto | 9 + .../protobuf/proto3-optional.golden.json | 140 ++ .../protobuf/proto3-optional.proto | 8 + .../conformance/protobuf/repeated.golden.json | 225 ++++ testdata/conformance/protobuf/repeated.proto | 9 + .../conformance/protobuf/reserved.golden.json | 210 +++ testdata/conformance/protobuf/reserved.proto | 16 + .../protobuf/scalar-encoding.golden.json | 263 ++++ .../protobuf/scalar-encoding.proto | 11 + .../conformance/protobuf/services.golden.json | 551 ++++++++ testdata/conformance/protobuf/services.proto | 27 + .../protobuf/well-known.golden.json | 379 ++++++ .../conformance/protobuf/well-known.proto | 22 + testdata/golden/protobuf/library.golden.json | 1131 +++++++++++++++++ testdata/golden/protobuf/library.proto | 72 ++ 46 files changed, 6496 insertions(+), 7 deletions(-) create mode 100644 compilers/protobuf/compile_test.go create mode 100644 compilers/protobuf/conformance_test.go create mode 100644 compilers/protobuf/golden_test.go create mode 100644 testdata/conformance/protobuf/comments.golden.json create mode 100644 testdata/conformance/protobuf/comments.proto create mode 100644 testdata/conformance/protobuf/custom-options.golden.json create mode 100644 testdata/conformance/protobuf/custom-options.proto create mode 100644 testdata/conformance/protobuf/defaults.golden.json create mode 100644 testdata/conformance/protobuf/defaults.proto create mode 100644 testdata/conformance/protobuf/deprecation.golden.json create mode 100644 testdata/conformance/protobuf/deprecation.proto create mode 100644 testdata/conformance/protobuf/editions.golden.json create mode 100644 testdata/conformance/protobuf/editions.proto create mode 100644 testdata/conformance/protobuf/enum-closed.golden.json create mode 100644 testdata/conformance/protobuf/enum-closed.proto create mode 100644 testdata/conformance/protobuf/enum-open.golden.json create mode 100644 testdata/conformance/protobuf/enum-open.proto create mode 100644 testdata/conformance/protobuf/extensions.golden.json create mode 100644 testdata/conformance/protobuf/extensions.proto create mode 100644 testdata/conformance/protobuf/map.golden.json create mode 100644 testdata/conformance/protobuf/map.proto create mode 100644 testdata/conformance/protobuf/messages.golden.json create mode 100644 testdata/conformance/protobuf/messages.proto create mode 100644 testdata/conformance/protobuf/nested.golden.json create mode 100644 testdata/conformance/protobuf/nested.proto create mode 100644 testdata/conformance/protobuf/oneof.golden.json create mode 100644 testdata/conformance/protobuf/oneof.proto create mode 100644 testdata/conformance/protobuf/package.golden.json create mode 100644 testdata/conformance/protobuf/package.proto create mode 100644 testdata/conformance/protobuf/presence.golden.json create mode 100644 testdata/conformance/protobuf/presence.proto create mode 100644 testdata/conformance/protobuf/proto3-optional.golden.json create mode 100644 testdata/conformance/protobuf/proto3-optional.proto create mode 100644 testdata/conformance/protobuf/repeated.golden.json create mode 100644 testdata/conformance/protobuf/repeated.proto create mode 100644 testdata/conformance/protobuf/reserved.golden.json create mode 100644 testdata/conformance/protobuf/reserved.proto create mode 100644 testdata/conformance/protobuf/scalar-encoding.golden.json create mode 100644 testdata/conformance/protobuf/scalar-encoding.proto create mode 100644 testdata/conformance/protobuf/services.golden.json create mode 100644 testdata/conformance/protobuf/services.proto create mode 100644 testdata/conformance/protobuf/well-known.golden.json create mode 100644 testdata/conformance/protobuf/well-known.proto create mode 100644 testdata/golden/protobuf/library.golden.json create mode 100644 testdata/golden/protobuf/library.proto diff --git a/compilers/protobuf/compile_test.go b/compilers/protobuf/compile_test.go new file mode 100644 index 0000000..7482ab6 --- /dev/null +++ b/compilers/protobuf/compile_test.go @@ -0,0 +1,144 @@ +package protobuf_test // external test package — exercises only the public API + +import ( + "context" + "encoding/json" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dexpace/morphic/compilers" + "github.com/dexpace/morphic/compilers/protobuf" + "github.com/dexpace/morphic/ir" +) + +const wrappersProto = `syntax = "proto3"; +package w; +import "google/protobuf/wrappers.proto"; +message Box { + google.protobuf.StringValue note = 1; +} +` + +func compile(t *testing.T, path, src string, opts compilers.Options) (*ir.Document, []ir.Diagnostic, error) { + t.Helper() + return protobuf.New().Compile(context.Background(), + []compilers.Source{{Path: path, Data: []byte(src)}}, opts) +} + +func TestCompile_EndToEnd(t *testing.T) { + t.Parallel() + doc, diags, err := compile(t, "w.proto", wrappersProto, compilers.Options{}) + require.NoError(t, err) + require.NotNil(t, doc) + for _, d := range diags { + assert.NotEqual(t, ir.SeverityError, d.Severity, "diag: %+v", d) + } + assert.Equal(t, ir.IRVersion, doc.IRVersion) + assert.Equal(t, "w", doc.Name) + require.Len(t, doc.Services, 1) + require.Len(t, doc.Sources, 1) + assert.Equal(t, "protobuf@3", doc.Sources[0].Format) + assert.Len(t, doc.Sources[0].Hash, 64) +} + +func TestCompile_JSONRoundTrip(t *testing.T) { + t.Parallel() + data, err := os.ReadFile("../../testdata/golden/protobuf/library.proto") + require.NoError(t, err) + doc, _, err := compile(t, "library.proto", string(data), compilers.Options{}) + require.NoError(t, err) + raw, err := json.Marshal(doc) + require.NoError(t, err) + var back ir.Document + require.NoError(t, json.Unmarshal(raw, &back)) + if diff := cmp.Diff(doc, &back); diff != "" { + t.Errorf("round-trip mismatch (-want +got):\n%s", diff) + } + again, err := json.Marshal(&back) + require.NoError(t, err) + assert.Equal(t, string(raw), string(again), "marshal must be deterministic") +} + +func TestCompile_RegistersInRegistry(t *testing.T) { + t.Parallel() + reg := compilers.NewRegistry() + require.NoError(t, reg.Register(protobuf.New())) + for _, ver := range []string{"2", "3", "2023"} { + got, ok := reg.Lookup(compilers.SourceFormat{Name: "protobuf", Version: ver}) + require.True(t, ok, "format protobuf@%s registered", ver) + assert.NotNil(t, got) + } +} + +func TestCompile_RejectsMultipleSources(t *testing.T) { + t.Parallel() + _, _, err := protobuf.New().Compile(context.Background(), + []compilers.Source{ + {Path: "a.proto", Data: []byte(wrappersProto)}, + {Path: "b.proto", Data: []byte(wrappersProto)}, + }, compilers.Options{}) + require.Error(t, err) +} + +func TestCompile_RejectsWrongOptions(t *testing.T) { + t.Parallel() + _, _, err := protobuf.New().Compile(context.Background(), + []compilers.Source{{Path: "w.proto", Data: []byte(wrappersProto)}}, + compilers.Options{FormatOptions: "nonsense"}) + require.Error(t, err) +} + +func TestCompile_UnresolvedImport(t *testing.T) { + t.Parallel() + const src = `syntax = "proto3"; +package u; +import "other/thing.proto"; +message M { + other.Thing t = 1; +} +` + doc, diags, err := compile(t, "u.proto", src, compilers.Options{}) + require.NoError(t, err, "an unresolved import is a spec problem, not a Go error") + assert.Nil(t, doc, "the compiler refuses to lower a document it cannot link") + var found bool + for _, d := range diags { + if d.Code == "protobuf/unresolved-import" { + found = true + assert.Equal(t, ir.SeverityError, d.Severity) + } + } + assert.True(t, found, "unresolved import reported as a diagnostic") +} + +func TestCompile_WrapperExternalPolicy(t *testing.T) { + t.Parallel() + doc, _, err := compile(t, "w.proto", wrappersProto, + compilers.Options{FormatOptions: protobuf.Options{Wrappers: protobuf.WrapperExternal}}) + require.NoError(t, err) + require.NotNil(t, doc) + box, ok := doc.Types[ir.TypeID("t/protobuf/w.Box")].(*ir.Model) + require.True(t, ok) + require.Len(t, box.Properties, 1) + target := box.Properties[0].Type.Target + ext, ok := doc.Types[target].(*ir.External) + require.True(t, ok, "WrapperExternal policy lowers wrappers to External") + assert.Equal(t, "google.protobuf.StringValue", ext.Identity) +} + +func TestCompile_SyntaxDigit(t *testing.T) { + t.Parallel() + const proto2Src = `syntax = "proto2"; +package p2; +message M { + required string id = 1; +} +` + doc, _, err := compile(t, "p2.proto", proto2Src, compilers.Options{}) + require.NoError(t, err) + require.NotNil(t, doc) + assert.Equal(t, "protobuf@2", doc.Sources[0].Format) +} diff --git a/compilers/protobuf/conformance_test.go b/compilers/protobuf/conformance_test.go new file mode 100644 index 0000000..69bef2c --- /dev/null +++ b/compilers/protobuf/conformance_test.go @@ -0,0 +1,427 @@ +package protobuf_test // external test package — exercises only the public API + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dexpace/morphic/compilers" + "github.com/dexpace/morphic/compilers/protobuf" + "github.com/dexpace/morphic/ir" + "github.com/dexpace/morphic/ir/irtest" +) + +// conformanceDir is the corpus of one minimal .proto per capability row of +// ir-spec-matrix.md that protobuf/gRPC can express, addressed relative to this +// test file. +const conformanceDir = "../../testdata/conformance/protobuf" + +// TestConformance drives one minimal spec per protobuf/gRPC-expressible +// capability through the full compiler and asserts lossless capture: a focused +// capability-specific assertion plus a byte-exact golden IR snapshot. Regenerate +// the goldens with `go test ./compilers/protobuf -run TestConformance -update`. +func TestConformance(t *testing.T) { + t.Parallel() + cases := []struct { + file string + assert func(*testing.T, *ir.Document, []ir.Diagnostic) + }{ + {"messages", assertMessages}, + {"nested", assertNested}, + {"enum-open", assertEnumOpen}, + {"enum-closed", assertEnumClosed}, + {"oneof", assertOneof}, + {"proto3-optional", assertProto3Optional}, + {"map", assertMap}, + {"repeated", assertRepeated}, + {"scalar-encoding", assertScalarEncoding}, + {"presence", assertPresence}, + {"defaults", assertDefaults}, + {"extensions", assertExtensions}, + {"reserved", assertReserved}, + {"package", assertPackage}, + {"well-known", assertWellKnown}, + {"services", assertServices}, + {"deprecation", assertDeprecation}, + {"comments", assertComments}, + {"custom-options", assertCustomOptions}, + {"editions", assertEditions}, + } + for _, tc := range cases { + t.Run(tc.file, func(t *testing.T) { + t.Parallel() + doc, diags := parseCorpus(t, tc.file) + assertNoErrorDiags(t, diags) + tc.assert(t, doc, diags) + irtest.CompareGolden(t, filepath.Join(conformanceDir, tc.file+".golden.json"), doc) + }) + } +} + +// parseCorpus reads and compiles one corpus spec through the full compiler. +func parseCorpus(t *testing.T, name string) (*ir.Document, []ir.Diagnostic) { + t.Helper() + data, err := os.ReadFile(filepath.Join(conformanceDir, name+".proto")) + require.NoError(t, err) + doc, diags, err := protobuf.New().Compile(t.Context(), + []compilers.Source{{Path: name + ".proto", Data: data}}, compilers.Options{}) + require.NoError(t, err) + require.NotNil(t, doc) + return doc, diags +} + +// assertNoErrorDiags fails when any diagnostic has error severity. +func assertNoErrorDiags(t *testing.T, diags []ir.Diagnostic) { + t.Helper() + for _, d := range diags { + assert.NotEqual(t, ir.SeverityError, d.Severity, "unexpected error diagnostic: %+v", d) + } +} + +// typeID is the stable TypeID of a message or enum by fully-qualified name. +func typeID(fullName string) ir.TypeID { return ir.TypeID("t/protobuf/" + fullName) } + +// modelOf resolves a Model by fully-qualified name. +func modelOf(t *testing.T, doc *ir.Document, fullName string) *ir.Model { + t.Helper() + m, ok := doc.Types[typeID(fullName)].(*ir.Model) + require.True(t, ok, "message %s present as a Model", fullName) + return m +} + +// enumOf resolves an Enum by fully-qualified name. +func enumOf(t *testing.T, doc *ir.Document, fullName string) *ir.Enum { + t.Helper() + e, ok := doc.Types[typeID(fullName)].(*ir.Enum) + require.True(t, ok, "enum %s present as an Enum", fullName) + return e +} + +// propByName returns the property of m whose source name matches. +func propByName(t *testing.T, m *ir.Model, name string) ir.Property { + t.Helper() + for _, p := range m.Properties { + if p.Name.Source == name { + return p + } + } + require.Failf(t, "property not found", "model %s has no property %q", m.Name.Source, name) + return ir.Property{} +} + +// allOperations flattens every operation across a document's service groups. +func allOperations(doc *ir.Document) []ir.Operation { + var out []ir.Operation + for _, svc := range doc.Services { + for _, g := range svc.Groups { + out = append(out, g.Operations...) + } + } + return out +} + +// opByName finds an operation by its source rpc name. +func opByName(t *testing.T, doc *ir.Document, name string) ir.Operation { + t.Helper() + for _, op := range allOperations(doc) { + if op.Name.Source == name { + return op + } + } + require.Failf(t, "operation not found", "no rpc named %q", name) + return ir.Operation{} +} + +func assertMessages(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + product := modelOf(t, doc, "shop.Product") + assert.False(t, product.Anonymous) + assert.Equal(t, []string{"shop"}, product.Namespace) + id := propByName(t, product, "id") + require.NotNil(t, id.WireID) + assert.Equal(t, 1, *id.WireID, "field number becomes the wire ID") + assert.Equal(t, ir.PresenceImplicit, id.Presence, "proto3 no-label field is implicit-presence") + assert.Equal(t, ir.TypeID("t/prim/int64"), id.Type.Target) + category := propByName(t, product, "category") + assert.Equal(t, typeID("shop.Category"), category.Type.Target) + assert.Equal(t, ir.PresenceExplicit, category.Presence, "message fields always have presence") + _ = modelOf(t, doc, "shop.Category") +} + +func assertNested(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + outer := modelOf(t, doc, "nested.Outer") + inner := modelOf(t, doc, "nested.Outer.Inner") + assert.Equal(t, []string{"nested"}, inner.Namespace, "nested type keeps the file package as namespace") + assert.Equal(t, typeID("nested.Outer.Inner"), propByName(t, outer, "inner").Type.Target) + kind := enumOf(t, doc, "nested.Outer.Kind") + assert.Equal(t, typeID("nested.Outer.Kind"), propByName(t, outer, "kind").Type.Target) + assert.Len(t, kind.Members, 2) +} + +func assertEnumOpen(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + color := enumOf(t, doc, "enums.Color") + assert.False(t, color.Closed, "proto3 enums are open") + assert.Equal(t, ir.PrimInt32, color.ValueType) + require.Len(t, color.Members, 4) + assert.Equal(t, "COLOR_UNSPECIFIED", color.Members[0].Name.Source) + assert.Equal(t, ir.BigVal("2"), color.Members[2].Value.Num) +} + +func assertEnumClosed(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + status := enumOf(t, doc, "enums2.Status") + assert.True(t, status.Closed, "proto2 enums are closed") + require.Len(t, status.Members, 4) + // allow_alias: STARTED and RUNNING share value 1, kept as distinct members. + assert.Equal(t, ir.BigVal("1"), status.Members[1].Value.Num) + assert.Equal(t, ir.BigVal("1"), status.Members[2].Value.Num) + assert.Equal(t, "RUNNING", status.Members[2].Name.Source) +} + +func assertOneof(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + shape := modelOf(t, doc, "oneofs.Shape") + form := propByName(t, shape, "form") + assert.True(t, form.Flatten, "the oneof wrapper hoists its members to top-level wire fields") + u, ok := doc.Types[form.Type.Target].(*ir.Union) + require.True(t, ok, "oneof lowers to a Union node") + assert.True(t, u.Exclusive) + assert.True(t, u.WireTagged, "protobuf oneof is tagged on the wire") + require.Len(t, u.Variants, 3) + require.NotNil(t, u.Variants[0].WireID) + assert.Equal(t, 1, *u.Variants[0].WireID, "variant keeps its field number") + // The non-oneof field remains an ordinary property alongside the wrapper. + _ = propByName(t, shape, "name") +} + +func assertProto3Optional(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + config := modelOf(t, doc, "opt3.Config") + assert.Equal(t, ir.PresenceExplicit, propByName(t, config, "name").Presence, + "proto3 optional distinguishes unset") + assert.Equal(t, ir.PresenceImplicit, propByName(t, config, "count").Presence) + for id, td := range doc.Types { + assert.NotEqual(t, ir.KindUnion, td.Kind(), + "a synthetic proto3-optional oneof must not lower to a Union (%s)", id) + } +} + +func assertMap(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + dict := modelOf(t, doc, "maps.Dict") + counts, ok := doc.Types[propByName(t, dict, "counts").Type.Target].(*ir.MapT) + require.True(t, ok, "map field lowers to a MapT") + assert.Equal(t, ir.TypeID("t/prim/string"), counts.Key.Target) + assert.Equal(t, ir.TypeID("t/prim/int32"), counts.Value.Target) + names, ok := doc.Types[propByName(t, dict, "names").Type.Target].(*ir.MapT) + require.True(t, ok) + assert.Equal(t, ir.TypeID("t/prim/int64"), names.Key.Target) + // The synthetic map-entry message is never hoisted as a model. + for id := range doc.Types { + assert.NotContains(t, string(id), "Entry", "map-entry message must not be hoisted") + } +} + +func assertRepeated(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + series := modelOf(t, doc, "rep.Series") + packed := listOf(t, doc, propByName(t, series, "packed_values")) + require.NotNil(t, packed.Encoding) + assert.Equal(t, "packed", packed.Encoding.Name, "proto3 repeated scalars pack by default") + expanded := listOf(t, doc, propByName(t, series, "expanded_values")) + require.NotNil(t, expanded.Encoding) + assert.Equal(t, "expanded", expanded.Encoding.Name, "[packed=false] lowers to expanded") + labels := listOf(t, doc, propByName(t, series, "labels")) + assert.Nil(t, labels.Encoding, "string lists carry no packing encoding") +} + +// listOf resolves the List node a property references. +func listOf(t *testing.T, doc *ir.Document, p ir.Property) *ir.List { + t.Helper() + l, ok := doc.Types[p.Type.Target].(*ir.List) + require.True(t, ok, "property %s references a List", p.Name.Source) + return l +} + +func assertScalarEncoding(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + nums := modelOf(t, doc, "enc.Nums") + a := propByName(t, nums, "a") + assert.Equal(t, ir.TypeID("t/prim/int32"), a.Type.Target) + require.NotNil(t, a.Encoding) + assert.Equal(t, "zigzag", a.Encoding.Name, "sint32 is a zigzag encoding of int32") + c := propByName(t, nums, "c") + assert.Equal(t, ir.TypeID("t/prim/uint32"), c.Type.Target) + require.NotNil(t, c.Encoding) + assert.Equal(t, "fixed", c.Encoding.Name, "fixed32 is a fixed encoding of uint32") + d := propByName(t, nums, "d") + assert.Equal(t, ir.TypeID("t/prim/int64"), d.Type.Target) + require.NotNil(t, d.Encoding) + assert.Equal(t, "fixed", d.Encoding.Name) +} + +func assertPresence(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + rec := modelOf(t, doc, "pres.Rec") + id := propByName(t, rec, "id") + assert.True(t, id.Required, "proto2 required maps to Required") + assert.Equal(t, ir.PresenceRequired, id.Presence) + assert.Equal(t, ir.PresenceExplicit, propByName(t, rec, "note").Presence, "proto2 optional is explicit") + tags := propByName(t, rec, "tags") + assert.False(t, tags.Required) + assert.Equal(t, ir.PresenceDefault, tags.Presence, "repeated fields carry no presence discipline") +} + +func assertDefaults(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + s := modelOf(t, doc, "def.Settings") + assert.Equal(t, ir.BigVal("3"), propByName(t, s, "retries").Default.Num) + assert.Equal(t, "hello", propByName(t, s, "label").Default.Str) + assert.True(t, propByName(t, s, "enabled").Default.Bool) + assert.Equal(t, ir.BigVal("2.5"), propByName(t, s, "ratio").Default.Num, + "floating default is an exact decimal string, never float64") + mode := propByName(t, s, "mode").Default + require.Equal(t, ir.ValueRefKind, mode.Kind, "enum default references a member") + require.NotNil(t, mode.Ref) + assert.Equal(t, typeID("def.Mode"), mode.Ref.Type) + assert.Equal(t, "SLOW", mode.Ref.Member) +} + +func assertExtensions(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + base := modelOf(t, doc, "ext.Base") + require.Len(t, base.ExtensionRanges, 1) + assert.Equal(t, ir.WireIDRange{From: 100, To: 200}, base.ExtensionRanges[0]) + priority := propByName(t, base, "priority") + require.NotNil(t, priority.WireID) + assert.Equal(t, 100, *priority.WireID) + assert.Equal(t, "ext", priority.ExtensionOf, "extend field records its declaring scope") + tag := propByName(t, base, "tag") + assert.Equal(t, "ext", tag.ExtensionOf) +} + +func assertReserved(t *testing.T, doc *ir.Document, diags []ir.Diagnostic) { + m := modelOf(t, doc, "resv.M") + raw, ok := m.Extensions["protobuf:reserved"] + require.True(t, ok, "reserved numbers/names preserved in Extensions") + assert.JSONEq(t, `{"ranges":[{"from":2,"to":2},{"from":15,"to":15},{"from":9,"to":11}],"names":["foo","bar"]}`, + string(raw)) + e := enumOf(t, doc, "resv.E") + rawE, ok := e.Extensions["protobuf:reserved"] + require.True(t, ok) + assert.JSONEq(t, `{"ranges":[{"from":2,"to":2},{"from":5,"to":7}],"names":["OLD"]}`, string(rawE)) + var found bool + for _, d := range diags { + if d.Code == "protobuf/reserved" { + found = true + assert.Equal(t, ir.SeverityInfo, d.Severity) + } + } + assert.True(t, found, "reserved constructs raise an info diagnostic") +} + +func assertPackage(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + thing := modelOf(t, doc, "deep.nested.pkg.Thing") + assert.Equal(t, []string{"deep", "nested", "pkg"}, thing.Namespace) + assert.Equal(t, "deep.nested.pkg", doc.Name) + require.Len(t, doc.Services, 1) + assert.Equal(t, []string{"deep", "nested", "pkg"}, doc.Services[0].Namespace) +} + +func assertWellKnown(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + b := modelOf(t, doc, "wkt.Bundle") + assert.Equal(t, ir.TypeID("t/prim/datetime"), propByName(t, b, "ts").Type.Target) + assert.Equal(t, ir.TypeID("t/prim/duration"), propByName(t, b, "dur").Type.Target) + assert.Equal(t, ir.TypeID("t/protobuf/any"), propByName(t, b, "any").Type.Target) + assert.Equal(t, ir.TypeID("t/protobuf/any"), propByName(t, b, "props").Type.Target, + "Struct lowers to the schemaless Any node") + mask := propByName(t, b, "mask") + ext, ok := doc.Types[mask.Type.Target].(*ir.External) + require.True(t, ok, "FieldMask lowers to an External") + assert.Equal(t, "google.protobuf.FieldMask", ext.Identity) + count := propByName(t, b, "count") + assert.Equal(t, ir.TypeID("t/prim/int32"), count.Type.Target) + assert.True(t, count.Type.Nullable, "Int32Value lowers to a nullable primitive") + note := propByName(t, b, "note") + assert.Equal(t, ir.TypeID("t/prim/string"), note.Type.Target) + assert.True(t, note.Type.Nullable) + _, ok = doc.Types[propByName(t, b, "nothing").Type.Target].(*ir.External) + assert.True(t, ok, "Empty as a field type lowers to an External") +} + +func assertServices(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + require.Len(t, doc.Services, 1) + require.Len(t, doc.Services[0].Groups, 1) + assert.Equal(t, "Echo", doc.Services[0].Groups[0].Name.Source) + + unary := opByName(t, doc, "Unary") + assert.Equal(t, ir.StreamingMode(""), unary.Streaming, "unary rpc has no streaming mode") + require.NotNil(t, unary.Bindings.RPC) + assert.Equal(t, "grpc", unary.Bindings.RPC.System) + assert.Equal(t, "/svc.Echo/Unary", unary.Bindings.RPC.FullMethod) + require.NotNil(t, unary.Bindings.RPC.InputType) + + client := opByName(t, doc, "ClientStream") + assert.Equal(t, ir.StreamingClient, client.Streaming) + assert.NotNil(t, client.RequestStream) + assert.Nil(t, client.ResponseStream) + + server := opByName(t, doc, "ServerStream") + assert.Equal(t, ir.StreamingServer, server.Streaming) + assert.NotNil(t, server.ResponseStream) + + bidi := opByName(t, doc, "BidiStream") + assert.Equal(t, ir.StreamingBidi, bidi.Streaming) + assert.NotNil(t, bidi.RequestStream) + assert.NotNil(t, bidi.ResponseStream) + + fetch := opByName(t, doc, "Fetch") + assert.Equal(t, ir.IdempotencySafe, fetch.Idempotency.Kind) + assert.Equal(t, "NO_SIDE_EFFECTS", fetch.Bindings.RPC.IdempotencyLevel) + assert.Equal(t, ir.IdempotencyIdempotent, opByName(t, doc, "Replace").Idempotency.Kind) + + notify := opByName(t, doc, "Notify") + require.Len(t, notify.Responses, 1) + assert.Nil(t, notify.Responses[0].Payload, "an Empty response carries no payload") + assert.NotNil(t, notify.Request, "Notify still has a request payload") +} + +func assertDeprecation(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + old := modelOf(t, doc, "dep.Old") + assert.NotNil(t, old.Deprecation, "deprecated message") + assert.NotNil(t, propByName(t, old, "id").Deprecation, "deprecated field") + legacy := enumOf(t, doc, "dep.Legacy") + require.Len(t, legacy.Members, 2) + assert.NotNil(t, legacy.Members[1].Deprecation, "deprecated enum value") +} + +func assertComments(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + d := modelOf(t, doc, "docs.Doc") + assert.Equal(t, "A documented message.", d.Docs.Description) + assert.Equal(t, "The identifier.", propByName(t, d, "id").Docs.Description) +} + +func assertCustomOptions(t *testing.T, doc *ir.Document, diags []ir.Diagnostic) { + account := modelOf(t, doc, "copt.Account") + audited, ok := account.Extensions["protobuf:option:copt.audited"] + require.True(t, ok, "message custom option preserved in Extensions") + assert.JSONEq(t, "true", string(audited)) + ssn := propByName(t, account, "ssn") + sensitivity, ok := ssn.Extensions["protobuf:option:copt.sensitivity"] + require.True(t, ok, "field custom option preserved in Extensions") + assert.JSONEq(t, `"high"`, string(sensitivity)) + _, ok = doc.Extensions["protobuf:custom-option:copt.sensitivity"] + assert.True(t, ok, "custom-option definition preserved at document level") + var found bool + for _, d := range diags { + if d.Code == "protobuf/custom-option-definition" { + found = true + } + } + assert.True(t, found) +} + +func assertEditions(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + rec := modelOf(t, doc, "ed.Rec") + assert.Equal(t, ir.PresenceExplicit, propByName(t, rec, "id").Presence, + "editions default field presence is explicit") + assert.Equal(t, ir.PresenceImplicit, propByName(t, rec, "count").Presence, + "features.field_presence = IMPLICIT resolves to implicit presence") + raw, ok := doc.Extensions["protobuf:file"] + require.True(t, ok) + assert.Contains(t, string(raw), "editions") + assert.Contains(t, string(raw), "EDITION_2023") +} diff --git a/compilers/protobuf/golden_test.go b/compilers/protobuf/golden_test.go new file mode 100644 index 0000000..ae8cb26 --- /dev/null +++ b/compilers/protobuf/golden_test.go @@ -0,0 +1,31 @@ +package protobuf_test // external test package — exercises only the public API + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dexpace/morphic/compilers" + "github.com/dexpace/morphic/compilers/protobuf" + "github.com/dexpace/morphic/ir" + "github.com/dexpace/morphic/ir/irtest" +) + +// TestGolden lowers a full gRPC library service and compares its IR against a +// byte-exact golden snapshot. Regenerate it with +// `go test ./compilers/protobuf -run TestGolden -update`. +func TestGolden(t *testing.T) { + t.Parallel() + data, err := os.ReadFile("../../testdata/golden/protobuf/library.proto") + require.NoError(t, err) + doc, diags, err := protobuf.New().Compile(t.Context(), + []compilers.Source{{Path: "library.proto", Data: data}}, compilers.Options{}) + require.NoError(t, err) + require.NotNil(t, doc) + for _, d := range diags { + assert.NotEqual(t, ir.SeverityError, d.Severity, "unexpected error diagnostic: %+v", d) + } + irtest.CompareGolden(t, "../../testdata/golden/protobuf/library.golden.json", doc) +} diff --git a/internal/archtest/arch_test.go b/internal/archtest/arch_test.go index 3051783..dad5825 100644 --- a/internal/archtest/arch_test.go +++ b/internal/archtest/arch_test.go @@ -22,13 +22,14 @@ const module = "github.com/dexpace/morphic" // here so the assertion is ready the moment those packages land; absent // directories are skipped by the walk (see TestImportGraph_LayeringHolds). var rules = map[string][]string{ - "ir": {}, - "ir/irtest": {module + "/ir", "github.com/google/go-cmp"}, - "compilers": {module + "/ir"}, - "compilers/openapi": {module + "/ir", module + "/compilers", "github.com/speakeasy-api/openapi", "gopkg.in/yaml.v3"}, - "pass": {module + "/ir"}, - "engine": {module + "/ir", module + "/compilers", module + "/pass", "gopkg.in/yaml.v3"}, - "cmd/morphic": {module + "/ir", module + "/engine"}, + "ir": {}, + "ir/irtest": {module + "/ir", "github.com/google/go-cmp"}, + "compilers": {module + "/ir"}, + "compilers/openapi": {module + "/ir", module + "/compilers", "github.com/speakeasy-api/openapi", "gopkg.in/yaml.v3"}, + "compilers/protobuf": {module + "/ir", module + "/compilers", "github.com/bufbuild/protocompile", "google.golang.org/protobuf"}, + "pass": {module + "/ir"}, + "engine": {module + "/ir", module + "/compilers", module + "/pass", "gopkg.in/yaml.v3"}, + "cmd/morphic": {module + "/ir", module + "/engine"}, } // TestImportGraph_LayeringHolds parses every non-test Go file under each ruled diff --git a/testdata/conformance/protobuf/comments.golden.json b/testdata/conformance/protobuf/comments.golden.json new file mode 100644 index 0000000..c14fdae --- /dev/null +++ b/testdata/conformance/protobuf/comments.golden.json @@ -0,0 +1,104 @@ +{ + "irVersion": "0.1.0", + "name": "docs", + "docs": {}, + "services": [ + { + "id": "s/protobuf/docs", + "name": { + "source": "docs", + "canonical": "docs" + }, + "docs": {}, + "namespace": [ + "docs" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "comments.proto" + } + } + ], + "types": { + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/docs.Doc": { + "kind": "model", + "id": "t/protobuf/docs.Doc", + "name": { + "source": "Doc", + "canonical": "doc" + }, + "namespace": [ + "docs" + ], + "anonymous": false, + "docs": { + "description": "A documented message." + }, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "docs.Doc" + }, + "properties": [ + { + "id": "p/protobuf/docs.Doc.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": { + "description": "The identifier." + }, + "provenance": { + "source": 0, + "pointer": "docs.Doc.id" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "comments.proto", + "hash": "b81110b251ae51f177158daf18b813c08cf7652ab2639f1873ebf0af5b0d40fe" + } + ] +} diff --git a/testdata/conformance/protobuf/comments.proto b/testdata/conformance/protobuf/comments.proto new file mode 100644 index 0000000..c8f55dc --- /dev/null +++ b/testdata/conformance/protobuf/comments.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package docs; + +// A documented message. +message Doc { + // The identifier. + string id = 1; +} diff --git a/testdata/conformance/protobuf/custom-options.golden.json b/testdata/conformance/protobuf/custom-options.golden.json new file mode 100644 index 0000000..acc30cb --- /dev/null +++ b/testdata/conformance/protobuf/custom-options.golden.json @@ -0,0 +1,136 @@ +{ + "irVersion": "0.1.0", + "name": "copt", + "docs": {}, + "services": [ + { + "id": "s/protobuf/copt", + "name": { + "source": "copt", + "canonical": "copt" + }, + "docs": {}, + "namespace": [ + "copt" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "custom-options.proto" + } + } + ], + "types": { + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/copt.Account": { + "kind": "model", + "id": "t/protobuf/copt.Account", + "name": { + "source": "Account", + "canonical": "account" + }, + "namespace": [ + "copt" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "extensions": { + "protobuf:option:copt.audited": true + }, + "provenance": { + "source": 0, + "pointer": "copt.Account" + }, + "properties": [ + { + "id": "p/protobuf/copt.Account.ssn", + "name": { + "source": "ssn", + "canonical": "ssn" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "extensions": { + "protobuf:option:copt.sensitivity": "high" + }, + "provenance": { + "source": 0, + "pointer": "copt.Account.ssn" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:custom-option:copt.audited": { + "extends": "google.protobuf.MessageOptions", + "name": "copt.audited", + "number": 50001 + }, + "protobuf:custom-option:copt.sensitivity": { + "extends": "google.protobuf.FieldOptions", + "name": "copt.sensitivity", + "number": 50000 + }, + "protobuf:file": { + "syntax": "proto2" + } + }, + "diagnostics": [ + { + "severity": "info", + "code": "protobuf/custom-option-definition", + "message": "extension copt.sensitivity defines a custom option on google.protobuf.FieldOptions", + "provenance": { + "source": 0, + "pointer": "copt.sensitivity" + } + }, + { + "severity": "info", + "code": "protobuf/custom-option-definition", + "message": "extension copt.audited defines a custom option on google.protobuf.MessageOptions", + "provenance": { + "source": 0, + "pointer": "copt.audited" + } + } + ], + "sources": [ + { + "format": "protobuf@2", + "path": "custom-options.proto", + "hash": "8cd9783b74609aab97c0e7b700cf9e72f0cca2f5aff6873a8b2acbb138f5da63" + } + ] +} diff --git a/testdata/conformance/protobuf/custom-options.proto b/testdata/conformance/protobuf/custom-options.proto new file mode 100644 index 0000000..5270f19 --- /dev/null +++ b/testdata/conformance/protobuf/custom-options.proto @@ -0,0 +1,18 @@ +syntax = "proto2"; + +package copt; + +import "google/protobuf/descriptor.proto"; + +extend google.protobuf.FieldOptions { + optional string sensitivity = 50000; +} + +extend google.protobuf.MessageOptions { + optional bool audited = 50001; +} + +message Account { + option (audited) = true; + optional string ssn = 1 [(sensitivity) = "high"]; +} diff --git a/testdata/conformance/protobuf/defaults.golden.json b/testdata/conformance/protobuf/defaults.golden.json new file mode 100644 index 0000000..9833135 --- /dev/null +++ b/testdata/conformance/protobuf/defaults.golden.json @@ -0,0 +1,337 @@ +{ + "irVersion": "0.1.0", + "name": "def", + "docs": {}, + "services": [ + { + "id": "s/protobuf/def", + "name": { + "source": "def", + "canonical": "def" + }, + "docs": {}, + "namespace": [ + "def" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "defaults.proto" + } + } + ], + "types": { + "t/prim/bool": { + "kind": "primitive", + "id": "t/prim/bool", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "bool" + }, + "t/prim/float64": { + "kind": "primitive", + "id": "t/prim/float64", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "float64" + }, + "t/prim/int32": { + "kind": "primitive", + "id": "t/prim/int32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int32" + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/def.Mode": { + "kind": "enum", + "id": "t/protobuf/def.Mode", + "name": { + "source": "Mode", + "canonical": "mode" + }, + "namespace": [ + "def" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "def.Mode" + }, + "valueType": "int32", + "members": [ + { + "name": { + "source": "FAST", + "canonical": "fast" + }, + "value": { + "kind": "number", + "num": "0", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "SLOW", + "canonical": "slow" + }, + "value": { + "kind": "number", + "num": "1", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + } + ], + "closed": true, + "flags": false + }, + "t/protobuf/def.Settings": { + "kind": "model", + "id": "t/protobuf/def.Settings", + "name": { + "source": "Settings", + "canonical": "settings" + }, + "namespace": [ + "def" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "def.Settings" + }, + "properties": [ + { + "id": "p/protobuf/def.Settings.retries", + "name": { + "source": "retries", + "canonical": "retries" + }, + "wireID": 1, + "type": { + "target": "t/prim/int32", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "default": { + "kind": "number", + "num": "3", + "bytes": null, + "list": null, + "object": null + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "def.Settings.retries" + } + }, + { + "id": "p/protobuf/def.Settings.label", + "name": { + "source": "label", + "canonical": "label" + }, + "wireID": 2, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "default": { + "kind": "string", + "str": "hello", + "bytes": null, + "list": null, + "object": null + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "def.Settings.label" + } + }, + { + "id": "p/protobuf/def.Settings.enabled", + "name": { + "source": "enabled", + "canonical": "enabled" + }, + "wireID": 3, + "type": { + "target": "t/prim/bool", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "default": { + "kind": "bool", + "bool": true, + "bytes": null, + "list": null, + "object": null + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "def.Settings.enabled" + } + }, + { + "id": "p/protobuf/def.Settings.ratio", + "name": { + "source": "ratio", + "canonical": "ratio" + }, + "wireID": 4, + "type": { + "target": "t/prim/float64", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "default": { + "kind": "number", + "num": "2.5", + "bytes": null, + "list": null, + "object": null + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "def.Settings.ratio" + } + }, + { + "id": "p/protobuf/def.Settings.mode", + "name": { + "source": "mode", + "canonical": "mode" + }, + "wireID": 5, + "type": { + "target": "t/protobuf/def.Mode", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "default": { + "kind": "ref", + "bytes": null, + "list": null, + "object": null, + "ref": { + "type": "t/protobuf/def.Mode", + "member": "SLOW" + } + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "def.Settings.mode" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto2" + } + }, + "sources": [ + { + "format": "protobuf@2", + "path": "defaults.proto", + "hash": "7ddd00df317d583441ea85fcd3c3035213d7e51afd71b0618045c143d9c66685" + } + ] +} diff --git a/testdata/conformance/protobuf/defaults.proto b/testdata/conformance/protobuf/defaults.proto new file mode 100644 index 0000000..b4b4ee6 --- /dev/null +++ b/testdata/conformance/protobuf/defaults.proto @@ -0,0 +1,16 @@ +syntax = "proto2"; + +package def; + +enum Mode { + FAST = 0; + SLOW = 1; +} + +message Settings { + optional int32 retries = 1 [default = 3]; + optional string label = 2 [default = "hello"]; + optional bool enabled = 3 [default = true]; + optional double ratio = 4 [default = 2.5]; + optional Mode mode = 5 [default = SLOW]; +} diff --git a/testdata/conformance/protobuf/deprecation.golden.json b/testdata/conformance/protobuf/deprecation.golden.json new file mode 100644 index 0000000..88467fd --- /dev/null +++ b/testdata/conformance/protobuf/deprecation.golden.json @@ -0,0 +1,154 @@ +{ + "irVersion": "0.1.0", + "name": "dep", + "docs": {}, + "services": [ + { + "id": "s/protobuf/dep", + "name": { + "source": "dep", + "canonical": "dep" + }, + "docs": {}, + "namespace": [ + "dep" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "deprecation.proto" + } + } + ], + "types": { + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/dep.Legacy": { + "kind": "enum", + "id": "t/protobuf/dep.Legacy", + "name": { + "source": "Legacy", + "canonical": "legacy" + }, + "namespace": [ + "dep" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "dep.Legacy" + }, + "valueType": "int32", + "members": [ + { + "name": { + "source": "LEGACY_UNSPECIFIED", + "canonical": "legacy_unspecified" + }, + "value": { + "kind": "number", + "num": "0", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "OLD_VALUE", + "canonical": "old_value" + }, + "value": { + "kind": "number", + "num": "1", + "bytes": null, + "list": null, + "object": null + }, + "docs": {}, + "deprecation": {} + } + ], + "closed": false, + "flags": false + }, + "t/protobuf/dep.Old": { + "kind": "model", + "id": "t/protobuf/dep.Old", + "name": { + "source": "Old", + "canonical": "old" + }, + "namespace": [ + "dep" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "deprecation": {}, + "provenance": { + "source": 0, + "pointer": "dep.Old" + }, + "properties": [ + { + "id": "p/protobuf/dep.Old.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "deprecation": {}, + "provenance": { + "source": 0, + "pointer": "dep.Old.id" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "deprecation.proto", + "hash": "7e79b43c57e82ade00dd797a4bdd0825e8ea63a402e06c17be3a916ed96ad8f2" + } + ] +} diff --git a/testdata/conformance/protobuf/deprecation.proto b/testdata/conformance/protobuf/deprecation.proto new file mode 100644 index 0000000..709c9a6 --- /dev/null +++ b/testdata/conformance/protobuf/deprecation.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package dep; + +message Old { + option deprecated = true; + string id = 1 [deprecated = true]; +} + +enum Legacy { + LEGACY_UNSPECIFIED = 0; + OLD_VALUE = 1 [deprecated = true]; +} diff --git a/testdata/conformance/protobuf/editions.golden.json b/testdata/conformance/protobuf/editions.golden.json new file mode 100644 index 0000000..6b1fe85 --- /dev/null +++ b/testdata/conformance/protobuf/editions.golden.json @@ -0,0 +1,141 @@ +{ + "irVersion": "0.1.0", + "name": "ed", + "docs": {}, + "services": [ + { + "id": "s/protobuf/ed", + "name": { + "source": "ed", + "canonical": "ed" + }, + "docs": {}, + "namespace": [ + "ed" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "editions.proto" + } + } + ], + "types": { + "t/prim/int32": { + "kind": "primitive", + "id": "t/prim/int32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int32" + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/ed.Rec": { + "kind": "model", + "id": "t/protobuf/ed.Rec", + "name": { + "source": "Rec", + "canonical": "rec" + }, + "namespace": [ + "ed" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "ed.Rec" + }, + "properties": [ + { + "id": "p/protobuf/ed.Rec.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ed.Rec.id" + } + }, + { + "id": "p/protobuf/ed.Rec.count", + "name": { + "source": "count", + "canonical": "count" + }, + "wireID": 2, + "type": { + "target": "t/prim/int32", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ed.Rec.count" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "edition": "EDITION_2023", + "syntax": "editions" + } + }, + "sources": [ + { + "format": "protobuf@2023", + "path": "editions.proto", + "hash": "1d9bf761e53c93cec98f1a226e8dab3cacb29f473f049cc7091bd8b4d4516dcc" + } + ] +} diff --git a/testdata/conformance/protobuf/editions.proto b/testdata/conformance/protobuf/editions.proto new file mode 100644 index 0000000..55a4ec4 --- /dev/null +++ b/testdata/conformance/protobuf/editions.proto @@ -0,0 +1,8 @@ +edition = "2023"; + +package ed; + +message Rec { + string id = 1; + int32 count = 2 [features.field_presence = IMPLICIT]; +} diff --git a/testdata/conformance/protobuf/enum-closed.golden.json b/testdata/conformance/protobuf/enum-closed.golden.json new file mode 100644 index 0000000..d63fab7 --- /dev/null +++ b/testdata/conformance/protobuf/enum-closed.golden.json @@ -0,0 +1,167 @@ +{ + "irVersion": "0.1.0", + "name": "enums2", + "docs": {}, + "services": [ + { + "id": "s/protobuf/enums2", + "name": { + "source": "enums2", + "canonical": "enums_2" + }, + "docs": {}, + "namespace": [ + "enums2" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "enum-closed.proto" + } + } + ], + "types": { + "t/protobuf/enums2.Job": { + "kind": "model", + "id": "t/protobuf/enums2.Job", + "name": { + "source": "Job", + "canonical": "job" + }, + "namespace": [ + "enums2" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "enums2.Job" + }, + "properties": [ + { + "id": "p/protobuf/enums2.Job.status", + "name": { + "source": "status", + "canonical": "status" + }, + "wireID": 1, + "type": { + "target": "t/protobuf/enums2.Status", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "enums2.Job.status" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/enums2.Status": { + "kind": "enum", + "id": "t/protobuf/enums2.Status", + "name": { + "source": "Status", + "canonical": "status" + }, + "namespace": [ + "enums2" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "enums2.Status" + }, + "valueType": "int32", + "members": [ + { + "name": { + "source": "UNKNOWN", + "canonical": "unknown" + }, + "value": { + "kind": "number", + "num": "0", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "STARTED", + "canonical": "started" + }, + "value": { + "kind": "number", + "num": "1", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "RUNNING", + "canonical": "running" + }, + "value": { + "kind": "number", + "num": "1", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "DONE", + "canonical": "done" + }, + "value": { + "kind": "number", + "num": "2", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + } + ], + "closed": true, + "flags": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto2" + } + }, + "sources": [ + { + "format": "protobuf@2", + "path": "enum-closed.proto", + "hash": "09dcaa0e3133211768de80f6116f928131facdc0da0f76349b0af4262d9d5eaa" + } + ] +} diff --git a/testdata/conformance/protobuf/enum-closed.proto b/testdata/conformance/protobuf/enum-closed.proto new file mode 100644 index 0000000..c41ffe3 --- /dev/null +++ b/testdata/conformance/protobuf/enum-closed.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; + +package enums2; + +enum Status { + option allow_alias = true; + UNKNOWN = 0; + STARTED = 1; + RUNNING = 1; + DONE = 2; +} + +message Job { + optional Status status = 1; +} diff --git a/testdata/conformance/protobuf/enum-open.golden.json b/testdata/conformance/protobuf/enum-open.golden.json new file mode 100644 index 0000000..4e78828 --- /dev/null +++ b/testdata/conformance/protobuf/enum-open.golden.json @@ -0,0 +1,167 @@ +{ + "irVersion": "0.1.0", + "name": "enums", + "docs": {}, + "services": [ + { + "id": "s/protobuf/enums", + "name": { + "source": "enums", + "canonical": "enums" + }, + "docs": {}, + "namespace": [ + "enums" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "enum-open.proto" + } + } + ], + "types": { + "t/protobuf/enums.Color": { + "kind": "enum", + "id": "t/protobuf/enums.Color", + "name": { + "source": "Color", + "canonical": "color" + }, + "namespace": [ + "enums" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "enums.Color" + }, + "valueType": "int32", + "members": [ + { + "name": { + "source": "COLOR_UNSPECIFIED", + "canonical": "color_unspecified" + }, + "value": { + "kind": "number", + "num": "0", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "RED", + "canonical": "red" + }, + "value": { + "kind": "number", + "num": "1", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "GREEN", + "canonical": "green" + }, + "value": { + "kind": "number", + "num": "2", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "BLUE", + "canonical": "blue" + }, + "value": { + "kind": "number", + "num": "3", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + } + ], + "closed": false, + "flags": false + }, + "t/protobuf/enums.Paint": { + "kind": "model", + "id": "t/protobuf/enums.Paint", + "name": { + "source": "Paint", + "canonical": "paint" + }, + "namespace": [ + "enums" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "enums.Paint" + }, + "properties": [ + { + "id": "p/protobuf/enums.Paint.color", + "name": { + "source": "color", + "canonical": "color" + }, + "wireID": 1, + "type": { + "target": "t/protobuf/enums.Color", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "enums.Paint.color" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "enum-open.proto", + "hash": "b4e74769a7d4d9817ae79b24c15d1c08973f9a7cc7dd4923076de094fe69ca89" + } + ] +} diff --git a/testdata/conformance/protobuf/enum-open.proto b/testdata/conformance/protobuf/enum-open.proto new file mode 100644 index 0000000..1875ff9 --- /dev/null +++ b/testdata/conformance/protobuf/enum-open.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package enums; + +enum Color { + COLOR_UNSPECIFIED = 0; + RED = 1; + GREEN = 2; + BLUE = 3; +} + +message Paint { + Color color = 1; +} diff --git a/testdata/conformance/protobuf/extensions.golden.json b/testdata/conformance/protobuf/extensions.golden.json new file mode 100644 index 0000000..a41d982 --- /dev/null +++ b/testdata/conformance/protobuf/extensions.golden.json @@ -0,0 +1,178 @@ +{ + "irVersion": "0.1.0", + "name": "ext", + "docs": {}, + "services": [ + { + "id": "s/protobuf/ext", + "name": { + "source": "ext", + "canonical": "ext" + }, + "docs": {}, + "namespace": [ + "ext" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "extensions.proto" + } + } + ], + "types": { + "t/prim/int32": { + "kind": "primitive", + "id": "t/prim/int32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int32" + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/ext.Base": { + "kind": "model", + "id": "t/protobuf/ext.Base", + "name": { + "source": "Base", + "canonical": "base" + }, + "namespace": [ + "ext" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "ext.Base" + }, + "properties": [ + { + "id": "p/protobuf/ext.Base.name", + "name": { + "source": "name", + "canonical": "name" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ext.Base.name" + } + }, + { + "id": "p/protobuf/ext.priority", + "name": { + "source": "priority", + "canonical": "priority" + }, + "wireName": "[ext.priority]", + "wireID": 100, + "extensionOf": "ext", + "type": { + "target": "t/prim/int32", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ext.priority" + } + }, + { + "id": "p/protobuf/ext.tag", + "name": { + "source": "tag", + "canonical": "tag" + }, + "wireName": "[ext.tag]", + "wireID": 150, + "extensionOf": "ext", + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ext.tag" + } + } + ], + "abstract": false, + "positional": false, + "extensionRanges": [ + { + "from": 100, + "to": 200 + } + ], + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto2" + } + }, + "sources": [ + { + "format": "protobuf@2", + "path": "extensions.proto", + "hash": "91968728dbb693b75e73efaf8d59fa5bf943a7d358d0111b3bd8831ee5d2d3f1" + } + ] +} diff --git a/testdata/conformance/protobuf/extensions.proto b/testdata/conformance/protobuf/extensions.proto new file mode 100644 index 0000000..516e41e --- /dev/null +++ b/testdata/conformance/protobuf/extensions.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; + +package ext; + +message Base { + optional string name = 1; + extensions 100 to 200; +} + +extend Base { + optional int32 priority = 100; + optional string tag = 150; +} diff --git a/testdata/conformance/protobuf/map.golden.json b/testdata/conformance/protobuf/map.golden.json new file mode 100644 index 0000000..1c52641 --- /dev/null +++ b/testdata/conformance/protobuf/map.golden.json @@ -0,0 +1,194 @@ +{ + "irVersion": "0.1.0", + "name": "maps", + "docs": {}, + "services": [ + { + "id": "s/protobuf/maps", + "name": { + "source": "maps", + "canonical": "maps" + }, + "docs": {}, + "namespace": [ + "maps" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "map.proto" + } + } + ], + "types": { + "t/anon/protobuf/maps.Dict.counts/map": { + "kind": "map", + "id": "t/anon/protobuf/maps.Dict.counts/map", + "name": { + "hint": "counts" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "maps.Dict.counts" + }, + "key": { + "target": "t/prim/string", + "nullable": false + }, + "value": { + "target": "t/prim/int32", + "nullable": false + } + }, + "t/anon/protobuf/maps.Dict.names/map": { + "kind": "map", + "id": "t/anon/protobuf/maps.Dict.names/map", + "name": { + "hint": "names" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "maps.Dict.names" + }, + "key": { + "target": "t/prim/int64", + "nullable": false + }, + "value": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/prim/int32": { + "kind": "primitive", + "id": "t/prim/int32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int32" + }, + "t/prim/int64": { + "kind": "primitive", + "id": "t/prim/int64", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int64" + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/maps.Dict": { + "kind": "model", + "id": "t/protobuf/maps.Dict", + "name": { + "source": "Dict", + "canonical": "dict" + }, + "namespace": [ + "maps" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "maps.Dict" + }, + "properties": [ + { + "id": "p/protobuf/maps.Dict.counts", + "name": { + "source": "counts", + "canonical": "counts" + }, + "wireID": 1, + "type": { + "target": "t/anon/protobuf/maps.Dict.counts/map", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "maps.Dict.counts" + } + }, + { + "id": "p/protobuf/maps.Dict.names", + "name": { + "source": "names", + "canonical": "names" + }, + "wireID": 2, + "type": { + "target": "t/anon/protobuf/maps.Dict.names/map", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "maps.Dict.names" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "map.proto", + "hash": "1ad16634cc1ed4e150038a5f7c3ae6daf5ff4c16e2f9390cfbafee68daa76eb6" + } + ] +} diff --git a/testdata/conformance/protobuf/map.proto b/testdata/conformance/protobuf/map.proto new file mode 100644 index 0000000..56beef0 --- /dev/null +++ b/testdata/conformance/protobuf/map.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package maps; + +message Dict { + map counts = 1; + map names = 2; +} diff --git a/testdata/conformance/protobuf/messages.golden.json b/testdata/conformance/protobuf/messages.golden.json new file mode 100644 index 0000000..f8b578b --- /dev/null +++ b/testdata/conformance/protobuf/messages.golden.json @@ -0,0 +1,369 @@ +{ + "irVersion": "0.1.0", + "name": "shop", + "docs": {}, + "services": [ + { + "id": "s/protobuf/shop", + "name": { + "source": "shop", + "canonical": "shop" + }, + "docs": {}, + "namespace": [ + "shop" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "messages.proto" + } + } + ], + "types": { + "t/prim/bool": { + "kind": "primitive", + "id": "t/prim/bool", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "bool" + }, + "t/prim/bytes": { + "kind": "primitive", + "id": "t/prim/bytes", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "bytes" + }, + "t/prim/float64": { + "kind": "primitive", + "id": "t/prim/float64", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "float64" + }, + "t/prim/int64": { + "kind": "primitive", + "id": "t/prim/int64", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int64" + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/shop.Category": { + "kind": "model", + "id": "t/protobuf/shop.Category", + "name": { + "source": "Category", + "canonical": "category" + }, + "namespace": [ + "shop" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "shop.Category" + }, + "properties": [ + { + "id": "p/protobuf/shop.Category.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/int64", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "shop.Category.id" + } + }, + { + "id": "p/protobuf/shop.Category.label", + "name": { + "source": "label", + "canonical": "label" + }, + "wireID": 2, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "shop.Category.label" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/shop.Product": { + "kind": "model", + "id": "t/protobuf/shop.Product", + "name": { + "source": "Product", + "canonical": "product" + }, + "namespace": [ + "shop" + ], + "anonymous": false, + "docs": { + "description": "A product for sale." + }, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "shop.Product" + }, + "properties": [ + { + "id": "p/protobuf/shop.Product.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/int64", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "shop.Product.id" + } + }, + { + "id": "p/protobuf/shop.Product.name", + "name": { + "source": "name", + "canonical": "name" + }, + "wireID": 2, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "shop.Product.name" + } + }, + { + "id": "p/protobuf/shop.Product.active", + "name": { + "source": "active", + "canonical": "active" + }, + "wireID": 3, + "type": { + "target": "t/prim/bool", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "shop.Product.active" + } + }, + { + "id": "p/protobuf/shop.Product.price", + "name": { + "source": "price", + "canonical": "price" + }, + "wireID": 4, + "type": { + "target": "t/prim/float64", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "shop.Product.price" + } + }, + { + "id": "p/protobuf/shop.Product.image", + "name": { + "source": "image", + "canonical": "image" + }, + "wireID": 5, + "type": { + "target": "t/prim/bytes", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "shop.Product.image" + } + }, + { + "id": "p/protobuf/shop.Product.category", + "name": { + "source": "category", + "canonical": "category" + }, + "wireID": 6, + "type": { + "target": "t/protobuf/shop.Category", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "shop.Product.category" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "messages.proto", + "hash": "b312aa53fa9bf2a6c72218d4adc822033a3c6c8bdab16ac502ffc3f3f4faed5f" + } + ] +} diff --git a/testdata/conformance/protobuf/messages.proto b/testdata/conformance/protobuf/messages.proto new file mode 100644 index 0000000..3a0886c --- /dev/null +++ b/testdata/conformance/protobuf/messages.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package shop; + +// A product for sale. +message Product { + int64 id = 1; + string name = 2; + bool active = 3; + double price = 4; + bytes image = 5; + Category category = 6; +} + +message Category { + int64 id = 1; + string label = 2; +} diff --git a/testdata/conformance/protobuf/nested.golden.json b/testdata/conformance/protobuf/nested.golden.json new file mode 100644 index 0000000..6905676 --- /dev/null +++ b/testdata/conformance/protobuf/nested.golden.json @@ -0,0 +1,230 @@ +{ + "irVersion": "0.1.0", + "name": "nested", + "docs": {}, + "services": [ + { + "id": "s/protobuf/nested", + "name": { + "source": "nested", + "canonical": "nested" + }, + "docs": {}, + "namespace": [ + "nested" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "nested.proto" + } + } + ], + "types": { + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/nested.Outer": { + "kind": "model", + "id": "t/protobuf/nested.Outer", + "name": { + "source": "Outer", + "canonical": "outer" + }, + "namespace": [ + "nested" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "nested.Outer" + }, + "properties": [ + { + "id": "p/protobuf/nested.Outer.inner", + "name": { + "source": "inner", + "canonical": "inner" + }, + "wireID": 1, + "type": { + "target": "t/protobuf/nested.Outer.Inner", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "nested.Outer.inner" + } + }, + { + "id": "p/protobuf/nested.Outer.kind", + "name": { + "source": "kind", + "canonical": "kind" + }, + "wireID": 2, + "type": { + "target": "t/protobuf/nested.Outer.Kind", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "nested.Outer.kind" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/nested.Outer.Inner": { + "kind": "model", + "id": "t/protobuf/nested.Outer.Inner", + "name": { + "source": "Inner", + "canonical": "inner" + }, + "namespace": [ + "nested" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "nested.Outer.Inner" + }, + "properties": [ + { + "id": "p/protobuf/nested.Outer.Inner.value", + "name": { + "source": "value", + "canonical": "value" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "nested.Outer.Inner.value" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/nested.Outer.Kind": { + "kind": "enum", + "id": "t/protobuf/nested.Outer.Kind", + "name": { + "source": "Kind", + "canonical": "kind" + }, + "namespace": [ + "nested" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "nested.Outer.Kind" + }, + "valueType": "int32", + "members": [ + { + "name": { + "source": "KIND_UNSPECIFIED", + "canonical": "kind_unspecified" + }, + "value": { + "kind": "number", + "num": "0", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "PRIMARY", + "canonical": "primary" + }, + "value": { + "kind": "number", + "num": "1", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + } + ], + "closed": false, + "flags": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "nested.proto", + "hash": "3ca66400c2c97a08e4fdca5c19f7d447b4bede545693eb6f491c75eb36763b8a" + } + ] +} diff --git a/testdata/conformance/protobuf/nested.proto b/testdata/conformance/protobuf/nested.proto new file mode 100644 index 0000000..d4fc7e4 --- /dev/null +++ b/testdata/conformance/protobuf/nested.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package nested; + +message Outer { + message Inner { + string value = 1; + } + enum Kind { + KIND_UNSPECIFIED = 0; + PRIMARY = 1; + } + Inner inner = 1; + Kind kind = 2; +} diff --git a/testdata/conformance/protobuf/oneof.golden.json b/testdata/conformance/protobuf/oneof.golden.json new file mode 100644 index 0000000..98a0311 --- /dev/null +++ b/testdata/conformance/protobuf/oneof.golden.json @@ -0,0 +1,193 @@ +{ + "irVersion": "0.1.0", + "name": "oneofs", + "docs": {}, + "services": [ + { + "id": "s/protobuf/oneofs", + "name": { + "source": "oneofs", + "canonical": "oneofs" + }, + "docs": {}, + "namespace": [ + "oneofs" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "oneof.proto" + } + } + ], + "types": { + "t/anon/protobuf/oneofs.Shape.form/oneof": { + "kind": "union", + "id": "t/anon/protobuf/oneofs.Shape.form/oneof", + "name": { + "hint": "form" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "oneofs.Shape.form" + }, + "variants": [ + { + "name": { + "source": "radius", + "canonical": "radius" + }, + "type": { + "target": "t/prim/float64", + "nullable": false + }, + "wireID": 1, + "docs": {} + }, + { + "name": { + "source": "side", + "canonical": "side" + }, + "type": { + "target": "t/prim/float64", + "nullable": false + }, + "wireID": 2, + "docs": {} + }, + { + "name": { + "source": "label", + "canonical": "label" + }, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "wireID": 3, + "docs": {} + } + ], + "exclusive": true, + "wireTagged": true + }, + "t/prim/float64": { + "kind": "primitive", + "id": "t/prim/float64", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "float64" + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/oneofs.Shape": { + "kind": "model", + "id": "t/protobuf/oneofs.Shape", + "name": { + "source": "Shape", + "canonical": "shape" + }, + "namespace": [ + "oneofs" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "oneofs.Shape" + }, + "properties": [ + { + "id": "p/protobuf/oneofs.Shape.form", + "name": { + "source": "form", + "canonical": "form" + }, + "type": { + "target": "t/anon/protobuf/oneofs.Shape.form/oneof", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": true, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "oneofs.Shape.form" + } + }, + { + "id": "p/protobuf/oneofs.Shape.name", + "name": { + "source": "name", + "canonical": "name" + }, + "wireID": 4, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "oneofs.Shape.name" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "oneof.proto", + "hash": "30cce65ed9fa8b783f064d87b9309eb86678260a5867b9bfbf2e40bc71f7077a" + } + ] +} diff --git a/testdata/conformance/protobuf/oneof.proto b/testdata/conformance/protobuf/oneof.proto new file mode 100644 index 0000000..59d9dbd --- /dev/null +++ b/testdata/conformance/protobuf/oneof.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package oneofs; + +message Shape { + oneof form { + double radius = 1; + double side = 2; + string label = 3; + } + string name = 4; +} diff --git a/testdata/conformance/protobuf/package.golden.json b/testdata/conformance/protobuf/package.golden.json new file mode 100644 index 0000000..d1d6f87 --- /dev/null +++ b/testdata/conformance/protobuf/package.golden.json @@ -0,0 +1,104 @@ +{ + "irVersion": "0.1.0", + "name": "deep.nested.pkg", + "docs": {}, + "services": [ + { + "id": "s/protobuf/deep.nested.pkg", + "name": { + "source": "deep.nested.pkg", + "canonical": "deep_nested_pkg" + }, + "docs": {}, + "namespace": [ + "deep", + "nested", + "pkg" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "package.proto" + } + } + ], + "types": { + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/deep.nested.pkg.Thing": { + "kind": "model", + "id": "t/protobuf/deep.nested.pkg.Thing", + "name": { + "source": "Thing", + "canonical": "thing" + }, + "namespace": [ + "deep", + "nested", + "pkg" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "deep.nested.pkg.Thing" + }, + "properties": [ + { + "id": "p/protobuf/deep.nested.pkg.Thing.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "deep.nested.pkg.Thing.id" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "package.proto", + "hash": "cb0d01ae57ec6d36b897bb9caeb5592ae44b01f4457ccbe91d5625577409a54a" + } + ] +} diff --git a/testdata/conformance/protobuf/package.proto b/testdata/conformance/protobuf/package.proto new file mode 100644 index 0000000..00c8d1c --- /dev/null +++ b/testdata/conformance/protobuf/package.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package deep.nested.pkg; + +message Thing { + string id = 1; +} diff --git a/testdata/conformance/protobuf/presence.golden.json b/testdata/conformance/protobuf/presence.golden.json new file mode 100644 index 0000000..f52d53f --- /dev/null +++ b/testdata/conformance/protobuf/presence.golden.json @@ -0,0 +1,173 @@ +{ + "irVersion": "0.1.0", + "name": "pres", + "docs": {}, + "services": [ + { + "id": "s/protobuf/pres", + "name": { + "source": "pres", + "canonical": "pres" + }, + "docs": {}, + "namespace": [ + "pres" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "presence.proto" + } + } + ], + "types": { + "t/anon/protobuf/pres.Rec.tags/list": { + "kind": "list", + "id": "t/anon/protobuf/pres.Rec.tags/list", + "name": { + "hint": "tags" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "pres.Rec.tags" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/pres.Rec": { + "kind": "model", + "id": "t/protobuf/pres.Rec", + "name": { + "source": "Rec", + "canonical": "rec" + }, + "namespace": [ + "pres" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "pres.Rec" + }, + "properties": [ + { + "id": "p/protobuf/pres.Rec.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": true, + "presence": "required", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "pres.Rec.id" + } + }, + { + "id": "p/protobuf/pres.Rec.note", + "name": { + "source": "note", + "canonical": "note" + }, + "wireID": 2, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "pres.Rec.note" + } + }, + { + "id": "p/protobuf/pres.Rec.tags", + "name": { + "source": "tags", + "canonical": "tags" + }, + "wireID": 3, + "type": { + "target": "t/anon/protobuf/pres.Rec.tags/list", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "pres.Rec.tags" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto2" + } + }, + "sources": [ + { + "format": "protobuf@2", + "path": "presence.proto", + "hash": "5b017ab69e3fdebbe2e6b2e869325af0ff64ce0056836c74dd758b36191af1bb" + } + ] +} diff --git a/testdata/conformance/protobuf/presence.proto b/testdata/conformance/protobuf/presence.proto new file mode 100644 index 0000000..6eed9d6 --- /dev/null +++ b/testdata/conformance/protobuf/presence.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; + +package pres; + +message Rec { + required string id = 1; + optional string note = 2; + repeated string tags = 3; +} diff --git a/testdata/conformance/protobuf/proto3-optional.golden.json b/testdata/conformance/protobuf/proto3-optional.golden.json new file mode 100644 index 0000000..759ee7f --- /dev/null +++ b/testdata/conformance/protobuf/proto3-optional.golden.json @@ -0,0 +1,140 @@ +{ + "irVersion": "0.1.0", + "name": "opt3", + "docs": {}, + "services": [ + { + "id": "s/protobuf/opt3", + "name": { + "source": "opt3", + "canonical": "opt_3" + }, + "docs": {}, + "namespace": [ + "opt3" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "proto3-optional.proto" + } + } + ], + "types": { + "t/prim/int32": { + "kind": "primitive", + "id": "t/prim/int32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int32" + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/opt3.Config": { + "kind": "model", + "id": "t/protobuf/opt3.Config", + "name": { + "source": "Config", + "canonical": "config" + }, + "namespace": [ + "opt3" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "opt3.Config" + }, + "properties": [ + { + "id": "p/protobuf/opt3.Config.name", + "name": { + "source": "name", + "canonical": "name" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "opt3.Config.name" + } + }, + { + "id": "p/protobuf/opt3.Config.count", + "name": { + "source": "count", + "canonical": "count" + }, + "wireID": 2, + "type": { + "target": "t/prim/int32", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "opt3.Config.count" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "proto3-optional.proto", + "hash": "1bed764c8b72adcd9e2a4e4868e19546b5d1c7aba1e771db15bbf0cd3464abef" + } + ] +} diff --git a/testdata/conformance/protobuf/proto3-optional.proto b/testdata/conformance/protobuf/proto3-optional.proto new file mode 100644 index 0000000..3817c10 --- /dev/null +++ b/testdata/conformance/protobuf/proto3-optional.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package opt3; + +message Config { + optional string name = 1; + int32 count = 2; +} diff --git a/testdata/conformance/protobuf/repeated.golden.json b/testdata/conformance/protobuf/repeated.golden.json new file mode 100644 index 0000000..5a64e2e --- /dev/null +++ b/testdata/conformance/protobuf/repeated.golden.json @@ -0,0 +1,225 @@ +{ + "irVersion": "0.1.0", + "name": "rep", + "docs": {}, + "services": [ + { + "id": "s/protobuf/rep", + "name": { + "source": "rep", + "canonical": "rep" + }, + "docs": {}, + "namespace": [ + "rep" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "repeated.proto" + } + } + ], + "types": { + "t/anon/protobuf/rep.Series.expanded_values/list": { + "kind": "list", + "id": "t/anon/protobuf/rep.Series.expanded_values/list", + "name": { + "hint": "expanded_values" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "rep.Series.expanded_values" + }, + "elem": { + "target": "t/prim/int32", + "nullable": false + }, + "encoding": { + "name": "expanded" + } + }, + "t/anon/protobuf/rep.Series.labels/list": { + "kind": "list", + "id": "t/anon/protobuf/rep.Series.labels/list", + "name": { + "hint": "labels" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "rep.Series.labels" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/protobuf/rep.Series.packed_values/list": { + "kind": "list", + "id": "t/anon/protobuf/rep.Series.packed_values/list", + "name": { + "hint": "packed_values" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "rep.Series.packed_values" + }, + "elem": { + "target": "t/prim/int32", + "nullable": false + }, + "encoding": { + "name": "packed" + } + }, + "t/prim/int32": { + "kind": "primitive", + "id": "t/prim/int32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int32" + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/rep.Series": { + "kind": "model", + "id": "t/protobuf/rep.Series", + "name": { + "source": "Series", + "canonical": "series" + }, + "namespace": [ + "rep" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "rep.Series" + }, + "properties": [ + { + "id": "p/protobuf/rep.Series.packed_values", + "name": { + "source": "packed_values", + "canonical": "packed_values" + }, + "wireID": 1, + "type": { + "target": "t/anon/protobuf/rep.Series.packed_values/list", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "rep.Series.packed_values" + } + }, + { + "id": "p/protobuf/rep.Series.expanded_values", + "name": { + "source": "expanded_values", + "canonical": "expanded_values" + }, + "wireID": 2, + "type": { + "target": "t/anon/protobuf/rep.Series.expanded_values/list", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "rep.Series.expanded_values" + } + }, + { + "id": "p/protobuf/rep.Series.labels", + "name": { + "source": "labels", + "canonical": "labels" + }, + "wireID": 3, + "type": { + "target": "t/anon/protobuf/rep.Series.labels/list", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "rep.Series.labels" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "repeated.proto", + "hash": "7375f034c6eaaced0ea0403727f69d70579a1d195d0a281b6fb3f9c3f13727de" + } + ] +} diff --git a/testdata/conformance/protobuf/repeated.proto b/testdata/conformance/protobuf/repeated.proto new file mode 100644 index 0000000..8cb3f8a --- /dev/null +++ b/testdata/conformance/protobuf/repeated.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package rep; + +message Series { + repeated int32 packed_values = 1; + repeated int32 expanded_values = 2 [packed = false]; + repeated string labels = 3; +} diff --git a/testdata/conformance/protobuf/reserved.golden.json b/testdata/conformance/protobuf/reserved.golden.json new file mode 100644 index 0000000..949a603 --- /dev/null +++ b/testdata/conformance/protobuf/reserved.golden.json @@ -0,0 +1,210 @@ +{ + "irVersion": "0.1.0", + "name": "resv", + "docs": {}, + "services": [ + { + "id": "s/protobuf/resv", + "name": { + "source": "resv", + "canonical": "resv" + }, + "docs": {}, + "namespace": [ + "resv" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "reserved.proto" + } + } + ], + "types": { + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/resv.E": { + "kind": "enum", + "id": "t/protobuf/resv.E", + "name": { + "source": "E", + "canonical": "e" + }, + "namespace": [ + "resv" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "extensions": { + "protobuf:reserved": { + "ranges": [ + { + "from": 2, + "to": 2 + }, + { + "from": 5, + "to": 7 + } + ], + "names": [ + "OLD" + ] + } + }, + "provenance": { + "source": 0, + "pointer": "resv.E" + }, + "valueType": "int32", + "members": [ + { + "name": { + "source": "E_UNSPECIFIED", + "canonical": "e_unspecified" + }, + "value": { + "kind": "number", + "num": "0", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "KEEP", + "canonical": "keep" + }, + "value": { + "kind": "number", + "num": "1", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + } + ], + "closed": false, + "flags": false + }, + "t/protobuf/resv.M": { + "kind": "model", + "id": "t/protobuf/resv.M", + "name": { + "source": "M", + "canonical": "m" + }, + "namespace": [ + "resv" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "extensions": { + "protobuf:reserved": { + "ranges": [ + { + "from": 2, + "to": 2 + }, + { + "from": 15, + "to": 15 + }, + { + "from": 9, + "to": 11 + } + ], + "names": [ + "foo", + "bar" + ] + } + }, + "provenance": { + "source": 0, + "pointer": "resv.M" + }, + "properties": [ + { + "id": "p/protobuf/resv.M.keep", + "name": { + "source": "keep", + "canonical": "keep" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "resv.M.keep" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "diagnostics": [ + { + "severity": "info", + "code": "protobuf/reserved", + "message": "reserved field numbers/names for resv.M preserved in Extensions", + "provenance": { + "source": 0, + "pointer": "resv.M" + } + }, + { + "severity": "info", + "code": "protobuf/reserved", + "message": "reserved enum numbers/names for resv.E preserved in Extensions", + "provenance": { + "source": 0, + "pointer": "resv.E" + } + } + ], + "sources": [ + { + "format": "protobuf@3", + "path": "reserved.proto", + "hash": "287139755354e17cecdf6cdf8b3af2fae88931dc13c30f7633eedefb5e4a8498" + } + ] +} diff --git a/testdata/conformance/protobuf/reserved.proto b/testdata/conformance/protobuf/reserved.proto new file mode 100644 index 0000000..46ee32c --- /dev/null +++ b/testdata/conformance/protobuf/reserved.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package resv; + +message M { + string keep = 1; + reserved 2, 15, 9 to 11; + reserved "foo", "bar"; +} + +enum E { + E_UNSPECIFIED = 0; + KEEP = 1; + reserved 2, 5 to 7; + reserved "OLD"; +} diff --git a/testdata/conformance/protobuf/scalar-encoding.golden.json b/testdata/conformance/protobuf/scalar-encoding.golden.json new file mode 100644 index 0000000..8b4cc10 --- /dev/null +++ b/testdata/conformance/protobuf/scalar-encoding.golden.json @@ -0,0 +1,263 @@ +{ + "irVersion": "0.1.0", + "name": "enc", + "docs": {}, + "services": [ + { + "id": "s/protobuf/enc", + "name": { + "source": "enc", + "canonical": "enc" + }, + "docs": {}, + "namespace": [ + "enc" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "scalar-encoding.proto" + } + } + ], + "types": { + "t/prim/int32": { + "kind": "primitive", + "id": "t/prim/int32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int32" + }, + "t/prim/int64": { + "kind": "primitive", + "id": "t/prim/int64", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int64" + }, + "t/prim/uint32": { + "kind": "primitive", + "id": "t/prim/uint32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "uint32" + }, + "t/prim/uint64": { + "kind": "primitive", + "id": "t/prim/uint64", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "uint64" + }, + "t/protobuf/enc.Nums": { + "kind": "model", + "id": "t/protobuf/enc.Nums", + "name": { + "source": "Nums", + "canonical": "nums" + }, + "namespace": [ + "enc" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "enc.Nums" + }, + "properties": [ + { + "id": "p/protobuf/enc.Nums.a", + "name": { + "source": "a", + "canonical": "a" + }, + "wireID": 1, + "type": { + "target": "t/prim/int32", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "encoding": { + "name": "zigzag" + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "enc.Nums.a" + } + }, + { + "id": "p/protobuf/enc.Nums.b", + "name": { + "source": "b", + "canonical": "b" + }, + "wireID": 2, + "type": { + "target": "t/prim/int64", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "encoding": { + "name": "zigzag" + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "enc.Nums.b" + } + }, + { + "id": "p/protobuf/enc.Nums.c", + "name": { + "source": "c", + "canonical": "c" + }, + "wireID": 3, + "type": { + "target": "t/prim/uint32", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "encoding": { + "name": "fixed" + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "enc.Nums.c" + } + }, + { + "id": "p/protobuf/enc.Nums.d", + "name": { + "source": "d", + "canonical": "d" + }, + "wireID": 4, + "type": { + "target": "t/prim/int64", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "encoding": { + "name": "fixed" + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "enc.Nums.d" + } + }, + { + "id": "p/protobuf/enc.Nums.e", + "name": { + "source": "e", + "canonical": "e" + }, + "wireID": 5, + "type": { + "target": "t/prim/uint64", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "encoding": { + "name": "fixed" + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "enc.Nums.e" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "scalar-encoding.proto", + "hash": "7ea34f3a40f03b5f0edcfbcdfd744a034c7b447ce3f8000a016a23d47828b2a8" + } + ] +} diff --git a/testdata/conformance/protobuf/scalar-encoding.proto b/testdata/conformance/protobuf/scalar-encoding.proto new file mode 100644 index 0000000..02a98ef --- /dev/null +++ b/testdata/conformance/protobuf/scalar-encoding.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package enc; + +message Nums { + sint32 a = 1; + sint64 b = 2; + fixed32 c = 3; + sfixed64 d = 4; + fixed64 e = 5; +} diff --git a/testdata/conformance/protobuf/services.golden.json b/testdata/conformance/protobuf/services.golden.json new file mode 100644 index 0000000..cfe2650 --- /dev/null +++ b/testdata/conformance/protobuf/services.golden.json @@ -0,0 +1,551 @@ +{ + "irVersion": "0.1.0", + "name": "svc", + "docs": {}, + "services": [ + { + "id": "s/protobuf/svc", + "name": { + "source": "svc", + "canonical": "svc" + }, + "docs": {}, + "namespace": [ + "svc" + ], + "groups": [ + { + "name": { + "source": "Echo", + "canonical": "echo" + }, + "docs": {}, + "operations": [ + { + "id": "op/protobuf/svc.Echo.Unary", + "name": { + "source": "Unary", + "canonical": "unary" + }, + "docs": {}, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Pong", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "idempotency": {}, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/svc.Echo/Unary", + "inputType": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + }, + "provenance": { + "source": 0, + "pointer": "svc.Echo.Unary" + } + }, + { + "id": "op/protobuf/svc.Echo.ClientStream", + "name": { + "source": "ClientStream", + "canonical": "client_stream" + }, + "docs": {}, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Pong", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "streaming": "client", + "requestStream": { + "requiresLength": false + }, + "idempotency": {}, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/svc.Echo/ClientStream", + "inputType": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + }, + "provenance": { + "source": 0, + "pointer": "svc.Echo.ClientStream" + } + }, + { + "id": "op/protobuf/svc.Echo.ServerStream", + "name": { + "source": "ServerStream", + "canonical": "server_stream" + }, + "docs": {}, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Pong", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "streaming": "server", + "responseStream": { + "requiresLength": false + }, + "idempotency": {}, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/svc.Echo/ServerStream", + "inputType": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + }, + "provenance": { + "source": 0, + "pointer": "svc.Echo.ServerStream" + } + }, + { + "id": "op/protobuf/svc.Echo.BidiStream", + "name": { + "source": "BidiStream", + "canonical": "bidi_stream" + }, + "docs": {}, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Pong", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "streaming": "bidi", + "requestStream": { + "requiresLength": false + }, + "responseStream": { + "requiresLength": false + }, + "idempotency": {}, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/svc.Echo/BidiStream", + "inputType": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + }, + "provenance": { + "source": 0, + "pointer": "svc.Echo.BidiStream" + } + }, + { + "id": "op/protobuf/svc.Echo.Fetch", + "name": { + "source": "Fetch", + "canonical": "fetch" + }, + "docs": {}, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Pong", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "idempotency": { + "kind": "safe" + }, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/svc.Echo/Fetch", + "inputType": { + "target": "t/protobuf/svc.Ping", + "nullable": false + }, + "idempotencyLevel": "NO_SIDE_EFFECTS" + } + }, + "provenance": { + "source": 0, + "pointer": "svc.Echo.Fetch" + } + }, + { + "id": "op/protobuf/svc.Echo.Replace", + "name": { + "source": "Replace", + "canonical": "replace" + }, + "docs": {}, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Pong", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "idempotency": { + "kind": "idempotent" + }, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/svc.Echo/Replace", + "inputType": { + "target": "t/protobuf/svc.Ping", + "nullable": false + }, + "idempotencyLevel": "IDEMPOTENT" + } + }, + "provenance": { + "source": 0, + "pointer": "svc.Echo.Replace" + } + }, + { + "id": "op/protobuf/svc.Echo.Notify", + "name": { + "source": "Notify", + "canonical": "notify" + }, + "docs": {}, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "docs": {} + } + ], + "oneWay": false, + "idempotency": {}, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/svc.Echo/Notify", + "inputType": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + }, + "provenance": { + "source": 0, + "pointer": "svc.Echo.Notify" + } + } + ] + } + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "services.proto" + } + } + ], + "types": { + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/svc.Ping": { + "kind": "model", + "id": "t/protobuf/svc.Ping", + "name": { + "source": "Ping", + "canonical": "ping" + }, + "namespace": [ + "svc" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "svc.Ping" + }, + "properties": [ + { + "id": "p/protobuf/svc.Ping.msg", + "name": { + "source": "msg", + "canonical": "msg" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "svc.Ping.msg" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/svc.Pong": { + "kind": "model", + "id": "t/protobuf/svc.Pong", + "name": { + "source": "Pong", + "canonical": "pong" + }, + "namespace": [ + "svc" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "svc.Pong" + }, + "properties": [ + { + "id": "p/protobuf/svc.Pong.msg", + "name": { + "source": "msg", + "canonical": "msg" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "svc.Pong.msg" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "services.proto", + "hash": "b93910b5b74c4c859ac5063d914154d539a5f83124a60273c168c4f977c0cb9c" + } + ] +} diff --git a/testdata/conformance/protobuf/services.proto b/testdata/conformance/protobuf/services.proto new file mode 100644 index 0000000..bdf6f56 --- /dev/null +++ b/testdata/conformance/protobuf/services.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package svc; + +import "google/protobuf/empty.proto"; + +message Ping { + string msg = 1; +} + +message Pong { + string msg = 1; +} + +service Echo { + rpc Unary(Ping) returns (Pong); + rpc ClientStream(stream Ping) returns (Pong); + rpc ServerStream(Ping) returns (stream Pong); + rpc BidiStream(stream Ping) returns (stream Pong); + rpc Fetch(Ping) returns (Pong) { + option idempotency_level = NO_SIDE_EFFECTS; + } + rpc Replace(Ping) returns (Pong) { + option idempotency_level = IDEMPOTENT; + } + rpc Notify(Ping) returns (google.protobuf.Empty); +} diff --git a/testdata/conformance/protobuf/well-known.golden.json b/testdata/conformance/protobuf/well-known.golden.json new file mode 100644 index 0000000..7674215 --- /dev/null +++ b/testdata/conformance/protobuf/well-known.golden.json @@ -0,0 +1,379 @@ +{ + "irVersion": "0.1.0", + "name": "wkt", + "docs": {}, + "services": [ + { + "id": "s/protobuf/wkt", + "name": { + "source": "wkt", + "canonical": "wkt" + }, + "docs": {}, + "namespace": [ + "wkt" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "well-known.proto" + } + } + ], + "types": { + "t/prim/datetime": { + "kind": "primitive", + "id": "t/prim/datetime", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "datetime" + }, + "t/prim/duration": { + "kind": "primitive", + "id": "t/prim/duration", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "duration" + }, + "t/prim/int32": { + "kind": "primitive", + "id": "t/prim/int32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int32" + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/any": { + "kind": "any", + "id": "t/protobuf/any", + "name": { + "hint": "any" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + } + }, + "t/protobuf/external/google.protobuf.Empty": { + "kind": "external", + "id": "t/protobuf/external/google.protobuf.Empty", + "name": { + "source": "google.protobuf.Empty", + "canonical": "empty" + }, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "google.protobuf.Empty" + }, + "identity": "google.protobuf.Empty", + "package": "google.golang.org/protobuf/types/known/emptypb" + }, + "t/protobuf/external/google.protobuf.FieldMask": { + "kind": "external", + "id": "t/protobuf/external/google.protobuf.FieldMask", + "name": { + "source": "google.protobuf.FieldMask", + "canonical": "field_mask" + }, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "google.protobuf.FieldMask" + }, + "identity": "google.protobuf.FieldMask", + "package": "google.golang.org/protobuf/types/known/fieldmaskpb" + }, + "t/protobuf/wkt.Bundle": { + "kind": "model", + "id": "t/protobuf/wkt.Bundle", + "name": { + "source": "Bundle", + "canonical": "bundle" + }, + "namespace": [ + "wkt" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "wkt.Bundle" + }, + "properties": [ + { + "id": "p/protobuf/wkt.Bundle.ts", + "name": { + "source": "ts", + "canonical": "ts" + }, + "wireID": 1, + "type": { + "target": "t/prim/datetime", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "wkt.Bundle.ts" + } + }, + { + "id": "p/protobuf/wkt.Bundle.dur", + "name": { + "source": "dur", + "canonical": "dur" + }, + "wireID": 2, + "type": { + "target": "t/prim/duration", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "wkt.Bundle.dur" + } + }, + { + "id": "p/protobuf/wkt.Bundle.any", + "name": { + "source": "any", + "canonical": "any" + }, + "wireID": 3, + "type": { + "target": "t/protobuf/any", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "wkt.Bundle.any" + } + }, + { + "id": "p/protobuf/wkt.Bundle.props", + "name": { + "source": "props", + "canonical": "props" + }, + "wireID": 4, + "type": { + "target": "t/protobuf/any", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "wkt.Bundle.props" + } + }, + { + "id": "p/protobuf/wkt.Bundle.mask", + "name": { + "source": "mask", + "canonical": "mask" + }, + "wireID": 5, + "type": { + "target": "t/protobuf/external/google.protobuf.FieldMask", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "wkt.Bundle.mask" + } + }, + { + "id": "p/protobuf/wkt.Bundle.count", + "name": { + "source": "count", + "canonical": "count" + }, + "wireID": 6, + "type": { + "target": "t/prim/int32", + "nullable": true + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "wkt.Bundle.count" + } + }, + { + "id": "p/protobuf/wkt.Bundle.note", + "name": { + "source": "note", + "canonical": "note" + }, + "wireID": 7, + "type": { + "target": "t/prim/string", + "nullable": true + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "wkt.Bundle.note" + } + }, + { + "id": "p/protobuf/wkt.Bundle.nothing", + "name": { + "source": "nothing", + "canonical": "nothing" + }, + "wireID": 8, + "type": { + "target": "t/protobuf/external/google.protobuf.Empty", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "wkt.Bundle.nothing" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "well-known.proto", + "hash": "bb1e63ecc4c6e64308158b089e858e63f873889d602453d5a5240ccbda3aa37e" + } + ] +} diff --git a/testdata/conformance/protobuf/well-known.proto b/testdata/conformance/protobuf/well-known.proto new file mode 100644 index 0000000..cb00aef --- /dev/null +++ b/testdata/conformance/protobuf/well-known.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package wkt; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/wrappers.proto"; + +message Bundle { + google.protobuf.Timestamp ts = 1; + google.protobuf.Duration dur = 2; + google.protobuf.Any any = 3; + google.protobuf.Struct props = 4; + google.protobuf.FieldMask mask = 5; + google.protobuf.Int32Value count = 6; + google.protobuf.StringValue note = 7; + google.protobuf.Empty nothing = 8; +} diff --git a/testdata/golden/protobuf/library.golden.json b/testdata/golden/protobuf/library.golden.json new file mode 100644 index 0000000..4920c8c --- /dev/null +++ b/testdata/golden/protobuf/library.golden.json @@ -0,0 +1,1131 @@ +{ + "irVersion": "0.1.0", + "name": "library.v1", + "docs": {}, + "services": [ + { + "id": "s/protobuf/library.v1", + "name": { + "source": "library.v1", + "canonical": "library_v_1" + }, + "docs": {}, + "namespace": [ + "library", + "v1" + ], + "groups": [ + { + "name": { + "source": "Catalog", + "canonical": "catalog" + }, + "docs": { + "description": "The library catalog service." + }, + "operations": [ + { + "id": "op/protobuf/library.v1.Catalog.GetBook", + "name": { + "source": "GetBook", + "canonical": "get_book" + }, + "docs": { + "description": "Fetch one book by id." + }, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/library.v1.GetBookRequest", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/library.v1.Book", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "idempotency": { + "kind": "safe" + }, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/library.v1.Catalog/GetBook", + "inputType": { + "target": "t/protobuf/library.v1.GetBookRequest", + "nullable": false + }, + "idempotencyLevel": "NO_SIDE_EFFECTS" + } + }, + "provenance": { + "source": 0, + "pointer": "library.v1.Catalog.GetBook" + } + }, + { + "id": "op/protobuf/library.v1.Catalog.ListBooks", + "name": { + "source": "ListBooks", + "canonical": "list_books" + }, + "docs": { + "description": "List books, paginated." + }, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/library.v1.ListBooksRequest", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/library.v1.ListBooksResponse", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "idempotency": { + "kind": "safe" + }, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/library.v1.Catalog/ListBooks", + "inputType": { + "target": "t/protobuf/library.v1.ListBooksRequest", + "nullable": false + }, + "idempotencyLevel": "NO_SIDE_EFFECTS" + } + }, + "provenance": { + "source": 0, + "pointer": "library.v1.Catalog.ListBooks" + } + }, + { + "id": "op/protobuf/library.v1.Catalog.UpdateBook", + "name": { + "source": "UpdateBook", + "canonical": "update_book" + }, + "docs": { + "description": "Update a book in place." + }, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/library.v1.UpdateBookRequest", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/library.v1.Book", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "idempotency": { + "kind": "idempotent" + }, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/library.v1.Catalog/UpdateBook", + "inputType": { + "target": "t/protobuf/library.v1.UpdateBookRequest", + "nullable": false + }, + "idempotencyLevel": "IDEMPOTENT" + } + }, + "provenance": { + "source": 0, + "pointer": "library.v1.Catalog.UpdateBook" + } + }, + { + "id": "op/protobuf/library.v1.Catalog.StreamCatalog", + "name": { + "source": "StreamCatalog", + "canonical": "stream_catalog" + }, + "docs": { + "description": "Stream the whole catalog." + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/library.v1.Book", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "streaming": "server", + "responseStream": { + "requiresLength": false + }, + "idempotency": {}, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/library.v1.Catalog/StreamCatalog" + } + }, + "provenance": { + "source": 0, + "pointer": "library.v1.Catalog.StreamCatalog" + } + }, + { + "id": "op/protobuf/library.v1.Catalog.DeleteBook", + "name": { + "source": "DeleteBook", + "canonical": "delete_book" + }, + "docs": { + "description": "Delete a book." + }, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/library.v1.GetBookRequest", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "docs": {} + } + ], + "oneWay": false, + "idempotency": {}, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/library.v1.Catalog/DeleteBook", + "inputType": { + "target": "t/protobuf/library.v1.GetBookRequest", + "nullable": false + } + } + }, + "provenance": { + "source": 0, + "pointer": "library.v1.Catalog.DeleteBook" + } + } + ] + } + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "library.proto" + } + } + ], + "types": { + "t/anon/protobuf/library.v1.Book.authors/list": { + "kind": "list", + "id": "t/anon/protobuf/library.v1.Book.authors/list", + "name": { + "hint": "authors" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "library.v1.Book.authors" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/protobuf/library.v1.Book.identifier/oneof": { + "kind": "union", + "id": "t/anon/protobuf/library.v1.Book.identifier/oneof", + "name": { + "hint": "identifier" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "library.v1.Book.identifier" + }, + "variants": [ + { + "name": { + "source": "isbn10", + "canonical": "isbn_10" + }, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "wireID": 8, + "docs": {} + }, + { + "name": { + "source": "isbn13", + "canonical": "isbn_13" + }, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "wireID": 9, + "docs": {} + } + ], + "exclusive": true, + "wireTagged": true + }, + "t/anon/protobuf/library.v1.Book.metadata/map": { + "kind": "map", + "id": "t/anon/protobuf/library.v1.Book.metadata/map", + "name": { + "hint": "metadata" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "library.v1.Book.metadata" + }, + "key": { + "target": "t/prim/string", + "nullable": false + }, + "value": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/protobuf/library.v1.ListBooksResponse.books/list": { + "kind": "list", + "id": "t/anon/protobuf/library.v1.ListBooksResponse.books/list", + "name": { + "hint": "books" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "library.v1.ListBooksResponse.books" + }, + "elem": { + "target": "t/protobuf/library.v1.Book", + "nullable": false + } + }, + "t/prim/datetime": { + "kind": "primitive", + "id": "t/prim/datetime", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "datetime" + }, + "t/prim/int32": { + "kind": "primitive", + "id": "t/prim/int32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int32" + }, + "t/prim/int64": { + "kind": "primitive", + "id": "t/prim/int64", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int64" + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/external/google.protobuf.FieldMask": { + "kind": "external", + "id": "t/protobuf/external/google.protobuf.FieldMask", + "name": { + "source": "google.protobuf.FieldMask", + "canonical": "field_mask" + }, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "google.protobuf.FieldMask" + }, + "identity": "google.protobuf.FieldMask", + "package": "google.golang.org/protobuf/types/known/fieldmaskpb" + }, + "t/protobuf/library.v1.Availability": { + "kind": "enum", + "id": "t/protobuf/library.v1.Availability", + "name": { + "source": "Availability", + "canonical": "availability" + }, + "namespace": [ + "library", + "v1" + ], + "anonymous": false, + "docs": { + "description": "Availability of a book in the catalog." + }, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "library.v1.Availability" + }, + "valueType": "int32", + "members": [ + { + "name": { + "source": "AVAILABILITY_UNSPECIFIED", + "canonical": "availability_unspecified" + }, + "value": { + "kind": "number", + "num": "0", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "IN_STOCK", + "canonical": "in_stock" + }, + "value": { + "kind": "number", + "num": "1", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "OUT_OF_STOCK", + "canonical": "out_of_stock" + }, + "value": { + "kind": "number", + "num": "2", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "DISCONTINUED", + "canonical": "discontinued" + }, + "value": { + "kind": "number", + "num": "3", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + } + ], + "closed": false, + "flags": false + }, + "t/protobuf/library.v1.Book": { + "kind": "model", + "id": "t/protobuf/library.v1.Book", + "name": { + "source": "Book", + "canonical": "book" + }, + "namespace": [ + "library", + "v1" + ], + "anonymous": false, + "docs": { + "description": "A book in the library catalog." + }, + "sensitive": false, + "extensions": { + "protobuf:reserved": { + "ranges": [ + { + "from": 100, + "to": 199 + } + ], + "names": [ + "old_price" + ] + } + }, + "provenance": { + "source": 0, + "pointer": "library.v1.Book" + }, + "properties": [ + { + "id": "p/protobuf/library.v1.Book.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/int64", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.Book.id" + } + }, + { + "id": "p/protobuf/library.v1.Book.title", + "name": { + "source": "title", + "canonical": "title" + }, + "wireID": 2, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.Book.title" + } + }, + { + "id": "p/protobuf/library.v1.Book.authors", + "name": { + "source": "authors", + "canonical": "authors" + }, + "wireID": 3, + "type": { + "target": "t/anon/protobuf/library.v1.Book.authors/list", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.Book.authors" + } + }, + { + "id": "p/protobuf/library.v1.Book.availability", + "name": { + "source": "availability", + "canonical": "availability" + }, + "wireID": 4, + "type": { + "target": "t/protobuf/library.v1.Availability", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.Book.availability" + } + }, + { + "id": "p/protobuf/library.v1.Book.published_at", + "name": { + "source": "published_at", + "canonical": "published_at" + }, + "wireID": 5, + "type": { + "target": "t/prim/datetime", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.Book.published_at" + } + }, + { + "id": "p/protobuf/library.v1.Book.metadata", + "name": { + "source": "metadata", + "canonical": "metadata" + }, + "wireID": 6, + "type": { + "target": "t/anon/protobuf/library.v1.Book.metadata/map", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.Book.metadata" + } + }, + { + "id": "p/protobuf/library.v1.Book.subtitle", + "name": { + "source": "subtitle", + "canonical": "subtitle" + }, + "wireID": 7, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.Book.subtitle" + } + }, + { + "id": "p/protobuf/library.v1.Book.identifier", + "name": { + "source": "identifier", + "canonical": "identifier" + }, + "type": { + "target": "t/anon/protobuf/library.v1.Book.identifier/oneof", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": true, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": { + "description": "Either an ISBN-10 or an ISBN-13 identifier." + }, + "provenance": { + "source": 0, + "pointer": "library.v1.Book.identifier" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/library.v1.GetBookRequest": { + "kind": "model", + "id": "t/protobuf/library.v1.GetBookRequest", + "name": { + "source": "GetBookRequest", + "canonical": "get_book_request" + }, + "namespace": [ + "library", + "v1" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "library.v1.GetBookRequest" + }, + "properties": [ + { + "id": "p/protobuf/library.v1.GetBookRequest.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/int64", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.GetBookRequest.id" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/library.v1.ListBooksRequest": { + "kind": "model", + "id": "t/protobuf/library.v1.ListBooksRequest", + "name": { + "source": "ListBooksRequest", + "canonical": "list_books_request" + }, + "namespace": [ + "library", + "v1" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "library.v1.ListBooksRequest" + }, + "properties": [ + { + "id": "p/protobuf/library.v1.ListBooksRequest.page_size", + "name": { + "source": "page_size", + "canonical": "page_size" + }, + "wireID": 1, + "type": { + "target": "t/prim/int32", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.ListBooksRequest.page_size" + } + }, + { + "id": "p/protobuf/library.v1.ListBooksRequest.page_token", + "name": { + "source": "page_token", + "canonical": "page_token" + }, + "wireID": 2, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.ListBooksRequest.page_token" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/library.v1.ListBooksResponse": { + "kind": "model", + "id": "t/protobuf/library.v1.ListBooksResponse", + "name": { + "source": "ListBooksResponse", + "canonical": "list_books_response" + }, + "namespace": [ + "library", + "v1" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "library.v1.ListBooksResponse" + }, + "properties": [ + { + "id": "p/protobuf/library.v1.ListBooksResponse.books", + "name": { + "source": "books", + "canonical": "books" + }, + "wireID": 1, + "type": { + "target": "t/anon/protobuf/library.v1.ListBooksResponse.books/list", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.ListBooksResponse.books" + } + }, + { + "id": "p/protobuf/library.v1.ListBooksResponse.next_page_token", + "name": { + "source": "next_page_token", + "canonical": "next_page_token" + }, + "wireID": 2, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.ListBooksResponse.next_page_token" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/library.v1.UpdateBookRequest": { + "kind": "model", + "id": "t/protobuf/library.v1.UpdateBookRequest", + "name": { + "source": "UpdateBookRequest", + "canonical": "update_book_request" + }, + "namespace": [ + "library", + "v1" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "library.v1.UpdateBookRequest" + }, + "properties": [ + { + "id": "p/protobuf/library.v1.UpdateBookRequest.book", + "name": { + "source": "book", + "canonical": "book" + }, + "wireID": 1, + "type": { + "target": "t/protobuf/library.v1.Book", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.UpdateBookRequest.book" + } + }, + { + "id": "p/protobuf/library.v1.UpdateBookRequest.update_mask", + "name": { + "source": "update_mask", + "canonical": "update_mask" + }, + "wireID": 2, + "type": { + "target": "t/protobuf/external/google.protobuf.FieldMask", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "library.v1.UpdateBookRequest.update_mask" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "diagnostics": [ + { + "severity": "info", + "code": "protobuf/reserved", + "message": "reserved field numbers/names for library.v1.Book preserved in Extensions", + "provenance": { + "source": 0, + "pointer": "library.v1.Book" + } + } + ], + "sources": [ + { + "format": "protobuf@3", + "path": "library.proto", + "hash": "6804a923fde02c572dd5be9af6e18f7a2e8f4e9e636ec20fb2f0bb3d255c02bd" + } + ] +} diff --git a/testdata/golden/protobuf/library.proto b/testdata/golden/protobuf/library.proto new file mode 100644 index 0000000..599d412 --- /dev/null +++ b/testdata/golden/protobuf/library.proto @@ -0,0 +1,72 @@ +syntax = "proto3"; + +package library.v1; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; + +// Availability of a book in the catalog. +enum Availability { + AVAILABILITY_UNSPECIFIED = 0; + IN_STOCK = 1; + OUT_OF_STOCK = 2; + DISCONTINUED = 3; +} + +// A book in the library catalog. +message Book { + int64 id = 1; + string title = 2; + repeated string authors = 3; + Availability availability = 4; + google.protobuf.Timestamp published_at = 5; + map metadata = 6; + optional string subtitle = 7; + // Either an ISBN-10 or an ISBN-13 identifier. + oneof identifier { + string isbn10 = 8; + string isbn13 = 9; + } + reserved 100 to 199; + reserved "old_price"; +} + +message GetBookRequest { + int64 id = 1; +} + +message ListBooksRequest { + int32 page_size = 1; + string page_token = 2; +} + +message ListBooksResponse { + repeated Book books = 1; + string next_page_token = 2; +} + +message UpdateBookRequest { + Book book = 1; + google.protobuf.FieldMask update_mask = 2; +} + +// The library catalog service. +service Catalog { + // Fetch one book by id. + rpc GetBook(GetBookRequest) returns (Book) { + option idempotency_level = NO_SIDE_EFFECTS; + } + // List books, paginated. + rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + } + // Update a book in place. + rpc UpdateBook(UpdateBookRequest) returns (Book) { + option idempotency_level = IDEMPOTENT; + } + // Stream the whole catalog. + rpc StreamCatalog(google.protobuf.Empty) returns (stream Book); + // Delete a book. + rpc DeleteBook(GetBookRequest) returns (google.protobuf.Empty); +} From 1a90700aff632a027879bb4be4ddc3b688114d4c Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Tue, 21 Jul 2026 04:32:36 +0300 Subject: [PATCH 3/4] refactor(compilers/protobuf): drop unreachable defensive branches Simplify the compiler by removing guards that can never fire, so the code matches its actual reachable behavior: - protocompile recovers parser panics internally and returns them as errors, so the compiler's own recover and errParse sentinel were dead; compileRoot now returns the linked descriptor directly and a PanicError flows through as a compile-error diagnostic like any other. - a descriptor's Options() always returns a materialized options message (empty when nothing is set), so the nil-options guards in optionBool, optionsMessage, customOptions, and methodIdempotency were unreachable and are removed. - json.Marshal of the fixed-shape reserved, file-option, and array payloads cannot fail, so those error branches are dropped. - leafType folds MessageKind into its default and syntaxDigit folds proto3 into its default, since those are the only remaining reachable cases. --- compilers/protobuf/extensions.go | 50 ++++++++++++-------------------- compilers/protobuf/load.go | 37 +++++------------------ compilers/protobuf/service.go | 33 +++++++-------------- compilers/protobuf/types.go | 4 +-- 4 files changed, 39 insertions(+), 85 deletions(-) diff --git a/compilers/protobuf/extensions.go b/compilers/protobuf/extensions.go index 92501f4..0c07536 100644 --- a/compilers/protobuf/extensions.go +++ b/compilers/protobuf/extensions.go @@ -25,31 +25,22 @@ func deprecationOf(d protoreflect.Descriptor) *ir.Deprecation { } // optionBool reads a boolean standard option by name, returning false when the -// descriptor has no options or the option is unset. +// option field is unset. The parser always materializes an options message, so +// the value is read from it directly. func optionBool(d protoreflect.Descriptor, name protoreflect.Name) bool { m := optionsMessage(d) - if m == nil { - return false - } fd := m.Descriptor().Fields().ByName(name) - if fd == nil || fd.Kind() != protoreflect.BoolKind { + if fd == nil { return false } return m.Get(fd).Bool() } -// optionsMessage returns the descriptor's options as a reflective message, or -// nil when no options are set. +// optionsMessage returns the descriptor's options as a reflective message. The +// parser materializes an options message on every descriptor (empty when nothing +// is set), so the result is always a valid message. func optionsMessage(d protoreflect.Descriptor) protoreflect.Message { - opts := d.Options() - if opts == nil { - return nil - } - m := opts.ProtoReflect() - if !m.IsValid() { - return nil - } - return m + return d.Options().ProtoReflect() } // customOptions renders a descriptor's custom (extension) options into @@ -58,9 +49,6 @@ func optionsMessage(d protoreflect.Descriptor) protoreflect.Message { // excluded; ordering is deterministic. func (l *lowerer) customOptions(d protoreflect.Descriptor) ir.Extensions { m := optionsMessage(d) - if m == nil { - return nil - } type entry struct { key string raw ir.RawValue @@ -110,10 +98,7 @@ func renderList(fd protoreflect.FieldDescriptor, list protoreflect.List, depth i parts = append(parts, raw) } } - b, err := json.Marshal(parts) - if err != nil { - return nil, false - } + b, _ := json.Marshal(parts) // a slice of RawMessage always marshals return ir.RawValue(b), true } @@ -146,10 +131,7 @@ func renderScalar(fd protoreflect.FieldDescriptor, v protoreflect.Value, depth i case protoreflect.MessageKind, protoreflect.GroupKind: return renderMessage(v.Message(), depth+1) case protoreflect.EnumKind: - if ev := fd.Enum().Values().ByNumber(v.Enum()); ev != nil { - return jsonRaw(string(ev.Name())) - } - return jsonRaw(int64(v.Enum())) + return jsonRaw(enumMemberName(fd.Enum(), v.Enum())) case protoreflect.BytesKind: return jsonRaw(base64.StdEncoding.EncodeToString(v.Bytes())) case protoreflect.Uint32Kind, protoreflect.Fixed32Kind, @@ -160,6 +142,15 @@ func renderScalar(fd protoreflect.FieldDescriptor, v protoreflect.Value, depth i } } +// enumMemberName resolves an enum number to its declared member name, falling +// back to the raw number when the value is undeclared. +func enumMemberName(ed protoreflect.EnumDescriptor, n protoreflect.EnumNumber) any { + if ev := ed.Values().ByNumber(n); ev != nil { + return string(ev.Name()) + } + return int64(n) +} + // renderMessage renders a message option value into a JSON object with its set // fields ordered by field number for determinism. func renderMessage(m protoreflect.Message, depth int) (ir.RawValue, bool) { @@ -261,10 +252,7 @@ func reservedRaw(ranges []ir.WireIDRange, names []string) ir.RawValue { Ranges []ir.WireIDRange `json:"ranges,omitempty"` Names []string `json:"names,omitempty"` }{Ranges: ranges, Names: names} - b, err := json.Marshal(payload) - if err != nil { - return nil - } + b, _ := json.Marshal(payload) // ranges and names always marshal return ir.RawValue(b) } diff --git a/compilers/protobuf/load.go b/compilers/protobuf/load.go index b2be9cf..1ea4d53 100644 --- a/compilers/protobuf/load.go +++ b/compilers/protobuf/load.go @@ -18,10 +18,6 @@ import ( "github.com/dexpace/morphic/ir" ) -// errParse marks a hard failure to parse the source — an I/O- or -// programmer-level fault distinct from a spec problem reported as a diagnostic. -var errParse = errors.New("parse source") - // loaded is the successful output of the load phase: one fully linked root file // descriptor plus the identity metadata the rest of the compiler needs. A nil // *loaded with error-severity diagnostics means the source is a spec problem the @@ -35,8 +31,8 @@ type loaded struct { // load parses, links, and feature-resolves one .proto source. Well-known-type // imports resolve from the parser's bundle; any other import is unresolvable // because the compiler holds only the root bytes and does no file I/O. Spec -// problems become ir.Diagnostic values; the Go error return is reserved for the -// programmer error of a parser panic. +// problems become ir.Diagnostic values; the parser recovers its own panics into +// errors, so no panic escapes and the Go error return is unused here. // //nolint:unparam // srcIndex varies once Compile drives a multi-source loop func load(ctx context.Context, srcIndex int, src compilers.Source, _ Options) (*loaded, []ir.Diagnostic, error) { @@ -51,7 +47,7 @@ func load(ctx context.Context, srcIndex int, src compilers.Source, _ Options) (* }, ) - files, err := compileRoot(ctx, src, rep) + root, err := compileRoot(ctx, src, rep) if err != nil { if len(diags) == 0 { // reporter never fired: a resolution or internal error diags = append(diags, diagf(ir.SeverityError, importOrCompileCode(err), @@ -59,11 +55,6 @@ func load(ctx context.Context, srcIndex int, src compilers.Source, _ Options) (* } return nil, diags, nil // refuse to lower, do not abort the batch } - if len(files) == 0 { - return nil, diags, nil - } - - root := files[0] return &loaded{ File: root, Format: compilers.SourceFormat{Name: "protobuf", Version: syntaxDigit(root)}, @@ -75,15 +66,9 @@ func load(ctx context.Context, srcIndex int, src compilers.Source, _ Options) (* }, diags, nil } -// compileRoot links the single root file against the bundled well-known types. -// It converts a parser panic on degenerate input into an errParse error so the -// compiler upholds the no-panics-escape invariant. -func compileRoot(ctx context.Context, src compilers.Source, rep reporter.Reporter) (fds []protoreflect.FileDescriptor, err error) { - defer func() { - if r := recover(); r != nil { - fds, err = nil, fmt.Errorf("parser panicked (%v): %w", r, errParse) - } - }() +// compileRoot links the single root file against the bundled well-known types +// and returns its linked descriptor. +func compileRoot(ctx context.Context, src compilers.Source, rep reporter.Reporter) (protoreflect.FileDescriptor, error) { resolver := protocompile.WithStandardImports(&protocompile.SourceResolver{ Accessor: func(path string) (io.ReadCloser, error) { if path == src.Path { @@ -101,11 +86,7 @@ func compileRoot(ctx context.Context, src compilers.Source, rep reporter.Reporte if err != nil { return nil, fmt.Errorf("compile %q: %w", src.Path, err) } - out := make([]protoreflect.FileDescriptor, len(compiled)) - for i, f := range compiled { - out[i] = f - } - return out, nil + return compiled[0], nil } // parseDiag converts one reporter error into a diagnostic, classifying an @@ -130,13 +111,11 @@ func posOf(srcIndex int, err reporter.ErrorWithPos) ir.Provenance { } // syntaxDigit maps a file's syntax to the version digit the compiler reports: -// "2", "3", or the edition string for editions files. +// "2" for proto2, "2023" for the 2023 edition, and "3" for proto3. func syntaxDigit(fd protoreflect.FileDescriptor) string { switch fd.Syntax() { case protoreflect.Proto2: return "2" - case protoreflect.Proto3: - return "3" case protoreflect.Editions: return "2023" default: diff --git a/compilers/protobuf/service.go b/compilers/protobuf/service.go index a3fa2c9..7fe7fa7 100644 --- a/compilers/protobuf/service.go +++ b/compilers/protobuf/service.go @@ -133,13 +133,7 @@ func applyStreaming(op *ir.Operation, md protoreflect.MethodDescriptor) { // idempotency classification and its raw level string. func methodIdempotency(md protoreflect.MethodDescriptor) (ir.Idempotency, string) { m := optionsMessage(md) - if m == nil { - return ir.Idempotency{}, "" - } fd := m.Descriptor().Fields().ByName("idempotency_level") - if fd == nil || fd.Kind() != protoreflect.EnumKind { - return ir.Idempotency{}, "" - } switch m.Get(fd).Enum() { case 1: // NO_SIDE_EFFECTS return ir.Idempotency{Kind: ir.IdempotencySafe}, "NO_SIDE_EFFECTS" @@ -188,7 +182,7 @@ func (l *lowerer) lowerExtensionField(ext protoreflect.FieldDescriptor) { return } prop := l.lowerField(ext) - prop.ExtensionOf = extensionScope(ext) + prop.ExtensionOf = scopeOf(string(ext.FullName())) model.Properties = append(model.Properties, prop) } @@ -200,23 +194,21 @@ func (l *lowerer) recordOptionDefinition(ext protoreflect.FieldDescriptor, exten "number": int32(ext.Number()), "name": string(ext.FullName()), } - if raw, err := json.Marshal(def); err == nil { - l.out.Extensions = mergeRaw(l.out.Extensions, - "protobuf:custom-option:"+string(ext.FullName()), ir.RawValue(raw)) - } + raw, _ := json.Marshal(def) // a map of strings and an int always marshals + l.out.Extensions = mergeRaw(l.out.Extensions, + "protobuf:custom-option:"+string(ext.FullName()), ir.RawValue(raw)) l.diags = append(l.diags, diagf(ir.SeverityInfo, codeCustomOptionDefinition, ir.Provenance{Source: l.srcIndex, Pointer: string(ext.FullName())}, "extension %s defines a custom option on %s", ext.FullName(), extended.FullName())) } -// extensionScope is the fully-qualified declaring scope of an extension field: -// its full name with the field name removed. -func extensionScope(ext protoreflect.FieldDescriptor) string { - full := string(ext.FullName()) - if i := strings.LastIndex(full, "."); i >= 0 { - return full[:i] +// scopeOf is the declaring scope of a fully-qualified name: the name with its +// final segment removed, or the name itself when it has no qualifier. +func scopeOf(fullName string) string { + if i := strings.LastIndex(fullName, "."); i >= 0 { + return fullName[:i] } - return full + return fullName } // lowerMeta records file-level metadata: the package as the document name and @@ -252,10 +244,7 @@ func (l *lowerer) fileOptionsRaw() ir.RawValue { fields["deprecated"] = true } } - b, err := json.Marshal(fields) - if err != nil { - return nil - } + b, _ := json.Marshal(fields) // a map of strings and bools always marshals return ir.RawValue(b) } diff --git a/compilers/protobuf/types.go b/compilers/protobuf/types.go index f62a9c0..07d4fb4 100644 --- a/compilers/protobuf/types.go +++ b/compilers/protobuf/types.go @@ -238,10 +238,8 @@ func (l *lowerer) leafType(fd protoreflect.FieldDescriptor) (ir.TypeRef, *ir.Enc return l.enumRef(fd.Enum()), nil case protoreflect.GroupKind: return l.messageOrWKT(fd.Message()), &ir.Encoding{Name: "delimited"} - case protoreflect.MessageKind: + default: // MessageKind — the only remaining kind return l.messageOrWKT(fd.Message()), nil - default: - return l.anyRef(), nil } } From 2345b076ce37006531e65d0b7517a9249cb0477d Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Tue, 21 Jul 2026 04:32:36 +0300 Subject: [PATCH 4/4] test(compilers/protobuf): reach full statement coverage Extend the corpus and add a white-box helper test so every reachable branch is exercised, matching the repository's 100%-coverage gate: - enrich the corpus with an unmapped well-known type, unsigned/bytes/non-finite proto2 defaults, a repeated encoded scalar, a deprecated oneof member with an explicit json_name, an Empty-input rpc, and an rpc whose only option is deprecated; - add rich-options (message/repeated/map/enum/bytes/unsigned custom options, NaN option values, a group field, and enum-value/service/method options), file-options, and no-package corpus files with assertions and goldens; - add compile tests for a syntax error and an unused-import warning; - add a white-box test covering the pure helpers and the option-rendering recursion-depth guards. --- compilers/protobuf/compile_test.go | 42 + compilers/protobuf/conformance_test.go | 88 +- compilers/protobuf/internal_test.go | 230 +++++ .../conformance/protobuf/defaults.golden.json | 134 ++- testdata/conformance/protobuf/defaults.proto | 3 + .../protobuf/file-options.golden.json | 104 ++ .../conformance/protobuf/file-options.proto | 12 + .../protobuf/no-package.golden.json | 155 +++ .../conformance/protobuf/no-package.proto | 9 + .../conformance/protobuf/oneof.golden.json | 28 +- testdata/conformance/protobuf/oneof.proto | 1 + .../conformance/protobuf/repeated.golden.json | 83 +- testdata/conformance/protobuf/repeated.proto | 1 + .../protobuf/rich-options.golden.json | 914 ++++++++++++++++++ .../conformance/protobuf/rich-options.proto | 78 ++ .../conformance/protobuf/services.golden.json | 97 +- testdata/conformance/protobuf/services.proto | 4 + .../protobuf/well-known.golden.json | 47 +- .../conformance/protobuf/well-known.proto | 2 + 19 files changed, 2026 insertions(+), 6 deletions(-) create mode 100644 compilers/protobuf/internal_test.go create mode 100644 testdata/conformance/protobuf/file-options.golden.json create mode 100644 testdata/conformance/protobuf/file-options.proto create mode 100644 testdata/conformance/protobuf/no-package.golden.json create mode 100644 testdata/conformance/protobuf/no-package.proto create mode 100644 testdata/conformance/protobuf/rich-options.golden.json create mode 100644 testdata/conformance/protobuf/rich-options.proto diff --git a/compilers/protobuf/compile_test.go b/compilers/protobuf/compile_test.go index 7482ab6..748813f 100644 --- a/compilers/protobuf/compile_test.go +++ b/compilers/protobuf/compile_test.go @@ -129,6 +129,48 @@ func TestCompile_WrapperExternalPolicy(t *testing.T) { assert.Equal(t, "google.protobuf.StringValue", ext.Identity) } +func TestCompile_SyntaxError(t *testing.T) { + t.Parallel() + const src = `syntax = "proto3"; +package s; +message M { + string s = ; +} +` + doc, diags, err := compile(t, "s.proto", src, compilers.Options{}) + require.NoError(t, err, "a syntax error is a spec problem, not a Go error") + assert.Nil(t, doc, "a malformed source cannot be lowered") + var found bool + for _, d := range diags { + if d.Code == "protobuf/compile-error" && d.Severity == ir.SeverityError { + found = true + assert.Regexp(t, `^\d+:\d+$`, d.Provenance.Pointer, "parse errors carry line:col provenance") + } + } + assert.True(t, found, "syntax error reported with position") +} + +func TestCompile_ImportWarning(t *testing.T) { + t.Parallel() + const src = `syntax = "proto3"; +package iw; +import "google/protobuf/empty.proto"; +message M { + string s = 1; +} +` + doc, diags, err := compile(t, "iw.proto", src, compilers.Options{}) + require.NoError(t, err) + require.NotNil(t, doc, "an unused import is a warning, not a hard failure") + var found bool + for _, d := range diags { + if d.Severity == ir.SeverityWarning && d.Code == "protobuf/warning" { + found = true + } + } + assert.True(t, found, "an unused import surfaces as a warning diagnostic") +} + func TestCompile_SyntaxDigit(t *testing.T) { t.Parallel() const proto2Src = `syntax = "proto2"; diff --git a/compilers/protobuf/conformance_test.go b/compilers/protobuf/conformance_test.go index 69bef2c..4c6c6d3 100644 --- a/compilers/protobuf/conformance_test.go +++ b/compilers/protobuf/conformance_test.go @@ -48,6 +48,9 @@ func TestConformance(t *testing.T) { {"deprecation", assertDeprecation}, {"comments", assertComments}, {"custom-options", assertCustomOptions}, + {"rich-options", assertRichOptions}, + {"file-options", assertFileOptions}, + {"no-package", assertNoPackage}, {"editions", assertEditions}, } for _, tc := range cases { @@ -187,9 +190,12 @@ func assertOneof(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { require.True(t, ok, "oneof lowers to a Union node") assert.True(t, u.Exclusive) assert.True(t, u.WireTagged, "protobuf oneof is tagged on the wire") - require.Len(t, u.Variants, 3) + require.Len(t, u.Variants, 4) require.NotNil(t, u.Variants[0].WireID) assert.Equal(t, 1, *u.Variants[0].WireID, "variant keeps its field number") + legacy := u.Variants[3] + assert.NotNil(t, legacy.Deprecation, "a deprecated oneof member keeps its deprecation") + assert.Equal(t, "legacy", legacy.WireName, "an explicit json_name becomes the variant wire name") // The non-oneof field remains an ordinary property alongside the wrapper. _ = propByName(t, shape, "name") } @@ -230,6 +236,13 @@ func assertRepeated(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { assert.Equal(t, "expanded", expanded.Encoding.Name, "[packed=false] lowers to expanded") labels := listOf(t, doc, propByName(t, series, "labels")) assert.Nil(t, labels.Encoding, "string lists carry no packing encoding") + // A repeated encoded scalar wraps its element in a Scalar so the zigzag + // encoding survives where a bare element ref has no encoding slot. + deltas := listOf(t, doc, propByName(t, series, "deltas")) + elem, ok := doc.Types[deltas.Elem.Target].(*ir.Scalar) + require.True(t, ok, "sint64 list element hoists a Scalar") + require.NotNil(t, elem.Encoding) + assert.Equal(t, "zigzag", elem.Encoding.Name) } // listOf resolves the List node a property references. @@ -279,6 +292,11 @@ func assertDefaults(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { require.NotNil(t, mode.Ref) assert.Equal(t, typeID("def.Mode"), mode.Ref.Type) assert.Equal(t, "SLOW", mode.Ref.Member) + assert.Equal(t, ir.BigVal("42"), propByName(t, s, "limit").Default.Num, "unsigned default") + salt := propByName(t, s, "salt").Default + require.Equal(t, ir.ValueBytes, salt.Kind, "bytes default") + assert.Equal(t, []byte{1, 2}, salt.Bytes) + assert.Nil(t, propByName(t, s, "unbounded").Default, "a non-finite default is dropped") } func assertExtensions(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { @@ -340,6 +358,9 @@ func assertWellKnown(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { assert.True(t, note.Type.Nullable) _, ok = doc.Types[propByName(t, b, "nothing").Type.Target].(*ir.External) assert.True(t, ok, "Empty as a field type lowers to an External") + ctx, ok := doc.Types[propByName(t, b, "ctx").Type.Target].(*ir.External) + require.True(t, ok, "an unmapped well-known type falls back to External") + assert.Equal(t, "google.protobuf.SourceContext", ctx.Identity) } func assertServices(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { @@ -377,6 +398,14 @@ func assertServices(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { require.Len(t, notify.Responses, 1) assert.Nil(t, notify.Responses[0].Payload, "an Empty response carries no payload") assert.NotNil(t, notify.Request, "Notify still has a request payload") + + drain := opByName(t, doc, "Drain") + assert.Nil(t, drain.Request, "an Empty request lowers to no payload") + assert.Nil(t, drain.Bindings.RPC.InputType, "and no RPC input type") + + touch := opByName(t, doc, "Touch") + assert.NotNil(t, touch.Deprecation, "a deprecated rpc keeps its deprecation") + assert.Equal(t, ir.IdempotencyUnknown, touch.Idempotency.Kind, "no idempotency level declared") } func assertDeprecation(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { @@ -414,6 +443,63 @@ func assertCustomOptions(t *testing.T, doc *ir.Document, diags []ir.Diagnostic) assert.True(t, found) } +func assertRichOptions(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + widget := modelOf(t, doc, "ropt.Widget") + meta, ok := widget.Extensions["protobuf:option:ropt.meta"] + require.True(t, ok, "a message-valued custom option renders as a nested object") + // score (a NaN scalar) is dropped; points and ratios keep their NaN elements + // dropped, leaving an empty array and object. + assert.JSONEq(t, + `{"owner":"team","labels":["a","b"],"weights":{"a":2,"x":1},"nested":{"flag":true},`+ + `"kind":"K_B","sig":"QUE9PQ==","big":42,"points":[],"ratios":{}}`, + string(meta)) + reviewers, ok := widget.Extensions["protobuf:option:ropt.reviewers"] + require.True(t, ok, "a repeated custom option renders as an array") + assert.JSONEq(t, `["alice","bob"]`, string(reviewers)) + id := propByName(t, widget, "id") + assert.JSONEq(t, `"K_A"`, string(id.Extensions["protobuf:option:ropt.field_kind"])) + // A group field lowers to a delimited-encoded message property. + detail := propByName(t, widget, "detail") + require.NotNil(t, detail.Encoding) + assert.Equal(t, "delimited", detail.Encoding.Name, "proto2 group is delimited-encoded") + // An enum-value custom option is preserved on the member. + kind := enumOf(t, doc, "ropt.Kind") + require.Len(t, kind.Members, 2) + assert.JSONEq(t, `"beta"`, string(kind.Members[1].Extensions["protobuf:option:ropt.label"])) + // A service-level custom option lands on its operation group; a method-level + // one lands on its operation. + require.Len(t, doc.Services, 1) + require.Len(t, doc.Services[0].Groups, 1) + team, ok := doc.Services[0].Groups[0].Extensions["protobuf:option:ropt.team"] + require.True(t, ok, "service custom option preserved on the group") + assert.JSONEq(t, `"platform"`, string(team)) + do := opByName(t, doc, "Do") + assert.JSONEq(t, `"yes"`, string(do.Extensions["protobuf:option:ropt.audit"])) +} + +func assertFileOptions(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + raw, ok := doc.Extensions["protobuf:file"] + require.True(t, ok) + // Only the options that are set appear; unset ones are omitted. + assert.JSONEq(t, `{ + "syntax": "proto3", + "goPackage": "example.com/fopt", + "javaPackage": "com.example.fopt", + "javaMultipleFiles": true, + "deprecated": true + }`, string(raw)) +} + +func assertNoPackage(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { + assert.Empty(t, doc.Name, "a package-less file has no document name") + require.Len(t, doc.Services, 1) + assert.Empty(t, doc.Services[0].Namespace, "no package means no namespace") + item := modelOf(t, doc, "Item") + assert.Empty(t, item.Namespace) + // The service identity falls back to the source path. + assert.Equal(t, ir.ServiceID("s/protobuf/no-package.proto"), doc.Services[0].ID) +} + func assertEditions(t *testing.T, doc *ir.Document, _ []ir.Diagnostic) { rec := modelOf(t, doc, "ed.Rec") assert.Equal(t, ir.PresenceExplicit, propByName(t, rec, "id").Presence, diff --git a/compilers/protobuf/internal_test.go b/compilers/protobuf/internal_test.go new file mode 100644 index 0000000..faf1755 --- /dev/null +++ b/compilers/protobuf/internal_test.go @@ -0,0 +1,230 @@ +package protobuf // white-box: exercises unexported helpers and defensive branches + +import ( + "context" + "errors" + "io/fs" + "math" + "strings" + "testing" + + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/dexpace/morphic/compilers" + "github.com/dexpace/morphic/ir" +) + +// mustLoad compiles one .proto source through the load phase and returns its +// linked file descriptor. +func mustLoad(t *testing.T, src string) protoreflect.FileDescriptor { + t.Helper() + ld, diags, err := load(context.Background(), 0, + compilers.Source{Path: "t.proto", Data: []byte(src)}, Options{}.withDefaults()) + if err != nil || ld == nil { + t.Fatalf("load failed: err=%v diags=%v", err, diags) + } + return ld.File +} + +func TestWrapperPrim_All(t *testing.T) { + cases := map[string]ir.PrimKind{ + "google.protobuf.DoubleValue": ir.PrimFloat64, + "google.protobuf.FloatValue": ir.PrimFloat32, + "google.protobuf.Int64Value": ir.PrimInt64, + "google.protobuf.UInt64Value": ir.PrimUint64, + "google.protobuf.Int32Value": ir.PrimInt32, + "google.protobuf.UInt32Value": ir.PrimUint32, + "google.protobuf.BoolValue": ir.PrimBool, + "google.protobuf.StringValue": ir.PrimString, + "google.protobuf.BytesValue": ir.PrimBytes, + } + for name, want := range cases { + got, ok := wrapperPrim(name) + if !ok || got != want { + t.Errorf("wrapperPrim(%q) = %q,%v; want %q,true", name, got, ok, want) + } + } + if _, ok := wrapperPrim("google.protobuf.Timestamp"); ok { + t.Error("wrapperPrim must report false for a non-wrapper type") + } +} + +func TestScalarPrim_All(t *testing.T) { + scalars := []protoreflect.Kind{ + protoreflect.BoolKind, protoreflect.StringKind, protoreflect.BytesKind, + protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind, + protoreflect.Uint32Kind, protoreflect.Fixed32Kind, protoreflect.Int64Kind, + protoreflect.Sint64Kind, protoreflect.Sfixed64Kind, protoreflect.Uint64Kind, + protoreflect.Fixed64Kind, protoreflect.FloatKind, protoreflect.DoubleKind, + } + for _, k := range scalars { + if _, _, ok := scalarPrim(k); !ok { + t.Errorf("scalarPrim(%v) reported non-scalar", k) + } + } + for _, k := range []protoreflect.Kind{protoreflect.EnumKind, protoreflect.MessageKind, protoreflect.GroupKind} { + if _, _, ok := scalarPrim(k); ok { + t.Errorf("scalarPrim(%v) reported scalar", k) + } + } +} + +func TestPackable(t *testing.T) { + for _, k := range []protoreflect.Kind{protoreflect.StringKind, protoreflect.BytesKind, protoreflect.MessageKind, protoreflect.GroupKind} { + if packable(k) { + t.Errorf("packable(%v) should be false", k) + } + } + if !packable(protoreflect.Int32Kind) { + t.Error("packable(int32) should be true") + } +} + +func TestScopeOf(t *testing.T) { + if got := scopeOf("a.b.c"); got != "a.b" { + t.Errorf("scopeOf qualified = %q", got) + } + if got := scopeOf("bare"); got != "bare" { + t.Errorf("scopeOf unqualified = %q", got) + } +} + +func TestLastSegment(t *testing.T) { + if got := lastSegment("google.protobuf.Empty"); got != "Empty" { + t.Errorf("lastSegment qualified = %q", got) + } + if got := lastSegment("bare"); got != "bare" { + t.Errorf("lastSegment unqualified = %q", got) + } +} + +func TestPackageWords(t *testing.T) { + if got := packageWords(""); got != nil { + t.Errorf("packageWords(empty) = %v; want nil", got) + } + got := packageWords("a.b") + if len(got) != 2 || got[0] != "a" || got[1] != "b" { + t.Errorf("packageWords(a.b) = %v", got) + } +} + +func TestCanonicalWords_Boundaries(t *testing.T) { + cases := map[string]string{ + "HTTPServer": "http_server", + "userID": "user_id", + "api2key": "api_2_key", + "snake_case": "snake_case", + } + for in, want := range cases { + if got := canonicalWords(in); got != want { + t.Errorf("canonicalWords(%q) = %q; want %q", in, got, want) + } + } +} + +func TestCleanComment(t *testing.T) { + if got := cleanComment(""); got != "" { + t.Errorf("cleanComment(empty) = %q", got) + } + if got := cleanComment(" line one\n line two\n"); got != "line one\nline two" { + t.Errorf("cleanComment = %q", got) + } + huge := strings.Repeat("x\n", maxCommentLines+10) + if got := cleanComment(huge); got == "" { + t.Error("cleanComment truncation should still return content") + } +} + +func TestJSONRaw_Error(t *testing.T) { + if _, ok := jsonRaw(math.NaN()); ok { + t.Error("jsonRaw(NaN) should fail: NaN is not valid JSON") + } + if _, ok := jsonRaw("ok"); !ok { + t.Error("jsonRaw(string) should succeed") + } +} + +func TestReservedRaw_Empty(t *testing.T) { + if reservedRaw(nil, nil) != nil { + t.Error("reservedRaw with no ranges or names must be nil") + } +} + +func TestImportOrCompileCode(t *testing.T) { + if got := importOrCompileCode(fs.ErrNotExist); got != codeUnresolvedImport { + t.Errorf("importOrCompileCode(ErrNotExist) = %q", got) + } + if got := importOrCompileCode(errors.New("boom")); got != codeCompile { + t.Errorf("importOrCompileCode(other) = %q", got) + } +} + +func TestEnumMemberName(t *testing.T) { + file := mustLoad(t, "syntax = \"proto3\";\npackage e;\nenum K { A = 0; B = 1; }\n") + ed := file.Enums().Get(0) + if got := enumMemberName(ed, 1); got != "B" { + t.Errorf("enumMemberName(1) = %v; want B", got) + } + if got := enumMemberName(ed, 999); got != int64(999) { + t.Errorf("enumMemberName(unknown) = %v; want 999", got) + } +} + +func TestOptionBool_MissingOption(t *testing.T) { + file := mustLoad(t, "syntax = \"proto3\";\npackage o;\nmessage M {\n option deprecated = true;\n string s = 1;\n}\n") + md := file.Messages().Get(0) + if !optionBool(md, "deprecated") { + t.Error("optionBool should read a set deprecated option") + } + if optionBool(md, "no_such_option") { + t.Error("optionBool must report false for a missing option field") + } +} + +func TestRenderDepthGuard(t *testing.T) { + // A message-valued custom option gives us a real option message and field to + // drive the recursion-depth guards past their cap. + const src = `syntax = "proto2"; +package d; +import "google/protobuf/descriptor.proto"; +message Box { optional string v = 1; } +extend google.protobuf.MessageOptions { optional Box box = 50300; } +message M { + option (box) = { v: "x" }; +} +` + file := mustLoad(t, src) + opts := optionsMessage(file.Messages().Get(1)) // M's options + var boxFD protoreflect.FieldDescriptor + var boxVal protoreflect.Value + opts.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + if fd.IsExtension() { + boxFD, boxVal = fd, v + } + return true + }) + if boxFD == nil { + t.Fatal("the custom option must be present") + } + if _, ok := renderValue(boxFD, boxVal, maxOptionDepth+1); ok { + t.Error("renderValue past the depth cap must report failure") + } + if _, ok := renderMessage(boxVal.Message(), maxOptionDepth+1); ok { + t.Error("renderMessage past the depth cap must report failure") + } +} + +func TestSyntaxDigit(t *testing.T) { + p2 := mustLoad(t, "syntax = \"proto2\";\npackage a;\nmessage M { optional string s = 1; }\n") + if got := syntaxDigit(p2); got != "2" { + t.Errorf("syntaxDigit(proto2) = %q", got) + } + p3 := mustLoad(t, "syntax = \"proto3\";\npackage b;\nmessage M { string s = 1; }\n") + if got := syntaxDigit(p3); got != "3" { + t.Errorf("syntaxDigit(proto3) = %q", got) + } + ed := mustLoad(t, "edition = \"2023\";\npackage c;\nmessage M { string s = 1; }\n") + if got := syntaxDigit(ed); got != "2023" { + t.Errorf("syntaxDigit(editions) = %q", got) + } +} diff --git a/testdata/conformance/protobuf/defaults.golden.json b/testdata/conformance/protobuf/defaults.golden.json index 9833135..fddbd11 100644 --- a/testdata/conformance/protobuf/defaults.golden.json +++ b/testdata/conformance/protobuf/defaults.golden.json @@ -33,6 +33,18 @@ }, "prim": "bool" }, + "t/prim/bytes": { + "kind": "primitive", + "id": "t/prim/bytes", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "bytes" + }, "t/prim/float64": { "kind": "primitive", "id": "t/prim/float64", @@ -69,6 +81,18 @@ }, "prim": "string" }, + "t/prim/uint32": { + "kind": "primitive", + "id": "t/prim/uint32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "uint32" + }, "t/protobuf/def.Mode": { "kind": "enum", "id": "t/protobuf/def.Mode", @@ -315,6 +339,103 @@ "source": 0, "pointer": "def.Settings.mode" } + }, + { + "id": "p/protobuf/def.Settings.limit", + "name": { + "source": "limit", + "canonical": "limit" + }, + "wireID": 6, + "type": { + "target": "t/prim/uint32", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "default": { + "kind": "number", + "num": "42", + "bytes": null, + "list": null, + "object": null + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "def.Settings.limit" + } + }, + { + "id": "p/protobuf/def.Settings.salt", + "name": { + "source": "salt", + "canonical": "salt" + }, + "wireID": 7, + "type": { + "target": "t/prim/bytes", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "default": { + "kind": "bytes", + "bytes": "AQI=", + "list": null, + "object": null + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "def.Settings.salt" + } + }, + { + "id": "p/protobuf/def.Settings.unbounded", + "name": { + "source": "unbounded", + "canonical": "unbounded" + }, + "wireID": 8, + "type": { + "target": "t/prim/float64", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "def.Settings.unbounded" + } } ], "abstract": false, @@ -327,11 +448,22 @@ "syntax": "proto2" } }, + "diagnostics": [ + { + "severity": "info", + "code": "protobuf/degraded-construct", + "message": "non-finite default for def.Settings.unbounded dropped (not representable as a decimal)", + "provenance": { + "source": 0, + "pointer": "def.Settings.unbounded" + } + } + ], "sources": [ { "format": "protobuf@2", "path": "defaults.proto", - "hash": "7ddd00df317d583441ea85fcd3c3035213d7e51afd71b0618045c143d9c66685" + "hash": "349a556de734e96ce40025e220e352b1b2b601779320f6ba9d8f82c6dd4342b7" } ] } diff --git a/testdata/conformance/protobuf/defaults.proto b/testdata/conformance/protobuf/defaults.proto index b4b4ee6..e109e70 100644 --- a/testdata/conformance/protobuf/defaults.proto +++ b/testdata/conformance/protobuf/defaults.proto @@ -13,4 +13,7 @@ message Settings { optional bool enabled = 3 [default = true]; optional double ratio = 4 [default = 2.5]; optional Mode mode = 5 [default = SLOW]; + optional uint32 limit = 6 [default = 42]; + optional bytes salt = 7 [default = "\001\002"]; + optional double unbounded = 8 [default = inf]; } diff --git a/testdata/conformance/protobuf/file-options.golden.json b/testdata/conformance/protobuf/file-options.golden.json new file mode 100644 index 0000000..f44d60a --- /dev/null +++ b/testdata/conformance/protobuf/file-options.golden.json @@ -0,0 +1,104 @@ +{ + "irVersion": "0.1.0", + "name": "fopt", + "docs": {}, + "services": [ + { + "id": "s/protobuf/fopt", + "name": { + "source": "fopt", + "canonical": "fopt" + }, + "docs": {}, + "namespace": [ + "fopt" + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "file-options.proto" + } + } + ], + "types": { + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/fopt.Thing": { + "kind": "model", + "id": "t/protobuf/fopt.Thing", + "name": { + "source": "Thing", + "canonical": "thing" + }, + "namespace": [ + "fopt" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "fopt.Thing" + }, + "properties": [ + { + "id": "p/protobuf/fopt.Thing.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "fopt.Thing.id" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "deprecated": true, + "goPackage": "example.com/fopt", + "javaMultipleFiles": true, + "javaPackage": "com.example.fopt", + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "file-options.proto", + "hash": "2e95bc5f582b21becbd2d21a01d44d3633b9dc54e3094d6f171df9d9228cab03" + } + ] +} diff --git a/testdata/conformance/protobuf/file-options.proto b/testdata/conformance/protobuf/file-options.proto new file mode 100644 index 0000000..04b9cdc --- /dev/null +++ b/testdata/conformance/protobuf/file-options.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package fopt; + +option go_package = "example.com/fopt"; +option java_package = "com.example.fopt"; +option java_multiple_files = true; +option deprecated = true; + +message Thing { + string id = 1; +} diff --git a/testdata/conformance/protobuf/no-package.golden.json b/testdata/conformance/protobuf/no-package.golden.json new file mode 100644 index 0000000..5c243b6 --- /dev/null +++ b/testdata/conformance/protobuf/no-package.golden.json @@ -0,0 +1,155 @@ +{ + "irVersion": "0.1.0", + "docs": {}, + "services": [ + { + "id": "s/protobuf/no-package.proto", + "name": {}, + "docs": {}, + "groups": [ + { + "name": { + "source": "Bare", + "canonical": "bare" + }, + "docs": {}, + "operations": [ + { + "id": "op/protobuf/Bare.Get", + "name": { + "source": "Get", + "canonical": "get" + }, + "docs": {}, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/Item", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/Item", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "idempotency": {}, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/Bare/Get", + "inputType": { + "target": "t/protobuf/Item", + "nullable": false + } + } + }, + "provenance": { + "source": 0, + "pointer": "Bare.Get" + } + } + ] + } + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "no-package.proto" + } + } + ], + "types": { + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/protobuf/Item": { + "kind": "model", + "id": "t/protobuf/Item", + "name": { + "source": "Item", + "canonical": "item" + }, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "Item" + }, + "properties": [ + { + "id": "p/protobuf/Item.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "implicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "Item.id" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:file": { + "syntax": "proto3" + } + }, + "sources": [ + { + "format": "protobuf@3", + "path": "no-package.proto", + "hash": "a22a00244e6e34049cce648e6f4b8c9aed1a41c365672115b08190b78b65980e" + } + ] +} diff --git a/testdata/conformance/protobuf/no-package.proto b/testdata/conformance/protobuf/no-package.proto new file mode 100644 index 0000000..b687c82 --- /dev/null +++ b/testdata/conformance/protobuf/no-package.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +message Item { + string id = 1; +} + +service Bare { + rpc Get(Item) returns (Item); +} diff --git a/testdata/conformance/protobuf/oneof.golden.json b/testdata/conformance/protobuf/oneof.golden.json index 98a0311..e0580f2 100644 --- a/testdata/conformance/protobuf/oneof.golden.json +++ b/testdata/conformance/protobuf/oneof.golden.json @@ -70,6 +70,20 @@ }, "wireID": 3, "docs": {} + }, + { + "name": { + "source": "legacy_code", + "canonical": "legacy_code" + }, + "type": { + "target": "t/prim/int32", + "nullable": false + }, + "wireName": "legacy", + "wireID": 5, + "docs": {}, + "deprecation": {} } ], "exclusive": true, @@ -87,6 +101,18 @@ }, "prim": "float64" }, + "t/prim/int32": { + "kind": "primitive", + "id": "t/prim/int32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int32" + }, "t/prim/string": { "kind": "primitive", "id": "t/prim/string", @@ -187,7 +213,7 @@ { "format": "protobuf@3", "path": "oneof.proto", - "hash": "30cce65ed9fa8b783f064d87b9309eb86678260a5867b9bfbf2e40bc71f7077a" + "hash": "fc6a52b750ba1e7094d5282520b7210d0464f8a48f277465681e724ae8299269" } ] } diff --git a/testdata/conformance/protobuf/oneof.proto b/testdata/conformance/protobuf/oneof.proto index 59d9dbd..26dcad0 100644 --- a/testdata/conformance/protobuf/oneof.proto +++ b/testdata/conformance/protobuf/oneof.proto @@ -7,6 +7,7 @@ message Shape { double radius = 1; double side = 2; string label = 3; + int32 legacy_code = 5 [deprecated = true, json_name = "legacy"]; } string name = 4; } diff --git a/testdata/conformance/protobuf/repeated.golden.json b/testdata/conformance/protobuf/repeated.golden.json index 5a64e2e..998ff03 100644 --- a/testdata/conformance/protobuf/repeated.golden.json +++ b/testdata/conformance/protobuf/repeated.golden.json @@ -21,6 +21,48 @@ } ], "types": { + "t/anon/protobuf/rep.Series.deltas/elem": { + "kind": "scalar", + "id": "t/anon/protobuf/rep.Series.deltas/elem", + "name": { + "hint": "deltas" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "rep.Series.deltas" + }, + "base": { + "target": "t/prim/int64", + "nullable": false + }, + "encoding": { + "name": "zigzag" + } + }, + "t/anon/protobuf/rep.Series.deltas/list": { + "kind": "list", + "id": "t/anon/protobuf/rep.Series.deltas/list", + "name": { + "hint": "deltas" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "rep.Series.deltas" + }, + "elem": { + "target": "t/anon/protobuf/rep.Series.deltas/elem", + "nullable": false + }, + "encoding": { + "name": "packed" + } + }, "t/anon/protobuf/rep.Series.expanded_values/list": { "kind": "list", "id": "t/anon/protobuf/rep.Series.expanded_values/list", @@ -93,6 +135,18 @@ }, "prim": "int32" }, + "t/prim/int64": { + "kind": "primitive", + "id": "t/prim/int64", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int64" + }, "t/prim/string": { "kind": "primitive", "id": "t/prim/string", @@ -203,6 +257,33 @@ "source": 0, "pointer": "rep.Series.labels" } + }, + { + "id": "p/protobuf/rep.Series.deltas", + "name": { + "source": "deltas", + "canonical": "deltas" + }, + "wireID": 4, + "type": { + "target": "t/anon/protobuf/rep.Series.deltas/list", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "rep.Series.deltas" + } } ], "abstract": false, @@ -219,7 +300,7 @@ { "format": "protobuf@3", "path": "repeated.proto", - "hash": "7375f034c6eaaced0ea0403727f69d70579a1d195d0a281b6fb3f9c3f13727de" + "hash": "6f9fc1237966d1face9c7e72b5b70999c95ea66758c70a28ed6dc5b2d0c2473d" } ] } diff --git a/testdata/conformance/protobuf/repeated.proto b/testdata/conformance/protobuf/repeated.proto index 8cb3f8a..fe3ce1c 100644 --- a/testdata/conformance/protobuf/repeated.proto +++ b/testdata/conformance/protobuf/repeated.proto @@ -6,4 +6,5 @@ message Series { repeated int32 packed_values = 1; repeated int32 expanded_values = 2 [packed = false]; repeated string labels = 3; + repeated sint64 deltas = 4; } diff --git a/testdata/conformance/protobuf/rich-options.golden.json b/testdata/conformance/protobuf/rich-options.golden.json new file mode 100644 index 0000000..fc3dfde --- /dev/null +++ b/testdata/conformance/protobuf/rich-options.golden.json @@ -0,0 +1,914 @@ +{ + "irVersion": "0.1.0", + "name": "ropt", + "docs": {}, + "services": [ + { + "id": "s/protobuf/ropt", + "name": { + "source": "ropt", + "canonical": "ropt" + }, + "docs": {}, + "namespace": [ + "ropt" + ], + "groups": [ + { + "name": { + "source": "Ops", + "canonical": "ops" + }, + "docs": {}, + "operations": [ + { + "id": "op/protobuf/ropt.Ops.Do", + "name": { + "source": "Do", + "canonical": "do" + }, + "docs": {}, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/ropt.Widget", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/ropt.Widget", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "idempotency": {}, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/ropt.Ops/Do", + "inputType": { + "target": "t/protobuf/ropt.Widget", + "nullable": false + } + } + }, + "extensions": { + "protobuf:option:ropt.audit": "yes" + }, + "provenance": { + "source": 0, + "pointer": "ropt.Ops.Do" + } + } + ], + "extensions": { + "protobuf:option:ropt.team": "platform" + } + } + ], + "auth": null, + "provenance": { + "source": 0, + "pointer": "rich-options.proto" + } + } + ], + "types": { + "t/anon/protobuf/ropt.Meta.labels/list": { + "kind": "list", + "id": "t/anon/protobuf/ropt.Meta.labels/list", + "name": { + "hint": "labels" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.labels" + }, + "elem": { + "target": "t/prim/string", + "nullable": false + } + }, + "t/anon/protobuf/ropt.Meta.points/list": { + "kind": "list", + "id": "t/anon/protobuf/ropt.Meta.points/list", + "name": { + "hint": "points" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.points" + }, + "elem": { + "target": "t/prim/float64", + "nullable": false + }, + "encoding": { + "name": "expanded" + } + }, + "t/anon/protobuf/ropt.Meta.ratios/map": { + "kind": "map", + "id": "t/anon/protobuf/ropt.Meta.ratios/map", + "name": { + "hint": "ratios" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.ratios" + }, + "key": { + "target": "t/prim/string", + "nullable": false + }, + "value": { + "target": "t/prim/float64", + "nullable": false + } + }, + "t/anon/protobuf/ropt.Meta.weights/map": { + "kind": "map", + "id": "t/anon/protobuf/ropt.Meta.weights/map", + "name": { + "hint": "weights" + }, + "anonymous": true, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.weights" + }, + "key": { + "target": "t/prim/string", + "nullable": false + }, + "value": { + "target": "t/prim/int32", + "nullable": false + } + }, + "t/prim/bool": { + "kind": "primitive", + "id": "t/prim/bool", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "bool" + }, + "t/prim/bytes": { + "kind": "primitive", + "id": "t/prim/bytes", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "bytes" + }, + "t/prim/float64": { + "kind": "primitive", + "id": "t/prim/float64", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "float64" + }, + "t/prim/int32": { + "kind": "primitive", + "id": "t/prim/int32", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "int32" + }, + "t/prim/string": { + "kind": "primitive", + "id": "t/prim/string", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "string" + }, + "t/prim/uint64": { + "kind": "primitive", + "id": "t/prim/uint64", + "name": {}, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0 + }, + "prim": "uint64" + }, + "t/protobuf/ropt.Kind": { + "kind": "enum", + "id": "t/protobuf/ropt.Kind", + "name": { + "source": "Kind", + "canonical": "kind" + }, + "namespace": [ + "ropt" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "ropt.Kind" + }, + "valueType": "int32", + "members": [ + { + "name": { + "source": "K_A", + "canonical": "k_a" + }, + "value": { + "kind": "number", + "num": "0", + "bytes": null, + "list": null, + "object": null + }, + "docs": {} + }, + { + "name": { + "source": "K_B", + "canonical": "k_b" + }, + "value": { + "kind": "number", + "num": "1", + "bytes": null, + "list": null, + "object": null + }, + "docs": {}, + "extensions": { + "protobuf:option:ropt.label": "beta" + } + } + ], + "closed": true, + "flags": false + }, + "t/protobuf/ropt.Meta": { + "kind": "model", + "id": "t/protobuf/ropt.Meta", + "name": { + "source": "Meta", + "canonical": "meta" + }, + "namespace": [ + "ropt" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "ropt.Meta" + }, + "properties": [ + { + "id": "p/protobuf/ropt.Meta.owner", + "name": { + "source": "owner", + "canonical": "owner" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.owner" + } + }, + { + "id": "p/protobuf/ropt.Meta.labels", + "name": { + "source": "labels", + "canonical": "labels" + }, + "wireID": 2, + "type": { + "target": "t/anon/protobuf/ropt.Meta.labels/list", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.labels" + } + }, + { + "id": "p/protobuf/ropt.Meta.weights", + "name": { + "source": "weights", + "canonical": "weights" + }, + "wireID": 3, + "type": { + "target": "t/anon/protobuf/ropt.Meta.weights/map", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.weights" + } + }, + { + "id": "p/protobuf/ropt.Meta.nested", + "name": { + "source": "nested", + "canonical": "nested" + }, + "wireID": 4, + "type": { + "target": "t/protobuf/ropt.Nested", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.nested" + } + }, + { + "id": "p/protobuf/ropt.Meta.kind", + "name": { + "source": "kind", + "canonical": "kind" + }, + "wireID": 5, + "type": { + "target": "t/protobuf/ropt.Kind", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.kind" + } + }, + { + "id": "p/protobuf/ropt.Meta.sig", + "name": { + "source": "sig", + "canonical": "sig" + }, + "wireID": 6, + "type": { + "target": "t/prim/bytes", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.sig" + } + }, + { + "id": "p/protobuf/ropt.Meta.big", + "name": { + "source": "big", + "canonical": "big" + }, + "wireID": 7, + "type": { + "target": "t/prim/uint64", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.big" + } + }, + { + "id": "p/protobuf/ropt.Meta.score", + "name": { + "source": "score", + "canonical": "score" + }, + "wireID": 8, + "type": { + "target": "t/prim/float64", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.score" + } + }, + { + "id": "p/protobuf/ropt.Meta.points", + "name": { + "source": "points", + "canonical": "points" + }, + "wireID": 9, + "type": { + "target": "t/anon/protobuf/ropt.Meta.points/list", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.points" + } + }, + { + "id": "p/protobuf/ropt.Meta.ratios", + "name": { + "source": "ratios", + "canonical": "ratios" + }, + "wireID": 10, + "type": { + "target": "t/anon/protobuf/ropt.Meta.ratios/map", + "nullable": false + }, + "required": false, + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Meta.ratios" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/ropt.Nested": { + "kind": "model", + "id": "t/protobuf/ropt.Nested", + "name": { + "source": "Nested", + "canonical": "nested" + }, + "namespace": [ + "ropt" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "ropt.Nested" + }, + "properties": [ + { + "id": "p/protobuf/ropt.Nested.flag", + "name": { + "source": "flag", + "canonical": "flag" + }, + "wireID": 1, + "type": { + "target": "t/prim/bool", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Nested.flag" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/ropt.Widget": { + "kind": "model", + "id": "t/protobuf/ropt.Widget", + "name": { + "source": "Widget", + "canonical": "widget" + }, + "namespace": [ + "ropt" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "extensions": { + "protobuf:option:ropt.meta": { + "owner": "team", + "labels": [ + "a", + "b" + ], + "weights": { + "a": 2, + "x": 1 + }, + "nested": { + "flag": true + }, + "kind": "K_B", + "sig": "QUE9PQ==", + "big": 42, + "points": [], + "ratios": {} + }, + "protobuf:option:ropt.reviewers": [ + "alice", + "bob" + ] + }, + "provenance": { + "source": 0, + "pointer": "ropt.Widget" + }, + "properties": [ + { + "id": "p/protobuf/ropt.Widget.id", + "name": { + "source": "id", + "canonical": "id" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "extensions": { + "protobuf:option:ropt.field_kind": "K_A" + }, + "provenance": { + "source": 0, + "pointer": "ropt.Widget.id" + } + }, + { + "id": "p/protobuf/ropt.Widget.detail", + "name": { + "source": "detail", + "canonical": "detail" + }, + "wireID": 2, + "type": { + "target": "t/protobuf/ropt.Widget.Detail", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "encoding": { + "name": "delimited" + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Widget.detail" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + }, + "t/protobuf/ropt.Widget.Detail": { + "kind": "model", + "id": "t/protobuf/ropt.Widget.Detail", + "name": { + "source": "Detail", + "canonical": "detail" + }, + "namespace": [ + "ropt" + ], + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "ropt.Widget.Detail" + }, + "properties": [ + { + "id": "p/protobuf/ropt.Widget.Detail.note", + "name": { + "source": "note", + "canonical": "note" + }, + "wireID": 1, + "type": { + "target": "t/prim/string", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "ropt.Widget.Detail.note" + } + } + ], + "abstract": false, + "positional": false, + "inputOnly": false + } + }, + "extensions": { + "protobuf:custom-option:ropt.audit": { + "extends": "google.protobuf.MethodOptions", + "name": "ropt.audit", + "number": 50104 + }, + "protobuf:custom-option:ropt.field_kind": { + "extends": "google.protobuf.FieldOptions", + "name": "ropt.field_kind", + "number": 50102 + }, + "protobuf:custom-option:ropt.label": { + "extends": "google.protobuf.EnumValueOptions", + "name": "ropt.label", + "number": 50099 + }, + "protobuf:custom-option:ropt.meta": { + "extends": "google.protobuf.MessageOptions", + "name": "ropt.meta", + "number": 50100 + }, + "protobuf:custom-option:ropt.reviewers": { + "extends": "google.protobuf.MessageOptions", + "name": "ropt.reviewers", + "number": 50101 + }, + "protobuf:custom-option:ropt.team": { + "extends": "google.protobuf.ServiceOptions", + "name": "ropt.team", + "number": 50103 + }, + "protobuf:file": { + "syntax": "proto2" + } + }, + "diagnostics": [ + { + "severity": "info", + "code": "protobuf/custom-option-definition", + "message": "extension ropt.label defines a custom option on google.protobuf.EnumValueOptions", + "provenance": { + "source": 0, + "pointer": "ropt.label" + } + }, + { + "severity": "info", + "code": "protobuf/custom-option-definition", + "message": "extension ropt.meta defines a custom option on google.protobuf.MessageOptions", + "provenance": { + "source": 0, + "pointer": "ropt.meta" + } + }, + { + "severity": "info", + "code": "protobuf/custom-option-definition", + "message": "extension ropt.reviewers defines a custom option on google.protobuf.MessageOptions", + "provenance": { + "source": 0, + "pointer": "ropt.reviewers" + } + }, + { + "severity": "info", + "code": "protobuf/custom-option-definition", + "message": "extension ropt.field_kind defines a custom option on google.protobuf.FieldOptions", + "provenance": { + "source": 0, + "pointer": "ropt.field_kind" + } + }, + { + "severity": "info", + "code": "protobuf/custom-option-definition", + "message": "extension ropt.team defines a custom option on google.protobuf.ServiceOptions", + "provenance": { + "source": 0, + "pointer": "ropt.team" + } + }, + { + "severity": "info", + "code": "protobuf/custom-option-definition", + "message": "extension ropt.audit defines a custom option on google.protobuf.MethodOptions", + "provenance": { + "source": 0, + "pointer": "ropt.audit" + } + } + ], + "sources": [ + { + "format": "protobuf@2", + "path": "rich-options.proto", + "hash": "c0ac97529c4fe06f0eaf7f122c5a7c1dd3eabe9a2aa9eda083fc26c528f87d1e" + } + ] +} diff --git a/testdata/conformance/protobuf/rich-options.proto b/testdata/conformance/protobuf/rich-options.proto new file mode 100644 index 0000000..c8d1e5e --- /dev/null +++ b/testdata/conformance/protobuf/rich-options.proto @@ -0,0 +1,78 @@ +syntax = "proto2"; + +package ropt; + +import "google/protobuf/descriptor.proto"; + +enum Kind { + K_A = 0; + K_B = 1 [(label) = "beta"]; +} + +message Nested { + optional bool flag = 1; +} + +message Meta { + optional string owner = 1; + repeated string labels = 2; + map weights = 3; + optional Nested nested = 4; + optional Kind kind = 5; + optional bytes sig = 6; + optional uint64 big = 7; + optional double score = 8; + repeated double points = 9; + map ratios = 10; +} + +extend google.protobuf.EnumValueOptions { + optional string label = 50099; +} + +extend google.protobuf.MessageOptions { + optional Meta meta = 50100; + repeated string reviewers = 50101; +} + +extend google.protobuf.FieldOptions { + optional Kind field_kind = 50102; +} + +extend google.protobuf.ServiceOptions { + optional string team = 50103; +} + +extend google.protobuf.MethodOptions { + optional string audit = 50104; +} + +message Widget { + option (meta) = { + owner: "team" + labels: "a" + labels: "b" + weights: [{ key: "x", value: 1 }, { key: "a", value: 2 }] + nested: { flag: true } + kind: K_B + sig: "AA==" + big: 42 + score: nan + points: [nan] + ratios: [{ key: "a", value: nan }] + }; + option (reviewers) = "alice"; + option (reviewers) = "bob"; + + optional string id = 1 [(field_kind) = K_A]; + optional group Detail = 2 { + optional string note = 1; + } +} + +service Ops { + option (team) = "platform"; + rpc Do(Widget) returns (Widget) { + option (audit) = "yes"; + } +} diff --git a/testdata/conformance/protobuf/services.golden.json b/testdata/conformance/protobuf/services.golden.json index cfe2650..c8fb388 100644 --- a/testdata/conformance/protobuf/services.golden.json +++ b/testdata/conformance/protobuf/services.golden.json @@ -409,6 +409,101 @@ "source": 0, "pointer": "svc.Echo.Notify" } + }, + { + "id": "op/protobuf/svc.Echo.Drain", + "name": { + "source": "Drain", + "canonical": "drain" + }, + "docs": {}, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Pong", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "idempotency": {}, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/svc.Echo/Drain" + } + }, + "provenance": { + "source": 0, + "pointer": "svc.Echo.Drain" + } + }, + { + "id": "op/protobuf/svc.Echo.Touch", + "name": { + "source": "Touch", + "canonical": "touch" + }, + "docs": {}, + "deprecation": {}, + "request": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + ] + }, + "responses": [ + { + "name": { + "hint": "response" + }, + "conditions": {}, + "payload": { + "contents": [ + { + "type": { + "target": "t/protobuf/svc.Pong", + "nullable": false + } + } + ] + }, + "docs": {} + } + ], + "oneWay": false, + "idempotency": {}, + "auth": null, + "bindings": { + "rpc": { + "system": "grpc", + "fullMethod": "/svc.Echo/Touch", + "inputType": { + "target": "t/protobuf/svc.Ping", + "nullable": false + } + } + }, + "provenance": { + "source": 0, + "pointer": "svc.Echo.Touch" + } } ] } @@ -545,7 +640,7 @@ { "format": "protobuf@3", "path": "services.proto", - "hash": "b93910b5b74c4c859ac5063d914154d539a5f83124a60273c168c4f977c0cb9c" + "hash": "376960e79ff5192b1518fbaab727afe3e97761a43c86b65a8f12c3a4035b1d7d" } ] } diff --git a/testdata/conformance/protobuf/services.proto b/testdata/conformance/protobuf/services.proto index bdf6f56..0965fdc 100644 --- a/testdata/conformance/protobuf/services.proto +++ b/testdata/conformance/protobuf/services.proto @@ -24,4 +24,8 @@ service Echo { option idempotency_level = IDEMPOTENT; } rpc Notify(Ping) returns (google.protobuf.Empty); + rpc Drain(google.protobuf.Empty) returns (Pong); + rpc Touch(Ping) returns (Pong) { + option deprecated = true; + } } diff --git a/testdata/conformance/protobuf/well-known.golden.json b/testdata/conformance/protobuf/well-known.golden.json index 7674215..c8d3fba 100644 --- a/testdata/conformance/protobuf/well-known.golden.json +++ b/testdata/conformance/protobuf/well-known.golden.json @@ -116,6 +116,23 @@ "identity": "google.protobuf.FieldMask", "package": "google.golang.org/protobuf/types/known/fieldmaskpb" }, + "t/protobuf/external/google.protobuf.SourceContext": { + "kind": "external", + "id": "t/protobuf/external/google.protobuf.SourceContext", + "name": { + "source": "google.protobuf.SourceContext", + "canonical": "source_context" + }, + "anonymous": false, + "docs": {}, + "sensitive": false, + "provenance": { + "source": 0, + "pointer": "google.protobuf.SourceContext" + }, + "identity": "google.protobuf.SourceContext", + "package": "google.golang.org/protobuf/types/known" + }, "t/protobuf/wkt.Bundle": { "kind": "model", "id": "t/protobuf/wkt.Bundle", @@ -357,6 +374,34 @@ "source": 0, "pointer": "wkt.Bundle.nothing" } + }, + { + "id": "p/protobuf/wkt.Bundle.ctx", + "name": { + "source": "ctx", + "canonical": "ctx" + }, + "wireID": 9, + "type": { + "target": "t/protobuf/external/google.protobuf.SourceContext", + "nullable": false + }, + "required": false, + "presence": "explicit", + "clientOptional": false, + "defaultAdded": false, + "visibility": { + "none": false + }, + "flatten": false, + "eventHeader": false, + "eventPayload": false, + "secret": false, + "docs": {}, + "provenance": { + "source": 0, + "pointer": "wkt.Bundle.ctx" + } } ], "abstract": false, @@ -373,7 +418,7 @@ { "format": "protobuf@3", "path": "well-known.proto", - "hash": "bb1e63ecc4c6e64308158b089e858e63f873889d602453d5a5240ccbda3aa37e" + "hash": "deb7aa62e0ab7e88208e04b3a909903ca629ab4f98659bb55b8d067994479520" } ] } diff --git a/testdata/conformance/protobuf/well-known.proto b/testdata/conformance/protobuf/well-known.proto index cb00aef..3632f22 100644 --- a/testdata/conformance/protobuf/well-known.proto +++ b/testdata/conformance/protobuf/well-known.proto @@ -9,6 +9,7 @@ import "google/protobuf/empty.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/field_mask.proto"; import "google/protobuf/wrappers.proto"; +import "google/protobuf/source_context.proto"; message Bundle { google.protobuf.Timestamp ts = 1; @@ -19,4 +20,5 @@ message Bundle { google.protobuf.Int32Value count = 6; google.protobuf.StringValue note = 7; google.protobuf.Empty nothing = 8; + google.protobuf.SourceContext ctx = 9; }