Repository: googleapis/api-linter (go)

Issue:
Issues running api-linter on googleapis/googleapis
This is using latest commit https://github.com/googleapis/api-linter/commit/b7c8cacd5574838c908c88ad5c472258156aaba7

```
$ git remote -v
origin	https://github.com/googleapis/googleapis (fetch)
origin	https://github.com/googleapis/googleapis (push)

$ git log -n 1
commit 698b05fe2b05893dfe74a156a5665f879de7fceb (HEAD -> master, origin/master, origin/HEAD)
Author: Google APIs <noreply@google.com>
Date:   Wed Oct 23 08:10:26 2019 -0700

    Synchronize new proto/yaml changes.

    PiperOrigin-RevId: 276276483

$ api-linter $(find . -name '*.proto')
2019/11/05 16:41:01 google/ads/googleads/v1/resources/ad.proto:42:1: duplicate symbol google.ads.googleads.v1.resources.Ad: already defined as message in "./google/ads/googleads/v1/resources/ad.proto"

# removing the leading './' from find output
$ api-linter $(find . -name '*.proto' | sed 's/^\.\///')
2019/11/05 16:41:12 runtime error: invalid memory address or nil pointer dereference

# prints something with relative path for one file and exits with code 0
$ api-linter google/type/date.proto
- file_path: google/type/date.proto
  problems: []

# change the above to absolute path
$ api-linter $(pwd)/google/type/date.proto
2019/11/05 16:57:16 /Users/foo/googleapis/google/type/date.proto: open Users/foo/googleapis/google/type/date.proto: no such file or directory
```

Pull request:
[refactor] Move location functions into their own package.
This creates a Go package for `locations` to group these helper
functions together. It also adds several more:

  - FieldType
  - MethodRequestType
  - MethodResponseType

This will better group these functions together in godoc, which
will be useful to rule authors.

Conveniently, also fixes #306.

Patch:
diff --git a/lint/problem.go b/lint/problem.go
index 9fda25b..5f5dede 100644
--- a/lint/problem.go
+++ b/lint/problem.go
@@ -120,7 +120,10 @@ type fileLocation struct {
 // protocol buffer SourceCodeInfo_Location
 func fileLocationFromPBLocation(l *dpb.SourceCodeInfo_Location) fileLocation {
 	// Spans are guaranteed by protobuf to have either three or four ints.
-	span := l.Span
+	span := []int32{0, 0, 1}
+	if l != nil {
+		span = l.Span
+	}
 
 	// If `span` has four ints; they correspond to
 	// [start line, start column, end line, end column].
diff --git a/locations/descriptor_locations.go b/locations/descriptor_locations.go
new file mode 100644
index 0000000..0112857
--- /dev/null
+++ b/locations/descriptor_locations.go
@@ -0,0 +1,32 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package locations
+
+import (
+	dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// DescriptorName returns the precise location for a descriptor's name.
+//
+// This works for any descriptor, regardless of type (message, field, etc.).
+func DescriptorName(d desc.Descriptor) *dpb.SourceCodeInfo_Location {
+	if sourceInfo := d.GetSourceInfo(); sourceInfo != nil {
+		// All descriptors seem to have `string name = 1`, so this conveniently works.
+		path := append(sourceInfo.Path, 1)
+		return pathLocation(d.GetFile(), path)
+	}
+	return nil
+}
diff --git a/locations/field_locations.go b/locations/field_locations.go
new file mode 100644
index 0000000..482d617
--- /dev/null
+++ b/locations/field_locations.go
@@ -0,0 +1,34 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package locations
+
+import (
+	dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// FieldType returns the precise location for a field's type.
+func FieldType(f *desc.FieldDescriptor) *dpb.SourceCodeInfo_Location {
+	if sourceInfo := f.GetSourceInfo(); sourceInfo != nil {
+		var path []int32
+		if f.GetMessageType() != nil || f.GetEnumType() != nil {
+			path = append(sourceInfo.Path, 6) // type_name
+		} else {
+			path = append(sourceInfo.Path, 5) // type
+		}
+		return pathLocation(f.GetFile(), path)
+	}
+	return nil
+}
diff --git a/locations/file_locations.go b/locations/file_locations.go
new file mode 100644
index 0000000..94f4bd7
--- /dev/null
+++ b/locations/file_locations.go
@@ -0,0 +1,36 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package locations
+
+import (
+	dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// FileSyntax returns the location of the syntax definition in a file descriptor.
+//
+// If the location can not be found (for example, because there is no syntax
+// statement), it returns nil.
+func FileSyntax(f *desc.FileDescriptor) *dpb.SourceCodeInfo_Location {
+	return pathLocation(f, []int32{12}) // syntax == 12
+}
+
+// FilePackage returns the location of the package definition in a file descriptor.
+//
+// If the location can not be found (for example, because there is no package
+// statement), it returns nil.
+func FilePackage(f *desc.FileDescriptor) *dpb.SourceCodeInfo_Location {
+	return pathLocation(f, []int32{2}) // package == 2
+}
diff --git a/lint/location.go b/locations/locations.go
similarity index 68%
rename from lint/location.go
rename to locations/locations.go
index 79b5439..aa00fb5 100644
--- a/lint/location.go
+++ b/locations/locations.go
@@ -12,43 +12,26 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package lint
+// Package locations provides functions to get the location of a particular part
+// of a descriptor, allowing Problems to be attached to just a descriptor's
+// name, type, etc.. This allows for better auto-replacement functionality in
+// code review tools.
+//
+// All functions in this package accept a descriptor and return a
+// protobuf SourceCodeInfo_Location object, which can be passed directly
+// to the Location property on Problem.
+package locations
 
 import (
 	dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
 	"github.com/jhump/protoreflect/desc"
 )
 
-// PathLocation returns the precise location within a file for a given path.
-func PathLocation(f *desc.FileDescriptor, path []int32) *dpb.SourceCodeInfo_Location {
+// pathLocation returns the precise location within a file for a given path.
+func pathLocation(f *desc.FileDescriptor, path []int32) *dpb.SourceCodeInfo_Location {
 	return sourceInfoRegistry.sourceInfo(f).findLocation(path)
 }
 
-// SyntaxLocation returns the location of the syntax definition in a file
-// descriptor.
-//
-// If the location can not be found (for example, because there is no syntax
-// statement), it returns nil.
-func SyntaxLocation(f *desc.FileDescriptor) *dpb.SourceCodeInfo_Location {
-	return PathLocation(f, []int32{12}) // syntax == 12
-}
-
-// PackageLocation returns the location of the package definition in a file
-// descriptor.
-//
-// If the location can not be found (for example, because there is no package
-// statement), it returns nil.
-func PackageLocation(f *desc.FileDescriptor) *dpb.SourceCodeInfo_Location {
-	return PathLocation(f, []int32{2}) // package == 2
-}
-
-// DescriptorNameLocation returns the precise location for a descriptor's name.
-func DescriptorNameLocation(d desc.Descriptor) *dpb.SourceCodeInfo_Location {
-	// All descriptors seem to have `string name = 1`, so this conveniently works.
-	path := append(d.GetSourceInfo().Path, 1)
-	return PathLocation(d.GetFile(), path)
-}
-
 type sourceInfo map[string]*dpb.SourceCodeInfo_Location
 
 // findLocation returns the Location for a given path.
diff --git a/locations/method_locations.go b/locations/method_locations.go
new file mode 100644
index 0000000..f5f1b1e
--- /dev/null
+++ b/locations/method_locations.go
@@ -0,0 +1,38 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package locations
+
+import (
+	dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// MethodRequestType returns the precise location of the method's input type.
+func MethodRequestType(m *desc.MethodDescriptor) *dpb.SourceCodeInfo_Location {
+	if sourceInfo := m.GetSourceInfo(); sourceInfo != nil {
+		path := append(sourceInfo.Path, 2) // input_type == 2
+		return pathLocation(m.GetFile(), path)
+	}
+	return nil
+}
+
+// MethodResponseType returns the precise location of the method's output type.
+func MethodResponseType(m *desc.MethodDescriptor) *dpb.SourceCodeInfo_Location {
+	if sourceInfo := m.GetSourceInfo(); sourceInfo != nil {
+		path := append(m.GetSourceInfo().Path, 3) // output_type == 3
+		return pathLocation(m.GetFile(), path)
+	}
+	return nil
+}
diff --git a/rules/aip0126/unspecified.go b/rules/aip0126/unspecified.go
index 571be1b..f27a76c 100644
--- a/rules/aip0126/unspecified.go
+++ b/rules/aip0126/unspecified.go
@@ -19,6 +19,7 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 	"github.com/stoewer/go-strcase"
 )
@@ -33,7 +34,7 @@ var unspecified = &lint.EnumRule{
 				Message:    fmt.Sprintf("The first enum value should be %q", want),
 				Suggestion: want,
 				Descriptor: firstValue,
-				Location:   lint.DescriptorNameLocation(firstValue),
+				Location:   locations.DescriptorName(firstValue),
 			}}
 		}
 
diff --git a/rules/aip0126/upper_snake_values.go b/rules/aip0126/upper_snake_values.go
index 07283f0..b210f46 100644
--- a/rules/aip0126/upper_snake_values.go
+++ b/rules/aip0126/upper_snake_values.go
@@ -5,6 +5,7 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 	"github.com/stoewer/go-strcase"
 )
@@ -20,7 +21,7 @@ var enumValueUpperSnakeCase = &lint.EnumRule{
 					Message:    fmt.Sprintf("Enum value %q must use UPPER_SNAKE_CASE.", got),
 					Suggestion: want,
 					Descriptor: v,
-					Location:   lint.DescriptorNameLocation(v),
+					Location:   locations.DescriptorName(v),
 				})
 			}
 		}
diff --git a/rules/aip0131/synonyms.go b/rules/aip0131/synonyms.go
index 7386683..b5aa6bc 100644
--- a/rules/aip0131/synonyms.go
+++ b/rules/aip0131/synonyms.go
@@ -19,6 +19,7 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 )
 
@@ -36,7 +37,7 @@ var synonyms = &lint.MethodRule{
 					),
 					Suggestion: strings.Replace(name, syn, "Get", 1),
 					Descriptor: m,
-					Location:   lint.DescriptorNameLocation(m),
+					Location:   locations.DescriptorName(m),
 				}}
 			}
 		}
diff --git a/rules/aip0133/request_resource_field.go b/rules/aip0133/request_resource_field.go
index 446f8d5..9475e1c 100644
--- a/rules/aip0133/request_resource_field.go
+++ b/rules/aip0133/request_resource_field.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 	"github.com/stoewer/go-strcase"
 )
@@ -39,7 +40,7 @@ var resourceField = &lint.MessageRule{
 						Message:    fmt.Sprintf("Resource field should be named %q.", want),
 						Descriptor: fieldDesc,
 						Suggestion: want,
-						Location:   lint.DescriptorNameLocation(fieldDesc),
+						Location:   locations.DescriptorName(fieldDesc),
 					}}
 				}
 				return nil
diff --git a/rules/aip0133/synonyms.go b/rules/aip0133/synonyms.go
index 6877d00..0331174 100644
--- a/rules/aip0133/synonyms.go
+++ b/rules/aip0133/synonyms.go
@@ -19,6 +19,7 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 )
 
@@ -35,7 +36,7 @@ var synonyms = &lint.MethodRule{
 						syn,
 					),
 					Descriptor: m,
-					Location:   lint.DescriptorNameLocation(m),
+					Location:   locations.DescriptorName(m),
 					Suggestion: strings.Replace(name, syn, "Create", 1),
 				}}
 			}
diff --git a/rules/aip0134/request_resource_field.go b/rules/aip0134/request_resource_field.go
index 0b44e07..f321f98 100644
--- a/rules/aip0134/request_resource_field.go
+++ b/rules/aip0134/request_resource_field.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 	"github.com/stoewer/go-strcase"
 )
@@ -39,7 +40,7 @@ var resourceField = &lint.MessageRule{
 						Message:    fmt.Sprintf("Resource field should be named %q.", want),
 						Descriptor: fieldDesc,
 						Suggestion: want,
-						Location:   lint.DescriptorNameLocation(fieldDesc),
+						Location:   locations.DescriptorName(fieldDesc),
 					}}
 				}
 				return nil
diff --git a/rules/aip0134/synonyms.go b/rules/aip0134/synonyms.go
index db4eda3..cae753e 100644
--- a/rules/aip0134/synonyms.go
+++ b/rules/aip0134/synonyms.go
@@ -19,6 +19,7 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 )
 
@@ -35,7 +36,7 @@ var synonyms = &lint.MethodRule{
 						syn,
 					),
 					Descriptor: m,
-					Location:   lint.DescriptorNameLocation(m),
+					Location:   locations.DescriptorName(m),
 					Suggestion: strings.Replace(name, syn, "Update", 1),
 				}}
 			}
diff --git a/rules/aip0136/prepositions.go b/rules/aip0136/prepositions.go
index 812c376..c70651b 100644
--- a/rules/aip0136/prepositions.go
+++ b/rules/aip0136/prepositions.go
@@ -19,6 +19,7 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/googleapis/api-linter/rules/internal/data"
 	"github.com/jhump/protoreflect/desc"
 	"github.com/stoewer/go-strcase"
@@ -32,7 +33,7 @@ var noPrepositions = &lint.MethodRule{
 				problems = append(problems, lint.Problem{
 					Message:    fmt.Sprintf("Method names should not include prepositions (%q).", word),
 					Descriptor: m,
-					Location:   lint.DescriptorNameLocation(m),
+					Location:   locations.DescriptorName(m),
 				})
 			}
 		}
diff --git a/rules/aip0136/verb_noun.go b/rules/aip0136/verb_noun.go
index cb368c8..c48e81d 100644
--- a/rules/aip0136/verb_noun.go
+++ b/rules/aip0136/verb_noun.go
@@ -18,6 +18,7 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 	"github.com/stoewer/go-strcase"
 )
@@ -33,7 +34,7 @@ var verbNoun = &lint.MethodRule{
 			return []lint.Problem{{
 				Message:    "Custom methods should be named using a verb followed by a noun.",
 				Descriptor: m,
-				Location:   lint.DescriptorNameLocation(m),
+				Location:   locations.DescriptorName(m),
 			}}
 		}
 
diff --git a/rules/aip0140/abbreviations.go b/rules/aip0140/abbreviations.go
index 2fb4d50..ec1fb54 100644
--- a/rules/aip0140/abbreviations.go
+++ b/rules/aip0140/abbreviations.go
@@ -19,6 +19,7 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 )
 
@@ -59,7 +60,7 @@ var abbreviations = &lint.DescriptorRule{
 					),
 					Suggestion: strings.ReplaceAll(d.GetName(), caseFunc(long), caseFunc(short)),
 					Descriptor: d,
-					Location:   lint.DescriptorNameLocation(d),
+					Location:   locations.DescriptorName(d),
 				})
 			}
 		}
diff --git a/rules/aip0140/prepositions.go b/rules/aip0140/prepositions.go
index 93f151d..aded406 100644
--- a/rules/aip0140/prepositions.go
+++ b/rules/aip0140/prepositions.go
@@ -19,6 +19,7 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/googleapis/api-linter/rules/internal/data"
 	"github.com/jhump/protoreflect/desc"
 )
@@ -31,7 +32,7 @@ var noPrepositions = &lint.FieldRule{
 				problems = append(problems, lint.Problem{
 					Message:    fmt.Sprintf("Avoid using %q in field names.", word),
 					Descriptor: f,
-					Location:   lint.DescriptorNameLocation(f),
+					Location:   locations.DescriptorName(f),
 				})
 			}
 		}
diff --git a/rules/aip0141/count_suffix.go b/rules/aip0141/count_suffix.go
index b16d590..dd2045d 100644
--- a/rules/aip0141/count_suffix.go
+++ b/rules/aip0141/count_suffix.go
@@ -19,6 +19,7 @@ import (
 
 	pluralize "github.com/gertd/go-pluralize"
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 )
 
@@ -30,7 +31,7 @@ var count = &lint.FieldRule{
 			return []lint.Problem{{
 				Message:    "Quantities: Use a _count suffix, not a num_ prefix.",
 				Descriptor: f,
-				Location:   lint.DescriptorNameLocation(f),
+				Location:   locations.DescriptorName(f),
 				Suggestion: want,
 			}}
 		}
diff --git a/rules/aip0142/time_field_names.go b/rules/aip0142/time_field_names.go
index f0efece..246500e 100644
--- a/rules/aip0142/time_field_names.go
+++ b/rules/aip0142/time_field_names.go
@@ -18,6 +18,7 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/googleapis/api-linter/rules/internal/utils"
 	"github.com/jhump/protoreflect/desc"
 )
@@ -37,7 +38,7 @@ var fieldNames = &lint.FieldRule{
 				return []lint.Problem{{
 					Message:    "Use the imperative mood and a `_time` suffix for timestamps.",
 					Descriptor: f,
-					Location:   lint.DescriptorNameLocation(f),
+					Location:   locations.DescriptorName(f),
 					Suggestion: want,
 				}}
 			}
@@ -48,7 +49,7 @@ var fieldNames = &lint.FieldRule{
 			return []lint.Problem{{
 				Message:    "Timestamp fields should end in `_time`.",
 				Descriptor: f,
-				Location:   lint.DescriptorNameLocation(f),
+				Location:   locations.DescriptorName(f),
 			}}
 		}
 
diff --git a/rules/aip0143/standardized_codes.go b/rules/aip0143/standardized_codes.go
index 20701c5..4b186f7 100644
--- a/rules/aip0143/standardized_codes.go
+++ b/rules/aip0143/standardized_codes.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 )
 
@@ -39,7 +40,7 @@ var fieldNames = &lint.FieldRule{
 			return []lint.Problem{{
 				Message:    fmt.Sprintf("Use %q in place of %q.", want, f.GetName()),
 				Descriptor: f,
-				Location:   lint.DescriptorNameLocation(f),
+				Location:   locations.DescriptorName(f),
 				Suggestion: want,
 			}}
 		}
diff --git a/rules/aip0191/filenames.go b/rules/aip0191/filenames.go
index 29eda14..67fe290 100644
--- a/rules/aip0191/filenames.go
+++ b/rules/aip0191/filenames.go
@@ -19,6 +19,7 @@ import (
 	"strings"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 )
 
@@ -30,7 +31,7 @@ var filename = &lint.FileRule{
 			return []lint.Problem{{
 				Message:    "The proto version must not be used as the filename.",
 				Descriptor: f,
-				Location:   lint.PackageLocation(f),
+				Location:   locations.FilePackage(f),
 			}}
 		}
 		return nil
diff --git a/rules/aip0191/syntax.go b/rules/aip0191/syntax.go
index 2edbb86..eb6720c 100644
--- a/rules/aip0191/syntax.go
+++ b/rules/aip0191/syntax.go
@@ -16,6 +16,7 @@ package aip0191
 
 import (
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 )
 
@@ -28,7 +29,7 @@ var syntax = &lint.FileRule{
 				Message:    "All API proto files must use proto3 syntax.",
 				Suggestion: "syntax = \"proto3\";",
 				Descriptor: f,
-				Location:   lint.SyntaxLocation(f),
+				Location:   locations.FileSyntax(f),
 			}}
 		}
 		return nil
diff --git a/rules/aip0192/has_comments.go b/rules/aip0192/has_comments.go
index 037c599..24f8048 100644
--- a/rules/aip0192/has_comments.go
+++ b/rules/aip0192/has_comments.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
 	"github.com/jhump/protoreflect/desc"
 )
 
@@ -30,7 +31,7 @@ var hasComments = &lint.DescriptorRule{
 			problems = append(problems, lint.Problem{
 				Message:    fmt.Sprintf("Missing comment over %q.", d.GetName()),
 				Descriptor: d,
-				Location:   lint.DescriptorNameLocation(d),
+				Location:   locations.DescriptorName(d),
 			})
 		}
 		return


Test patch:
diff --git a/locations/descriptor_locations_test.go b/locations/descriptor_locations_test.go
new file mode 100644
index 0000000..7b4a4e7
--- /dev/null
+++ b/locations/descriptor_locations_test.go
@@ -0,0 +1,57 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package locations
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestDescriptorName(t *testing.T) {
+	f := parse(t, `
+		message Foo {
+		  string bar = 1;
+		  map<string, string> baz = 2;
+		}
+	`)
+
+	tests := []struct {
+		testName string
+		d        desc.Descriptor
+		wantSpan []int32
+	}{
+		{"Message", f.GetMessageTypes()[0], []int32{2, 8, 11}},
+		{"Field", f.GetMessageTypes()[0].GetFields()[0], []int32{3, 9, 12}},
+		{"MapField", f.GetMessageTypes()[0].GetFields()[1], []int32{4, 22, 25}},
+	}
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			if diff := cmp.Diff(DescriptorName(test.d).Span, test.wantSpan); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
+
+func TestFileDescriptorName(t *testing.T) {
+	f := parse(t, `
+		message Foo {}
+	`)
+	if got := DescriptorName(f); got != nil {
+		t.Errorf("%v", got)
+	}
+}
diff --git a/locations/field_locations_test.go b/locations/field_locations_test.go
new file mode 100644
index 0000000..6be5c4c
--- /dev/null
+++ b/locations/field_locations_test.go
@@ -0,0 +1,48 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package locations
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestFieldLocations(t *testing.T) {
+	f := parse(t, `
+		message Book {
+		  string name = 1;
+		  Author author = 2;
+		}
+		message Author {}
+	`)
+	tests := []struct {
+		name  string
+		field *desc.FieldDescriptor
+		span  []int32
+	}{
+		{"Primitive", f.GetMessageTypes()[0].GetFields()[0], []int32{3, 2, 8}},
+		{"Composite", f.GetMessageTypes()[0].GetFields()[1], []int32{4, 2, 8}},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			l := FieldType(test.field)
+			if diff := cmp.Diff(l.GetSpan(), test.span); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/lint/location_test.go b/locations/file_locations_test.go
similarity index 59%
rename from lint/location_test.go
rename to locations/file_locations_test.go
index b221c3e..2d7c768 100644
--- a/lint/location_test.go
+++ b/locations/file_locations_test.go
@@ -12,41 +12,28 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package lint
+package locations
 
 import (
-	"strings"
 	"testing"
 
 	dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
 	"github.com/google/go-cmp/cmp"
 	"github.com/jhump/protoreflect/desc"
 	"github.com/jhump/protoreflect/desc/builder"
-	"github.com/jhump/protoreflect/desc/protoparse"
-	"github.com/lithammer/dedent"
 )
 
 func TestLocations(t *testing.T) {
-	parser := protoparse.Parser{
-		Accessor: protoparse.FileContentsFromMap(map[string]string{
-			"test.proto": strings.TrimSpace(dedent.Dedent(`
-				// proto3 rules!
-				syntax = "proto3";
+	f := parse(t, `
+		// proto3 rules!
+		syntax = "proto3";
 
-				package google.api.linter;
+		package google.api.linter;
 
-				message Foo {
-				  string bar = 1;
-				}
-			`)),
-		}),
-		IncludeSourceCodeInfo: true,
-	}
-	fds, err := parser.ParseFiles("test.proto")
-	if err != nil {
-		t.Fatalf("%v", err)
-	}
-	f := fds[0]
+		message Foo {
+			string bar = 1;
+		}
+	`)
 
 	// Test the file location functions.
 	t.Run("File", func(t *testing.T) {
@@ -55,8 +42,8 @@ func TestLocations(t *testing.T) {
 			fx       func(f *desc.FileDescriptor) *dpb.SourceCodeInfo_Location
 			wantSpan []int32
 		}{
-			{"Syntax", SyntaxLocation, []int32{1, 0, int32(len("syntax = \"proto3\";"))}},
-			{"Package", PackageLocation, []int32{3, 0, int32(len("package google.api.linter;"))}},
+			{"Syntax", FileSyntax, []int32{1, 0, int32(len("syntax = \"proto3\";"))}},
+			{"Package", FilePackage, []int32{3, 0, int32(len("package google.api.linter;"))}},
 		}
 		for _, test := range tests {
 			t.Run(test.testName, func(t *testing.T) {
@@ -67,25 +54,6 @@ func TestLocations(t *testing.T) {
 		}
 	})
 
-	// Test descriptor names.
-	t.Run("DescriptorNames", func(t *testing.T) {
-		tests := []struct {
-			testName string
-			d        desc.Descriptor
-			wantSpan []int32
-		}{
-			{"Message", f.GetMessageTypes()[0], []int32{5, 8, 11}},
-			{"Field", f.GetMessageTypes()[0].GetFields()[0], []int32{6, 9, 12}},
-		}
-		for _, test := range tests {
-			t.Run(test.testName, func(t *testing.T) {
-				if diff := cmp.Diff(DescriptorNameLocation(test.d).Span, test.wantSpan); diff != "" {
-					t.Errorf(diff)
-				}
-			})
-		}
-	})
-
 	// Test bogus locations.
 	t.Run("Bogus", func(t *testing.T) {
 		tests := []struct {
@@ -96,7 +64,7 @@ func TestLocations(t *testing.T) {
 		}
 		for _, test := range tests {
 			t.Run(test.testName, func(t *testing.T) {
-				if loc := PathLocation(f, test.path); loc != nil {
+				if loc := pathLocation(f, test.path); loc != nil {
 					t.Errorf("%v", loc)
 				}
 			})
@@ -114,8 +82,8 @@ func TestMissingLocations(t *testing.T) {
 		testName string
 		fx       func(f *desc.FileDescriptor) *dpb.SourceCodeInfo_Location
 	}{
-		{"Syntax", SyntaxLocation},
-		{"Package", PackageLocation},
+		{"Syntax", FileSyntax},
+		{"Package", FilePackage},
 	}
 	for _, test := range tests {
 		t.Run(test.testName, func(t *testing.T) {
diff --git a/locations/locations_test.go b/locations/locations_test.go
new file mode 100644
index 0000000..07d9eb3
--- /dev/null
+++ b/locations/locations_test.go
@@ -0,0 +1,42 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package locations
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/jhump/protoreflect/desc"
+	"github.com/jhump/protoreflect/desc/protoparse"
+	"github.com/lithammer/dedent"
+)
+
+func parse(t *testing.T, s string) *desc.FileDescriptor {
+	s = strings.TrimSpace(dedent.Dedent(s))
+	if !strings.Contains(s, "syntax = ") {
+		s = "syntax = \"proto3\";\n\n" + s
+	}
+	parser := protoparse.Parser{
+		Accessor: protoparse.FileContentsFromMap(map[string]string{
+			"test.proto": strings.TrimSpace(dedent.Dedent(s)),
+		}),
+		IncludeSourceCodeInfo: true,
+	}
+	fds, err := parser.ParseFiles("test.proto")
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+	return fds[0]
+}
diff --git a/locations/method_locations_test.go b/locations/method_locations_test.go
new file mode 100644
index 0000000..0a2cd6d
--- /dev/null
+++ b/locations/method_locations_test.go
@@ -0,0 +1,49 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 		https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package locations
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestMethodRequestType(t *testing.T) {
+	f := parse(t, `
+		service Library {
+		  rpc GetBook(GetBookRequest) returns (Book);
+		}
+		message GetBookRequest {}
+		message Book {}
+	`)
+	loc := MethodRequestType(f.GetServices()[0].GetMethods()[0])
+	if diff := cmp.Diff(loc.GetSpan(), []int32{3, 14, 28}); diff != "" {
+		t.Errorf(diff)
+	}
+}
+
+func TestMethodResponseType(t *testing.T) {
+	f := parse(t, `
+		service Library {
+		  rpc GetBook(GetBookRequest) returns (Book);
+		}
+		message GetBookRequest {}
+		message Book {}
+	`)
+	loc := MethodResponseType(f.GetServices()[0].GetMethods()[0])
+	if diff := cmp.Diff(loc.GetSpan(), []int32{3, 39, 43}); diff != "" {
+		t.Errorf(diff)
+	}
+}