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  }