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 }