github.com/nikron/prototool@v1.3.0/internal/lint/lint.go (about)

     1  // Copyright (c) 2018 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package lint
    22  
    23  import (
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  
    28  	"github.com/emicklei/proto"
    29  	"github.com/uber/prototool/internal/file"
    30  	"github.com/uber/prototool/internal/settings"
    31  	"github.com/uber/prototool/internal/text"
    32  	"go.uber.org/zap"
    33  )
    34  
    35  var (
    36  	// AllLinters is the slice of all known Linters.
    37  	AllLinters = []Linter{
    38  		commentsNoCStyleLinter,
    39  		enumFieldNamesUppercaseLinter,
    40  		enumFieldNamesUpperSnakeCaseLinter,
    41  		enumFieldPrefixesLinter,
    42  		enumNamesCamelCaseLinter,
    43  		enumNamesCapitalizedLinter,
    44  		enumZeroValuesInvalidLinter,
    45  		enumsHaveCommentsLinter,
    46  		enumsNoAllowAliasLinter,
    47  		fileOptionsEqualGoPackagePbSuffixLinter,
    48  		fileOptionsEqualJavaMultipleFilesTrueLinter,
    49  		fileOptionsEqualJavaOuterClassnameProtoSuffixLinter,
    50  		fileOptionsEqualJavaPackageComPrefixLinter,
    51  		fileOptionsGoPackageNotLongFormLinter,
    52  		fileOptionsGoPackageSameInDirLinter,
    53  		fileOptionsJavaMultipleFilesSameInDirLinter,
    54  		fileOptionsJavaPackageSameInDirLinter,
    55  		fileOptionsRequireGoPackageLinter,
    56  		fileOptionsRequireJavaMultipleFilesLinter,
    57  		fileOptionsRequireJavaOuterClassnameLinter,
    58  		fileOptionsRequireJavaPackageLinter,
    59  		fileOptionsUnsetJavaMultipleFilesLinter,
    60  		fileOptionsUnsetJavaOuterClassnameLinter,
    61  		messageFieldsNotFloatsLinter,
    62  		messageFieldNamesLowerSnakeCaseLinter,
    63  		messageFieldNamesLowercaseLinter,
    64  		messageNamesCamelCaseLinter,
    65  		messageNamesCapitalizedLinter,
    66  		messagesHaveCommentsLinter,
    67  		messagesHaveCommentsExceptRequestResponseTypesLinter,
    68  		oneofNamesLowerSnakeCaseLinter,
    69  		packageIsDeclaredLinter,
    70  		packageLowerSnakeCaseLinter,
    71  		packagesSameInDirLinter,
    72  		rpcsHaveCommentsLinter,
    73  		rpcNamesCamelCaseLinter,
    74  		rpcNamesCapitalizedLinter,
    75  		requestResponseTypesInSameFileLinter,
    76  		requestResponseTypesUniqueLinter,
    77  		requestResponseNamesMatchRPCLinter,
    78  		servicesHaveCommentsLinter,
    79  		serviceNamesCamelCaseLinter,
    80  		serviceNamesCapitalizedLinter,
    81  		syntaxProto3Linter,
    82  		wktDirectlyImportedLinter,
    83  	}
    84  
    85  	// DefaultLinters is the slice of default Linters.
    86  	DefaultLinters = copyLintersWithout(
    87  		AllLinters,
    88  		enumFieldNamesUppercaseLinter,
    89  		enumsHaveCommentsLinter,
    90  		fileOptionsUnsetJavaMultipleFilesLinter,
    91  		fileOptionsUnsetJavaOuterClassnameLinter,
    92  		messageFieldsNotFloatsLinter,
    93  		messagesHaveCommentsLinter,
    94  		messagesHaveCommentsExceptRequestResponseTypesLinter,
    95  		messageFieldNamesLowercaseLinter,
    96  		requestResponseNamesMatchRPCLinter,
    97  		rpcsHaveCommentsLinter,
    98  		servicesHaveCommentsLinter,
    99  	)
   100  
   101  	// DefaultGroup is the default group.
   102  	DefaultGroup = "default"
   103  
   104  	// AllGroup is the group of all known linters.
   105  	AllGroup = "all"
   106  
   107  	// GroupToLinters is the map from linter group to the corresponding slice of linters.
   108  	GroupToLinters = map[string][]Linter{
   109  		DefaultGroup: DefaultLinters,
   110  		AllGroup:     AllLinters,
   111  	}
   112  )
   113  
   114  func init() {
   115  	ids := make(map[string]struct{})
   116  	for _, linter := range AllLinters {
   117  		if _, ok := ids[linter.ID()]; ok {
   118  			panic(fmt.Sprintf("duplicate linter id %s", linter.ID()))
   119  		}
   120  		ids[linter.ID()] = struct{}{}
   121  	}
   122  }
   123  
   124  // Runner runs a lint job.
   125  type Runner interface {
   126  	Run(*file.ProtoSet) ([]*text.Failure, error)
   127  }
   128  
   129  // RunnerOption is an option for a new Runner.
   130  type RunnerOption func(*runner)
   131  
   132  // RunnerWithLogger returns a RunnerOption that uses the given logger.
   133  //
   134  // The default is to use zap.NewNop().
   135  func RunnerWithLogger(logger *zap.Logger) RunnerOption {
   136  	return func(runner *runner) {
   137  		runner.logger = logger
   138  	}
   139  }
   140  
   141  // NewRunner returns a new Runner.
   142  func NewRunner(options ...RunnerOption) Runner {
   143  	return newRunner(options...)
   144  }
   145  
   146  // The below should not be needed in the CLI
   147  // TODO make private
   148  
   149  // Linter is a linter for Protobuf files.
   150  type Linter interface {
   151  	// Return the ID of this Linter. This should be all UPPER_SNAKE_CASE.
   152  	ID() string
   153  	// Return the purpose of this Linter. This should be a human-readable string.
   154  	Purpose() string
   155  	// Check the file data for the descriptors in a common directgory.
   156  	// If there is a lint failure, this returns it in the
   157  	// slice and does not return an error. An error is returned if something
   158  	// unexpected happens. Callers should verify the files are compilable
   159  	// before running this.
   160  	Check(dirPath string, descriptors []*proto.Proto) ([]*text.Failure, error)
   161  }
   162  
   163  // NewLinter is a convenience function that returns a new Linter for the
   164  // given parameters, using a function to record failures.
   165  //
   166  // The ID will be upper-cased.
   167  //
   168  // Failures returned from check do not need to set the ID, this will be overwritten.
   169  func NewLinter(id string, purpose string, addCheck func(func(*text.Failure), string, []*proto.Proto) error) Linter {
   170  	return newBaseLinter(id, purpose, addCheck)
   171  }
   172  
   173  // GetLinters returns the Linters for the LintConfig.
   174  //
   175  // The configuration is expected to be valid, deduplicated, and all upper-case.
   176  // IncludeIDs and ExcludeIDs MUST NOT have an intersection.
   177  //
   178  // If the config came from the settings package, this is already validated.
   179  func GetLinters(config settings.LintConfig) ([]Linter, error) {
   180  	var linters []Linter
   181  	if !config.NoDefault {
   182  		linters = DefaultLinters
   183  	}
   184  	if len(config.IncludeIDs) == 0 && len(config.ExcludeIDs) == 0 {
   185  		return linters, nil
   186  	}
   187  
   188  	// Apply the configured linters to the default group.
   189  	linterMap := make(map[string]Linter, len(linters)+len(config.IncludeIDs)-len(config.ExcludeIDs))
   190  	for _, l := range linters {
   191  		linterMap[l.ID()] = l
   192  	}
   193  	if len(config.IncludeIDs) > 0 {
   194  		for _, l := range AllLinters {
   195  			for _, id := range config.IncludeIDs {
   196  				if l.ID() == id {
   197  					linterMap[id] = l
   198  				}
   199  			}
   200  		}
   201  	}
   202  	for _, excludeID := range config.ExcludeIDs {
   203  		delete(linterMap, excludeID)
   204  	}
   205  
   206  	result := make([]Linter, 0, len(linterMap))
   207  	for _, l := range linterMap {
   208  		result = append(result, l)
   209  	}
   210  	return result, nil
   211  }
   212  
   213  // GetDirPathToDescriptors is a convenience function that gets the
   214  // descriptors for the given ProtoSet.
   215  func GetDirPathToDescriptors(protoSet *file.ProtoSet) (map[string][]*proto.Proto, error) {
   216  	dirPathToDescriptors := make(map[string][]*proto.Proto, len(protoSet.DirPathToFiles))
   217  	for dirPath, protoFiles := range protoSet.DirPathToFiles {
   218  		descriptors := make([]*proto.Proto, len(protoFiles))
   219  		for i, protoFile := range protoFiles {
   220  			file, err := os.Open(protoFile.Path)
   221  			if err != nil {
   222  				return nil, err
   223  			}
   224  			parser := proto.NewParser(file)
   225  			parser.Filename(protoFile.DisplayPath)
   226  			descriptor, err := parser.Parse()
   227  			_ = file.Close()
   228  			if err != nil {
   229  				return nil, err
   230  			}
   231  			descriptors[i] = descriptor
   232  		}
   233  		dirPathToDescriptors[dirPath] = descriptors
   234  	}
   235  	return dirPathToDescriptors, nil
   236  }
   237  
   238  // CheckMultiple is a convenience function that checks multiple linters and multiple descriptors.
   239  func CheckMultiple(linters []Linter, dirPathToDescriptors map[string][]*proto.Proto, ignoreIDToFilePaths map[string][]string) ([]*text.Failure, error) {
   240  	var allFailures []*text.Failure
   241  	for dirPath, descriptors := range dirPathToDescriptors {
   242  		for _, linter := range linters {
   243  			failures, err := checkOne(linter, dirPath, descriptors, ignoreIDToFilePaths)
   244  			if err != nil {
   245  				return nil, err
   246  			}
   247  			allFailures = append(allFailures, failures...)
   248  		}
   249  	}
   250  	text.SortFailures(allFailures)
   251  	return allFailures, nil
   252  }
   253  
   254  func checkOne(linter Linter, dirPath string, descriptors []*proto.Proto, ignoreIDToFilePaths map[string][]string) ([]*text.Failure, error) {
   255  	filteredDescriptors, err := filterIgnores(linter, descriptors, ignoreIDToFilePaths)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  	return linter.Check(dirPath, filteredDescriptors)
   260  }
   261  
   262  func filterIgnores(linter Linter, descriptors []*proto.Proto, ignoreIDToFilePaths map[string][]string) ([]*proto.Proto, error) {
   263  	var filteredDescriptors []*proto.Proto
   264  	for _, descriptor := range descriptors {
   265  		ignore, err := shouldIgnore(linter, descriptor, ignoreIDToFilePaths)
   266  		if err != nil {
   267  			return nil, err
   268  		}
   269  		if !ignore {
   270  			filteredDescriptors = append(filteredDescriptors, descriptor)
   271  		}
   272  	}
   273  	return filteredDescriptors, nil
   274  }
   275  
   276  func shouldIgnore(linter Linter, descriptor *proto.Proto, ignoreIDToFilePaths map[string][]string) (bool, error) {
   277  	filePath := descriptor.Filename
   278  	var err error
   279  	if !filepath.IsAbs(filePath) {
   280  		filePath, err = filepath.Abs(filePath)
   281  		if err != nil {
   282  			return false, err
   283  		}
   284  	}
   285  	ignoreFilePaths, ok := ignoreIDToFilePaths[linter.ID()]
   286  	if !ok {
   287  		return false, nil
   288  	}
   289  	for _, ignoreFilePath := range ignoreFilePaths {
   290  		if filePath == ignoreFilePath {
   291  			return true, nil
   292  		}
   293  	}
   294  	return false, nil
   295  }
   296  
   297  func copyLintersWithout(linters []Linter, remove ...Linter) []Linter {
   298  	c := make([]Linter, 0, len(linters))
   299  	for _, linter := range linters {
   300  		if !linterIn(linter, remove) {
   301  			c = append(c, linter)
   302  		}
   303  	}
   304  	return c
   305  }
   306  
   307  func linterIn(linter Linter, s []Linter) bool {
   308  	for _, e := range s {
   309  		if e == linter {
   310  			return true
   311  		}
   312  	}
   313  	return false
   314  }