1// Copyright 2023 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 15package python 16 17import ( 18 "flag" 19 "fmt" 20 "log" 21 "os" 22 "path/filepath" 23 "strconv" 24 "strings" 25 26 "github.com/bazelbuild/bazel-gazelle/config" 27 "github.com/bazelbuild/bazel-gazelle/rule" 28 "github.com/bmatcuk/doublestar/v4" 29 30 "github.com/bazelbuild/rules_python/gazelle/manifest" 31 "github.com/bazelbuild/rules_python/gazelle/pythonconfig" 32) 33 34// Configurer satisfies the config.Configurer interface. It's the 35// language-specific configuration extension. 36type Configurer struct{} 37 38// RegisterFlags registers command-line flags used by the extension. This 39// method is called once with the root configuration when Gazelle 40// starts. RegisterFlags may set an initial values in Config.Exts. When flags 41// are set, they should modify these values. 42func (py *Configurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {} 43 44// CheckFlags validates the configuration after command line flags are parsed. 45// This is called once with the root configuration when Gazelle starts. 46// CheckFlags may set default values in flags or make implied changes. 47func (py *Configurer) CheckFlags(fs *flag.FlagSet, c *config.Config) error { 48 return nil 49} 50 51// KnownDirectives returns a list of directive keys that this Configurer can 52// interpret. Gazelle prints errors for directives that are not recoginized by 53// any Configurer. 54func (py *Configurer) KnownDirectives() []string { 55 return []string{ 56 pythonconfig.PythonExtensionDirective, 57 pythonconfig.PythonRootDirective, 58 pythonconfig.PythonManifestFileNameDirective, 59 pythonconfig.IgnoreFilesDirective, 60 pythonconfig.IgnoreDependenciesDirective, 61 pythonconfig.ValidateImportStatementsDirective, 62 pythonconfig.GenerationMode, 63 pythonconfig.GenerationModePerFileIncludeInit, 64 pythonconfig.GenerationModePerPackageRequireTestEntryPoint, 65 pythonconfig.LibraryNamingConvention, 66 pythonconfig.BinaryNamingConvention, 67 pythonconfig.TestNamingConvention, 68 pythonconfig.DefaultVisibilty, 69 pythonconfig.Visibility, 70 pythonconfig.TestFilePattern, 71 pythonconfig.LabelConvention, 72 pythonconfig.LabelNormalization, 73 } 74} 75 76// Configure modifies the configuration using directives and other information 77// extracted from a build file. Configure is called in each directory. 78// 79// c is the configuration for the current directory. It starts out as a copy 80// of the configuration for the parent directory. 81// 82// rel is the slash-separated relative path from the repository root to 83// the current directory. It is "" for the root directory itself. 84// 85// f is the build file for the current directory or nil if there is no 86// existing build file. 87func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) { 88 // Create the root config. 89 if _, exists := c.Exts[languageName]; !exists { 90 rootConfig := pythonconfig.New(c.RepoRoot, "") 91 c.Exts[languageName] = pythonconfig.Configs{"": rootConfig} 92 } 93 94 configs := c.Exts[languageName].(pythonconfig.Configs) 95 96 config, exists := configs[rel] 97 if !exists { 98 parent := configs.ParentForPackage(rel) 99 config = parent.NewChild() 100 configs[rel] = config 101 } 102 103 if f == nil { 104 return 105 } 106 107 gazelleManifestFilename := "gazelle_python.yaml" 108 109 for _, d := range f.Directives { 110 switch d.Key { 111 case "exclude": 112 // We record the exclude directive for coarse-grained packages 113 // since we do manual tree traversal in this mode. 114 config.AddExcludedPattern(filepath.Join(rel, strings.TrimSpace(d.Value))) 115 case pythonconfig.PythonExtensionDirective: 116 switch d.Value { 117 case "enabled": 118 config.SetExtensionEnabled(true) 119 case "disabled": 120 config.SetExtensionEnabled(false) 121 default: 122 err := fmt.Errorf("invalid value for directive %q: %s: possible values are enabled/disabled", 123 pythonconfig.PythonExtensionDirective, d.Value) 124 log.Fatal(err) 125 } 126 case pythonconfig.PythonRootDirective: 127 config.SetPythonProjectRoot(rel) 128 config.SetDefaultVisibility([]string{fmt.Sprintf(pythonconfig.DefaultVisibilityFmtString, rel)}) 129 case pythonconfig.PythonManifestFileNameDirective: 130 gazelleManifestFilename = strings.TrimSpace(d.Value) 131 case pythonconfig.IgnoreFilesDirective: 132 for _, ignoreFile := range strings.Split(d.Value, ",") { 133 config.AddIgnoreFile(ignoreFile) 134 } 135 case pythonconfig.IgnoreDependenciesDirective: 136 for _, ignoreDependency := range strings.Split(d.Value, ",") { 137 config.AddIgnoreDependency(ignoreDependency) 138 } 139 case pythonconfig.ValidateImportStatementsDirective: 140 v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) 141 if err != nil { 142 log.Fatal(err) 143 } 144 config.SetValidateImportStatements(v) 145 case pythonconfig.GenerationMode: 146 switch pythonconfig.GenerationModeType(strings.TrimSpace(d.Value)) { 147 case pythonconfig.GenerationModePackage: 148 config.SetCoarseGrainedGeneration(false) 149 config.SetPerFileGeneration(false) 150 case pythonconfig.GenerationModeFile: 151 config.SetCoarseGrainedGeneration(false) 152 config.SetPerFileGeneration(true) 153 case pythonconfig.GenerationModeProject: 154 config.SetCoarseGrainedGeneration(true) 155 config.SetPerFileGeneration(false) 156 default: 157 err := fmt.Errorf("invalid value for directive %q: %s", 158 pythonconfig.GenerationMode, d.Value) 159 log.Fatal(err) 160 } 161 case pythonconfig.GenerationModePerFileIncludeInit: 162 v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) 163 if err != nil { 164 log.Fatal(err) 165 } 166 config.SetPerFileGenerationIncludeInit(v) 167 case pythonconfig.GenerationModePerPackageRequireTestEntryPoint: 168 v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) 169 if err != nil { 170 log.Printf("invalid value for gazelle:%s in %q: %q", 171 pythonconfig.GenerationModePerPackageRequireTestEntryPoint, rel, d.Value) 172 } else { 173 config.SetPerPackageGenerationRequireTestEntryPoint(v) 174 } 175 case pythonconfig.LibraryNamingConvention: 176 config.SetLibraryNamingConvention(strings.TrimSpace(d.Value)) 177 case pythonconfig.BinaryNamingConvention: 178 config.SetBinaryNamingConvention(strings.TrimSpace(d.Value)) 179 case pythonconfig.TestNamingConvention: 180 config.SetTestNamingConvention(strings.TrimSpace(d.Value)) 181 case pythonconfig.DefaultVisibilty: 182 switch directiveArg := strings.TrimSpace(d.Value); directiveArg { 183 case "NONE": 184 config.SetDefaultVisibility([]string{}) 185 case "DEFAULT": 186 pythonProjectRoot := config.PythonProjectRoot() 187 defaultVisibility := fmt.Sprintf(pythonconfig.DefaultVisibilityFmtString, pythonProjectRoot) 188 config.SetDefaultVisibility([]string{defaultVisibility}) 189 default: 190 // Handle injecting the python root. Assume that the user used the 191 // exact string "$python_root$". 192 labels := strings.ReplaceAll(directiveArg, "$python_root$", config.PythonProjectRoot()) 193 config.SetDefaultVisibility(strings.Split(labels, ",")) 194 } 195 case pythonconfig.Visibility: 196 labels := strings.ReplaceAll(strings.TrimSpace(d.Value), "$python_root$", config.PythonProjectRoot()) 197 config.AppendVisibility(labels) 198 case pythonconfig.TestFilePattern: 199 value := strings.TrimSpace(d.Value) 200 if value == "" { 201 log.Fatal("directive 'python_test_file_pattern' requires a value") 202 } 203 globStrings := strings.Split(value, ",") 204 for _, g := range globStrings { 205 if !doublestar.ValidatePattern(g) { 206 log.Fatalf("invalid glob pattern '%s'", g) 207 } 208 } 209 config.SetTestFilePattern(globStrings) 210 case pythonconfig.LabelConvention: 211 value := strings.TrimSpace(d.Value) 212 if value == "" { 213 log.Fatalf("directive '%s' requires a value", pythonconfig.LabelConvention) 214 } 215 config.SetLabelConvention(value) 216 case pythonconfig.LabelNormalization: 217 switch directiveArg := strings.ToLower(strings.TrimSpace(d.Value)); directiveArg { 218 case "pep503": 219 config.SetLabelNormalization(pythonconfig.Pep503LabelNormalizationType) 220 case "none": 221 config.SetLabelNormalization(pythonconfig.NoLabelNormalizationType) 222 case "snake_case": 223 config.SetLabelNormalization(pythonconfig.SnakeCaseLabelNormalizationType) 224 default: 225 config.SetLabelNormalization(pythonconfig.DefaultLabelNormalizationType) 226 } 227 } 228 } 229 230 gazelleManifestPath := filepath.Join(c.RepoRoot, rel, gazelleManifestFilename) 231 gazelleManifest, err := py.loadGazelleManifest(gazelleManifestPath) 232 if err != nil { 233 log.Fatal(err) 234 } 235 if gazelleManifest != nil { 236 config.SetGazelleManifest(gazelleManifest) 237 } 238} 239 240func (py *Configurer) loadGazelleManifest(gazelleManifestPath string) (*manifest.Manifest, error) { 241 if _, err := os.Stat(gazelleManifestPath); err != nil { 242 if os.IsNotExist(err) { 243 return nil, nil 244 } 245 return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err) 246 } 247 manifestFile := new(manifest.File) 248 if err := manifestFile.Decode(gazelleManifestPath); err != nil { 249 return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err) 250 } 251 return manifestFile.Manifest, nil 252} 253