go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/cli/base/config.go (about) 1 // Copyright 2022 The LUCI Authors. 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 // Package base contains code shared by other CLI subpackages. 16 package base 17 18 import ( 19 "fmt" 20 "io/fs" 21 "os" 22 "path/filepath" 23 "strings" 24 25 "github.com/bazelbuild/buildtools/build" 26 "go.chromium.org/luci/common/data/stringset" 27 "go.chromium.org/luci/common/errors" 28 "go.chromium.org/luci/common/system/filesystem" 29 "go.chromium.org/luci/lucicfg/buildifier" 30 "go.chromium.org/luci/lucicfg/vars" 31 "google.golang.org/protobuf/encoding/prototext" 32 ) 33 34 // ConfigName is the file name we will be used for lucicfg formatting 35 const ConfigName = ".lucicfgfmtrc" 36 37 // sentinel is used to prevent the walking functions in this package from walking 38 // across a source control boundary. As of 2023 Q1 we are only worried about Git 39 // repos, but should we ever support more VCS's and this walking code is still 40 // required (i.e. this hasn't been replaced with a WORKSPACE style config file), 41 // this should be extended. 42 var sentinel = []string{".git"} 43 44 // RewriterFactory is used to map from 'file to be formatted' to a Rewriter object, 45 // via its GetRewriter method. 46 // 47 // This struct is obtained via the GetRewriterFactory function. 48 type RewriterFactory struct { 49 rules []pathRules 50 configFilePath string 51 } 52 53 type pathRules struct { 54 path string // absolute path to the folder where this rules applies. 55 rules *buildifier.LucicfgFmtConfig_Rules 56 } 57 58 // CheckForBogusConfig will look for any config files contained in a subdirectory of entryPath 59 // (recursively). 60 // 61 // Because we intend for there to be at most one config file per workspace, and for that config 62 // file to be located at the root of the workspace, any such extra config files would be errors. 63 // Due to the 'stateless' nature of fmt and lint, we search down the directory hierarchy here to 64 // try to detect such misconfiguration, but in the future when these subcommands become 65 // stateful (like validate currently is), we may remove this check. 66 func CheckForBogusConfig(entryPath string) error { 67 // Traverse downwards 68 if err := filepath.WalkDir(entryPath, func(path string, d fs.DirEntry, err error) error { 69 if err != nil { 70 return err 71 } 72 // Skip checking of entry path, downwards is exclusive 73 if d.IsDir() && path != entryPath { 74 if _, err := os.Stat(filepath.Join(path, ConfigName)); err == nil { 75 return errors.Reason( 76 "\nFound a config in a subdirectory<%s> of a star file."+ 77 "Please move to the highest common ancestor directory - %s\n", 78 path, 79 entryPath).Err() 80 } else if !errors.Is(err, os.ErrNotExist) { 81 return err 82 } 83 } 84 return nil 85 }); err != nil { 86 return err 87 } else { 88 return nil 89 } 90 } 91 92 func findConfigPathUpwards(path string) (string, error) { 93 var currentDir = path 94 for { 95 if _, err := os.Stat(filepath.Join(currentDir, ConfigName)); err == nil { 96 return filepath.Join(currentDir, ConfigName), nil 97 } else if !errors.Is(err, os.ErrNotExist) { 98 return "", err 99 } else { 100 var parent = filepath.Dir(currentDir) 101 102 if _, err := os.Stat(filepath.Join(path, ".git")); err == nil || parent == currentDir { 103 return "", nil 104 } 105 106 currentDir = parent 107 } 108 } 109 } 110 111 func convertOrderingToTable(nameOrdering []string) map[string]int { 112 count := len(nameOrdering) 113 table := make(map[string]int, count) 114 // This sequentially gives the names a priority value in the range 115 // [-count, 0). This ensures that all names have distinct priority 116 // values that sort them in the specified order. Since all priority 117 // values are less than the default 0, all names present in the 118 // ordering will sort before names that don't appear in the ordering. 119 for i, n := range nameOrdering { 120 table[n] = i - count 121 } 122 return table 123 } 124 125 func rewriterFromConfig(nameOrdering map[string]int) *build.Rewriter { 126 var rewriter = vars.GetDefaultRewriter() 127 if nameOrdering != nil { 128 rewriter.NamePriority = nameOrdering 129 rewriter.RewriteSet = append(rewriter.RewriteSet, "callsort") 130 } 131 return rewriter 132 } 133 134 // GetRewriterFactory will attempt to create a RewriterFactory object 135 // 136 // If configPath is empty, or points to a file which doesn't exist, the returned 137 // factory will just produce GetDefaultRewriter() when asked about any path. 138 // We will return an error if the config file is invalid. 139 func GetRewriterFactory(configPath string) (rewriterFactory *RewriterFactory, err error) { 140 rewriterFactory = &RewriterFactory{ 141 rules: []pathRules{}, 142 configFilePath: "", 143 } 144 if configPath == "" { 145 return 146 } 147 contents, err := os.ReadFile(configPath) 148 if err != nil { 149 if !errors.Is(err, os.ErrNotExist) { 150 fmt.Printf("Failed on reading file - %s", configPath) 151 return nil, err 152 } else { 153 return rewriterFactory, nil 154 } 155 } 156 luci := &buildifier.LucicfgFmtConfig{} 157 158 if err := prototext.Unmarshal(contents, luci); err != nil { 159 return nil, err 160 } 161 return getPostProcessedRewriterFactory(configPath, luci) 162 } 163 164 // getPostProcessedRewriterFactory will contain all logic used to make sure 165 // RewriterFactory is normalized the way we want. 166 // 167 // Currently, we will fix paths so that they are absolute. 168 // We will also perform a check so that there are no duplicate paths and 169 // all paths are delimited with "/" 170 func getPostProcessedRewriterFactory(configPath string, cfg *buildifier.LucicfgFmtConfig) (*RewriterFactory, error) { 171 pathSet := stringset.New(0) 172 rules := cfg.Rules 173 rulesSlice := make([]pathRules, 0) 174 for ruleIndex, rule := range rules { 175 // If a rule doesn't have any paths, err out and notify users 176 if len(rule.Path) == 0 { 177 return nil, errors.Reason( 178 "rule[%d]: Does not contain any paths", 179 ruleIndex).Err() 180 } 181 for rulePathIndex, pathInDir := range rule.Path { 182 // Fix paths. Update to use absolute path. 183 fixedPathInDir := filepath.Clean( 184 filepath.Join(filepath.Dir(configPath), pathInDir), 185 ) 186 // Check for duplicate paths. If there is, return error 187 if pathSet.Contains(stringset.NewFromSlice(fixedPathInDir)) { 188 return nil, errors.Reason( 189 "rule[%d].path[%d]: Found duplicate path '%s'", 190 ruleIndex, rulePathIndex, pathInDir).Err() 191 } 192 // Check for backslash in path, if there is, return error 193 if strings.Contains(pathInDir, "\\") { 194 return nil, errors.Reason( 195 "rule[%d].path[%d]: Path should not contain backslash '%s'", 196 ruleIndex, rulePathIndex, pathInDir).Err() 197 } 198 // Add into set to check later if duplicate 199 pathSet.Add(fixedPathInDir) 200 if fixedPathInDirAbs, err := filepath.Abs(fixedPathInDir); err != nil { 201 return nil, errors.Annotate(err, "rule[%d].path[%d]: filepath.Abs error %s", 202 ruleIndex, rulePathIndex, pathInDir).Err() 203 } else { 204 fixedPathInDir = fixedPathInDirAbs 205 } 206 207 rulesSlice = append(rulesSlice, pathRules{ 208 fixedPathInDir, 209 rule, 210 }) 211 } 212 } 213 214 return &RewriterFactory{ 215 rulesSlice, 216 filepath.Dir(configPath), 217 }, nil 218 } 219 220 // GetRewriter will return the Rewriter which is appropriate for formatting 221 // the file at `path`, using the previously loaded formatting configuration. 222 // 223 // Note the method signature will pass in values that we need to evaluate 224 // the correct rewriter. 225 // 226 // We will accept both relative and absolute paths. 227 func (f *RewriterFactory) GetRewriter(path string) (*build.Rewriter, error) { 228 rules := f.rules 229 // Check if path is abs, if not, fix it 230 if !filepath.IsAbs(path) { 231 return nil, errors.Reason("GetRewriter got non-absolute path: %q", path).Err() 232 } 233 longestPathMatch := "" 234 var matchingRule *buildifier.LucicfgFmtConfig_Rules 235 236 // Find the path that best matches the one we are processing. 237 for _, rule := range rules { 238 commonAncestor, err := filesystem.GetCommonAncestor( 239 []string{rule.path, path}, 240 sentinel, 241 ) 242 243 if err != nil { 244 return nil, err 245 } 246 247 commonAncestor = filepath.Clean(commonAncestor) 248 if commonAncestor == rule.path && len(commonAncestor) > len(longestPathMatch) { 249 longestPathMatch = commonAncestor 250 matchingRule = rule.rules 251 } 252 } 253 if matchingRule != nil && matchingRule.FunctionArgsSort != nil { 254 return rewriterFromConfig( 255 convertOrderingToTable(matchingRule.FunctionArgsSort.Arg), 256 ), nil 257 } 258 259 return vars.GetDefaultRewriter(), nil 260 } 261 262 // GuessRewriterFactoryFunc will find the common ancestor dir from all given paths 263 // and return a func that returns the rewriter factory. 264 // 265 // Will look for a config file upwards(inclusive). If found, it will be used to determine 266 // rewriter properties. It will also look downwards(exclusive) to expose any misplaced 267 // config files. 268 func GuessRewriterFactoryFunc(paths []string) (*RewriterFactory, error) { 269 // Find the common ancestor 270 commonAncestorPath, err := filesystem.GetCommonAncestor(paths, sentinel) 271 272 if errors.Is(err, filesystem.ErrRootSentinel) { 273 // we hit the repo root, just return function that returns default rewriter 274 rewriterFactory, err := GetRewriterFactory("") 275 if err != nil { 276 return nil, err 277 } 278 return rewriterFactory, nil 279 } 280 if err != nil { 281 // other errors are fatal 282 return nil, err 283 } 284 if err := CheckForBogusConfig(commonAncestorPath); err != nil { 285 return nil, err 286 } 287 288 luciConfigPath, err := findConfigPathUpwards(commonAncestorPath) 289 if err != nil { 290 return nil, err 291 } 292 rewriterFactory, err := GetRewriterFactory(luciConfigPath) 293 if err != nil { 294 return nil, err 295 } 296 297 return rewriterFactory, nil 298 }