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 }