Repository: uber/prototool
Pull request #255 (2018-12-23 20:14:38)

Handle recursive imports
Fixes https://github.com/uber/prototool/issues/237.

codecov-io: # [Codecov](https://codecov.io/gh/uber/prototool/pull/255?src=pr&el=h1) Report
> Merging [#255](https://codecov.io/gh/uber/prototool/pull/255?src=pr&el=desc) into [dev](https://codecov.io/gh/uber/prototool/commit/d750e56dec5c7976a36b245360e7467803e45f88?src=pr&el=desc) will **increase** coverage by `0.06%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/uber/prototool/pull/255/graphs/tree.svg?width=650&token=A9V0DApqZg&height=150&src=pr)](https://codecov.io/gh/uber/prototool/pull/255?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##              dev     #255      +/-   ##
==========================================
+ Coverage   71.08%   71.15%   +0.06%     
==========================================
  Files          90       90              
  Lines        4509     4513       +4     
==========================================
+ Hits         3205     3211       +6     
+ Misses        950      949       -1     
+ Partials      354      353       -1
```


| [Impacted Files](https://codecov.io/gh/uber/prototool/pull/255?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [internal/protoc/compiler.go](https://codecov.io/gh/uber/prototool/pull/255/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG9jL2NvbXBpbGVyLmdv) | `46.01% <100%> (+1.08%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/uber/prototool/pull/255?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/uber/prototool/pull/255?src=pr&el=footer). Last update [d750e56...294cd08](https://codecov.io/gh/uber/prototool/pull/255?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/Makefile b/Makefile
index 6b1aac0..5ca8698 100644
--- a/Makefile
+++ b/Makefile
@@ -64,7 +64,8 @@ example: install
 	go build ./example/gen/proto/go/foo
 	go build ./example/gen/proto/go/sub
 	go build ./example/cmd/excited/main.go
-	prototool lint etc/style
+	prototool lint etc/style/google
+	prototool lint etc/style/uber
 
 .PHONY: internalgen
 internalgen: install
diff --git a/README.md b/README.md
index 3795790..ba57bca 100644
--- a/README.md
+++ b/README.md
@@ -4,16 +4,16 @@
 
 Prototool lets you:
 
-- Handle installation of protoc and the import of all of the Well-Known Types behind the scenes in a platform-independent manner without any work on the part of the user.
-- Standardize building of your Protobuf files with a common configuration, abstracting away all of the pain of protoc for you.
-- Lint your Protobuf files with common linting rules according to a Style Guide.
-- Format your Protobuf files in a consistent manner.
-- Generate Protobuf files from a template that passes lint, taking care of package naming for you.
-- Generate stubs using any plugin based on a simple configuration file, including handling imports of all the Well-Known Types.
-- Call gRPC endpoints with ease, taking care of the JSON to binary conversion for you.
-- Output errors and lint failures in a common file:line:column:message format, making integration with editors possible, Vim integration is provided out of the box.
+- Handle installation of `protoc` and the import of all of the Well-Known Types behind the scenes in a platform-independent manner without any work on the part of the user.
+- Standardize building of your Protobuf files with a common [configuration](#configuration), abstracting away all of the pain of protoc for you.
+- [Lint](#prototool-lint) your Protobuf files with common linting rules according to [Google' Style Guide](https://developers.google.com/protocol-buffers/docs/style), [Uber's Style Guide](https://github.com/uber/prototool/blob/master/etc/style/uber/uber.proto), or your own set of configured lint rules.
+- [Format](#prototool-format) your Protobuf files in a consistent manner.
+- [Create](#prototool-create) Protobuf files from a template that passes lint, taking care of package naming for you.
+- [Generate](#prototool-generate) stubs using any plugin based on a simple configuration file, including handling imports of all the Well-Known Types.
+- Call [gRPC](#prototool-grpc) endpoints with ease, taking care of the JSON to binary conversion for you.
+- Output errors and lint failures in a common `file:line:column:message` format, making integration with editors possible, [Vim integration](#vim-integration) is provided out of the box.
 
-Prototool accomplishes this by downloading and calling protoc on the fly for you, handing error messages from protoc and your plugins, and using the generated FileDescriptorSets for internal functionality, as well as wrapping a few great external libraries already in the Protobuf ecosystem.
+Prototool accomplishes this by downloading and calling `protoc` on the fly for you, handing error messages from `protoc` and your plugins, and using the generated `FileDescriptorSets` for internal functionality, as well as wrapping a few great external libraries already in the Protobuf ecosystem. Compiling, linting and formatting commands run in around 3/100ths of second for a single Protobuf file, or under a second for a larger number (500+) of Protobuf files.
 
   * [Installation](#installation)
   * [Quick Start](#quick-start)
@@ -67,10 +67,19 @@ curl -sSL https://github.com/uber/prototool/releases/download/v1.3.0/prototool-$
 
 You can also install the `prototool` binary using `go get` if using go1.11+ with module support enabled.
 
-```
+```bash
 go get github.com/uber/prototool/cmd/prototool@dev
 ```
 
+You may want to use [gobin](https://github.com/myitcv/gobin) to install `prototool` outside of a module.
+
+```bash
+# Install to $GOBIN, or $GOPATH/bin if $GOBIN is not set, or $HOME/go/bin if neither are set
+gobin github.com/uber/prototool/cmd/prototool@dev
+# Install to /path/to/bin
+GOBIN=/path/to/bin gobin github.com/uber/prototool/cmd/prototool@dev
+```
+
 ## Quick Start
 
 We'll start with a general overview of the commands. There are more commands, and we will get into usage below, but this shows the basic functionality.
@@ -83,6 +92,7 @@ prototool lint # same as "prototool lint .", by default the current directory is
 prototool create foo.proto # create the file foo.proto from a template that passes lint
 prototool files idl/uber # list the files that will be used after applying exclude_paths from corresponding prototool.yaml or prototool.json files
 prototool lint --list-linters # list all current lint rules being used
+prototool lint --list-all-lint-groups # list all available lint groups, currently "google" and "uber"
 prototool compile idl/uber # make sure all .proto files in idl/uber compile, but do not generate stubs
 prototool generate idl/uber # generate stubs, see the generation directives in the config file example
 prototool grpc idl/uber --address 0.0.0.0:8080 --method foo.ExcitedService/Exclamation --data '{"value":"hello"}' # call the foo.ExcitedService method Exclamation with the given data on 0.0.0.0:8080
@@ -149,7 +159,54 @@ Compile your Protobuf files and generate stubs according to the rules in your `p
 
 ##### `prototool lint`
 
-Lint your Protobuf files. The default rule set follows the Style Guide at [etc/style/uber/uber.proto](etc/style/uber/uber.proto). You can add or exclude lint rules in your `prototool.yaml` or `prototool.json` file. The default rule set is "strict", and we are working on having two main sets of rules.
+Lint your Protobuf files.
+
+Lint rules can be set using the configuration file. See the configuration at [etc/config/example/prototool.yaml](etc/config/example/prototool.yaml) for all available options. There are two pre-configured groups of rules:
+
+- `google`: This lint group follows the Style Guide at https://developers.google.com/protocol-buffers/docs/style. This is a small group of rules meant to enforce basic naming, and is widely followed. The style guide is copied to [etc/style/google/google.proto](etc/style/google/google.proto).
+- `uber`: This lint group follows the Style Guide at [etc/style/uber/uber.proto](etc/style/uber/uber.proto). This is a very strict rule group and is meant to enforce consistent development patterns.
+
+Configuration of your group can be done by setting the `lint.group` option in your `prototool.yaml` file:
+
+```yaml
+lint:
+  group: google
+```
+
+See the `prototool.yaml` files at [etc/style/google/prototool.yaml](etc/style/google/prototool.yaml) and
+[etc/style/uber/prototool.yaml](etc/style/uber/prototool.yaml) for examples.
+
+The `uber` lint group represents the default lint group, and will be used if no lint group is configured.
+
+See [internal/cmd/testdata/lint](internal/cmd/testdata/lint) for additional examples of configurations, and run `prototool lint internal/cmd/testdata/lint/DIR` from a checkout of this repository to see example failures.
+
+Files must be valid Protobuf that can be compiled with `protoc`, so prior to linting, `prototool lint` will compile your using `protoc`.
+Note, however, this is very fast - for the two files in [etc/uber/style](etc/uber/style), compiling and linting only takes approximately
+3/100ths of a second:
+
+```bash
+$ time prototool lint etc/style/uber
+
+real	0m0.037s
+user	0m0.026s
+sys	0m0.017s
+```
+
+For all 694 Protobuf files currently in [googleapis](https://github.com/googleapis/googleapis), this takes approximately 3/4ths of a second:
+
+```bash
+$ cat prototool.yaml
+protoc:
+  allow_unused_imports: true
+lint:
+  group: google
+
+$ time prototool lint .
+
+real	0m0.734s
+user	0m3.835s
+sys	0m0.924s
+```
 
 ##### `prototool format`
 
@@ -162,7 +219,7 @@ Format a Protobuf file and print the formatted file to stdout. There are flags t
 
 Concretely, the `-f` flag can be used so that the values for `java_multiple_files`, `java_outer_classname`, and `java_package` are updated to reflect what is expected by the
 [Google Cloud APIs file structure](https://cloud.google.com/apis/design/file_structure), and the value of `go_package` is updated to reflect what we expect for the
-default Style Guide. By formatting, the linting for these values will pass by default. See the documentation below for `prototool create` for an example.
+Uber Style Guide. By formatting, the linting for these values will pass by default. See the documentation below for `prototool create` for an example.
 
 ##### `prototool create`
 
@@ -507,6 +564,8 @@ the choices made in the formatter. Can we change some things?
 
 *Answer:* Sorry, but we can't - The goal of Prototool is to provide a straightforward Style Guide and consistent formatting that minimizes various issues that arise from Protobuf usage across large organizations. There are pros and cons to many of the choices in the Style Guide, but it's our belief that the best answer is a **single** answer, sometimes regardless of what that single answer is.
 
+We do have multiple lint groups available, see the help section on `prototool lint` above.
+
 It is possible to ignore lint rules via configuration. However, especially if starting from a clean slate, we highly recommend using all default lint rules for consistency.
 
 Many of the lint rules exist to mitigate backwards compatibility problems as schemas evolves. For example: requiring a unique request-response pair per RPC - while this potentially resuls in duplicated messages, this makes it impossible to affect an adjacent RPC by adding or modifying an existing field.
diff --git a/etc/config/example/prototool.yaml b/etc/config/example/prototool.yaml
index 1ceb387..af9a44e 100644
--- a/etc/config/example/prototool.yaml
+++ b/etc/config/example/prototool.yaml
@@ -36,6 +36,13 @@ create:
 
 # Lint directives.
 lint:
+  # The lint group to use.
+  # The default group is the "default" lint group, which is equal to the "uber" lint group.
+  # Run prototool lint --list-all-lint-groups to see all available lint groups.
+  # Run prototool lint --list-lint-group GROUP to list the linters in the given lint group.
+  # Setting this value will result in lint.rules.no_default being ignored.
+  group: uber
+
   # Linter files to ignore.
   ignores:
     - id: RPC_NAMES_CAMEL_CASE
@@ -47,9 +54,12 @@ lint:
         - path/to/foo.proto
 
   # Linter rules.
-  # Run prototool list-all-linters to see all available linters.
+  # Run prototool lint --list-all-linters to see all available linters.
+  # Run prototool lint --list-linters to see the currently configured linters.
   rules:
     # Determines whether or not to include the default set of linters.
+    # This allows all linters to be turned off except those explicitly specified in add.
+    # This value is ignored if lint.group is set.
     no_default: true
 
     # The specific linters to add.
diff --git a/etc/style/google/google.proto b/etc/style/google/google.proto
new file mode 100644
index 0000000..b19870b
--- /dev/null
+++ b/etc/style/google/google.proto
@@ -0,0 +1,32 @@
+// Protobuf Google Style Guide
+//
+// This style guide matches https://developers.google.com/protocol-buffers/docs/style.
+
+syntax = "proto2";
+
+package style.google;
+
+// Use CamelCase (with an initial capital) for message names – for example, SongServerRequest.
+// Use underscore_separated_names for field names – for example, song_name.
+
+message SongServerRequest {
+  required string song_name = 1;
+}
+
+// Use CamelCase (with an initial capital) for enum type names and CAPITALS_WITH_UNDERSCORES for value names.
+
+enum Foo {
+  FIRST_VALUE = 0;
+  SECOND_VALUE = 1;
+}
+
+// If your .proto defines an RPC service, you should use CamelCase (with an initial capital)
+// for both the service name and any RPC method names.
+
+message FooRequest {}
+
+message FooResponse {}
+
+service FooService {
+  rpc GetSomething(FooRequest) returns (FooResponse);
+}
diff --git a/etc/style/google/prototool.yaml b/etc/style/google/prototool.yaml
new file mode 100644
index 0000000..ee4685c
--- /dev/null
+++ b/etc/style/google/prototool.yaml
@@ -0,0 +1,2 @@
+lint:
+  group: google
diff --git a/etc/style/prototool.yaml b/etc/style/prototool.yaml
deleted file mode 100644
index 99f3819..0000000
--- a/etc/style/prototool.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-protoc:
-  allow_unused_imports: true
diff --git a/etc/style/dep/dep.proto b/etc/style/uber/dep/dep.proto
similarity index 72%
rename from etc/style/dep/dep.proto
rename to etc/style/uber/dep/dep.proto
index 4f5aa16..123232f 100644
--- a/etc/style/dep/dep.proto
+++ b/etc/style/uber/dep/dep.proto
@@ -1,11 +1,11 @@
 syntax = "proto3";
 
-package style.dep;
+package style.uber.dep;
 
 option go_package = "deppb";
 option java_multiple_files = true;
 option java_outer_classname = "DepProto";
-option java_package = "com.style.dep";
+option java_package = "com.style.uber.dep";
 
 // Dep is a dep.
 message Dep {
diff --git a/etc/style/uber/prototool.yaml b/etc/style/uber/prototool.yaml
new file mode 100644
index 0000000..22da473
--- /dev/null
+++ b/etc/style/uber/prototool.yaml
@@ -0,0 +1,2 @@
+lint:
+  group: uber
diff --git a/etc/style/uber/uber.proto b/etc/style/uber/uber.proto
index db290a3..bc813bd 100644
--- a/etc/style/uber/uber.proto
+++ b/etc/style/uber/uber.proto
@@ -5,13 +5,6 @@
 // There are places in this style guide that reference line places, i.e. the
 // first line is always the syntax line, however for demonstration purposes,
 // this is violated so we can comment on such style choices here.
-//
-// Topics not covered:
-//
-// - Extensions
-// - Reserved keyword
-// - Most options (message options, field options, etc)
-// - Custom options
 
 // Tab style is two spaces.
 // Comments are always //, not /**/.
@@ -51,7 +44,7 @@ option java_package = "com.style.uber";
 import "dep/dep.proto";
 // Google's well-known types should be directly imported from
 // "google/protobuf" as shown.
-import "google/protobuf/empty.proto";
+import "google/protobuf/timestamp.proto";
 
 // Next come the definitions.
 // There is one newline between the package options and the definitions.
@@ -158,6 +151,8 @@ message Foo {
   // developing.
 
   string bar_id = 5;
+  style.uber.dep.Dep dep = 6;
+  google.protobuf.Timestamp timestamp = 7;
 }
 
 
diff --git a/internal/cfginit/cfginit.go b/internal/cfginit/cfginit.go
index 00ce71f..55f7899 100644
--- a/internal/cfginit/cfginit.go
+++ b/internal/cfginit/cfginit.go
@@ -66,6 +66,13 @@ protoc:
 
 # Lint directives.
 {{.V}}lint:
+  # The lint group to use.
+  # The default group is the "default" lint group, which is equal to the "uber" lint group.
+  # Run prototool lint --list-all-lint-groups to see all available lint groups.
+  # Run prototool lint --list-lint-group GROUP to list the linters in the given lint group.
+  # Setting this value will result in lint.rules.no_default being ignored.
+{{.V}}  group: uber
+
   # Linter files to ignore.
 {{.V}}  ignores:
 {{.V}}    - id: RPC_NAMES_CAMEL_CASE
@@ -77,9 +84,12 @@ protoc:
 {{.V}}        - path/to/foo.proto
 
   # Linter rules.
-  # Run prototool list-all-linters to see all available linters.
+  # Run prototool lint --list-all-linters to see all available linters.
+  # Run prototool lint --list-linters to see the currently configured linters.
 {{.V}}  rules:
     # Determines whether or not to include the default set of linters.
+    # This allows all linters to be turned off except those explicitly specified in add.
+    # This value is ignored if lint.group is set.
 {{.V}}    no_default: true
 
     # The specific linters to add.
diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go
index f3fd166..3dbeefe 100644
--- a/internal/cmd/cmd.go
+++ b/internal/cmd/cmd.go
@@ -123,8 +123,6 @@ func getRootCommand(exitCodeAddr *int, develMode bool, args []string, stdin io.R
 		rootCmd.AddCommand(downloadCmdTemplate.Build(exitCodeAddr, stdin, stdout, stderr, flags))
 		rootCmd.AddCommand(fieldDescriptorProtoCmdTemplate.Build(exitCodeAddr, stdin, stdout, stderr, flags))
 		rootCmd.AddCommand(jsonToBinaryCmdTemplate.Build(exitCodeAddr, stdin, stdout, stderr, flags))
-		rootCmd.AddCommand(listAllLintGroupsCmdTemplate.Build(exitCodeAddr, stdin, stdout, stderr, flags))
-		rootCmd.AddCommand(listLintGroupCmdTemplate.Build(exitCodeAddr, stdin, stdout, stderr, flags))
 		rootCmd.AddCommand(serviceDescriptorProtoCmdTemplate.Build(exitCodeAddr, stdin, stdout, stderr, flags))
 
 		// we may or may not want to expose these to users
diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go
index 0b6afbc..c42c994 100644
--- a/internal/cmd/flags.go
+++ b/internal/cmd/flags.go
@@ -25,33 +25,35 @@ import (
 )
 
 type flags struct {
-	address        string
-	cachePath      string
-	callTimeout    string
-	configData     string
-	connectTimeout string
-	data           string
-	debug          bool
-	diffMode       bool
-	disableFormat  bool
-	disableLint    bool
-	dryRun         bool
-	fix            bool
-	headers        []string
-	keepaliveTime  string
-	json           bool
-	listAllLinters bool
-	listLinters    bool
-	lintMode       bool
-	method         string
-	overwrite      bool
-	pkg            string
-	printFields    string
-	protocBinPath  string
-	protocWKTPath  string
-	protocURL      string
-	stdin          bool
-	uncomment      bool
+	address           string
+	cachePath         string
+	callTimeout       string
+	configData        string
+	connectTimeout    string
+	data              string
+	debug             bool
+	diffMode          bool
+	disableFormat     bool
+	disableLint       bool
+	dryRun            bool
+	fix               bool
+	headers           []string
+	keepaliveTime     string
+	json              bool
+	listAllLinters    bool
+	listLinters       bool
+	listAllLintGroups bool
+	listLintGroup     string
+	lintMode          bool
+	method            string
+	overwrite         bool
+	pkg               string
+	printFields       string
+	protocBinPath     string
+	protocWKTPath     string
+	protocURL         string
+	stdin             bool
+	uncomment         bool
 }
 
 func (f *flags) bindAddress(flagSet *pflag.FlagSet) {
@@ -122,6 +124,14 @@ func (f *flags) bindListLinters(flagSet *pflag.FlagSet) {
 	flagSet.BoolVar(&f.listLinters, "list-linters", false, "List the configured linters instead of running lint.")
 }
 
+func (f *flags) bindListAllLintGroups(flagSet *pflag.FlagSet) {
+	flagSet.BoolVar(&f.listAllLintGroups, "list-all-lint-groups", false, "List all available lint groups instead of running lint.")
+}
+
+func (f *flags) bindListLintGroup(flagSet *pflag.FlagSet) {
+	flagSet.StringVar(&f.listLintGroup, "list-lint-group", "", "List the linters in the given lint group instead of running lint.")
+}
+
 func (f *flags) bindMethod(flagSet *pflag.FlagSet) {
 	flagSet.StringVar(&f.method, "method", "", "The GRPC method to call in the form package.Service/Method. This is required.")
 }
diff --git a/internal/cmd/templates.go b/internal/cmd/templates.go
index f29af2e..ea289c9 100644
--- a/internal/cmd/templates.go
+++ b/internal/cmd/templates.go
@@ -370,40 +370,60 @@ $ cat input.json | prototool grpc example \
 	lintCmdTemplate = &cmdTemplate{
 		Use:   "lint [dirOrFile]",
 		Short: "Lint proto files and compile with protoc to check for failures.",
-		Long:  `The default rule set follows the Style Guide at https://github.com/uber/prototool/blob/master/etc/style/uber/uber.proto. You can add or exclude lint rules in your configuration file. The default rule set is very strict and is meant to enforce consistent development patterns.`,
-		Args:  cobra.MaximumNArgs(1),
+		Long: `Lint rules can be set using the configuration file. See the configuration at https://github.com/uber/prototool/blob/dev/etc/config/example/prototool.yaml for all available options. There are two pre-configured groups of rules:
+
+google: This lint group follows the Style Guide at https://developers.google.com/protocol-buffers/docs/style. This is a small group of rules meant to enforce basic naming, and is widely followed.
+
+uber: This lint group follows the Style Guide at https://github.com/uber/prototool/blob/master/etc/style/uber/uber.proto. This is a very strict rule group and is meant to enforce consistent development patterns.
+
+Configuration of your group can be done by setting the "lint.group" option in your "prototool.yaml" file:
+
+lint:
+  group: google
+
+The "uber" lint group represents the default lint group, and will be used if no lint group is configured.
+
+Files must be valid Protobuf that can be compiled with protoc, so prior to linting, prototool lint will compile your using protoc.
+Note, however, this is very fast - for two files, compiling and linting only takes approximately
+3/100ths of a second:
+
+$ time prototool lint etc/style/uber
+
+real	0m0.037s
+user	0m0.026s
+sys	0m0.017s
+
+For all 694 Protobuf files currently in https://github.com/googleapis/googleapis, this takes approximately 3/4ths of a second:
+
+$ cat prototool.yaml
+protoc:
+  allow_unused_imports: true
+lint:
+  group: google
+
+$ time prototool lint .
+
+real	0m0.734s
+user	0m3.835s
+sys	0m0.924s`,
+
+		Args: cobra.MaximumNArgs(1),
 		Run: func(runner exec.Runner, args []string, flags *flags) error {
-			return runner.Lint(args, flags.listAllLinters, flags.listLinters)
+			return runner.Lint(args, flags.listAllLinters, flags.listLinters, flags.listAllLintGroups, flags.listLintGroup)
 		},
 		BindFlags: func(flagSet *pflag.FlagSet, flags *flags) {
 			flags.bindConfigData(flagSet)
 			flags.bindJSON(flagSet)
 			flags.bindListAllLinters(flagSet)
 			flags.bindListLinters(flagSet)
+			flags.bindListAllLintGroups(flagSet)
+			flags.bindListLintGroup(flagSet)
 			flags.bindProtocURL(flagSet)
 			flags.bindProtocBinPath(flagSet)
 			flags.bindProtocWKTPath(flagSet)
 		},
 	}
 
-	listAllLintGroupsCmdTemplate = &cmdTemplate{
-		Use:   "list-all-lint-groups",
-		Short: "List all the available lint groups.",
-		Args:  cobra.NoArgs,
-		Run: func(runner exec.Runner, args []string, flags *flags) error {
-			return runner.ListAllLintGroups()
-		},
-	}
-
-	listLintGroupCmdTemplate = &cmdTemplate{
-		Use:   "list-lint-group group",
-		Short: "List the linters in the given lint group.",
-		Args:  cobra.ExactArgs(1),
-		Run: func(runner exec.Runner, args []string, flags *flags) error {
-			return runner.ListLintGroup(args[0])
-		},
-	}
-
 	serviceDescriptorProtoCmdTemplate = &cmdTemplate{
 		Use:   "service-descriptor-proto [dirOrFile] servicePath",
 		Short: "Get the service descriptor proto for the service path.",
diff --git a/internal/exec/exec.go b/internal/exec/exec.go
index d2f522c..f920745 100644
--- a/internal/exec/exec.go
+++ b/internal/exec/exec.go
@@ -56,9 +56,7 @@ type Runner interface {
 	DescriptorProto(args []string) error
 	FieldDescriptorProto(args []string) error
 	ServiceDescriptorProto(args []string) error
-	Lint(args []string, listAllLinters bool, listLinters bool) error
-	ListLintGroup(group string) error
-	ListAllLintGroups() error
+	Lint(args []string, listAllLinters bool, listLinters bool, listAllLintGroups bool, listLintGroup string) error
 	Format(args []string, overwrite, diffMode, lintMode, fix bool) error
 	BinaryToJSON(args []string) error
 	JSONToBinary(args []string) error
diff --git a/internal/exec/runner.go b/internal/exec/runner.go
index 899a930..668d1b2 100644
--- a/internal/exec/runner.go
+++ b/internal/exec/runner.go
@@ -333,9 +333,9 @@ func (r *runner) printCommands(doGen bool, protoSet *file.ProtoSet) error {
 	return nil
 }
 
-func (r *runner) Lint(args []string, listAllLinters bool, listLinters bool) error {
-	if listAllLinters && listLinters {
-		return newExitErrorf(255, "can only set one of list-all-linters, list-linters")
+func (r *runner) Lint(args []string, listAllLinters bool, listLinters bool, listAllLintGroups bool, listLintGroup string) error {
+	if moreThanOneSet(listAllLinters, listLinters, listAllLintGroups, listLintGroup != "") {
+		return newExitErrorf(255, "can only set one of list-all-linters, list-linters, list-all-lint-groups, list-lint-group")
 	}
 	if listAllLinters {
 		return r.listAllLinters()
@@ -343,6 +343,12 @@ func (r *runner) Lint(args []string, listAllLinters bool, listLinters bool) erro
 	if listLinters {
 		return r.listLinters()
 	}
+	if listAllLintGroups {
+		return r.listAllLintGroups()
+	}
+	if listLintGroup != "" {
+		return r.listLintGroup(listLintGroup)
+	}
 	meta, err := r.getMeta(args, 1)
 	if err != nil {
 		return err
@@ -385,7 +391,7 @@ func (r *runner) listAllLinters() error {
 	return r.printLinters(lint.AllLinters)
 }
 
-func (r *runner) ListLintGroup(group string) error {
+func (r *runner) listLintGroup(group string) error {
 	linters, ok := lint.GroupToLinters[strings.ToLower(group)]
 	if !ok {
 		return newExitErrorf(255, "unknown lint group: %s", strings.ToLower(group))
@@ -393,7 +399,7 @@ func (r *runner) ListLintGroup(group string) error {
 	return r.printLinters(linters)
 }
 
-func (r *runner) ListAllLintGroups() error {
+func (r *runner) listAllLintGroups() error {
 	groups := make([]string, 0, len(lint.GroupToLinters))
 	for group := range lint.GroupToLinters {
 		groups = append(groups, group)
@@ -408,7 +414,7 @@ func (r *runner) ListAllLintGroups() error {
 }
 
 func (r *runner) Format(args []string, overwrite, diffMode, lintMode, fix bool) error {
-	if (overwrite && diffMode) || (overwrite && lintMode) || (diffMode && lintMode) {
+	if moreThanOneSet(overwrite, diffMode, lintMode) {
 		return newExitErrorf(255, "can only set one of overwrite, diff, lint")
 	}
 	meta, err := r.getMeta(args, 1)
@@ -930,3 +936,13 @@ func newExitErrorf(code int, format string, args ...interface{}) *ExitError {
 func newTabWriter(writer io.Writer) *tabwriter.Writer {
 	return tabwriter.NewWriter(writer, 0, 0, 2, ' ', 0)
 }
+
+func moreThanOneSet(values ...bool) bool {
+	numSet := 0
+	for _, value := range values {
+		if value {
+			numSet++
+		}
+	}
+	return numSet > 1
+}
diff --git a/internal/lint/lint.go b/internal/lint/lint.go
index 09cea02..e56e364 100644
--- a/internal/lint/lint.go
+++ b/internal/lint/lint.go
@@ -87,35 +87,64 @@ var (
 	}
 
 	// DefaultLinters is the slice of default Linters.
-	DefaultLinters = copyLintersWithout(
-		AllLinters,
-		enumFieldNamesUppercaseLinter,
-		enumFieldPrefixesExceptMessageLinter,
-		enumsHaveCommentsLinter,
-		enumZeroValuesInvalidExceptMessageLinter,
-		fileOptionsEqualGoPackageLastTwoSuffixLinter,
-		fileOptionsUnsetJavaMultipleFilesLinter,
-		fileOptionsUnsetJavaOuterClassnameLinter,
-		messageFieldsNotFloatsLinter,
-		messagesHaveCommentsLinter,
-		messagesHaveCommentsExceptRequestResponseTypesLinter,
-		messageFieldNamesLowercaseLinter,
-		packageMajorVersionedLinter,
-		requestResponseNamesMatchRPCLinter,
-		rpcsHaveCommentsLinter,
-		servicesHaveCommentsLinter,
-	)
+	DefaultLinters = UberLinters
 
-	// DefaultGroup is the default group.
-	DefaultGroup = "default"
+	// GoogleLinters is the slice of linters for the google lint group.
+	GoogleLinters = []Linter{
+		enumFieldNamesUpperSnakeCaseLinter,
+		enumNamesCamelCaseLinter,
+		enumNamesCapitalizedLinter,
+		messageFieldNamesLowerSnakeCaseLinter,
+		messageNamesCamelCaseLinter,
+		messageNamesCapitalizedLinter,
+		rpcNamesCamelCaseLinter,
+		rpcNamesCapitalizedLinter,
+		serviceNamesCamelCaseLinter,
+		serviceNamesCapitalizedLinter,
+	}
 
-	// AllGroup is the group of all known linters.
-	AllGroup = "all"
+	// UberLinters is the slice of linters for the uber lint group.
+	UberLinters = []Linter{
+		commentsNoCStyleLinter,
+		enumFieldNamesUpperSnakeCaseLinter,
+		enumFieldPrefixesLinter,
+		enumNamesCamelCaseLinter,
+		enumNamesCapitalizedLinter,
+		enumZeroValuesInvalidLinter,
+		enumsNoAllowAliasLinter,
+		fileOptionsEqualGoPackagePbSuffixLinter,
+		fileOptionsEqualJavaMultipleFilesTrueLinter,
+		fileOptionsEqualJavaOuterClassnameProtoSuffixLinter,
+		fileOptionsEqualJavaPackageComPrefixLinter,
+		fileOptionsGoPackageNotLongFormLinter,
+		fileOptionsGoPackageSameInDirLinter,
+		fileOptionsJavaMultipleFilesSameInDirLinter,
+		fileOptionsJavaPackageSameInDirLinter,
+		fileOptionsRequireGoPackageLinter,
+		fileOptionsRequireJavaMultipleFilesLinter,
+		fileOptionsRequireJavaOuterClassnameLinter,
+		fileOptionsRequireJavaPackageLinter,
+		messageFieldNamesLowerSnakeCaseLinter,
+		messageNamesCamelCaseLinter,
+		messageNamesCapitalizedLinter,
+		oneofNamesLowerSnakeCaseLinter,
+		packageIsDeclaredLinter,
+		packageLowerSnakeCaseLinter,
+		packagesSameInDirLinter,
+		rpcNamesCamelCaseLinter,
+		rpcNamesCapitalizedLinter,
+		requestResponseTypesInSameFileLinter,
+		requestResponseTypesUniqueLinter,
+		serviceNamesCamelCaseLinter,
+		serviceNamesCapitalizedLinter,
+		syntaxProto3Linter,
+		wktDirectlyImportedLinter,
+	}
 
 	// GroupToLinters is the map from linter group to the corresponding slice of linters.
 	GroupToLinters = map[string][]Linter{
-		DefaultGroup: DefaultLinters,
-		AllGroup:     AllLinters,
+		"google": GoogleLinters,
+		"uber":   UberLinters,
 	}
 )
 
@@ -180,13 +209,22 @@ func NewLinter(id string, purpose string, addCheck func(func(*text.Failure), str
 
 // GetLinters returns the Linters for the LintConfig.
 //
+// The group, if set, is expected to be lower-case.
+//
 // The configuration is expected to be valid, deduplicated, and all upper-case.
 // IncludeIDs and ExcludeIDs MUST NOT have an intersection.
 //
 // If the config came from the settings package, this is already validated.
 func GetLinters(config settings.LintConfig) ([]Linter, error) {
 	var linters []Linter
-	if !config.NoDefault {
+	var ok bool
+	if config.Group != "" {
+		linters, ok = GroupToLinters[config.Group]
+		if !ok {
+			return nil, fmt.Errorf("unknown lint group: %s", config.Group)
+		}
+	} else if !config.NoDefault {
+		// we ignore NoDefault if Group is set
 		linters = DefaultLinters
 	}
 	if len(config.IncludeIDs) == 0 && len(config.ExcludeIDs) == 0 {
@@ -301,22 +339,3 @@ func shouldIgnore(linter Linter, descriptor *proto.Proto, ignoreIDToFilePaths ma
 	}
 	return false, nil
 }
-
-func copyLintersWithout(linters []Linter, remove ...Linter) []Linter {
-	c := make([]Linter, 0, len(linters))
-	for _, linter := range linters {
-		if !linterIn(linter, remove) {
-			c = append(c, linter)
-		}
-	}
-	return c
-}
-
-func linterIn(linter Linter, s []Linter) bool {
-	for _, e := range s {
-		if e == linter {
-			return true
-		}
-	}
-	return false
-}
diff --git a/internal/protoc/compiler.go b/internal/protoc/compiler.go
index be90f1b..99c6589 100644
--- a/internal/protoc/compiler.go
+++ b/internal/protoc/compiler.go
@@ -50,8 +50,9 @@ var (
 	pluginFailedRegexp       = regexp.MustCompile("^--.*_out: protoc-gen-(.*): Plugin failed with status code (.*).$")
 	otherPluginFailureRegexp = regexp.MustCompile("^--(.*)_out: (.*)$")
 
-	extraImportRegexp  = regexp.MustCompile("^(.*): warning: Import (.*) but not used.$")
-	fileNotFoundRegexp = regexp.MustCompile("^(.*): File not found.$")
+	extraImportRegexp     = regexp.MustCompile("^(.*): warning: Import (.*) but not used.$")
+	recursiveImportRegexp = regexp.MustCompile("^(.*): File recursively imports itself: (.*)$")
+	fileNotFoundRegexp    = regexp.MustCompile("^(.*): File not found.$")
 	// protoc outputs both this line and fileNotFound, so we end up ignoring this one
 	// TODO figure out what the error is for errors in the import
 	importNotFoundRegexp              = regexp.MustCompile("^(.*): Import (.*) was not found or had errors.$")
@@ -575,6 +576,12 @@ func (c *compiler) parseProtocLine(cmdMeta *cmdMeta, protocLine string) *text.Fa
 				Message:  fmt.Sprintf(`Import "%s" was not used.`, matches[2]),
 			}
 		}
+		if matches := recursiveImportRegexp.FindStringSubmatch(protocLine); len(matches) > 2 {
+			return &text.Failure{
+				Filename: bestFilePath(cmdMeta, matches[1]),
+				Message:  fmt.Sprintf(`File recursively imports itself %s.`, matches[2]),
+			}
+		}
 		if matches := fileNotFoundRegexp.FindStringSubmatch(protocLine); len(matches) > 1 {
 			return &text.Failure{
 				// TODO: can we figure out the file name?
diff --git a/internal/settings/config_provider.go b/internal/settings/config_provider.go
index f6c58d9..63aa084 100644
--- a/internal/settings/config_provider.go
+++ b/internal/settings/config_provider.go
@@ -287,6 +287,7 @@ func externalConfigToConfig(e ExternalConfig, dirPath string) (Config, error) {
 		Lint: LintConfig{
 			IncludeIDs:          strs.DedupeSort(e.Lint.Rules.Add, strings.ToUpper),
 			ExcludeIDs:          strs.DedupeSort(e.Lint.Rules.Remove, strings.ToUpper),
+			Group:               strings.ToLower(e.Lint.Group),
 			NoDefault:           e.Lint.Rules.NoDefault,
 			IgnoreIDToFilePaths: ignoreIDToFilePaths,
 		},
diff --git a/internal/settings/settings.go b/internal/settings/settings.go
index c7409d9..afd2e95 100644
--- a/internal/settings/settings.go
+++ b/internal/settings/settings.go
@@ -169,7 +169,13 @@ type CreateConfig struct {
 
 // LintConfig is the lint config.
 type LintConfig struct {
+	// Group is the specific group of linters to use.
+	// The default group is the "default" lint group, which is equal
+	// to the "uber" lint group.
+	// Setting this value will result in NoDefault being ignored.
+	Group string
 	// NoDefault is set to exclude the default set of linters.
+	// This value is ignored if Group is set.
 	NoDefault bool
 	// IncludeIDs are the list of linter IDs to use in addition to the defaults.
 	// Expected to be all uppercase.
@@ -256,6 +262,7 @@ type ExternalConfig struct {
 		} `json:"packages,omitempty" yaml:"packages,omitempty"`
 	} `json:"create,omitempty" yaml:"create,omitempty"`
 	Lint struct {
+		Group   string `json:"group,omitempty" yaml:"group,omitempty"`
 		Ignores []struct {
 			ID    string   `json:"id,omitempty" yaml:"id,omitempty"`
 			Files []string `json:"files,omitempty" yaml:"files,omitempty"`


Test patch:
diff --git a/internal/cmd/cmd_test.go b/internal/cmd/cmd_test.go
index 33d5bb0..3f516bd 100644
--- a/internal/cmd/cmd_test.go
+++ b/internal/cmd/cmd_test.go
@@ -94,6 +94,13 @@ func TestCompile(t *testing.T) {
 		testdata/compile/syntax/missing_syntax.proto:4:3:Expected "required", "optional", or "repeated".`,
 		"testdata/compile/syntax/missing_syntax.proto",
 	)
+	assertDoCompileFiles(
+		t,
+		false,
+		false,
+		`testdata/compile/recursive/one.proto:1:1:File recursively imports itself one.proto -> two.proto -> one.proto.`,
+		"testdata/compile/recursive/one.proto",
+	)
 	assertDoCompileFiles(
 		t,
 		true,
@@ -145,6 +152,18 @@ func TestLint(t *testing.T) {
 		"",
 		"testdata/lint/version2",
 	)
+	assertDoLintFile(
+		t,
+		true,
+		"",
+		"../../etc/style/google",
+	)
+	assertDoLintFile(
+		t,
+		true,
+		"",
+		"../../etc/style/uber",
+	)
 	assertDoLintFile(
 		t,
 		false,
@@ -624,10 +643,6 @@ func TestVersionJSON(t *testing.T) {
 	assertRegexp(t, 0, fmt.Sprintf(`(?s){.*"version":.*"%s",.*"default_protoc_version":.*"%s".*}`, vars.Version, vars.DefaultProtocVersion), "version", "--json")
 }
 
-func TestListAllLintGroups(t *testing.T) {
-	assertExact(t, 0, "all\ndefault", "list-all-lint-groups")
-}
-
 func TestDescriptorProto(t *testing.T) {
 	assertExact(
 		t,
@@ -731,12 +746,22 @@ func TestServiceDescriptorProto(t *testing.T) {
 
 func TestListLinters(t *testing.T) {
 	assertLinters(t, lint.DefaultLinters, "lint", "--list-linters")
+	assertLinters(t, lint.UberLinters, "lint", "--list-linters")
 }
 
 func TestListAllLinters(t *testing.T) {
 	assertLinters(t, lint.AllLinters, "lint", "--list-all-linters")
 }
 
+func TestListAllLintGroups(t *testing.T) {
+	assertExact(t, 0, "google\nuber", "lint", "--list-all-lint-groups")
+}
+
+func TestListLintGroup(t *testing.T) {
+	assertLinters(t, lint.GoogleLinters, "lint", "--list-lint-group", "google")
+	assertLinters(t, lint.UberLinters, "lint", "--list-lint-group", "uber")
+}
+
 func assertLinters(t *testing.T, linters []lint.Linter, args ...string) {
 	linterIDs := make([]string, 0, len(linters))
 	for _, linter := range linters {
diff --git a/internal/cmd/testdata/compile/recursive/one.proto b/internal/cmd/testdata/compile/recursive/one.proto
new file mode 100644
index 0000000..630f42c
--- /dev/null
+++ b/internal/cmd/testdata/compile/recursive/one.proto
@@ -0,0 +1,7 @@
+syntax = "proto3";
+
+package foo;
+
+import "two.proto";
+
+message One {}
diff --git a/internal/cmd/testdata/compile/recursive/prototool.yaml b/internal/cmd/testdata/compile/recursive/prototool.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/internal/cmd/testdata/compile/recursive/two.proto b/internal/cmd/testdata/compile/recursive/two.proto
new file mode 100644
index 0000000..29f4bf8
--- /dev/null
+++ b/internal/cmd/testdata/compile/recursive/two.proto
@@ -0,0 +1,7 @@
+syntax = "proto3";
+
+package foo;
+
+import "one.proto";
+
+message Two {}