github.com/Zenithar/prototool@v1.3.0/internal/file/proto_set_provider.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 file 22 23 import ( 24 "fmt" 25 "os" 26 "path/filepath" 27 "sort" 28 "time" 29 30 "github.com/uber/prototool/internal/settings" 31 "go.uber.org/zap" 32 ) 33 34 type protoSetProvider struct { 35 logger *zap.Logger 36 configData string 37 walkTimeout time.Duration 38 configProvider settings.ConfigProvider 39 } 40 41 func newProtoSetProvider(options ...ProtoSetProviderOption) *protoSetProvider { 42 protoSetProvider := &protoSetProvider{ 43 logger: zap.NewNop(), 44 walkTimeout: DefaultWalkTimeout, 45 } 46 for _, option := range options { 47 option(protoSetProvider) 48 } 49 protoSetProvider.configProvider = settings.NewConfigProvider( 50 settings.ConfigProviderWithLogger(protoSetProvider.logger), 51 ) 52 return protoSetProvider 53 } 54 55 func (c *protoSetProvider) GetForDir(workDirPath string, dirPath string) (*ProtoSet, error) { 56 protoSets, err := c.GetMultipleForDir(workDirPath, dirPath) 57 if err != nil { 58 return nil, err 59 } 60 switch len(protoSets) { 61 case 0: 62 return nil, fmt.Errorf("no proto files found for dirPath %q", dirPath) 63 case 1: 64 return protoSets[0], nil 65 default: 66 configDirPaths := make([]string, 0, len(protoSets)) 67 for _, protoSet := range protoSets { 68 configDirPaths = append(configDirPaths, protoSet.Config.DirPath) 69 } 70 return nil, fmt.Errorf("expected exactly one configuration file for dirPath %q, but found multiple in directories: %v", dirPath, configDirPaths) 71 } 72 } 73 74 func (c *protoSetProvider) GetForFiles(workDirPath string, filePaths ...string) (*ProtoSet, error) { 75 protoSets, err := c.GetMultipleForFiles(workDirPath, filePaths...) 76 if err != nil { 77 return nil, err 78 } 79 switch len(protoSets) { 80 case 0: 81 return nil, fmt.Errorf("no proto files found for filePaths %v", filePaths) 82 case 1: 83 return protoSets[0], nil 84 default: 85 configDirPaths := make([]string, 0, len(protoSets)) 86 for _, protoSet := range protoSets { 87 configDirPaths = append(configDirPaths, protoSet.Config.DirPath) 88 } 89 return nil, fmt.Errorf("expected exactly one configuration file for filePaths %v, but found multiple in directories: %v", filePaths, configDirPaths) 90 } 91 } 92 93 func (c *protoSetProvider) GetMultipleForDir(workDirPath string, dirPath string) ([]*ProtoSet, error) { 94 workDirPath, err := AbsClean(workDirPath) 95 if err != nil { 96 return nil, err 97 } 98 absDirPath, err := AbsClean(dirPath) 99 if err != nil { 100 return nil, err 101 } 102 // If c.configData != ", the user has specified configuration via the command line. 103 // Set the configuration directory to the current working directory. 104 configDirPath := workDirPath 105 if c.configData == "" { 106 configFilePath, err := c.configProvider.GetFilePathForDir(absDirPath) 107 if err != nil { 108 return nil, err 109 } 110 // we need everything for generation, not just the files in the given directory 111 // so we go back to the config file if it is shallower 112 // display path will be unaffected as this is based on workDirPath 113 configDirPath = absDirPath 114 if configFilePath != "" { 115 configDirPath = filepath.Dir(configFilePath) 116 } 117 } 118 protoFiles, err := c.walkAndGetAllProtoFiles(workDirPath, configDirPath) 119 if err != nil { 120 return nil, err 121 } 122 dirPathToProtoFiles := getDirPathToProtoFiles(protoFiles) 123 protoSets, err := c.getBaseProtoSets(workDirPath, dirPathToProtoFiles) 124 if err != nil { 125 return nil, err 126 } 127 for _, protoSet := range protoSets { 128 protoSet.WorkDirPath = workDirPath 129 protoSet.DirPath = absDirPath 130 } 131 c.logger.Debug("returning ProtoSets", zap.String("workDirPath", workDirPath), zap.String("dirPath", dirPath), zap.Any("protoSets", protoSets)) 132 return protoSets, nil 133 } 134 135 func (c *protoSetProvider) GetMultipleForFiles(workDirPath string, filePaths ...string) ([]*ProtoSet, error) { 136 workDirPath, err := AbsClean(workDirPath) 137 if err != nil { 138 return nil, err 139 } 140 protoFiles, err := getProtoFiles(filePaths) 141 if err != nil { 142 return nil, err 143 } 144 dirPathToProtoFiles := getDirPathToProtoFiles(protoFiles) 145 protoSets, err := c.getBaseProtoSets(workDirPath, dirPathToProtoFiles) 146 if err != nil { 147 return nil, err 148 } 149 for _, protoSet := range protoSets { 150 protoSet.WorkDirPath = workDirPath 151 protoSet.DirPath = workDirPath 152 } 153 c.logger.Debug("returning ProtoSets", zap.String("workDirPath", workDirPath), zap.Strings("filePaths", filePaths), zap.Any("protoSets", protoSets)) 154 return protoSets, nil 155 } 156 157 func (c *protoSetProvider) getBaseProtoSets(absWorkDirPath string, dirPathToProtoFiles map[string][]*ProtoFile) ([]*ProtoSet, error) { 158 filePathToProtoSet := make(map[string]*ProtoSet) 159 for dirPath, protoFiles := range dirPathToProtoFiles { 160 var configFilePath string 161 var err error 162 // we only want one ProtoSet if we have set configData 163 // since we are overriding all configuration files 164 if c.configData == "" { 165 configFilePath, err = c.configProvider.GetFilePathForDir(dirPath) 166 if err != nil { 167 return nil, err 168 } 169 } 170 protoSet, ok := filePathToProtoSet[configFilePath] 171 if !ok { 172 protoSet = &ProtoSet{ 173 DirPathToFiles: make(map[string][]*ProtoFile), 174 } 175 filePathToProtoSet[configFilePath] = protoSet 176 } 177 protoSet.DirPathToFiles[dirPath] = append(protoSet.DirPathToFiles[dirPath], protoFiles...) 178 var config settings.Config 179 if c.configData != "" { 180 config, err = c.configProvider.GetForData(absWorkDirPath, c.configData) 181 if err != nil { 182 return nil, err 183 } 184 } else if configFilePath != "" { 185 // configFilePath is empty if no config file is found 186 config, err = c.configProvider.Get(configFilePath) 187 if err != nil { 188 return nil, err 189 } 190 } 191 protoSet.Config = config 192 } 193 protoSets := make([]*ProtoSet, 0, len(filePathToProtoSet)) 194 for _, protoSet := range filePathToProtoSet { 195 protoSets = append(protoSets, protoSet) 196 } 197 sort.Slice(protoSets, func(i int, j int) bool { 198 return protoSets[i].Config.DirPath < protoSets[j].Config.DirPath 199 }) 200 return protoSets, nil 201 } 202 203 // walkAndGetAllProtoFiles collects the .proto files nested under the given absDirPath. 204 // absDirPath represents the absolute path at which the configuration file is 205 // found, whereas absWorkDirPath represents absolute path at which prototool was invoked. 206 // absWorkDirPath is only used to determine the ProtoFile.DisplayPath, also known as 207 // the relative path from where prototool was invoked. 208 func (c *protoSetProvider) walkAndGetAllProtoFiles(absWorkDirPath string, absDirPath string) ([]*ProtoFile, error) { 209 var ( 210 protoFiles []*ProtoFile 211 numWalkedFiles int 212 timedOut bool 213 ) 214 allExcludes := make(map[string]struct{}) 215 // if we have a configData, we compute the exclude prefixes once 216 // from this dirPath and data, and do not do it again in the below walk function 217 if c.configData != "" { 218 excludes, err := c.configProvider.GetExcludePrefixesForData(absWorkDirPath, c.configData) 219 if err != nil { 220 return nil, err 221 } 222 for _, exclude := range excludes { 223 allExcludes[exclude] = struct{}{} 224 } 225 } 226 walkErrC := make(chan error) 227 go func() { 228 walkErrC <- filepath.Walk( 229 absDirPath, 230 func(filePath string, fileInfo os.FileInfo, err error) error { 231 if err != nil { 232 return err 233 } 234 numWalkedFiles++ 235 if timedOut { 236 return fmt.Errorf("walking the diectory structure looking for proto files "+ 237 "timed out after %v and having seen %d files, are you sure you are operating "+ 238 "in the right context?", c.walkTimeout, numWalkedFiles) 239 } 240 // Verify if we should skip this directory/file. 241 if fileInfo.IsDir() { 242 // Add the excluded files with respect to the current file path. 243 // Do not add if we have configData. 244 if c.configData == "" { 245 excludes, err := c.configProvider.GetExcludePrefixesForDir(filePath) 246 if err != nil { 247 return err 248 } 249 for _, exclude := range excludes { 250 allExcludes[exclude] = struct{}{} 251 } 252 } 253 if isExcluded(filePath, absDirPath, allExcludes) { 254 return filepath.SkipDir 255 } 256 return nil 257 } 258 if filepath.Ext(filePath) != ".proto" { 259 return nil 260 } 261 if isExcluded(filePath, absDirPath, allExcludes) { 262 return nil 263 } 264 265 // Visit this file. 266 displayPath, err := filepath.Rel(absWorkDirPath, filePath) 267 if err != nil { 268 displayPath = filePath 269 } 270 displayPath = filepath.Clean(displayPath) 271 protoFiles = append(protoFiles, &ProtoFile{ 272 Path: filePath, 273 DisplayPath: displayPath, 274 }) 275 return nil 276 }, 277 ) 278 }() 279 if c.walkTimeout == 0 { 280 if walkErr := <-walkErrC; walkErr != nil { 281 return nil, walkErr 282 } 283 return protoFiles, nil 284 } 285 select { 286 case walkErr := <-walkErrC: 287 if walkErr != nil { 288 return nil, walkErr 289 } 290 return protoFiles, nil 291 case <-time.After(c.walkTimeout): 292 timedOut = true 293 if walkErr := <-walkErrC; walkErr != nil { 294 return nil, walkErr 295 } 296 return nil, fmt.Errorf("internal prototool error") 297 } 298 } 299 300 func getDirPathToProtoFiles(protoFiles []*ProtoFile) map[string][]*ProtoFile { 301 dirPathToProtoFiles := make(map[string][]*ProtoFile) 302 for _, protoFile := range protoFiles { 303 dir := filepath.Dir(protoFile.Path) 304 dirPathToProtoFiles[dir] = append(dirPathToProtoFiles[dir], protoFile) 305 } 306 return dirPathToProtoFiles 307 } 308 309 func getProtoFiles(filePaths []string) ([]*ProtoFile, error) { 310 protoFiles := make([]*ProtoFile, 0, len(filePaths)) 311 for _, filePath := range filePaths { 312 absFilePath, err := AbsClean(filePath) 313 if err != nil { 314 return nil, err 315 } 316 protoFiles = append(protoFiles, &ProtoFile{ 317 Path: absFilePath, 318 DisplayPath: filePath, 319 }) 320 } 321 return protoFiles, nil 322 } 323 324 // isExcluded determines whether the given filePath should be excluded. 325 // Note that all excludes are assumed to be cleaned absolute paths at 326 // this point. 327 // stopPath represents the absolute path to the prototool configuration. 328 // This is used to determine when we should stop checking for excludes. 329 func isExcluded(filePath, stopPath string, excludes map[string]struct{}) bool { 330 // Use the root as a fallback so that we don't loop forever. 331 root := filepath.Dir(string(filepath.Separator)) 332 333 isNested := func(curr, exclude string) bool { 334 for { 335 if curr == stopPath || curr == root { 336 return false 337 } 338 if curr == exclude { 339 return true 340 } 341 curr = filepath.Dir(curr) 342 } 343 } 344 for exclude := range excludes { 345 if isNested(filePath, exclude) { 346 return true 347 } 348 } 349 return false 350 351 }