github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/walk/config.go (about)

     1  /* Copyright 2018 The Bazel Authors. All rights reserved.
     2  
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7     http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package walk
    17  
    18  import (
    19  	"bufio"
    20  	"errors"
    21  	"flag"
    22  	"fmt"
    23  	"io/fs"
    24  	"log"
    25  	"os"
    26  	"path"
    27  	"strings"
    28  	"sync"
    29  
    30  	"github.com/bazelbuild/bazel-gazelle/config"
    31  	"github.com/bazelbuild/bazel-gazelle/rule"
    32  	"github.com/bmatcuk/doublestar/v4"
    33  
    34  	gzflag "github.com/bazelbuild/bazel-gazelle/flag"
    35  )
    36  
    37  // TODO(#472): store location information to validate each exclude. They
    38  // may be set in one directory and used in another. Excludes work on
    39  // declared generated files, so we can't just stat.
    40  
    41  type walkConfig struct {
    42  	excludes []string
    43  	ignore   bool
    44  	follow   []string
    45  	loadOnce *sync.Once
    46  }
    47  
    48  const walkName = "_walk"
    49  
    50  func getWalkConfig(c *config.Config) *walkConfig {
    51  	return c.Exts[walkName].(*walkConfig)
    52  }
    53  
    54  func (wc *walkConfig) isExcluded(rel, base string) bool {
    55  	if base == ".git" {
    56  		return true
    57  	}
    58  	return matchAnyGlob(wc.excludes, path.Join(rel, base))
    59  }
    60  
    61  func (wc *walkConfig) shouldFollow(rel, base string) bool {
    62  	return matchAnyGlob(wc.follow, path.Join(rel, base))
    63  }
    64  
    65  type Configurer struct{}
    66  
    67  func (*Configurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {
    68  	wc := &walkConfig{loadOnce: &sync.Once{}}
    69  	c.Exts[walkName] = wc
    70  	fs.Var(&gzflag.MultiFlag{Values: &wc.excludes}, "exclude", "pattern that should be ignored (may be repeated)")
    71  }
    72  
    73  func (*Configurer) CheckFlags(fs *flag.FlagSet, c *config.Config) error { return nil }
    74  
    75  func (*Configurer) KnownDirectives() []string {
    76  	return []string{"exclude", "follow", "ignore"}
    77  }
    78  
    79  func (cr *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
    80  	wc := getWalkConfig(c)
    81  	wcCopy := &walkConfig{}
    82  	*wcCopy = *wc
    83  	wcCopy.ignore = false
    84  
    85  	wc.loadOnce.Do(func() {
    86  		if err := cr.loadBazelIgnore(c.RepoRoot, wcCopy); err != nil {
    87  			log.Printf("error loading .bazelignore: %v", err)
    88  		}
    89  	})
    90  
    91  	if f != nil {
    92  		for _, d := range f.Directives {
    93  			switch d.Key {
    94  			case "exclude":
    95  				if err := checkPathMatchPattern(path.Join(rel, d.Value)); err != nil {
    96  					log.Printf("the exclusion pattern is not valid %q: %s", path.Join(rel, d.Value), err)
    97  					continue
    98  				}
    99  				wcCopy.excludes = append(wcCopy.excludes, path.Join(rel, d.Value))
   100  			case "follow":
   101  				if err := checkPathMatchPattern(path.Join(rel, d.Value)); err != nil {
   102  					log.Printf("the follow pattern is not valid %q: %s", path.Join(rel, d.Value), err)
   103  					continue
   104  				}
   105  				wcCopy.follow = append(wcCopy.follow, path.Join(rel, d.Value))
   106  			case "ignore":
   107  				wcCopy.ignore = true
   108  			}
   109  		}
   110  	}
   111  
   112  	c.Exts[walkName] = wcCopy
   113  }
   114  
   115  func (c *Configurer) loadBazelIgnore(repoRoot string, wc *walkConfig) error {
   116  	ignorePath := path.Join(repoRoot, ".bazelignore")
   117  	file, err := os.Open(ignorePath)
   118  	if errors.Is(err, fs.ErrNotExist) {
   119  		return nil
   120  	}
   121  	if err != nil {
   122  		return fmt.Errorf(".bazelignore exists but couldn't be read: %v", err)
   123  	}
   124  	defer file.Close()
   125  
   126  	scanner := bufio.NewScanner(file)
   127  	for scanner.Scan() {
   128  		ignore := strings.TrimSpace(scanner.Text())
   129  		if ignore == "" || string(ignore[0]) == "#" {
   130  			continue
   131  		}
   132  		// Bazel ignore paths are always relative to repo root.
   133  		// Glob patterns are not supported.
   134  		if strings.ContainsAny(ignore, "*?[") {
   135  			log.Printf("the .bazelignore exclusion pattern must not be a glob %s", ignore)
   136  			continue
   137  		}
   138  		// Ensure we remove trailing slashes or the exclude matching won't work correctly
   139  		wc.excludes = append(wc.excludes, strings.TrimSuffix(ignore, "/"))
   140  	}
   141  	return nil
   142  }
   143  
   144  func checkPathMatchPattern(pattern string) error {
   145  	_, err := doublestar.Match(pattern, "x")
   146  	return err
   147  }
   148  
   149  func matchAnyGlob(patterns []string, path string) bool {
   150  	for _, x := range patterns {
   151  		matched, err := doublestar.Match(x, path)
   152  		if err != nil {
   153  			// doublestar.Match returns only one possible error, and only if the
   154  			// pattern is not valid. During the configuration of the walker (see
   155  			// Configure below), we discard any invalid pattern and thus an error
   156  			// here should not be possible.
   157  			log.Panicf("error during doublestar.Match. This should not happen, please file an issue https://github.com/bazelbuild/bazel-gazelle/issues/new: %s", err)
   158  		}
   159  		if matched {
   160  			return true
   161  		}
   162  	}
   163  	return false
   164  }