github.com/yoheimuta/protolint@v0.49.8-0.20240515023657-4ecaebb7575d/internal/addon/rules/fileNamesLowerSnakeCaseRule.go (about)

     1  package rules
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  	"strings"
     7  
     8  	"github.com/yoheimuta/protolint/internal/stringsutil"
     9  
    10  	"github.com/yoheimuta/go-protoparser/v4/parser"
    11  
    12  	"github.com/yoheimuta/protolint/linter/report"
    13  	"github.com/yoheimuta/protolint/linter/rule"
    14  	"github.com/yoheimuta/protolint/linter/strs"
    15  	"github.com/yoheimuta/protolint/linter/visitor"
    16  )
    17  
    18  // FileNamesLowerSnakeCaseRule verifies that all file names are lower_snake_case.proto.
    19  // See https://developers.google.com/protocol-buffers/docs/style#file-structure.
    20  type FileNamesLowerSnakeCaseRule struct {
    21  	RuleWithSeverity
    22  	excluded []string
    23  	fixMode  bool
    24  }
    25  
    26  // NewFileNamesLowerSnakeCaseRule creates a new FileNamesLowerSnakeCaseRule.
    27  func NewFileNamesLowerSnakeCaseRule(
    28  	severity rule.Severity,
    29  	excluded []string,
    30  	fixMode bool,
    31  ) FileNamesLowerSnakeCaseRule {
    32  	return FileNamesLowerSnakeCaseRule{
    33  		RuleWithSeverity: RuleWithSeverity{severity: severity},
    34  		excluded:         excluded,
    35  		fixMode:          fixMode,
    36  	}
    37  }
    38  
    39  // ID returns the ID of this rule.
    40  func (r FileNamesLowerSnakeCaseRule) ID() string {
    41  	return "FILE_NAMES_LOWER_SNAKE_CASE"
    42  }
    43  
    44  // Purpose returns the purpose of this rule.
    45  func (r FileNamesLowerSnakeCaseRule) Purpose() string {
    46  	return "Verifies that all file names are lower_snake_case.proto."
    47  }
    48  
    49  // IsOfficial decides whether or not this rule belongs to the official guide.
    50  func (r FileNamesLowerSnakeCaseRule) IsOfficial() bool {
    51  	return true
    52  }
    53  
    54  // Apply applies the rule to the proto.
    55  func (r FileNamesLowerSnakeCaseRule) Apply(proto *parser.Proto) ([]report.Failure, error) {
    56  	v := &fileNamesLowerSnakeCaseVisitor{
    57  		BaseAddVisitor: visitor.NewBaseAddVisitor(r.ID(), string(r.Severity())),
    58  		excluded:       r.excluded,
    59  		fixMode:        r.fixMode,
    60  	}
    61  	return visitor.RunVisitor(v, proto, r.ID())
    62  }
    63  
    64  type fileNamesLowerSnakeCaseVisitor struct {
    65  	*visitor.BaseAddVisitor
    66  	excluded []string
    67  	fixMode  bool
    68  }
    69  
    70  // OnStart checks the file.
    71  func (v *fileNamesLowerSnakeCaseVisitor) OnStart(proto *parser.Proto) error {
    72  	path := proto.Meta.Filename
    73  	if stringsutil.ContainsStringInSlice(path, v.excluded) {
    74  		return nil
    75  	}
    76  
    77  	filename := filepath.Base(path)
    78  	ext := filepath.Ext(filename)
    79  	base := strings.TrimSuffix(filename, ext)
    80  	if ext != ".proto" || !strs.IsLowerSnakeCase(base) {
    81  		expected := strs.ToLowerSnakeCase(base)
    82  		expected += ".proto"
    83  		v.AddFailurefWithProtoMeta(proto.Meta, "File name %q should be lower_snake_case.proto like %q.", filename, expected)
    84  
    85  		if v.fixMode {
    86  			dir := filepath.Dir(path)
    87  			newPath := filepath.Join(dir, expected)
    88  			if _, err := os.Stat(newPath); !os.IsNotExist(err) {
    89  				v.AddFailurefWithProtoMeta(proto.Meta, "Failed to rename %q because %q already exists.", filename, expected)
    90  				return nil
    91  			}
    92  			err := os.Rename(path, newPath)
    93  			if err != nil {
    94  				return err
    95  			}
    96  
    97  			// Notify the upstream this new filename by updating the proto.
    98  			proto.Meta.Filename = newPath
    99  		}
   100  	}
   101  	return nil
   102  }