Repository: uber/prototool
Pull request #345 (2019-02-18 17:13:26)

Minor cleanups
This does some cherry-picked cleanups from https://github.com/uber/prototool/tree/bazel-generate since it's unlikely that branch will be merged.

- Install a specific version of Bazel from the Makefile instead of relying on the local installation, and only installing on Travis CI.
- Some cleanups to the added validation logic in `internal/file` after testing.
- Clean up the `internal/file` testing.
- Some internal field renaming in `internal/settings` to be more consistent.
- Some cleanup of `internal/strs`.


Patch:
diff --git a/.gitignore b/.gitignore
index fde049e..e98f46b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@
 /internal/cmd/testdata/create/version2three/b/c/bar/v1beta1/baz.proto
 /internal/cmd/testdata/create/version2three/baz.proto
 /internal/cmd/testdata/create/version2four/baz.proto
+/internal/cmd/testdata/create/version2five/baz.proto
 /main
 /prototool.iml
 /release
diff --git a/.travis.yml b/.travis.yml
index 333c145..7aad1e2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,6 @@
 language: go
 go:
   - "1.11.x"
-before_install:
-  - bash -x etc/bin/travis-before-install.sh
 script:
   - make lint cover
   - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then make codecov; fi
diff --git a/Makefile b/Makefile
index 91537ba..e3fc631 100644
--- a/Makefile
+++ b/Makefile
@@ -15,6 +15,25 @@ TMP_BIN = $(TMP)/bin
 TMP_ETC := $(TMP)/etc
 TMP_LIB := $(TMP)/lib
 
+BAZEL_VERSION := 0.22.0
+
+BAZEL_LIB := $(TMP_LIB)/bazel-$(BAZEL_VERSION)
+BAZEL := $(abspath $(BAZEL_LIB)/bin/bazel)
+ifeq ($(UNAME_OS),Darwin)
+BAZEL_OS := darwin
+else
+BAZEL_OS = linux
+endif
+BAZEL_ARCH := $(UNAME_ARCH)
+$(BAZEL):
+	$Q rm -f $(TMP_BIN)/bazel
+	$Q mkdir -p $(TMP_BIN)
+	$Q rm -rf $(BAZEL_LIB)
+	$Q mkdir -p $(BAZEL_LIB)
+	$Q curl -SSL https://github.com/bazelbuild/bazel/releases/download/$(BAZEL_VERSION)/bazel-$(BAZEL_VERSION)-installer-$(BAZEL_OS)-$(BAZEL_ARCH).sh -o $(BAZEL_LIB)/bazel-installer.sh
+	$Q chmod +x $(BAZEL_LIB)/bazel-installer.sh
+	$Q $(BAZEL_LIB)/bazel-installer.sh --base=$(abspath $(BAZEL_LIB)) --bin=$(abspath $(TMP_BIN))
+
 unexport GOPATH
 export GO111MODULE := on
 export GOBIN := $(abspath $(TMP_BIN))
@@ -85,7 +104,7 @@ internalgen: install
 	prototool config init etc/config/example --uncomment
 
 .PHONY: bazelgen
-bazelgen:
+bazelgen: $(BAZEL)
 	bash etc/bin/bazelgen.sh
 
 .PHONY: generate
@@ -156,7 +175,7 @@ codecov:
 	bash <(curl -s https://codecov.io/bash) -c -f $(TMP_ETC)/coverage.txt
 
 .PHONY: bazelbuild
-bazelbuild:
+bazelbuild: $(BAZEL)
 	bazel build //...:all
 
 .PHONY: releasegen
diff --git a/etc/bin/travis-before-install.sh b/etc/bin/travis-before-install.sh
deleted file mode 100644
index cc4bf99..0000000
--- a/etc/bin/travis-before-install.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-
-set -euo pipefail
-
-DIR="$(cd "$(dirname "${0}")/../.." && pwd)"
-cd "${DIR}"
-
-BAZEL_VERSION=0.21.0
-BAZEL_OS=linux
-BAZEL_ARCH=x86_64
-BAZEL_URL="https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/bazel-${BAZEL_VERSION}-installer-${BAZEL_OS}-${BAZEL_ARCH}.sh"
-
-TMPDIR="$(mktemp -d)"
-trap 'rm -rf "${TMPDIR}"' EXIT
-
-BAZEL_INSTALL_SH="${TMPDIR}/bazel-install.sh"
-
-wget -O "${BAZEL_INSTALL_SH}" "${BAZEL_URL}"
-chmod +x "${BAZEL_INSTALL_SH}"
-"${BAZEL_INSTALL_SH}" --user
diff --git a/etc/config/example/prototool.yaml b/etc/config/example/prototool.yaml
index 3b928a7..e784118 100644
--- a/etc/config/example/prototool.yaml
+++ b/etc/config/example/prototool.yaml
@@ -1,4 +1,6 @@
 # Paths to exclude when searching for Protobuf files.
+# These can either be file or directory names.
+# If there is a directory name, that directory and all sub-directories will be excluded.
 excludes:
   - path/to/a
   - path/to/b/file.proto
@@ -44,6 +46,8 @@ lint:
   group: uber2
 
   # Linter files to ignore.
+  # These can either be file or directory names.
+  # If there is a directory name, that directory and all sub-directories will be ignored.
   ignores:
     - id: RPC_NAMES_CAMEL_CASE
       files:
@@ -51,7 +55,7 @@ lint:
         - path/to/bar.proto
     - id: SYNTAX_PROTO3
       files:
-        - path/to/foo.proto
+        - path/to/dir
 
   # Linter rules.
   # Run prototool lint --list-all-linters to see all available linters.
@@ -93,6 +97,10 @@ lint:
       // Acme, Inc. (c) 2019
       //
     is_commented: true
+  # Override the default java_package file option prefix of "com".
+  # If this is set, this will affect lint, create, and format --fix to use.
+  # this prefix instead of "com".
+  java_package_prefix: au.com
 
 # Code generation directives.
 generate:
diff --git a/internal/cfginit/cfginit.go b/internal/cfginit/cfginit.go
index dc22642..d9dc736 100644
--- a/internal/cfginit/cfginit.go
+++ b/internal/cfginit/cfginit.go
@@ -29,6 +29,8 @@ import (
 )
 
 var tmpl = template.Must(template.New("tmpl").Parse(`# Paths to exclude when searching for Protobuf files.
+# These can either be file or directory names.
+# If there is a directory name, that directory and all sub-directories will be excluded.
 {{.V}}excludes:
 {{.V}}  - path/to/a
 {{.V}}  - path/to/b/file.proto
@@ -74,6 +76,8 @@ protoc:
 {{.V}}  group: uber2
 
   # Linter files to ignore.
+  # These can either be file or directory names.
+  # If there is a directory name, that directory and all sub-directories will be ignored.
 {{.V}}  ignores:
 {{.V}}    - id: RPC_NAMES_CAMEL_CASE
 {{.V}}      files:
@@ -81,7 +85,7 @@ protoc:
 {{.V}}        - path/to/bar.proto
 {{.V}}    - id: SYNTAX_PROTO3
 {{.V}}      files:
-{{.V}}        - path/to/foo.proto
+{{.V}}        - path/to/dir
 
   # Linter rules.
   # Run prototool lint --list-all-linters to see all available linters.
@@ -123,6 +127,10 @@ protoc:
 {{.V}}      // Acme, Inc. (c) 2019
 {{.V}}      //
 {{.V}}    is_commented: true
+  # Override the default java_package file option prefix of "com".
+  # If this is set, this will affect lint, create, and format --fix to use.
+  # this prefix instead of "com".
+{{.V}}  java_package_prefix: au.com
 
 # Code generation directives.
 {{.V}}generate:
diff --git a/internal/create/handler.go b/internal/create/handler.go
index 31ff4fa..4424077 100644
--- a/internal/create/handler.go
+++ b/internal/create/handler.go
@@ -156,6 +156,10 @@ func (h *handler) create(filePath string) error {
 	if err != nil {
 		return err
 	}
+	javaPackagePrefix, err := h.getJavaPackagePrefix(filePath)
+	if err != nil {
+		return err
+	}
 	var data []byte
 	if isV2 {
 		data, err = getDataV2(
@@ -165,7 +169,7 @@ func (h *handler) create(filePath string) error {
 				CSharpNamespace:    protostrs.CSharpNamespace(pkg),
 				GoPkg:              protostrs.GoPackageV2(pkg),
 				JavaOuterClassname: protostrs.JavaOuterClassname(filePath),
-				JavaPkg:            protostrs.JavaPackage(pkg),
+				JavaPkg:            protostrs.JavaPackagePrefixOverride(pkg, javaPackagePrefix),
 				OBJCClassPrefix:    protostrs.OBJCClassPrefix(pkg),
 				PHPNamespace:       protostrs.PHPNamespace(pkg),
 			},
@@ -180,7 +184,7 @@ func (h *handler) create(filePath string) error {
 				Pkg:                pkg,
 				GoPkg:              protostrs.GoPackage(pkg),
 				JavaOuterClassname: protostrs.JavaOuterClassname(filePath),
-				JavaPkg:            protostrs.JavaPackage(pkg),
+				JavaPkg:            protostrs.JavaPackagePrefixOverride(pkg, javaPackagePrefix),
 			},
 		)
 		if err != nil {
@@ -191,12 +195,7 @@ func (h *handler) create(filePath string) error {
 }
 
 func (h *handler) isV2(filePath string) (bool, error) {
-	absFilePath, err := filepath.Abs(filePath)
-	if err != nil {
-		return false, err
-	}
-	absDirPath := filepath.Dir(absFilePath)
-	config, err := h.getConfig(absDirPath)
+	config, err := h.getConfig(filePath)
 	if err != nil {
 		return false, err
 	}
@@ -208,12 +207,19 @@ func (h *handler) isV2(filePath string) (bool, error) {
 }
 
 func (h *handler) getFileHeader(filePath string) (string, error) {
-	absFilePath, err := filepath.Abs(filePath)
+	config, err := h.getConfig(filePath)
 	if err != nil {
 		return "", err
 	}
-	absDirPath := filepath.Dir(absFilePath)
-	config, err := h.getConfig(absDirPath)
+	// no config file found
+	if config.DirPath == "" {
+		return "", nil
+	}
+	return config.Lint.FileHeader, nil
+}
+
+func (h *handler) getJavaPackagePrefix(filePath string) (string, error) {
+	config, err := h.getConfig(filePath)
 	if err != nil {
 		return "", err
 	}
@@ -221,19 +227,14 @@ func (h *handler) getFileHeader(filePath string) (string, error) {
 	if config.DirPath == "" {
 		return "", nil
 	}
-	return config.Lint.FileHeader, nil
+	return config.Lint.JavaPackagePrefix, nil
 }
 
 func (h *handler) getPkg(filePath string, defaultPackage string) (string, error) {
 	if h.pkg != "" {
 		return h.pkg, nil
 	}
-	absFilePath, err := filepath.Abs(filePath)
-	if err != nil {
-		return "", err
-	}
-	absDirPath := filepath.Dir(absFilePath)
-	config, err := h.getConfig(absDirPath)
+	config, err := h.getConfig(filePath)
 	if err != nil {
 		return "", err
 	}
@@ -241,6 +242,11 @@ func (h *handler) getPkg(filePath string, defaultPackage string) (string, error)
 	if config.DirPath == "" {
 		return defaultPackage, nil
 	}
+	absFilePath, err := filepath.Abs(filePath)
+	if err != nil {
+		return "", err
+	}
+	absDirPath := filepath.Dir(absFilePath)
 	// we need to get all the matching directories and then choose the longest
 	// ie if you have a, a/b, we choose a/b
 	var longestCreateDirPath string
@@ -280,7 +286,12 @@ func (h *handler) getPkg(filePath string, defaultPackage string) (string, error)
 	return getPkgFromRel(rel, "", defaultPackage), nil
 }
 
-func (h *handler) getConfig(absDirPath string) (settings.Config, error) {
+func (h *handler) getConfig(filePath string) (settings.Config, error) {
+	absFilePath, err := filepath.Abs(filePath)
+	if err != nil {
+		return settings.Config{}, err
+	}
+	absDirPath := filepath.Dir(absFilePath)
 	if h.configData != "" {
 		return h.configProvider.GetForData(absDirPath, h.configData)
 	}
diff --git a/internal/exec/runner.go b/internal/exec/runner.go
index 214b8ee..2f80147 100644
--- a/internal/exec/runner.go
+++ b/internal/exec/runner.go
@@ -390,10 +390,10 @@ func (r *runner) Format(args []string, overwrite, diffMode, lintMode, fixFlag bo
 	if _, err := r.compile(false, false, false, meta); err != nil {
 		return err
 	}
-	return r.format(overwrite, diffMode, lintMode, getFormatFixValue(fixFlag, meta), getFormatFileHeaderValue(fixFlag, meta), meta)
+	return r.format(overwrite, diffMode, lintMode, getFormatFixValue(fixFlag, meta), getFormatFileHeaderValue(fixFlag, meta), getFormatJavaPackagePrefixValue(fixFlag, meta), meta)
 }
 
-func (r *runner) format(overwrite, diffMode, lintMode bool, fix int, fileHeader string, meta *meta) error {
+func (r *runner) format(overwrite, diffMode, lintMode bool, fix int, fileHeader string, javaPackagePrefix string, meta *meta) error {
 	success := true
 	for dirPath, protoFiles := range meta.ProtoSet.DirPathToFiles {
 		// skip those files not under the directory
@@ -401,7 +401,7 @@ func (r *runner) format(overwrite, diffMode, lintMode bool, fix int, fileHeader
 			continue
 		}
 		for _, protoFile := range protoFiles {
-			fileSuccess, err := r.formatFile(overwrite, diffMode, lintMode, fix, fileHeader, meta, protoFile)
+			fileSuccess, err := r.formatFile(overwrite, diffMode, lintMode, fix, fileHeader, javaPackagePrefix, meta, protoFile)
 			if err != nil {
 				return err
 			}
@@ -419,7 +419,7 @@ func (r *runner) format(overwrite, diffMode, lintMode bool, fix int, fileHeader
 // return true if there was no unexpected diff and we should exit with 0
 // return false if we should exit with non-zero
 // if false and nil error, we will return an ExitError outside of this function
-func (r *runner) formatFile(overwrite bool, diffMode bool, lintMode bool, fix int, fileHeader string, meta *meta, protoFile *file.ProtoFile) (bool, error) {
+func (r *runner) formatFile(overwrite bool, diffMode bool, lintMode bool, fix int, fileHeader string, javaPackagePrefix string, meta *meta, protoFile *file.ProtoFile) (bool, error) {
 	absSingleFilename, err := file.AbsClean(meta.SingleFilename)
 	if err != nil {
 		return false, err
@@ -432,7 +432,7 @@ func (r *runner) formatFile(overwrite bool, diffMode bool, lintMode bool, fix in
 	if err != nil {
 		return false, err
 	}
-	data, failures, err := r.newTransformer(fix, fileHeader).Transform(protoFile.Path, input)
+	data, failures, err := r.newTransformer(fix, fileHeader, javaPackagePrefix).Transform(protoFile.Path, input)
 	if err != nil {
 		return false, err
 	}
@@ -485,7 +485,7 @@ func (r *runner) All(args []string, disableFormat, disableLint, fixFlag bool) er
 		return err
 	}
 	if !disableFormat {
-		if err := r.format(true, false, false, getFormatFixValue(fixFlag, meta), getFormatFileHeaderValue(fixFlag, meta), meta); err != nil {
+		if err := r.format(true, false, false, getFormatFixValue(fixFlag, meta), getFormatFileHeaderValue(fixFlag, meta), getFormatJavaPackagePrefixValue(fixFlag, meta), meta); err != nil {
 			return err
 		}
 	}
@@ -807,7 +807,7 @@ func (r *runner) newLintRunner() lint.Runner {
 	)
 }
 
-func (r *runner) newTransformer(fix int, fileHeader string) format.Transformer {
+func (r *runner) newTransformer(fix int, fileHeader string, javaPackagePrefix string) format.Transformer {
 	transformerOptions := []format.TransformerOption{format.TransformerWithLogger(r.logger)}
 	if fix != format.FixNone {
 		transformerOptions = append(transformerOptions, format.TransformerWithFix(fix))
@@ -815,6 +815,9 @@ func (r *runner) newTransformer(fix int, fileHeader string) format.Transformer {
 	if fileHeader != "" {
 		transformerOptions = append(transformerOptions, format.TransformerWithFileHeader(fileHeader))
 	}
+	if javaPackagePrefix != "" {
+		transformerOptions = append(transformerOptions, format.TransformerWithJavaPackagePrefix(javaPackagePrefix))
+	}
 	return format.NewTransformer(transformerOptions...)
 }
 
@@ -1037,6 +1040,13 @@ func getFormatFileHeaderValue(fixFlag bool, meta *meta) string {
 	return meta.ProtoSet.Config.Lint.FileHeader
 }
 
+func getFormatJavaPackagePrefixValue(fixFlag bool, meta *meta) string {
+	if !fixFlag {
+		return ""
+	}
+	return meta.ProtoSet.Config.Lint.JavaPackagePrefix
+}
+
 func extractSortPackageNames(m map[string]*extract.Package) []string {
 	s := make([]string, 0, len(m))
 	for key := range m {
diff --git a/internal/file/file.go b/internal/file/file.go
index ac42dbc..84244e1 100644
--- a/internal/file/file.go
+++ b/internal/file/file.go
@@ -32,6 +32,8 @@ import (
 // DefaultWalkTimeout is the default walk timeout.
 const DefaultWalkTimeout time.Duration = 3 * time.Second
 
+var rootDirPath = filepath.Dir(string(filepath.Separator))
+
 // ProtoSet represents a set of .proto files and an associated config.
 //
 // ProtoSets will be validated if returned from this package.
@@ -48,15 +50,14 @@ type ProtoSet struct {
 	// All paths must be absolute.
 	// All paths must reside within DirPath.
 	// Must be cleaned.
-	// The directory paths will always reside within DirPath,
-	// that is filepath.Rel(ProtoSetDirPath, DirPath) will never return
+	// The directory paths will always reside within the config DirPath,
+	// that is filepath.Rel(Config.DirPath, DirPath) will never return
 	// error and always return a non-empty string. Note the string could be ".".
 	// The ProtoFiles will always be in the directory specified by the key.
 	DirPathToFiles map[string][]*ProtoFile
 	// The associated Config.
 	// Must be valid.
-	// The DirPath on the config may differ from the DirPath
-	// on the ProtoSet.
+	// The DirPath on the config may differ from the DirPath on the ProtoSet.
 	Config settings.Config
 }
 
@@ -151,3 +152,19 @@ func CheckAbs(path string) error {
 	}
 	return nil
 }
+
+// IsExcluded determines whether the given filePath should be excluded.
+//
+// absConfigDirPath represents the absolute path to the configuration file.
+// This is used to determine when we should stop checking for excludes.
+func IsExcluded(absFilePath string, absConfigDirPath string, absExcludePaths ...string) bool {
+	for _, absExcludePath := range absExcludePaths {
+		for curFilePath := absFilePath; curFilePath != absConfigDirPath && curFilePath != rootDirPath; curFilePath = filepath.Dir(curFilePath) {
+			if curFilePath == absExcludePath {
+				return true
+			}
+		}
+	}
+	return false
+
+}
diff --git a/internal/file/proto_set_provider.go b/internal/file/proto_set_provider.go
index 8206881..a2e6eb9 100644
--- a/internal/file/proto_set_provider.go
+++ b/internal/file/proto_set_provider.go
@@ -25,6 +25,7 @@ import (
 	"os"
 	"path/filepath"
 	"sort"
+	"strings"
 	"time"
 
 	"github.com/uber/prototool/internal/settings"
@@ -211,22 +212,18 @@ func (c *protoSetProvider) getConfig(absDirPath string) (settings.Config, error)
 // absWorkDirPath is only used to determine the ProtoFile.DisplayPath, also known as
 // the relative path from where prototool was invoked.
 func (c *protoSetProvider) walkAndGetAllProtoFiles(absWorkDirPath string, absDirPath string) ([]*ProtoFile, error) {
-	var (
-		protoFiles     []*ProtoFile
-		numWalkedFiles int
-		timedOut       bool
-	)
-	allExcludes := make(map[string]struct{})
+	var protoFiles []*ProtoFile
+	var numWalkedFiles int
+	var timedOut bool
+	var excludes []string
+	var err error
 	// if we have a configData, we compute the exclude prefixes once
 	// from this dirPath and data, and do not do it again in the below walk function
 	if c.configData != "" {
-		excludes, err := c.configProvider.GetExcludePrefixesForData(absWorkDirPath, c.configData)
+		excludes, err = c.configProvider.GetExcludePrefixesForData(absWorkDirPath, c.configData)
 		if err != nil {
 			return nil, err
 		}
-		for _, exclude := range excludes {
-			allExcludes[exclude] = struct{}{}
-		}
 	}
 	walkErrC := make(chan error)
 	go func() {
@@ -247,15 +244,13 @@ func (c *protoSetProvider) walkAndGetAllProtoFiles(absWorkDirPath string, absDir
 					// Add the excluded files with respect to the current file path.
 					// Do not add if we have configData.
 					if c.configData == "" {
-						excludes, err := c.configProvider.GetExcludePrefixesForDir(filePath)
+						iExcludes, err := c.configProvider.GetExcludePrefixesForDir(filePath)
 						if err != nil {
 							return err
 						}
-						for _, exclude := range excludes {
-							allExcludes[exclude] = struct{}{}
-						}
+						excludes = append(excludes, iExcludes...)
 					}
-					if isExcluded(filePath, absDirPath, allExcludes) {
+					if IsExcluded(filePath, absDirPath, excludes...) {
 						return filepath.SkipDir
 					}
 					return nil
@@ -263,7 +258,7 @@ func (c *protoSetProvider) walkAndGetAllProtoFiles(absWorkDirPath string, absDir
 				if filepath.Ext(filePath) != ".proto" {
 					return nil
 				}
-				if isExcluded(filePath, absDirPath, allExcludes) {
+				if IsExcluded(filePath, absDirPath, excludes...) {
 					return nil
 				}
 
@@ -304,16 +299,12 @@ func (c *protoSetProvider) walkAndGetAllProtoFiles(absWorkDirPath string, absDir
 
 func validateProtoSet(protoSet *ProtoSet) error {
 	for dirPath, protoFiles := range protoSet.DirPathToFiles {
-		if err := validateIsRel(protoSet.DirPath, dirPath); err != nil {
-			return err
-		}
 		for _, protoFile := range protoFiles {
 			if filepath.Dir(protoFile.Path) != dirPath {
 				return fmt.Errorf("%s does not reside within %s, this is a system error", protoFile.Path, dirPath)
 			}
-			// This should always be true but just to make sure
-			if err := validateIsRel(protoSet.DirPath, protoFile.Path); err != nil {
-				return err
+			if err := validateIsRel(protoSet.Config.DirPath, protoFile.Path); err != nil {
+				return fmt.Errorf("proto file %s not within config dirPath %s: %v", protoFile.Path, protoSet.Config.DirPath, err)
 			}
 		}
 	}
@@ -328,6 +319,9 @@ func validateIsRel(base string, dir string) error {
 	if rel == "" {
 		return fmt.Errorf("could not make %s relative to %s, this is a system error", base, dir)
 	}
+	if strings.HasPrefix(rel, "..") {
+		return fmt.Errorf("could not make %s within %s, got %s, this is a system error", base, dir, rel)
+	}
 	return nil
 }
 
@@ -339,32 +333,3 @@ func getDirPathToProtoFiles(protoFiles []*ProtoFile) map[string][]*ProtoFile {
 	}
 	return dirPathToProtoFiles
 }
-
-// isExcluded determines whether the given filePath should be excluded.
-// Note that all excludes are assumed to be cleaned absolute paths at
-// this point.
-// stopPath represents the absolute path to the prototool configuration.
-// This is used to determine when we should stop checking for excludes.
-func isExcluded(filePath, stopPath string, excludes map[string]struct{}) bool {
-	// Use the root as a fallback so that we don't loop forever.
-	root := filepath.Dir(string(filepath.Separator))
-
-	isNested := func(curr, exclude string) bool {
-		for {
-			if curr == stopPath || curr == root {
-				return false
-			}
-			if curr == exclude {
-				return true
-			}
-			curr = filepath.Dir(curr)
-		}
-	}
-	for exclude := range excludes {
-		if isNested(filePath, exclude) {
-			return true
-		}
-	}
-	return false
-
-}
diff --git a/internal/format/first_pass_visitor.go b/internal/format/first_pass_visitor.go
index 48a134e..915a4a0 100644
--- a/internal/format/first_pass_visitor.go
+++ b/internal/format/first_pass_visitor.go
@@ -43,6 +43,7 @@ type firstPassVisitor struct {
 	filename                 string
 	fix                      int
 	fileHeader               string
+	javaPackagePrefix        string
 	goPackageOption          *proto.Option
 	javaMultipleFilesOption  *proto.Option
 	javaOuterClassnameOption *proto.Option
@@ -52,8 +53,8 @@ type firstPassVisitor struct {
 	phpNamespaceOption       *proto.Option
 }
 
-func newFirstPassVisitor(filename string, fix int, fileHeader string) *firstPassVisitor {
-	return &firstPassVisitor{baseVisitor: newBaseVisitor(), filename: filename, fix: fix, fileHeader: fileHeader}
+func newFirstPassVisitor(filename string, fix int, fileHeader string, javaPackagePrefix string) *firstPassVisitor {
+	return &firstPassVisitor{baseVisitor: newBaseVisitor(), filename: filename, fix: fix, fileHeader: fileHeader, javaPackagePrefix: javaPackagePrefix}
 }
 
 func (v *firstPassVisitor) Do() []*text.Failure {
@@ -119,7 +120,7 @@ func (v *firstPassVisitor) Do() []*text.Failure {
 			IsString: true,
 		}
 		v.javaPackageOption.Constant = proto.Literal{
-			Source:   protostrs.JavaPackage(v.Package.Name),
+			Source:   protostrs.JavaPackagePrefixOverride(v.Package.Name, v.javaPackagePrefix),
 			IsString: true,
 		}
 		if v.fix == FixV2 {
diff --git a/internal/format/format.go b/internal/format/format.go
index 973ef86..8d95453 100644
--- a/internal/format/format.go
+++ b/internal/format/format.go
@@ -74,6 +74,16 @@ func TransformerWithFileHeader(fileHeader string) TransformerOption {
 	}
 }
 
+// TransformerWithJavaPackagePrefix returns a TransformerOption that will override
+// the Java package prefix.
+//
+// This is only valid if fix is set to a value other than FixNone.
+func TransformerWithJavaPackagePrefix(javaPackagePrefix string) TransformerOption {
+	return func(transformer *transformer) {
+		transformer.javaPackagePrefix = javaPackagePrefix
+	}
+}
+
 // NewTransformer returns a new Transformer.
 func NewTransformer(options ...TransformerOption) Transformer {
 	return newTransformer(options...)
diff --git a/internal/format/transformer.go b/internal/format/transformer.go
index ed0d366..51cfcb4 100644
--- a/internal/format/transformer.go
+++ b/internal/format/transformer.go
@@ -31,9 +31,10 @@ import (
 )
 
 type transformer struct {
-	logger     *zap.Logger
-	fix        int
-	fileHeader string
+	logger            *zap.Logger
+	fix               int
+	fileHeader        string
+	javaPackagePrefix string
 }
 
 func newTransformer(options ...TransformerOption) *transformer {
@@ -57,7 +58,7 @@ func (t *transformer) Transform(filename string, data []byte) ([]byte, []*text.F
 	}
 	descriptor.Filename = filename
 
-	firstPassVisitor := newFirstPassVisitor(filename, t.fix, t.fileHeader)
+	firstPassVisitor := newFirstPassVisitor(filename, t.fix, t.fileHeader, t.javaPackagePrefix)
 	for _, element := range descriptor.Elements {
 		element.Accept(firstPassVisitor)
 	}
diff --git a/internal/lint/BUILD.bazel b/internal/lint/BUILD.bazel
index c212991..c50effe 100644
--- a/internal/lint/BUILD.bazel
+++ b/internal/lint/BUILD.bazel
@@ -33,6 +33,7 @@ go_library(
         "check_message_field_names_filepath.go",
         "check_message_field_names_lower_snake_case.go",
         "check_message_field_names_lowercase.go",
+        "check_message_field_names_no_descriptor.go",
         "check_message_fields_duration.go",
         "check_message_fields_no_json_name.go",
         "check_message_fields_not_floats.go",
diff --git a/internal/lint/check_file_options_equal.go b/internal/lint/check_file_options_equal.go
index c7efe6e..89eb549 100644
--- a/internal/lint/check_file_options_equal.go
+++ b/internal/lint/check_file_options_equal.go
@@ -76,6 +76,14 @@ var fileOptionsEqualJavaPackageComPrefixLinter = NewLinter(
 	}),
 )
 
+var fileOptionsEqualJavaPackagePrefixLinter = NewLinter(
+	"FILE_OPTIONS_EQUAL_JAVA_PACKAGE_PREFIX",
+	`Verifies that the file option "java_package" is equal to PREFIX.PACKAGE, with PREFIX defaulting to "com" and configurable in your configuration file.`,
+	newCheckFileOptionsEqual("java_package", func(descriptor *FileDescriptor, pkg *proto.Package) string {
+		return protostrs.JavaPackagePrefixOverride(pkg.Name, descriptor.ProtoSet.Config.Lint.JavaPackagePrefix)
+	}),
+)
+
 var fileOptionsEqualOBJCClassPrefixAbbrLinter = NewLinter(
 	"FILE_OPTIONS_EQUAL_OBJC_CLASS_PREFIX_ABBR",
 	`Verifies that the file option "objc_class_prefix" is the abbreviated version of the package.`,
diff --git a/internal/lint/check_message_field_names_no_descriptor.go b/internal/lint/check_message_field_names_no_descriptor.go
new file mode 100644
index 0000000..6f4939d
--- /dev/null
+++ b/internal/lint/check_message_field_names_no_descriptor.go
@@ -0,0 +1,74 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package lint
+
+import (
+	"strings"
+
+	"github.com/emicklei/proto"
+	"github.com/uber/prototool/internal/text"
+)
+
+var messageFieldNamesNoDescriptorLinter = NewLinter(
+	"MESSAGE_FIELD_NAMES_NO_DESCRIPTOR",
+	`Verifies that all message field names are not "descriptor", which results in a collision in Java-generated code.`,
+	checkMessageFieldNamesNoDescriptor,
+)
+
+func checkMessageFieldNamesNoDescriptor(add func(*text.Failure), dirPath string, descriptors []*FileDescriptor) error {
+	return runVisitor(messageFieldNamesNoDescriptorVisitor{baseAddVisitor: newBaseAddVisitor(add)}, descriptors)
+}
+
+type messageFieldNamesNoDescriptorVisitor struct {
+	baseAddVisitor
+}
+
+func (v messageFieldNamesNoDescriptorVisitor) VisitMessage(message *proto.Message) {
+	for _, element := range message.Elements {
+		element.Accept(v)
+	}
+}
+
+func (v messageFieldNamesNoDescriptorVisitor) VisitOneof(oneof *proto.Oneof) {
+	for _, element := range oneof.Elements {
+		element.Accept(v)
+	}
+}
+
+func (v messageFieldNamesNoDescriptorVisitor) VisitNormalField(field *proto.NormalField) {
+	v.handleField(field.Field)
+}
+
+func (v messageFieldNamesNoDescriptorVisitor) VisitOneofField(field *proto.OneOfField) {
+	v.handleField(field.Field)
+}
+
+func (v messageFieldNamesNoDescriptorVisitor) VisitMapField(field *proto.MapField) {
+	v.handleField(field.Field)
+}
+
+func (v messageFieldNamesNoDescriptorVisitor) handleField(field *proto.Field) {
+	// Descriptor, _descriptor, descriptor_, __descriptor, descriptor__ are also invalid
+	// https://github.com/protocolbuffers/protobuf/issues/5742
+	if strings.Trim(strings.ToLower(field.Name), "_") == "descriptor" {
+		v.AddFailuref(field.Position, `Field name %q causes a collision in Java-generated code.`, field.Name)
+	}
+}
diff --git a/internal/lint/lint.go b/internal/lint/lint.go
index d400efd..bff0839 100644
--- a/internal/lint/lint.go
+++ b/internal/lint/lint.go
@@ -61,6 +61,7 @@ var (
 		fileOptionsEqualJavaMultipleFilesTrueLinter,
 		fileOptionsEqualJavaOuterClassnameProtoSuffixLinter,
 		fileOptionsEqualJavaPackageComPrefixLinter,
+		fileOptionsEqualJavaPackagePrefixLinter,
 		fileOptionsEqualOBJCClassPrefixAbbrLinter,
 		fileOptionsEqualPHPNamespaceCapitalizedLinter,
 		fileOptionsGoPackageNotLongFormLinter,
@@ -89,6 +90,7 @@ var (
 		messageFieldNamesFilepathLinter,
 		messageFieldNamesLowerSnakeCaseLinter,
 		messageFieldNamesLowercaseLinter,
+		messageFieldNamesNoDescriptorLinter,
 		messageNamesCamelCaseLinter,
 		messageNamesCapitalizedLinter,
 		messagesHaveCommentsLinter,
@@ -204,7 +206,7 @@ var (
 		fileOptionsEqualGoPackageV2SuffixLinter,
 		fileOptionsEqualJavaMultipleFilesTrueLinter,
 		fileOptionsEqualJavaOuterClassnameProtoSuffixLinter,
-		fileOptionsEqualJavaPackageComPrefixLinter,
+		fileOptionsEqualJavaPackagePrefixLinter,
 		fileOptionsEqualOBJCClassPrefixAbbrLinter,
 		fileOptionsEqualPHPNamespaceCapitalizedLinter,
 		fileOptionsGoPackageNotLongFormLinter,
@@ -228,6 +230,7 @@ var (
 		messageFieldNamesFilenameLinter,
 		messageFieldNamesFilepathLinter,
 		messageFieldNamesLowerSnakeCaseLinter,
+		messageFieldNamesNoDescriptorLinter,
 		messageFieldsDurationLinter,
 		messageFieldsNoJSONNameLinter,
 		messageFieldsNotFloatsLinter,
@@ -509,12 +512,7 @@ func shouldIgnore(linter Linter, descriptor *FileDescriptor, ignoreIDToFilePaths
 	if !ok {
 		return false, nil
 	}
-	for _, ignoreFilePath := range ignoreFilePaths {
-		if filePath == ignoreFilePath {
-			return true, nil
-		}
-	}
-	return false, nil
+	return file.IsExcluded(filePath, descriptor.ProtoSet.Config.DirPath, ignoreFilePaths...), nil
 }
 
 func checkLintID(lintID string) error {
diff --git a/internal/protostrs/protostrs.go b/internal/protostrs/protostrs.go
index a63ff7f..b43641c 100644
--- a/internal/protostrs/protostrs.go
+++ b/internal/protostrs/protostrs.go
@@ -107,6 +107,20 @@ func JavaPackage(packageName string) string {
 	return "com." + packageName
 }
 
+// JavaPackagePrefixOverride returns the value for the file option "java_package" given
+// a package name. This will be equal to "com." followed by the package.
+// If prefixOverride is set, this will be equal to prefixOveride.package.
+// If packageName is empty, this will return an empty string.
+func JavaPackagePrefixOverride(packageName string, prefixOverride string) string {
+	if packageName == "" {
+		return ""
+	}
+	if prefixOverride != "" {
+		return prefixOverride + "." + packageName
+	}
+	return "com." + packageName
+}
+
 // OBJCClassPrefix returns the value for the file option "objc_class_prefix"
 // given a package name. It takes the first letter of each package part
 // and capitalizes it, then concatenates these. If the length is 2, an "X"
diff --git a/internal/reflect/reflect.go b/internal/reflect/reflect.go
index e2ef861..49bff9a 100644
--- a/internal/reflect/reflect.go
+++ b/internal/reflect/reflect.go
@@ -148,7 +148,7 @@ func populateDependencies(
 	}
 	for _, pkg := range packageNameToPackage {
 		if len(pkg.DependencyNames) != 0 {
-			pkg.DependencyNames = strs.DedupeSort(pkg.DependencyNames, nil)
+			pkg.DependencyNames = strs.SortUniq(pkg.DependencyNames)
 		}
 	}
 	return nil
diff --git a/internal/settings/config_provider.go b/internal/settings/config_provider.go
index 00ff93b..9790952 100644
--- a/internal/settings/config_provider.go
+++ b/internal/settings/config_provider.go
@@ -203,7 +203,7 @@ func externalConfigToConfig(develMode bool, e ExternalConfig, dirPath string) (C
 		return Config{}, err
 	}
 	includePaths := make([]string, 0, len(e.Protoc.Includes))
-	for _, includePath := range strs.DedupeSort(e.Protoc.Includes, nil) {
+	for _, includePath := range strs.SortUniq(e.Protoc.Includes) {
 		if !filepath.IsAbs(includePath) {
 			includePath = filepath.Join(dirPath, includePath)
 		}
@@ -225,8 +225,8 @@ func externalConfigToConfig(develMode bool, e ExternalConfig, dirPath string) (C
 		}
 	}
 
-	genPlugins := make([]GenPlugin, len(e.Gen.Plugins))
-	for i, plugin := range e.Gen.Plugins {
+	genPlugins := make([]GenPlugin, len(e.Generate.Plugins))
+	for i, plugin := range e.Generate.Plugins {
 		genPluginType, err := ParseGenPluginType(plugin.Type)
 		if err != nil {
 			return Config{}, err
@@ -345,18 +345,19 @@ func externalConfigToConfig(develMode bool, e ExternalConfig, dirPath string) (C
 			DirPathToBasePackage: createDirPathToBasePackage,
 		},
 		Lint: LintConfig{
-			IncludeIDs:          strs.DedupeSort(e.Lint.Rules.Add, strings.ToUpper),
-			ExcludeIDs:          strs.DedupeSort(e.Lint.Rules.Remove, strings.ToUpper),
+			IncludeIDs:          strs.SortUniqModify(e.Lint.Rules.Add, strings.ToUpper),
+			ExcludeIDs:          strs.SortUniqModify(e.Lint.Rules.Remove, strings.ToUpper),
 			Group:               strings.ToLower(e.Lint.Group),
 			NoDefault:           e.Lint.Rules.NoDefault,
 			IgnoreIDToFilePaths: ignoreIDToFilePaths,
 			FileHeader:          fileHeader,
+			JavaPackagePrefix:   e.Lint.JavaPackagePrefix,
 			AllowSuppression:    e.Lint.AllowSuppression,
 		},
 		Gen: GenConfig{
 			GoPluginOptions: GenGoPluginOptions{
-				ImportPath:     e.Gen.GoOptions.ImportPath,
-				ExtraModifiers: e.Gen.GoOptions.ExtraModifiers,
+				ImportPath:     e.Generate.GoOptions.ImportPath,
+				ExtraModifiers: e.Generate.GoOptions.ExtraModifiers,
 			},
 			Plugins: genPlugins,
 		},
@@ -403,7 +404,7 @@ func getExcludePrefixesForDir(dirPath string) ([]string, error) {
 
 func getExcludePrefixes(excludes []string, dirPath string) ([]string, error) {
 	excludePrefixes := make([]string, 0, len(excludes))
-	for _, excludePrefix := range strs.DedupeSort(excludes, nil) {
+	for _, excludePrefix := range strs.SortUniq(excludes) {
 		if !filepath.IsAbs(excludePrefix) {
 			excludePrefix = filepath.Join(dirPath, excludePrefix)
 		}
diff --git a/internal/settings/settings.go b/internal/settings/settings.go
index e128fd6..5623cba 100644
--- a/internal/settings/settings.go
+++ b/internal/settings/settings.go
@@ -125,7 +125,8 @@ func ParseGenPluginType(s string) (GenPluginType, error) {
 // all other internal packages should verify that all given paths are
 // absolute, except for the internal/text package.
 type Config struct {
-	// The working directory path.
+	// The directory path of the config file, or the working directory path.
+	// if no config file exists.
 	// Expected to be absolute path.
 	DirPath string
 	// The prefixes to exclude.
@@ -198,6 +199,11 @@ type LintConfig struct {
 	// header before the syntax declaration. Note that format --fix will delete
 	// anything before the syntax declaration if this is set.
 	FileHeader string
+	// JavaPackagePrefix is the prefix for java packages. This only has an
+	// effect if the linter FILE_OPTIONS_EQUAL_JAVA_PACKAGE_PREFIX is turned on.
+	// This also affects create and format --fix.
+	// The default behavior is to use "com".
+	JavaPackagePrefix string
 	// AllowSuppression says to honor @suppresswarnings annotations.
 	AllowSuppression bool
 }
@@ -291,21 +297,22 @@ type ExternalConfig struct {
 		Ignores []struct {
 			ID    string   `json:"id,omitempty" yaml:"id,omitempty"`
 			Files []string `json:"files,omitempty" yaml:"files,omitempty"`
-		}
+		} `json:"ignores,omitempty" yaml:"ignores,omitempty"`
 		Rules struct {
 			NoDefault bool     `json:"no_default,omitempty" yaml:"no_default,omitempty"`
 			Add       []string `json:"add" yaml:"add"`
 			Remove    []string `json:"remove" yaml:"remove"`
-		}
+		} `json:"rules,omitempty" yaml:"rules,omitempty"`
 		FileHeader struct {
 			Path        string `json:"path,omitempty" yaml:"path,omitempty"`
 			Content     string `json:"content,omitempty" yaml:"content,omitempty"`
 			IsCommented bool   `json:"is_commented,omitempty" yaml:"is_commented,omitempty"`
 		} `json:"file_header,omitempty" yaml:"file_header,omitempty"`
+		JavaPackagePrefix string `json:"java_package_prefix,omitempty" yaml:"java_package_prefix,omitempty"`
 		// devel-mode only
 		AllowSuppression bool `json:"allow_suppression,omitempty" yaml:"allow_suppression,omitempty"`
 	} `json:"lint,omitempty" yaml:"lint,omitempty"`
-	Gen struct {
+	Generate struct {
 		GoOptions struct {
 			ImportPath     string            `json:"import_path,omitempty" yaml:"import_path,omitempty"`
 			ExtraModifiers map[string]string `json:"extra_modifiers,omitempty" yaml:"extra_modifiers,omitempty"`
diff --git a/internal/strs/strs.go b/internal/strs/strs.go
index a2a4c5c..65d0737 100644
--- a/internal/strs/strs.go
+++ b/internal/strs/strs.go
@@ -167,27 +167,34 @@ func SplitSnakeCaseWord(s string) []string {
 	return words
 }
 
-// DedupeSort returns s with no duplicates and no empty strings, sorted.
+// SortUniq returns the unique sorted non-empty values of s.
+func SortUniq(s []string) []string {
+	return SortUniqModify(s, nil)
+}
+
+// SortUniqModify returns the unique sorted non-empty values of s.
 // If modifier is not nil, modifier will be applied to each element in s.
-func DedupeSort(s []string, modifier func(string) string) []string {
-	m := make(map[string]struct{}, len(s))
-	o := make([]string, 0, len(m))
+func SortUniqModify(s []string, modifier func(string) string) []string {
+	m := make(map[string]struct{})
 	for _, e := range s {
-		if e == "" {
-			continue
-		}
-		key := e
 		if modifier != nil {
-			key = modifier(e)
+			e = modifier(e)
 		}
-		if _, ok := m[key]; ok {
-			continue
+		if e != "" {
+			m[e] = struct{}{}
 		}
-		m[key] = struct{}{}
-		o = append(o, key)
 	}
-	sort.Strings(o)
-	return o
+	return MapToSortedSlice(m)
+}
+
+// MapToSortedSlice returns the sorted keys of m.
+func MapToSortedSlice(m map[string]struct{}) []string {
+	s := make([]string, 0, len(m))
+	for e := range m {
+		s = append(s, e)
+	}
+	sort.Strings(s)
+	return s
 }
 
 // Intersection return the intersection between one and


Test patch:
diff --git a/internal/cmd/cmd_test.go b/internal/cmd/cmd_test.go
index 9968146..4033580 100644
--- a/internal/cmd/cmd_test.go
+++ b/internal/cmd/cmd_test.go
@@ -198,6 +198,12 @@ func TestLint(t *testing.T) {
 		8:1:FILE_OPTIONS_EQUAL_JAVA_PACKAGE_COM_PREFIX`,
 		"testdata/lint/fileoptions/file_options_incorrect.proto",
 	)
+	assertDoLintFile(
+		t,
+		false,
+		`9:1:FILE_OPTIONS_EQUAL_JAVA_PACKAGE_PREFIX`,
+		"testdata/lint/fileoptionsjava/file_options_incorrect.proto",
+	)
 	assertDoLintFiles(
 		t,
 		false,
@@ -492,6 +498,18 @@ func TestLint(t *testing.T) {
 		"testdata/lint/notempty/foo/v1/hello_api.proto",
 	)
 
+	assertDoLintFile(
+		t,
+		false,
+		`14:3:MESSAGE_FIELD_NAMES_NO_DESCRIPTOR
+		18:3:MESSAGE_FIELD_NAMES_NO_DESCRIPTOR
+		22:3:MESSAGE_FIELD_NAMES_NO_DESCRIPTOR
+		26:3:MESSAGE_FIELD_NAMES_NO_DESCRIPTOR
+		30:3:MESSAGE_FIELD_NAMES_NO_DESCRIPTOR
+		34:3:MESSAGE_FIELD_NAMES_NO_DESCRIPTOR`,
+		"testdata/lint/nodescriptor/foo/v1/foo.proto",
+	)
+
 	assertDoLintFile(
 		t,
 		false,
@@ -578,6 +596,19 @@ func TestLint(t *testing.T) {
 		"testdata/lint/floatsnosuppress/foo/v1/foo.proto",
 	)
 
+	assertDoLintFile(
+		t,
+		false,
+		`14:3:MESSAGE_FIELDS_NOT_FLOATS`,
+		"testdata/lint/ignoredir/foo/v1/foo.proto",
+	)
+	assertDoLintFile(
+		t,
+		true,
+		``,
+		"testdata/lint/ignoredir/bar/v1/bar.proto",
+	)
+
 	assertDoLintFile(
 		t,
 		false,
@@ -1058,6 +1089,25 @@ option java_multiple_files = true;
 option java_outer_classname = "BazProto";
 option java_package = "com.foo.v1";
 option objc_class_prefix = "FXX";
+option php_namespace = "Foo\\V1";`,
+	)
+	// in dir with prototool.yaml with override with java_package_prefix
+	assertDoCreateFile(
+		t,
+		true,
+		true,
+		"testdata/create/version2five/baz.proto",
+		"",
+		`syntax = "proto3";
+
+package foo.v1;
+
+option csharp_namespace = "Foo.V1";
+option go_package = "foov1";
+option java_multiple_files = true;
+option java_outer_classname = "BazProto";
+option java_package = "au.com.foo.v1";
+option objc_class_prefix = "FXX";
 option php_namespace = "Foo\\V1";`,
 	)
 }
diff --git a/internal/cmd/testdata/create/version2five/prototool.yaml b/internal/cmd/testdata/create/version2five/prototool.yaml
new file mode 100644
index 0000000..10f2d8a
--- /dev/null
+++ b/internal/cmd/testdata/create/version2five/prototool.yaml
@@ -0,0 +1,7 @@
+lint:
+  group: uber2
+  java_package_prefix: au.com
+create:
+  packages:
+    - directory: .
+      name: foo.v1
diff --git a/internal/cmd/testdata/format-fix-v2/foo.proto.golden b/internal/cmd/testdata/format-fix-v2/foo.proto.golden
index e609541..43da883 100644
--- a/internal/cmd/testdata/format-fix-v2/foo.proto.golden
+++ b/internal/cmd/testdata/format-fix-v2/foo.proto.golden
@@ -13,7 +13,7 @@ option go_package = "barv1"; // inline comment4
 option java_multiple_files = true; //inline comment11
 // comment5
 option java_outer_classname = "FooProto"; // inline comment5
-option java_package = "com.foo.bar.v1";
+option java_package = "au.com.foo.bar.v1";
 option objc_class_prefix = "FBX";
 option php_namespace = "Foo\\Bar\\V1";
 
diff --git a/internal/cmd/testdata/format-fix-v2/prototool.yaml b/internal/cmd/testdata/format-fix-v2/prototool.yaml
index 092dbf5..cade831 100644
--- a/internal/cmd/testdata/format-fix-v2/prototool.yaml
+++ b/internal/cmd/testdata/format-fix-v2/prototool.yaml
@@ -1,2 +1,3 @@
 lint:
   group: uber2
+  java_package_prefix: au.com
diff --git a/internal/cmd/testdata/lint/fileoptionsjava/file_options_incorrect.proto b/internal/cmd/testdata/lint/fileoptionsjava/file_options_incorrect.proto
new file mode 100644
index 0000000..b0f9762
--- /dev/null
+++ b/internal/cmd/testdata/lint/fileoptionsjava/file_options_incorrect.proto
@@ -0,0 +1,11 @@
+syntax = "proto3";
+
+package foo;
+
+option csharp_namespace = "Foo";
+option go_package = "foopb";
+option java_multiple_files = true;
+option java_outer_classname = "FileOptionsIncorrectProto";
+option java_package = "com.foo";
+option objc_class_prefix = "FXX";
+option php_namespace = "Foo";
diff --git a/internal/cmd/testdata/lint/fileoptionsjava/prototool.yaml b/internal/cmd/testdata/lint/fileoptionsjava/prototool.yaml
new file mode 100644
index 0000000..61ecf2a
--- /dev/null
+++ b/internal/cmd/testdata/lint/fileoptionsjava/prototool.yaml
@@ -0,0 +1,6 @@
+lint:
+  group: uber2
+  java_package_prefix: au.com
+  rules:
+    remove:
+      - PACKAGE_MAJOR_BETA_VERSIONED
diff --git a/internal/cmd/testdata/lint/ignoredir/bar/v1/bar.proto b/internal/cmd/testdata/lint/ignoredir/bar/v1/bar.proto
new file mode 100644
index 0000000..c4dc80b
--- /dev/null
+++ b/internal/cmd/testdata/lint/ignoredir/bar/v1/bar.proto
@@ -0,0 +1,15 @@
+syntax = "proto3";
+
+package bar.v1;
+
+option csharp_namespace = "Bar.V1";
+option go_package = "barv1";
+option java_multiple_files = true;
+option java_outer_classname = "BarProto";
+option java_package = "com.bar.v1";
+option objc_class_prefix = "BXX";
+option php_namespace = "Bar\\V1";
+
+message Bar {
+  float one = 1;
+}
diff --git a/internal/cmd/testdata/lint/ignoredir/foo/v1/foo.proto b/internal/cmd/testdata/lint/ignoredir/foo/v1/foo.proto
new file mode 100644
index 0000000..ee89be0
--- /dev/null
+++ b/internal/cmd/testdata/lint/ignoredir/foo/v1/foo.proto
@@ -0,0 +1,15 @@
+syntax = "proto3";
+
+package foo.v1;
+
+option csharp_namespace = "Foo.V1";
+option go_package = "foov1";
+option java_multiple_files = true;
+option java_outer_classname = "FooProto";
+option java_package = "com.foo.v1";
+option objc_class_prefix = "FXX";
+option php_namespace = "Foo\\V1";
+
+message One {
+  float one = 1;
+}
diff --git a/internal/cmd/testdata/lint/ignoredir/prototool.yaml b/internal/cmd/testdata/lint/ignoredir/prototool.yaml
new file mode 100644
index 0000000..397b51d
--- /dev/null
+++ b/internal/cmd/testdata/lint/ignoredir/prototool.yaml
@@ -0,0 +1,14 @@
+lint:
+  group: uber2
+  rules:
+    remove:
+      - ENUMS_HAVE_SENTENCE_COMMENTS
+      - MESSAGES_HAVE_SENTENCE_COMMENTS_EXCEPT_REQUEST_RESPONSE_TYPES
+      - RPCS_HAVE_SENTENCE_COMMENTS
+      - SERVICES_HAVE_SENTENCE_COMMENTS
+  ignores:
+    - id: MESSAGE_FIELDS_NOT_FLOATS
+      files:
+        - bar/v1
+        - foo/v11
+
diff --git a/internal/cmd/testdata/lint/nodescriptor/foo/v1/foo.proto b/internal/cmd/testdata/lint/nodescriptor/foo/v1/foo.proto
new file mode 100644
index 0000000..59e7f20
--- /dev/null
+++ b/internal/cmd/testdata/lint/nodescriptor/foo/v1/foo.proto
@@ -0,0 +1,35 @@
+syntax = "proto3";
+
+package foo.v1;
+
+option csharp_namespace = "Foo.V1";
+option go_package = "foov1";
+option java_multiple_files = true;
+option java_outer_classname = "FooProto";
+option java_package = "com.foo.v1";
+option objc_class_prefix = "FXX";
+option php_namespace = "Foo\\V1";
+
+message One {
+  int64 descriptor = 1;
+}
+
+message Two {
+  int64 Descriptor = 1;
+}
+
+message Three {
+  int64 _descriptor = 1;
+}
+
+message Four {
+  int64 descriptor_ = 1;
+}
+
+message Five {
+  int64 __descriptor = 1;
+}
+
+message Six {
+  int64 descriptor__ = 1;
+}
diff --git a/internal/cmd/testdata/lint/nodescriptor/prototool.yaml b/internal/cmd/testdata/lint/nodescriptor/prototool.yaml
new file mode 100644
index 0000000..d86492d
--- /dev/null
+++ b/internal/cmd/testdata/lint/nodescriptor/prototool.yaml
@@ -0,0 +1,6 @@
+lint:
+  group: uber2
+  rules:
+    remove:
+      - MESSAGES_HAVE_SENTENCE_COMMENTS_EXCEPT_REQUEST_RESPONSE_TYPES
+      - MESSAGE_FIELD_NAMES_LOWER_SNAKE_CASE
diff --git a/internal/file/file_test.go b/internal/file/file_test.go
index 82bc6bf..d82aa6c 100644
--- a/internal/file/file_test.go
+++ b/internal/file/file_test.go
@@ -32,55 +32,38 @@ import (
 func TestAbsClean(t *testing.T) {
 	wd, err := os.Getwd()
 	require.NoError(t, err)
+	testAbsClean(t, "", "")
+	testAbsClean(t, "/", "/")
+	testAbsClean(t, "/path/to/foo", "/path/to/foo")
+	testAbsClean(t, "/path/to/foo", "/path//to//foo")
+	testAbsClean(t, filepath.Join(wd, "path/to/foo"), "path/to/foo")
+	testAbsClean(t, filepath.Join(wd, "path/to/foo"), "path//to//foo")
+}
 
-	tests := []struct {
-		desc string
-		give string
-		want string
-	}{
-		{
-			desc: "Empty path",
-		},
-		{
-			desc: "Root path",
-			give: "/",
-			want: "/",
-		},
-		{
-			desc: "Clean absolute path",
-			give: "/path/to/foo",
-			want: "/path/to/foo",
-		},
-		{
-			desc: "Messy absolute path",
-			give: "/path//to///foo",
-			want: "/path/to/foo",
-		},
-		{
-			desc: "Clean relative path",
-			give: "path/to/foo",
-			want: filepath.Join(wd, "path/to/foo"),
-		},
-		{
-			desc: "Messy relative path",
-			give: "path//to///foo",
-			want: filepath.Join(wd, "path/to/foo"),
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.desc, func(t *testing.T) {
-			res, err := AbsClean(tt.give)
-			assert.NoError(t, err)
-			assert.Equal(t, tt.want, res)
-		})
-	}
+func testAbsClean(t *testing.T, expected string, input string) {
+	actual, err := AbsClean(input)
+	assert.NoError(t, err)
+	assert.Equal(t, expected, actual)
 }
 
 func TestCheckAbs(t *testing.T) {
-	t.Run("Absolute path", func(t *testing.T) {
-		assert.NoError(t, CheckAbs("/path/to/foo"))
-	})
-	t.Run("Relative path", func(t *testing.T) {
-		assert.EqualError(t, CheckAbs("path/to/foo"), "expected absolute path but was path/to/foo")
-	})
+	assert.NoError(t, CheckAbs("/path/to/foo"))
+	assert.Error(t, CheckAbs("path/to/foo"))
+}
+
+func TestIsExcluded(t *testing.T) {
+	wd, err := os.Getwd()
+	require.NoError(t, err)
+	testIsExcluded(t, true, "/foo/bar", "/", "/foo")
+	testIsExcluded(t, true, "/foo", "/", "/foo")
+	testIsExcluded(t, false, filepath.Join(wd, "foo"), wd, filepath.Join(wd, "fooo"))
+	testIsExcluded(t, true, filepath.Join(wd, "foo/bar"), wd, filepath.Join(wd, "foo"))
+	testIsExcluded(t, false, filepath.Join(wd, "foo/bar"), wd, filepath.Join(wd, "bar"))
+	testIsExcluded(t, true, filepath.Join(wd, "foo.proto"), wd, filepath.Join(wd, "foo.proto"))
+	testIsExcluded(t, false, filepath.Join(wd, "bar"), wd, filepath.Join(wd, "bar/foo.proto"))
+	testIsExcluded(t, true, filepath.Join(wd, "bar/baz/foo.proto"), wd, filepath.Join(wd, "bar"))
+}
+
+func testIsExcluded(t *testing.T, expected bool, absFilePath string, absConfigDirPath string, absExcludePaths ...string) {
+	assert.Equal(t, expected, IsExcluded(absFilePath, absConfigDirPath, absExcludePaths...))
 }
diff --git a/internal/file/proto_set_provider_test.go b/internal/file/proto_set_provider_test.go
index 6cb40d3..ca4e9fc 100644
--- a/internal/file/proto_set_provider_test.go
+++ b/internal/file/proto_set_provider_test.go
@@ -442,57 +442,6 @@ func TestProtoSetProviderGetForDirConfigData(t *testing.T) {
 	)
 }
 
-func TestIsExcluded(t *testing.T) {
-	cwd, err := os.Getwd()
-	require.NoError(t, err)
-
-	tests := []struct {
-		desc     string
-		filePath string
-		stopPath string
-		excludes map[string]struct{}
-		excluded bool
-	}{
-		{
-			desc:     "Nothing excluded",
-			filePath: cwd,
-			excluded: false,
-		},
-		{
-			desc:     "String prefix of excluded dir is not excluded",
-			filePath: filepath.Join(cwd, "foo"),
-			stopPath: cwd,
-			excludes: map[string]struct{}{filepath.Join(cwd, "fooo"): struct{}{}},
-			excluded: false,
-		},
-		{
-			desc:     "Not excluded, terminates without stopPath",
-			filePath: filepath.Join(cwd, "foo"),
-			excludes: map[string]struct{}{filepath.Join(cwd, "bar"): struct{}{}},
-			excluded: false,
-		},
-		{
-			desc:     "Directory is exluded",
-			filePath: filepath.Join(cwd, "foo/bar"),
-			stopPath: cwd,
-			excludes: map[string]struct{}{filepath.Join(cwd, "foo"): struct{}{}},
-			excluded: true,
-		},
-		{
-			desc:     "File is exluded",
-			filePath: filepath.Join(cwd, "foo.proto"),
-			stopPath: cwd,
-			excludes: map[string]struct{}{filepath.Join(cwd, "foo.proto"): struct{}{}},
-			excluded: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.desc, func(t *testing.T) {
-			assert.Equal(t, tt.excluded, isExcluded(tt.filePath, tt.stopPath, tt.excludes))
-		})
-	}
-}
-
 func newTestProtoSetProvider(t *testing.T) *protoSetProvider {
 	return newProtoSetProvider(ProtoSetProviderWithLogger(newTestLogger(t)))
 }
diff --git a/internal/protostrs/protostrs_test.go b/internal/protostrs/protostrs_test.go
index 0987f9d..3679f1c 100644
--- a/internal/protostrs/protostrs_test.go
+++ b/internal/protostrs/protostrs_test.go
@@ -83,6 +83,14 @@ func TestJavaPackage(t *testing.T) {
 	assert.Equal(t, "com.foo.bar", JavaPackage("foo.bar"))
 }
 
+func TestJavaPackagePrefixOverride(t *testing.T) {
+	assert.Equal(t, "", JavaPackagePrefixOverride("", ""))
+	assert.Equal(t, "com.foo", JavaPackagePrefixOverride("foo", ""))
+	assert.Equal(t, "com.foo.bar", JavaPackagePrefixOverride("foo.bar", ""))
+	assert.Equal(t, "au.com.foo", JavaPackagePrefixOverride("foo", "au.com"))
+	assert.Equal(t, "au.com.foo.bar", JavaPackagePrefixOverride("foo.bar", "au.com"))
+}
+
 func TestOBJCClassPrefix(t *testing.T) {
 	assert.Equal(t, "", OBJCClassPrefix(""))
 	assert.Equal(t, "FXX", OBJCClassPrefix("foo"))
diff --git a/internal/strs/strs_test.go b/internal/strs/strs_test.go
index 9ffdafa..196deca 100644
--- a/internal/strs/strs_test.go
+++ b/internal/strs/strs_test.go
@@ -167,15 +167,22 @@ func TestToUpperCamelCase(t *testing.T) {
 	assert.Equal(t, "CAMELCase", ToUpperCamelCase("  CAMEL case"))
 }
 
-func TestDedupeSort(t *testing.T) {
-	assert.Equal(t, []string{"a", "b", "c"}, DedupeSort([]string{"b", "A", "c"}, strings.ToLower))
-	assert.Equal(t, []string{"a", "b", "c"}, DedupeSort([]string{"b", "A", "c", "a"}, strings.ToLower))
-	assert.Equal(t, []string{"a", "b", "c"}, DedupeSort([]string{"b", "A", "c", "a", "B"}, strings.ToLower))
-	assert.Equal(t, []string{"a", "b", "c"}, DedupeSort([]string{"b", "A", "c", "a", "B", "b"}, strings.ToLower))
-	assert.Equal(t, []string{"A", "b", "c"}, DedupeSort([]string{"b", "A", "c"}, nil))
-	assert.Equal(t, []string{"A", "a", "b", "c"}, DedupeSort([]string{"b", "A", "c", "a"}, nil))
-	assert.Equal(t, []string{"A", "B", "b", "c"}, DedupeSort([]string{"b", "A", "c", "A", "B"}, nil))
-	assert.Equal(t, []string{"A", "B", "b", "c"}, DedupeSort([]string{"b", "A", "c", "", "A", "B"}, nil))
+func TestSortUniq(t *testing.T) {
+	assert.Equal(t, []string{"A", "b", "c"}, SortUniq([]string{"b", "A", "c"}))
+	assert.Equal(t, []string{"A", "a", "b", "c"}, SortUniq([]string{"b", "A", "c", "a"}))
+	assert.Equal(t, []string{"A", "B", "b", "c"}, SortUniq([]string{"b", "A", "c", "A", "B"}))
+	assert.Equal(t, []string{"A", "B", "b", "c"}, SortUniq([]string{"b", "A", "c", "", "A", "B"}))
+}
+
+func TestSortUniqModify(t *testing.T) {
+	assert.Equal(t, []string{"a", "b", "c"}, SortUniqModify([]string{"b", "A", "c"}, strings.ToLower))
+	assert.Equal(t, []string{"a", "b", "c"}, SortUniqModify([]string{"b", "A", "c", "a"}, strings.ToLower))
+	assert.Equal(t, []string{"a", "b", "c"}, SortUniqModify([]string{"b", "A", "c", "a", "B"}, strings.ToLower))
+	assert.Equal(t, []string{"a", "b", "c"}, SortUniqModify([]string{"b", "A", "c", "a", "B", "b"}, strings.ToLower))
+	assert.Equal(t, []string{"A", "b", "c"}, SortUniqModify([]string{"b", "A", "c"}, nil))
+	assert.Equal(t, []string{"A", "a", "b", "c"}, SortUniqModify([]string{"b", "A", "c", "a"}, nil))
+	assert.Equal(t, []string{"A", "B", "b", "c"}, SortUniqModify([]string{"b", "A", "c", "A", "B"}, nil))
+	assert.Equal(t, []string{"A", "B", "b", "c"}, SortUniqModify([]string{"b", "A", "c", "", "A", "B"}, nil))
 }
 
 func TestIntersection(t *testing.T) {