Repository: uber/prototool
Pull request #290 (2019-01-10 20:52:31)

Update create and format --fix to reflect the uber2 lint group if set
This updates `prototool create` and `prototool format --fix` to work with the `uber2` lint group if it is set.

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

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

```diff
@@            Coverage Diff             @@
##              dev     #290      +/-   ##
==========================================
+ Coverage   72.51%   72.51%   +<.01%     
==========================================
  Files         124      124              
  Lines        5548     5639      +91     
==========================================
+ Hits         4023     4089      +66     
- Misses       1145     1163      +18     
- Partials      380      387       +7
```


| [Impacted Files](https://codecov.io/gh/uber/prototool/pull/290?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [internal/lint/lint.go](https://codecov.io/gh/uber/prototool/pull/290/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbGludC9saW50Lmdv) | `79.2% <ø> (ø)` | :arrow_up: |
| [internal/create/create.go](https://codecov.io/gh/uber/prototool/pull/290/diff?src=pr&el=tree#diff-aW50ZXJuYWwvY3JlYXRlL2NyZWF0ZS5nbw==) | `100% <ø> (ø)` | :arrow_up: |
| [internal/format/format.go](https://codecov.io/gh/uber/prototool/pull/290/diff?src=pr&el=tree#diff-aW50ZXJuYWwvZm9ybWF0L2Zvcm1hdC5nbw==) | `100% <100%> (ø)` | :arrow_up: |
| [internal/lint/check\_file\_options\_equal.go](https://codecov.io/gh/uber/prototool/pull/290/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbGludC9jaGVja19maWxlX29wdGlvbnNfZXF1YWwuZ28=) | `96.07% <100%> (ø)` | :arrow_up: |
| [internal/protostrs/protostrs.go](https://codecov.io/gh/uber/prototool/pull/290/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcHJvdG9zdHJzL3Byb3Rvc3Rycy5nbw==) | `95.74% <100%> (ø)` | :arrow_up: |
| [internal/format/transformer.go](https://codecov.io/gh/uber/prototool/pull/290/diff?src=pr&el=tree#diff-aW50ZXJuYWwvZm9ybWF0L3RyYW5zZm9ybWVyLmdv) | `79.54% <55.55%> (-6.17%)` | :arrow_down: |
| [internal/create/handler.go](https://codecov.io/gh/uber/prototool/pull/290/diff?src=pr&el=tree#diff-aW50ZXJuYWwvY3JlYXRlL2hhbmRsZXIuZ28=) | `69.46% <73.21%> (-0.43%)` | :arrow_down: |
| [internal/format/first\_pass\_visitor.go](https://codecov.io/gh/uber/prototool/pull/290/diff?src=pr&el=tree#diff-aW50ZXJuYWwvZm9ybWF0L2ZpcnN0X3Bhc3NfdmlzaXRvci5nbw==) | `70.32% <79.54%> (+1.94%)` | :arrow_up: |
| [internal/exec/runner.go](https://codecov.io/gh/uber/prototool/pull/290/diff?src=pr&el=tree#diff-aW50ZXJuYWwvZXhlYy9ydW5uZXIuZ28=) | `47.02% <86.66%> (+0.5%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/uber/prototool/pull/290?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/290?src=pr&el=footer). Last update [c42fb51...50cf533](https://codecov.io/gh/uber/prototool/pull/290?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.gitignore b/.gitignore
index 767ee72..5d39c61 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,15 @@
 /internal/cmd/testdata/create/one/b/c/bar/baz.proto
 /internal/cmd/testdata/create/one/baz.proto
 /internal/cmd/testdata/create/two/baz.proto
+/internal/cmd/testdata/create/version2one/a/b/bar/baz.proto
+/internal/cmd/testdata/create/version2one/a/c/bar/baz.proto
+/internal/cmd/testdata/create/version2one/b/c/bar/baz.proto
+/internal/cmd/testdata/create/version2two/baz.proto
+/internal/cmd/testdata/create/version2three/a/b/bar/v1/baz.proto
+/internal/cmd/testdata/create/version2three/a/c/bar/v1/baz.proto
+/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
 /main
 /prototool.iml
 /release
diff --git a/internal/create/create.go b/internal/create/create.go
index 4e521e0..c9c9ae8 100644
--- a/internal/create/create.go
+++ b/internal/create/create.go
@@ -22,9 +22,14 @@ package create
 
 import "go.uber.org/zap"
 
-// DefaultPackage is the default package to use in lieu of one being able to be
-// derived.
-const DefaultPackage = "uber.prototool.generated"
+const (
+	// DefaultPackage is the default package to use in lieu of one being able
+	// to be derived.
+	DefaultPackage = "uber.prototool.generated"
+	// DefaultPackageV2 is the default package to use in lieu of one being able
+	// to be derived if the lint group is "uber2".
+	DefaultPackageV2 = "uber.prototool.generated.v1"
+)
 
 // Handler handles creation of Protobuf files from a template.
 type Handler interface {
diff --git a/internal/create/handler.go b/internal/create/handler.go
index 30e36e1..5846a11 100644
--- a/internal/create/handler.go
+++ b/internal/create/handler.go
@@ -35,7 +35,8 @@ import (
 	"go.uber.org/zap"
 )
 
-var tmpl = template.Must(template.New("tmpl").Parse(`syntax = "proto3";
+var (
+	tmplV1 = template.Must(template.New("tmplV1").Parse(`syntax = "proto3";
 
 package {{.Pkg}};
 
@@ -44,11 +45,27 @@ option java_multiple_files = true;
 option java_outer_classname = "{{.JavaOuterClassname}}";
 option java_package = "{{.JavaPkg}}";`))
 
+	tmplV2 = template.Must(template.New("tmplV2").Parse(`syntax = "proto3";
+
+package {{.Pkg}};
+
+option csharp_namespace = "{{.CSharpNamespace}}";
+option go_package = "{{.GoPkg}}";
+option java_multiple_files = true;
+option java_outer_classname = "{{.JavaOuterClassname}}";
+option java_package = "{{.JavaPkg}}";
+option objc_class_prefix = "{{.OBJCClassPrefix}}";
+option php_namespace = "{{.PHPNamespace}}";`))
+)
+
 type tmplData struct {
 	Pkg                string
+	CSharpNamespace    string
 	GoPkg              string
 	JavaOuterClassname string
 	JavaPkg            string
+	OBJCClassPrefix    string
+	PHPNamespace       string
 }
 
 type handler struct {
@@ -110,25 +127,68 @@ func (h *handler) checkFilePath(filePath string) error {
 }
 
 func (h *handler) create(filePath string) error {
-	pkg, err := h.getPkg(filePath)
+	isV2, err := h.isV2(filePath)
 	if err != nil {
 		return err
 	}
-	data, err := getData(
-		&tmplData{
-			Pkg:                pkg,
-			GoPkg:              protostrs.GoPackage(pkg),
-			JavaOuterClassname: protostrs.JavaOuterClassname(filePath),
-			JavaPkg:            protostrs.JavaPackage(pkg),
-		},
-	)
+	defaultPackage := DefaultPackage
+	if isV2 {
+		defaultPackage = DefaultPackageV2
+	}
+	pkg, err := h.getPkg(filePath, defaultPackage)
 	if err != nil {
 		return err
 	}
+	var data []byte
+	if isV2 {
+		data, err = getDataV2(
+			&tmplData{
+				Pkg:                pkg,
+				CSharpNamespace:    protostrs.CSharpNamespace(pkg),
+				GoPkg:              protostrs.GoPackageV2(pkg),
+				JavaOuterClassname: protostrs.JavaOuterClassname(filePath),
+				JavaPkg:            protostrs.JavaPackage(pkg),
+				OBJCClassPrefix:    protostrs.OBJCClassPrefix(pkg),
+				PHPNamespace:       protostrs.PHPNamespace(pkg),
+			},
+		)
+		if err != nil {
+			return err
+		}
+	} else {
+		data, err = getDataV1(
+			&tmplData{
+				Pkg:                pkg,
+				GoPkg:              protostrs.GoPackage(pkg),
+				JavaOuterClassname: protostrs.JavaOuterClassname(filePath),
+				JavaPkg:            protostrs.JavaPackage(pkg),
+			},
+		)
+		if err != nil {
+			return err
+		}
+	}
 	return ioutil.WriteFile(filePath, data, 0644)
 }
 
-func (h *handler) getPkg(filePath string) (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.configProvider.GetForDir(absDirPath)
+	if err != nil {
+		return false, err
+	}
+	// no config file found
+	if config.DirPath == "" {
+		return false, nil
+	}
+	return strings.ToLower(config.Lint.Group) == "uber2", nil
+}
+
+func (h *handler) getPkg(filePath string, defaultPackage string) (string, error) {
 	if h.pkg != "" {
 		return h.pkg, nil
 	}
@@ -143,7 +203,7 @@ func (h *handler) getPkg(filePath string) (string, error) {
 	}
 	// no config file found, can't compute package
 	if config.DirPath == "" {
-		return DefaultPackage, nil
+		return defaultPackage, nil
 	}
 	// we need to get all the matching directories and then choose the longest
 	// ie if you have a, a/b, we choose a/b
@@ -167,7 +227,7 @@ func (h *handler) getPkg(filePath string) (string, error) {
 		if err != nil {
 			return "", err
 		}
-		return getPkgFromRel(rel, longestBasePkg), nil
+		return getPkgFromRel(rel, longestBasePkg, defaultPackage), nil
 	}
 
 	// no package mapping found, do default logic
@@ -175,19 +235,19 @@ func (h *handler) getPkg(filePath string) (string, error) {
 	// TODO: cannot do rel right away because it will do ../.. if necessary
 	// strings.HasPrefix is not OS independent however
 	if !strings.HasPrefix(absDirPath, config.DirPath) {
-		return DefaultPackage, nil
+		return defaultPackage, nil
 	}
 	rel, err := filepath.Rel(config.DirPath, absDirPath)
 	if err != nil {
 		return "", err
 	}
-	return getPkgFromRel(rel, ""), nil
+	return getPkgFromRel(rel, "", defaultPackage), nil
 }
 
-func getPkgFromRel(rel string, basePkg string) string {
+func getPkgFromRel(rel string, basePkg string, defaultPackage string) string {
 	if rel == "." {
 		if basePkg == "" {
-			return DefaultPackage
+			return defaultPackage
 		}
 		return basePkg
 	}
@@ -198,9 +258,17 @@ func getPkgFromRel(rel string, basePkg string) string {
 	return basePkg + "." + relPkg
 }
 
-func getData(tmplData *tmplData) ([]byte, error) {
+func getDataV1(tmplData *tmplData) ([]byte, error) {
+	buffer := bytes.NewBuffer(nil)
+	if err := tmplV1.Execute(buffer, tmplData); err != nil {
+		return nil, err
+	}
+	return buffer.Bytes(), nil
+}
+
+func getDataV2(tmplData *tmplData) ([]byte, error) {
 	buffer := bytes.NewBuffer(nil)
-	if err := tmpl.Execute(buffer, tmplData); err != nil {
+	if err := tmplV2.Execute(buffer, tmplData); err != nil {
 		return nil, err
 	}
 	return buffer.Bytes(), nil
diff --git a/internal/exec/runner.go b/internal/exec/runner.go
index fc5536d..174525b 100644
--- a/internal/exec/runner.go
+++ b/internal/exec/runner.go
@@ -376,7 +376,7 @@ func (r *runner) diffLintGroups(groups string) error {
 	return nil
 }
 
-func (r *runner) Format(args []string, overwrite, diffMode, lintMode, fix bool) error {
+func (r *runner) Format(args []string, overwrite, diffMode, lintMode, fixFlag bool) error {
 	if moreThanOneSet(overwrite, diffMode, lintMode) {
 		return newExitErrorf(255, "can only set one of overwrite, diff, lint")
 	}
@@ -388,10 +388,10 @@ func (r *runner) Format(args []string, overwrite, diffMode, lintMode, fix bool)
 	if _, err := r.compile(false, false, false, meta); err != nil {
 		return err
 	}
-	return r.format(overwrite, diffMode, lintMode, fix, meta)
+	return r.format(overwrite, diffMode, lintMode, getFormatFixValue(fixFlag, meta), meta)
 }
 
-func (r *runner) format(overwrite, diffMode, lintMode, fix bool, meta *meta) error {
+func (r *runner) format(overwrite, diffMode, lintMode bool, fix int, meta *meta) error {
 	success := true
 	for _, protoFiles := range meta.ProtoSet.DirPathToFiles {
 		for _, protoFile := range protoFiles {
@@ -413,7 +413,7 @@ func (r *runner) format(overwrite, diffMode, lintMode, fix bool, meta *meta) err
 // 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 bool, meta *meta, protoFile *file.ProtoFile) (bool, error) {
+func (r *runner) formatFile(overwrite bool, diffMode bool, lintMode bool, fix int, meta *meta, protoFile *file.ProtoFile) (bool, error) {
 	absSingleFilename, err := file.AbsClean(meta.SingleFilename)
 	if err != nil {
 		return false, err
@@ -469,7 +469,7 @@ func (r *runner) formatFile(overwrite bool, diffMode bool, lintMode bool, fix bo
 	return true, nil
 }
 
-func (r *runner) All(args []string, disableFormat, disableLint, fix bool) error {
+func (r *runner) All(args []string, disableFormat, disableLint, fixFlag bool) error {
 	meta, err := r.getMeta(args)
 	if err != nil {
 		return err
@@ -479,7 +479,7 @@ func (r *runner) All(args []string, disableFormat, disableLint, fix bool) error
 		return err
 	}
 	if !disableFormat {
-		if err := r.format(true, false, false, fix, meta); err != nil {
+		if err := r.format(true, false, false, getFormatFixValue(fixFlag, meta), meta); err != nil {
 			return err
 		}
 	}
@@ -798,10 +798,10 @@ func (r *runner) newLintRunner() lint.Runner {
 	)
 }
 
-func (r *runner) newTransformer(fix bool) format.Transformer {
+func (r *runner) newTransformer(fix int) format.Transformer {
 	transformerOptions := []format.TransformerOption{format.TransformerWithLogger(r.logger)}
-	if fix {
-		transformerOptions = append(transformerOptions, format.TransformerWithFix())
+	if fix != format.FixNone {
+		transformerOptions = append(transformerOptions, format.TransformerWithFix(fix))
 	}
 	return format.NewTransformer(transformerOptions...)
 }
@@ -1002,6 +1002,16 @@ func moreThanOneSet(values ...bool) bool {
 	return numSet > 1
 }
 
+func getFormatFixValue(fixFlag bool, meta *meta) int {
+	if !fixFlag {
+		return format.FixNone
+	}
+	if meta.ProtoSet.Config.Lint.Group == "uber2" {
+		return format.FixV2
+	}
+	return format.FixV1
+}
+
 func extractSortPackageNames(m map[string]*extract.Package) []string {
 	s := make([]string, 0, len(m))
 	for key := range m {
diff --git a/internal/format/first_pass_visitor.go b/internal/format/first_pass_visitor.go
index 73de5de..a62ffb9 100644
--- a/internal/format/first_pass_visitor.go
+++ b/internal/format/first_pass_visitor.go
@@ -41,14 +41,17 @@ type firstPassVisitor struct {
 	haveHitNonComment bool
 
 	filename                 string
-	fix                      bool
+	fix                      int
 	goPackageOption          *proto.Option
 	javaMultipleFilesOption  *proto.Option
 	javaOuterClassnameOption *proto.Option
 	javaPackageOption        *proto.Option
+	csharpNamespaceOption    *proto.Option
+	objcClassPrefixOption    *proto.Option
+	phpNamespaceOption       *proto.Option
 }
 
-func newFirstPassVisitor(filename string, fix bool) *firstPassVisitor {
+func newFirstPassVisitor(filename string, fix int) *firstPassVisitor {
 	return &firstPassVisitor{baseVisitor: newBaseVisitor(), filename: filename, fix: fix}
 }
 
@@ -68,7 +71,7 @@ func (v *firstPassVisitor) Do() []*text.Failure {
 		v.PWithInlineComment(v.Package.InlineComment, `package `, v.Package.Name, `;`)
 		v.P()
 	}
-	if v.fix && v.Package != nil {
+	if v.fix != FixNone && v.Package != nil {
 		if v.goPackageOption == nil {
 			v.goPackageOption = &proto.Option{Name: "go_package"}
 		}
@@ -81,9 +84,27 @@ func (v *firstPassVisitor) Do() []*text.Failure {
 		if v.javaPackageOption == nil {
 			v.javaPackageOption = &proto.Option{Name: "java_package"}
 		}
-		v.goPackageOption.Constant = proto.Literal{
-			Source:   protostrs.GoPackage(v.Package.Name),
-			IsString: true,
+		if v.fix == FixV2 {
+			if v.csharpNamespaceOption == nil {
+				v.csharpNamespaceOption = &proto.Option{Name: "csharp_namespace"}
+			}
+			if v.objcClassPrefixOption == nil {
+				v.objcClassPrefixOption = &proto.Option{Name: "objc_class_prefix"}
+			}
+			if v.phpNamespaceOption == nil {
+				v.phpNamespaceOption = &proto.Option{Name: "php_namespace"}
+			}
+		}
+		if v.fix == FixV2 {
+			v.goPackageOption.Constant = proto.Literal{
+				Source:   protostrs.GoPackageV2(v.Package.Name),
+				IsString: true,
+			}
+		} else {
+			v.goPackageOption.Constant = proto.Literal{
+				Source:   protostrs.GoPackage(v.Package.Name),
+				IsString: true,
+			}
 		}
 		v.javaMultipleFilesOption.Constant = proto.Literal{
 			Source: "true",
@@ -96,6 +117,20 @@ func (v *firstPassVisitor) Do() []*text.Failure {
 			Source:   protostrs.JavaPackage(v.Package.Name),
 			IsString: true,
 		}
+		if v.fix == FixV2 {
+			v.csharpNamespaceOption.Constant = proto.Literal{
+				Source:   protostrs.CSharpNamespace(v.Package.Name),
+				IsString: true,
+			}
+			v.objcClassPrefixOption.Constant = proto.Literal{
+				Source:   protostrs.OBJCClassPrefix(v.Package.Name),
+				IsString: true,
+			}
+			v.phpNamespaceOption.Constant = proto.Literal{
+				Source:   protostrs.PHPNamespace(v.Package.Name),
+				IsString: true,
+			}
+		}
 		v.Options = append(
 			v.Options,
 			v.goPackageOption,
@@ -103,6 +138,14 @@ func (v *firstPassVisitor) Do() []*text.Failure {
 			v.javaOuterClassnameOption,
 			v.javaPackageOption,
 		)
+		if v.fix == FixV2 {
+			v.Options = append(
+				v.Options,
+				v.csharpNamespaceOption,
+				v.objcClassPrefixOption,
+				v.phpNamespaceOption,
+			)
+		}
 	}
 	if len(v.Options) > 0 {
 		v.POptions(v.Options...)
@@ -145,8 +188,11 @@ func (v *firstPassVisitor) VisitOption(element *proto.Option) {
 	// this will only hit file options since we don't do any
 	// visiting of children in this visitor
 	v.haveHitNonComment = true
-	if v.fix {
+	if v.fix != FixNone {
 		switch element.Name {
+		case "csharp_namespace":
+			v.csharpNamespaceOption = element
+			return
 		case "go_package":
 			v.goPackageOption = element
 			return
@@ -159,6 +205,12 @@ func (v *firstPassVisitor) VisitOption(element *proto.Option) {
 		case "java_package":
 			v.javaPackageOption = element
 			return
+		case "objc_class_prefix":
+			v.objcClassPrefixOption = element
+			return
+		case "php_namespace":
+			v.phpNamespaceOption = element
+			return
 		}
 	}
 	v.Options = append(v.Options, element)
diff --git a/internal/format/format.go b/internal/format/format.go
index 95aeec5..3fc6127 100644
--- a/internal/format/format.go
+++ b/internal/format/format.go
@@ -25,6 +25,15 @@ import (
 	"go.uber.org/zap"
 )
 
+const (
+	// FixNone says to not do fixing.
+	FixNone = 0
+	// FixV1 says to do V1 fixing.
+	FixV1 = 1
+	// FixV2 says to do V2 fixing.
+	FixV2 = 2
+)
+
 // Transformer transforms an input file into an output file.
 type Transformer interface {
 	// Transform transforms the data.
@@ -48,10 +57,10 @@ func TransformerWithLogger(logger *zap.Logger) TransformerOption {
 }
 
 // TransformerWithFix returns a TransformerOption that will update the file options
-// go_package, java_package to match the package per the guidelines of the style guide.
-func TransformerWithFix() TransformerOption {
+// to match the package per the guidelines of the style guide.
+func TransformerWithFix(fix int) TransformerOption {
 	return func(transformer *transformer) {
-		transformer.fix = true
+		transformer.fix = fix
 	}
 }
 
diff --git a/internal/format/transformer.go b/internal/format/transformer.go
index 35d68c4..69ed6fa 100644
--- a/internal/format/transformer.go
+++ b/internal/format/transformer.go
@@ -32,12 +32,13 @@ import (
 
 type transformer struct {
 	logger *zap.Logger
-	fix    bool
+	fix    int
 }
 
 func newTransformer(options ...TransformerOption) *transformer {
 	transformer := &transformer{
 		logger: zap.NewNop(),
+		fix:    FixNone,
 	}
 	for _, option := range options {
 		option(transformer)
@@ -46,6 +47,9 @@ func newTransformer(options ...TransformerOption) *transformer {
 }
 
 func (t *transformer) Transform(filename string, data []byte) ([]byte, []*text.Failure, error) {
+	if err := checkFix(t.fix); err != nil {
+		return nil, nil, err
+	}
 	descriptor, err := proto.NewParser(bytes.NewReader(data)).Parse()
 	if err != nil {
 		return nil, nil, err
@@ -88,3 +92,12 @@ func (t *transformer) Transform(filename string, data []byte) ([]byte, []*text.F
 	}
 	return nil, failures, nil
 }
+
+func checkFix(fix int) error {
+	switch fix {
+	case FixNone, FixV1, FixV2:
+		return nil
+	default:
+		return fmt.Errorf("unknown format fix value: %d", fix)
+	}
+}
diff --git a/internal/lint/check_file_options_equal.go b/internal/lint/check_file_options_equal.go
index edd4061..c5805d9 100644
--- a/internal/lint/check_file_options_equal.go
+++ b/internal/lint/check_file_options_equal.go
@@ -44,11 +44,11 @@ var fileOptionsEqualGoPackagePbSuffixLinter = NewLinter(
 	}),
 )
 
-var fileOptionsEqualGoPackageLastTwoSuffixLinter = NewLinter(
-	"FILE_OPTIONS_EQUAL_GO_PACKAGE_LAST_TWO_SUFFIX",
+var fileOptionsEqualGoPackageV2SuffixLinter = NewLinter(
+	"FILE_OPTIONS_EQUAL_GO_PACKAGE_V2_SUFFIX",
 	`Verifies that the file option "go_package" is equal to the last two values of the package separated by "."s, or just the package name if there are no "."s.`,
 	newCheckFileOptionsEqual("go_package", func(_ *proto.Proto, pkg *proto.Package) string {
-		return protostrs.GoPackageLastTwo(pkg.Name)
+		return protostrs.GoPackageV2(pkg.Name)
 	}),
 )
 
diff --git a/internal/lint/lint.go b/internal/lint/lint.go
index d15d7ea..81af8d5 100644
--- a/internal/lint/lint.go
+++ b/internal/lint/lint.go
@@ -50,7 +50,7 @@ var (
 		fieldsNotReservedLinter,
 		fileOptionsCSharpNamespaceSameInDirLinter,
 		fileOptionsEqualCSharpNamespaceCapitalizedLinter,
-		fileOptionsEqualGoPackageLastTwoSuffixLinter,
+		fileOptionsEqualGoPackageV2SuffixLinter,
 		fileOptionsEqualGoPackagePbSuffixLinter,
 		fileOptionsEqualJavaMultipleFilesTrueLinter,
 		fileOptionsEqualJavaOuterClassnameProtoSuffixLinter,
@@ -172,7 +172,7 @@ var (
 		fieldsNotReservedLinter,
 		fileOptionsCSharpNamespaceSameInDirLinter,
 		fileOptionsEqualCSharpNamespaceCapitalizedLinter,
-		fileOptionsEqualGoPackageLastTwoSuffixLinter,
+		fileOptionsEqualGoPackageV2SuffixLinter,
 		fileOptionsEqualJavaMultipleFilesTrueLinter,
 		fileOptionsEqualJavaOuterClassnameProtoSuffixLinter,
 		fileOptionsEqualJavaPackageComPrefixLinter,
diff --git a/internal/protostrs/protostrs.go b/internal/protostrs/protostrs.go
index 0483283..a63ff7f 100644
--- a/internal/protostrs/protostrs.go
+++ b/internal/protostrs/protostrs.go
@@ -69,19 +69,17 @@ func GoPackage(packageName string) string {
 	return split[len(split)-1] + "pb"
 }
 
-// GoPackageLastTwo returns the value for the file option "go_package" given
+// GoPackageV2 returns the value for the file option "go_package" given
 // a package name. This will be equal to the last two values of the package
-// separated by "."s. If packageName is empty, this will return an empty string.
-// If packageName has only one value when separated by "."s, this will be that
-// value.
-func GoPackageLastTwo(packageName string) string {
+// separated by "."s if the package is a MajorBetaPackage, or GoPackage otherwise.
+func GoPackageV2(packageName string) string {
 	if packageName == "" {
 		return ""
 	}
-	split := strings.Split(packageName, ".")
-	if len(split) == 1 {
-		return split[0]
+	if _, _, ok := MajorBetaVersion(packageName); !ok {
+		return GoPackage(packageName)
 	}
+	split := strings.Split(packageName, ".")
 	return split[len(split)-2] + split[len(split)-1]
 }
 


Test patch:
diff --git a/internal/cmd/cmd_test.go b/internal/cmd/cmd_test.go
index 594cf0f..daccbcc 100644
--- a/internal/cmd/cmd_test.go
+++ b/internal/cmd/cmd_test.go
@@ -538,6 +538,7 @@ func TestGoldenFormat(t *testing.T) {
 	assertGoldenFormat(t, false, false, "testdata/format/proto3/foo/foo.proto")
 	assertGoldenFormat(t, false, false, "testdata/format/proto2/foo/foo_proto2.proto")
 	assertGoldenFormat(t, false, true, "testdata/format-fix/foo.proto")
+	assertGoldenFormat(t, false, true, "testdata/format-fix-v2/foo.proto")
 }
 
 func TestCreate(t *testing.T) {
@@ -650,6 +651,224 @@ option java_package = "com.foo";`,
 	)
 }
 
+func TestCreateV2(t *testing.T) {
+	t.Parallel()
+	// package override with also matching shorter override "a"
+	// make sure uses "a/b"
+	assertDoCreateFile(
+		t,
+		true,
+		true,
+		"testdata/create/version2one/a/b/bar/baz.proto",
+		"",
+		`syntax = "proto3";
+
+package foo.bar;
+
+option csharp_namespace = "Foo.Bar";
+option go_package = "barpb";
+option java_multiple_files = true;
+option java_outer_classname = "BazProto";
+option java_package = "com.foo.bar";
+option objc_class_prefix = "FBX";
+option php_namespace = "Foo\\Bar";`,
+	)
+	// create same file again but do not remove, should fail
+	assertDoCreateFile(
+		t,
+		false, // do not expect success
+		false, // do not remove
+		"testdata/create/version2one/a/b/bar/baz.proto",
+		"",
+		``,
+	)
+	// use the --package flag
+	assertDoCreateFile(
+		t,
+		true,
+		true,
+		"testdata/create/version2one/a/b/bar/baz.proto",
+		"bat", // --package value
+		`syntax = "proto3";
+
+package bat;
+
+option csharp_namespace = "Bat";
+option go_package = "batpb";
+option java_multiple_files = true;
+option java_outer_classname = "BazProto";
+option java_package = "com.bat";
+option objc_class_prefix = "BXX";
+option php_namespace = "Bat";`,
+	)
+	// package override but a shorter one "a"
+	assertDoCreateFile(
+		t,
+		true,
+		true,
+		"testdata/create/version2one/a/c/bar/baz.proto",
+		"",
+		`syntax = "proto3";
+
+package foobar.c.bar;
+
+option csharp_namespace = "Foobar.C.Bar";
+option go_package = "barpb";
+option java_multiple_files = true;
+option java_outer_classname = "BazProto";
+option java_package = "com.foobar.c.bar";
+option objc_class_prefix = "FCB";
+option php_namespace = "Foobar\\C\\Bar";`,
+	)
+	// no package override, do default b.c.bar
+	assertDoCreateFile(
+		t,
+		true,
+		true,
+		"testdata/create/version2one/b/c/bar/baz.proto",
+		"",
+		`syntax = "proto3";
+
+package b.c.bar;
+
+option csharp_namespace = "B.C.Bar";
+option go_package = "barpb";
+option java_multiple_files = true;
+option java_outer_classname = "BazProto";
+option java_package = "com.b.c.bar";
+option objc_class_prefix = "BCB";
+option php_namespace = "B\\C\\Bar";`,
+	)
+	// in dir with prototool.yaml with override
+	assertDoCreateFile(
+		t,
+		true,
+		true,
+		"testdata/create/version2two/baz.proto",
+		"",
+		`syntax = "proto3";
+
+package foo;
+
+option csharp_namespace = "Foo";
+option go_package = "foopb";
+option java_multiple_files = true;
+option java_outer_classname = "BazProto";
+option java_package = "com.foo";
+option objc_class_prefix = "FXX";
+option php_namespace = "Foo";`,
+	)
+}
+
+func TestCreateV2MajorBetaVersion(t *testing.T) {
+	t.Parallel()
+	// package override with also matching shorter override "a"
+	// make sure uses "a/b"
+	assertDoCreateFile(
+		t,
+		true,
+		true,
+		"testdata/create/version2three/a/b/bar/v1/baz.proto",
+		"",
+		`syntax = "proto3";
+
+package foo.bar.v1;
+
+option csharp_namespace = "Foo.Bar.V1";
+option go_package = "barv1";
+option java_multiple_files = true;
+option java_outer_classname = "BazProto";
+option java_package = "com.foo.bar.v1";
+option objc_class_prefix = "FBX";
+option php_namespace = "Foo\\Bar\\V1";`,
+	)
+	// create same file again but do not remove, should fail
+	assertDoCreateFile(
+		t,
+		false, // do not expect success
+		false, // do not remove
+		"testdata/create/version2three/a/b/bar/v1/baz.proto",
+		"",
+		``,
+	)
+	// package override but a shorter one "a"
+	assertDoCreateFile(
+		t,
+		true,
+		true,
+		"testdata/create/version2three/a/c/bar/v1/baz.proto",
+		"",
+		`syntax = "proto3";
+
+package foobar.c.bar.v1;
+
+option csharp_namespace = "Foobar.C.Bar.V1";
+option go_package = "barv1";
+option java_multiple_files = true;
+option java_outer_classname = "BazProto";
+option java_package = "com.foobar.c.bar.v1";
+option objc_class_prefix = "FCB";
+option php_namespace = "Foobar\\C\\Bar\\V1";`,
+	)
+	// no package override, do default b.c.bar
+	assertDoCreateFile(
+		t,
+		true,
+		true,
+		"testdata/create/version2three/b/c/bar/v1beta1/baz.proto",
+		"",
+		`syntax = "proto3";
+
+package b.c.bar.v1beta1;
+
+option csharp_namespace = "B.C.Bar.V1Beta1";
+option go_package = "barv1beta1";
+option java_multiple_files = true;
+option java_outer_classname = "BazProto";
+option java_package = "com.b.c.bar.v1beta1";
+option objc_class_prefix = "BCB";
+option php_namespace = "B\\C\\Bar\\V1Beta1";`,
+	)
+	// in dir with prototool.yaml, use default package
+	assertDoCreateFile(
+		t,
+		true,
+		true,
+		"testdata/create/version2three/baz.proto",
+		"",
+		`syntax = "proto3";
+
+package uber.prototool.generated.v1;
+
+option csharp_namespace = "Uber.Prototool.Generated.V1";
+option go_package = "generatedv1";
+option java_multiple_files = true;
+option java_outer_classname = "BazProto";
+option java_package = "com.uber.prototool.generated.v1";
+option objc_class_prefix = "UPG";
+option php_namespace = "Uber\\Prototool\\Generated\\V1";`,
+	)
+	// in dir with prototool.yaml with override
+	assertDoCreateFile(
+		t,
+		true,
+		true,
+		"testdata/create/version2four/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 = "com.foo.v1";
+option objc_class_prefix = "FXX";
+option php_namespace = "Foo\\V1";`,
+	)
+}
+
 func TestGRPC(t *testing.T) {
 	t.Parallel()
 	assertGRPC(t,
diff --git a/internal/cmd/testdata/create/version2four/prototool.yaml b/internal/cmd/testdata/create/version2four/prototool.yaml
new file mode 100644
index 0000000..0606809
--- /dev/null
+++ b/internal/cmd/testdata/create/version2four/prototool.yaml
@@ -0,0 +1,6 @@
+lint:
+  group: uber2
+create:
+  packages:
+    - directory: .
+      name: foo.v1
diff --git a/internal/cmd/testdata/create/version2one/prototool.yaml b/internal/cmd/testdata/create/version2one/prototool.yaml
new file mode 100644
index 0000000..e33130b
--- /dev/null
+++ b/internal/cmd/testdata/create/version2one/prototool.yaml
@@ -0,0 +1,8 @@
+lint:
+  group: uber2
+create:
+  packages:
+    - directory: a/b
+      name: foo
+    - directory: a
+      name: foobar
diff --git a/internal/cmd/testdata/create/version2three/prototool.yaml b/internal/cmd/testdata/create/version2three/prototool.yaml
new file mode 100644
index 0000000..e33130b
--- /dev/null
+++ b/internal/cmd/testdata/create/version2three/prototool.yaml
@@ -0,0 +1,8 @@
+lint:
+  group: uber2
+create:
+  packages:
+    - directory: a/b
+      name: foo
+    - directory: a
+      name: foobar
diff --git a/internal/cmd/testdata/create/version2two/prototool.yaml b/internal/cmd/testdata/create/version2two/prototool.yaml
new file mode 100644
index 0000000..02aa4fb
--- /dev/null
+++ b/internal/cmd/testdata/create/version2two/prototool.yaml
@@ -0,0 +1,6 @@
+lint:
+  group: uber2
+create:
+  packages:
+    - directory: .
+      name: foo
diff --git a/internal/cmd/testdata/format-fix-v2/foo.proto b/internal/cmd/testdata/format-fix-v2/foo.proto
new file mode 100644
index 0000000..b99f8f0
--- /dev/null
+++ b/internal/cmd/testdata/format-fix-v2/foo.proto
@@ -0,0 +1,76 @@
+// foo
+
+// bar
+
+syntax = "proto3"; // inline comment1
+
+// baz
+
+package foo.bar.v1; // inline comment3
+
+// bat
+// ban
+
+// comment9
+import "google/protobuf/timestamp.proto"; // inline comment9
+
+// comment11
+option java_multiple_files = true; //inline comment11
+// comment4
+option go_package = "issssoopb"; // inline comment4
+// comment5
+option java_outer_classname = "FdsddooProto"; // inline comment5
+
+// Baz is a baz.
+message Baz {
+
+  int64 hello = 1;
+
+  // unassociated comment
+
+  // another unassociated comment
+
+  google.protobuf.Timestamp timestamp = 3; /* inline c-style comment */
+
+
+  map<string, int64> m = 11;
+
+  oneof test_oneof {
+    int64 foo1 = 8;
+    string foo2 = 9;
+    // comment18
+  }
+
+}
+
+// Bat is a bat.
+message Bat {
+  reserved 2 to 10, 1000 to 1100;
+  reserved "foo";
+  reserved "bar", "baz";
+  // NestedBat is a nested bat.
+  message NestedBat {
+    int64 hello = 1;
+  }
+}
+
+// Empty is an empty message.
+message Empty {
+}
+
+// Something is something.
+enum Something {
+// comment25
+  SOMETHING_INVALID = 0; // inline comment25
+// comment27
+  SOMETHING_UNSET = 1; // inline comment27
+// comment28
+  SOMETHING_FOO = 2; // inline comment28
+}
+
+// Daylight is the daylight service.
+service Daylight {
+  rpc Hello(Bat) returns (Empty) {
+  } // inline comment23
+  rpc Foo(Empty) returns (Bat); // inline comment24
+}
diff --git a/internal/cmd/testdata/format-fix-v2/foo.proto.golden b/internal/cmd/testdata/format-fix-v2/foo.proto.golden
new file mode 100644
index 0000000..cdb546a
--- /dev/null
+++ b/internal/cmd/testdata/format-fix-v2/foo.proto.golden
@@ -0,0 +1,73 @@
+// foo
+
+// bar
+
+syntax = "proto3"; // inline comment1
+
+package foo.bar.v1; // inline comment3
+
+option csharp_namespace = "Foo.Bar.V1";
+// comment4
+option go_package = "barv1"; // inline comment4
+// comment11
+option java_multiple_files = true; //inline comment11
+// comment5
+option java_outer_classname = "FooProto"; // inline comment5
+option java_package = "com.foo.bar.v1";
+option objc_class_prefix = "FBX";
+option php_namespace = "Foo\\Bar\\V1";
+
+// comment9
+import "google/protobuf/timestamp.proto"; // inline comment9
+
+// baz
+
+// bat
+// ban
+
+// Baz is a baz.
+message Baz {
+  int64 hello = 1;
+  // unassociated comment
+
+  // another unassociated comment
+
+  google.protobuf.Timestamp timestamp = 3; // inline c-style comment 
+  map<string, int64> m = 11;
+  oneof test_oneof {
+    int64 foo1 = 8;
+    string foo2 = 9;
+    // comment18
+
+  }
+}
+
+// Bat is a bat.
+message Bat {
+  reserved 2 to 10, 1000 to 1100;
+  reserved "foo";
+  reserved "bar", "baz";
+  // NestedBat is a nested bat.
+  message NestedBat {
+    int64 hello = 1;
+  }
+}
+
+// Empty is an empty message.
+message Empty {}
+
+// Something is something.
+enum Something {
+  // comment25
+  SOMETHING_INVALID = 0; // inline comment25
+  // comment27
+  SOMETHING_UNSET = 1; // inline comment27
+  // comment28
+  SOMETHING_FOO = 2; // inline comment28
+}
+
+// Daylight is the daylight service.
+service Daylight {
+  rpc Hello(Bat) returns (Empty); // inline comment23
+  rpc Foo(Empty) returns (Bat); // inline comment24
+}
diff --git a/internal/cmd/testdata/format-fix-v2/prototool.yaml b/internal/cmd/testdata/format-fix-v2/prototool.yaml
new file mode 100644
index 0000000..092dbf5
--- /dev/null
+++ b/internal/cmd/testdata/format-fix-v2/prototool.yaml
@@ -0,0 +1,2 @@
+lint:
+  group: uber2
diff --git a/internal/protostrs/protostrs_test.go b/internal/protostrs/protostrs_test.go
index 84ef06e..0987f9d 100644
--- a/internal/protostrs/protostrs_test.go
+++ b/internal/protostrs/protostrs_test.go
@@ -52,11 +52,16 @@ func TestGoPackage(t *testing.T) {
 	assert.Equal(t, "barpb", GoPackage("foo.bar"))
 }
 
-func TestGoPackageLastTwo(t *testing.T) {
-	assert.Equal(t, "", GoPackageLastTwo(""))
-	assert.Equal(t, "foo", GoPackageLastTwo("foo"))
-	assert.Equal(t, "foobar", GoPackageLastTwo("foo.bar"))
-	assert.Equal(t, "foobar", GoPackageLastTwo("first.foo.bar"))
+func TestGoPackageV2(t *testing.T) {
+	assert.Equal(t, "", GoPackageV2(""))
+	assert.Equal(t, "foopb", GoPackageV2("foo"))
+	assert.Equal(t, "barpb", GoPackageV2("foo.bar"))
+	assert.Equal(t, "barpb", GoPackageV2("first.foo.bar"))
+	assert.Equal(t, "barv1", GoPackageV2("foo.bar.v1"))
+	assert.Equal(t, "barv1", GoPackageV2("first.foo.bar.v1"))
+	assert.Equal(t, "barv1beta1", GoPackageV2("foo.bar.v1beta1"))
+	assert.Equal(t, "barv1beta1", GoPackageV2("first.foo.bar.v1beta1"))
+	assert.Equal(t, "v1betaa1pb", GoPackageV2("foo.bar.v1betaa1"))
 }
 
 func TestJavaOuterClassname(t *testing.T) {