Repository: googleapis/api-linter
Pull request #123 (2019-07-29 19:52:22)

Added helper method for getting Location for a descriptor name instead of the entire descriptor


Patch:
diff --git a/lint/source.go b/lint/source.go
index d32124d..8652d4d 100644
--- a/lint/source.go
+++ b/lint/source.go
@@ -173,6 +173,12 @@ func (s DescriptorSource) DescriptorLocation(d protoreflect.Descriptor) (Locatio
 	return s.findLocationByPath(getPath(d))
 }
 
+// DescriptorNameLocation returns a `Location` for the name of a given descriptor. If not found,
+// returns (nil, ErrPathNotFound).
+func (s DescriptorSource) DescriptorNameLocation(d protoreflect.Descriptor) (Location, error) {
+	return s.findLocationByPath(append(getPath(d), 1))
+}
+
 // DescriptorLocationOrFileStart returns a `Location` for the given descriptor. If there was an
 // error finding the location, it returns the start location of the file instead (that is,
 // line 1 column 1 as the Start and End Positions).


Test patch:
diff --git a/lint/source_test.go b/lint/source_test.go
index 6eb25a5..2683cef 100644
--- a/lint/source_test.go
+++ b/lint/source_test.go
@@ -15,6 +15,7 @@
 package lint
 
 import (
+	"bytes"
 	"fmt"
 	"io/ioutil"
 	"path/filepath"
@@ -24,6 +25,8 @@ import (
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/reflect/protoreflect"
 	"google.golang.org/protobuf/types/descriptorpb"
+
+	"github.com/googleapis/api-linter/testutil"
 )
 
 //go:generate protoc --include_source_info --descriptor_set_out=testdata/test_source.protoset --proto_path=testdata testdata/test_source.proto
@@ -253,6 +256,207 @@ func TestIsRuleDisabled(t *testing.T) {
 	}
 }
 
+func TestGetDescriptorName(t *testing.T) {
+	source := `
+syntax = "proto3";
+
+message FooMessage {
+  string foo = 1;
+}
+
+service FooService {
+  rpc GetFoo(FooMessage) returns (FooMessage);
+}
+
+enum MyEnum {
+  FOO = 0;
+}
+`
+	fd := testutil.MustCreateFileDescriptorProto(
+		t,
+		testutil.FileDescriptorSpec{
+			Filename: "test.proto",
+			Template: source,
+		})
+
+	req, err := NewProtoRequest(fd, nil)
+	if err != nil {
+		t.Fatalf("Error creating proto request: %s", err)
+	}
+
+	t.Run("messages", func(t *testing.T) {
+		for i := 0; i < req.ProtoFile().Messages().Len(); i++ {
+			msg := req.ProtoFile().Messages().Get(i)
+
+			loc, err := req.DescriptorSource().DescriptorNameLocation(msg)
+			if err != nil {
+				t.Fatalf("Error finding location of descriptor name: %s", err)
+			}
+
+			if got, want := mustGetTextAtLocation(source, loc), string(msg.Name()); got != want {
+				t.Fatalf("Got %q as message name; want %q", got, want)
+			}
+		}
+	})
+
+	t.Run("fields", func(t *testing.T) {
+		msg := req.ProtoFile().Messages().Get(0)
+		for i := 0; i < msg.Fields().Len(); i++ {
+			f := msg.Fields().Get(i)
+			loc, err := req.DescriptorSource().DescriptorNameLocation(f)
+			if err != nil {
+				t.Fatalf("Error finding location of descriptor name: %s", err)
+			}
+
+			if got, want := mustGetTextAtLocation(source, loc), string(f.Name()); got != want {
+				t.Fatalf("Got %q as field name; want %q", got, want)
+			}
+		}
+	})
+
+	t.Run("services", func(t *testing.T) {
+		for i := 0; i < req.ProtoFile().Services().Len(); i++ {
+			svc := req.ProtoFile().Services().Get(i)
+
+			loc, err := req.DescriptorSource().DescriptorNameLocation(svc)
+			if err != nil {
+				t.Fatalf("Error finding location of descriptor name: %s", err)
+			}
+
+			if got, want := mustGetTextAtLocation(source, loc), string(svc.Name()); got != want {
+				t.Fatalf("Got %q as service name; want %q", got, want)
+			}
+		}
+	})
+
+	t.Run("rpcs", func(t *testing.T) {
+		svc := req.ProtoFile().Services().Get(0)
+		for i := 0; i < svc.Methods().Len(); i++ {
+			rpc := svc.Methods().Get(i)
+			loc, err := req.DescriptorSource().DescriptorNameLocation(rpc)
+			if err != nil {
+				t.Fatalf("Error finding location of descriptor name: %s", err)
+			}
+
+			if got, want := mustGetTextAtLocation(source, loc), string(rpc.Name()); got != want {
+				t.Fatalf("Got %q as RPC name; want %q", got, want)
+			}
+		}
+	})
+
+	t.Run("enums", func(t *testing.T) {
+		for i := 0; i < req.ProtoFile().Enums().Len(); i++ {
+			enum := req.ProtoFile().Enums().Get(i)
+
+			loc, err := req.DescriptorSource().DescriptorNameLocation(enum)
+			if err != nil {
+				t.Fatalf("Error finding location of descriptor name: %s", err)
+			}
+
+			if got, want := mustGetTextAtLocation(source, loc), string(enum.Name()); got != want {
+				t.Fatalf("Got %q as enum name; want %q", got, want)
+			}
+		}
+	})
+
+	t.Run("enum values", func(t *testing.T) {
+		enum := req.ProtoFile().Enums().Get(0)
+		for i := 0; i < enum.Values().Len(); i++ {
+			val := enum.Values().Get(i)
+
+			loc, err := req.DescriptorSource().DescriptorNameLocation(val)
+			if err != nil {
+				t.Fatalf("Error finding location of descriptor name: %s", err)
+			}
+
+			if got, want := mustGetTextAtLocation(source, loc), string(val.Name()); got != want {
+				t.Fatalf("Got %q as enum value name; want %q", got, want)
+			}
+		}
+	})
+}
+
+func TestMustGetTextAtLocation(t *testing.T) {
+	tests := []struct {
+		name           string
+		source, result string
+		loc            Location
+	}{
+		{
+			name: "simple 3 line test",
+			source: `foo
+bar
+baz`,
+			result: "bar",
+			loc: Location{
+				Start: Position{2, 1},
+				End:   Position{2, 4},
+			},
+		},
+		{
+			name: "multiline result",
+			source: `foo
+bar
+
+baz
+qux`,
+			result: `ar
+
+ba`,
+			loc: Location{
+				Start: Position{2, 2},
+				End:   Position{4, 3},
+			},
+		},
+		{
+			name: "text at end of file",
+			source: `foo
+bar
+baz
+qux`,
+			result: "qux",
+			loc: Location{
+				Start: Position{4, 1},
+				End: Position{4, 4},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if got, want := mustGetTextAtLocation(test.source, test.loc), test.result; got != want {
+				t.Fatalf("Got %q, but expecting %q for location %+v", got, want, test.loc)
+			}
+		})
+	}
+}
+
+// mustGetTextAtLocation returns the contents of source at location loc. If the location is not valid
+// or no text exists in source for the provided Location, panic.
+func mustGetTextAtLocation(source string, loc Location) string {
+	if !loc.IsValid() {
+		panic("Invalid location provided")
+	}
+
+	var buf bytes.Buffer
+	line, col := 1, 1
+	for _, char := range source {
+		if line > loc.Start.Line || line == loc.Start.Line && col >= loc.Start.Column {
+			buf.WriteRune(char)
+		}
+		col += 1
+		if char == '\n' {
+			line += 1
+			col = 1
+		}
+		if line > loc.End.Line || (line == loc.End.Line && col >= loc.End.Column) {
+			return buf.String()
+		}
+	}
+
+	panic("Failed to find text at provided location")
+}
+
 func readProtoFile(t *testing.T, fileName string) Request {
 	path := filepath.Join("testdata", fileName)
 	bs, err := ioutil.ReadFile(path)