|  | // Copyright 2023 The Bazel Authors. All rights reserved. | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | package python | 
|  |  | 
|  | import ( | 
|  | "flag" | 
|  | "fmt" | 
|  | "log" | 
|  | "os" | 
|  | "path/filepath" | 
|  | "strconv" | 
|  | "strings" | 
|  |  | 
|  | "github.com/bazelbuild/bazel-gazelle/config" | 
|  | "github.com/bazelbuild/bazel-gazelle/rule" | 
|  | "github.com/bmatcuk/doublestar/v4" | 
|  |  | 
|  | "github.com/bazelbuild/rules_python/gazelle/manifest" | 
|  | "github.com/bazelbuild/rules_python/gazelle/pythonconfig" | 
|  | ) | 
|  |  | 
|  | // Configurer satisfies the config.Configurer interface. It's the | 
|  | // language-specific configuration extension. | 
|  | type Configurer struct{} | 
|  |  | 
|  | // RegisterFlags registers command-line flags used by the extension. This | 
|  | // method is called once with the root configuration when Gazelle | 
|  | // starts. RegisterFlags may set an initial values in Config.Exts. When flags | 
|  | // are set, they should modify these values. | 
|  | func (py *Configurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {} | 
|  |  | 
|  | // CheckFlags validates the configuration after command line flags are parsed. | 
|  | // This is called once with the root configuration when Gazelle starts. | 
|  | // CheckFlags may set default values in flags or make implied changes. | 
|  | func (py *Configurer) CheckFlags(fs *flag.FlagSet, c *config.Config) error { | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // KnownDirectives returns a list of directive keys that this Configurer can | 
|  | // interpret. Gazelle prints errors for directives that are not recoginized by | 
|  | // any Configurer. | 
|  | func (py *Configurer) KnownDirectives() []string { | 
|  | return []string{ | 
|  | pythonconfig.PythonExtensionDirective, | 
|  | pythonconfig.PythonRootDirective, | 
|  | pythonconfig.PythonManifestFileNameDirective, | 
|  | pythonconfig.IgnoreFilesDirective, | 
|  | pythonconfig.IgnoreDependenciesDirective, | 
|  | pythonconfig.ValidateImportStatementsDirective, | 
|  | pythonconfig.GenerationMode, | 
|  | pythonconfig.GenerationModePerFileIncludeInit, | 
|  | pythonconfig.GenerationModePerPackageRequireTestEntryPoint, | 
|  | pythonconfig.LibraryNamingConvention, | 
|  | pythonconfig.BinaryNamingConvention, | 
|  | pythonconfig.TestNamingConvention, | 
|  | pythonconfig.DefaultVisibilty, | 
|  | pythonconfig.Visibility, | 
|  | pythonconfig.TestFilePattern, | 
|  | pythonconfig.LabelConvention, | 
|  | pythonconfig.LabelNormalization, | 
|  | } | 
|  | } | 
|  |  | 
|  | // Configure modifies the configuration using directives and other information | 
|  | // extracted from a build file. Configure is called in each directory. | 
|  | // | 
|  | // c is the configuration for the current directory. It starts out as a copy | 
|  | // of the configuration for the parent directory. | 
|  | // | 
|  | // rel is the slash-separated relative path from the repository root to | 
|  | // the current directory. It is "" for the root directory itself. | 
|  | // | 
|  | // f is the build file for the current directory or nil if there is no | 
|  | // existing build file. | 
|  | func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) { | 
|  | // Create the root config. | 
|  | if _, exists := c.Exts[languageName]; !exists { | 
|  | rootConfig := pythonconfig.New(c.RepoRoot, "") | 
|  | c.Exts[languageName] = pythonconfig.Configs{"": rootConfig} | 
|  | } | 
|  |  | 
|  | configs := c.Exts[languageName].(pythonconfig.Configs) | 
|  |  | 
|  | config, exists := configs[rel] | 
|  | if !exists { | 
|  | parent := configs.ParentForPackage(rel) | 
|  | config = parent.NewChild() | 
|  | configs[rel] = config | 
|  | } | 
|  |  | 
|  | if f == nil { | 
|  | return | 
|  | } | 
|  |  | 
|  | gazelleManifestFilename := "gazelle_python.yaml" | 
|  |  | 
|  | for _, d := range f.Directives { | 
|  | switch d.Key { | 
|  | case "exclude": | 
|  | // We record the exclude directive for coarse-grained packages | 
|  | // since we do manual tree traversal in this mode. | 
|  | config.AddExcludedPattern(filepath.Join(rel, strings.TrimSpace(d.Value))) | 
|  | case pythonconfig.PythonExtensionDirective: | 
|  | switch d.Value { | 
|  | case "enabled": | 
|  | config.SetExtensionEnabled(true) | 
|  | case "disabled": | 
|  | config.SetExtensionEnabled(false) | 
|  | default: | 
|  | err := fmt.Errorf("invalid value for directive %q: %s: possible values are enabled/disabled", | 
|  | pythonconfig.PythonExtensionDirective, d.Value) | 
|  | log.Fatal(err) | 
|  | } | 
|  | case pythonconfig.PythonRootDirective: | 
|  | config.SetPythonProjectRoot(rel) | 
|  | config.SetDefaultVisibility([]string{fmt.Sprintf(pythonconfig.DefaultVisibilityFmtString, rel)}) | 
|  | case pythonconfig.PythonManifestFileNameDirective: | 
|  | gazelleManifestFilename = strings.TrimSpace(d.Value) | 
|  | case pythonconfig.IgnoreFilesDirective: | 
|  | for _, ignoreFile := range strings.Split(d.Value, ",") { | 
|  | config.AddIgnoreFile(ignoreFile) | 
|  | } | 
|  | case pythonconfig.IgnoreDependenciesDirective: | 
|  | for _, ignoreDependency := range strings.Split(d.Value, ",") { | 
|  | config.AddIgnoreDependency(ignoreDependency) | 
|  | } | 
|  | case pythonconfig.ValidateImportStatementsDirective: | 
|  | v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) | 
|  | if err != nil { | 
|  | log.Fatal(err) | 
|  | } | 
|  | config.SetValidateImportStatements(v) | 
|  | case pythonconfig.GenerationMode: | 
|  | switch pythonconfig.GenerationModeType(strings.TrimSpace(d.Value)) { | 
|  | case pythonconfig.GenerationModePackage: | 
|  | config.SetCoarseGrainedGeneration(false) | 
|  | config.SetPerFileGeneration(false) | 
|  | case pythonconfig.GenerationModeFile: | 
|  | config.SetCoarseGrainedGeneration(false) | 
|  | config.SetPerFileGeneration(true) | 
|  | case pythonconfig.GenerationModeProject: | 
|  | config.SetCoarseGrainedGeneration(true) | 
|  | config.SetPerFileGeneration(false) | 
|  | default: | 
|  | err := fmt.Errorf("invalid value for directive %q: %s", | 
|  | pythonconfig.GenerationMode, d.Value) | 
|  | log.Fatal(err) | 
|  | } | 
|  | case pythonconfig.GenerationModePerFileIncludeInit: | 
|  | v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) | 
|  | if err != nil { | 
|  | log.Fatal(err) | 
|  | } | 
|  | config.SetPerFileGenerationIncludeInit(v) | 
|  | case pythonconfig.GenerationModePerPackageRequireTestEntryPoint: | 
|  | v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) | 
|  | if err != nil { | 
|  | log.Printf("invalid value for gazelle:%s in %q: %q", | 
|  | pythonconfig.GenerationModePerPackageRequireTestEntryPoint, rel, d.Value) | 
|  | } else { | 
|  | config.SetPerPackageGenerationRequireTestEntryPoint(v) | 
|  | } | 
|  | case pythonconfig.LibraryNamingConvention: | 
|  | config.SetLibraryNamingConvention(strings.TrimSpace(d.Value)) | 
|  | case pythonconfig.BinaryNamingConvention: | 
|  | config.SetBinaryNamingConvention(strings.TrimSpace(d.Value)) | 
|  | case pythonconfig.TestNamingConvention: | 
|  | config.SetTestNamingConvention(strings.TrimSpace(d.Value)) | 
|  | case pythonconfig.DefaultVisibilty: | 
|  | switch directiveArg := strings.TrimSpace(d.Value); directiveArg { | 
|  | case "NONE": | 
|  | config.SetDefaultVisibility([]string{}) | 
|  | case "DEFAULT": | 
|  | pythonProjectRoot := config.PythonProjectRoot() | 
|  | defaultVisibility := fmt.Sprintf(pythonconfig.DefaultVisibilityFmtString, pythonProjectRoot) | 
|  | config.SetDefaultVisibility([]string{defaultVisibility}) | 
|  | default: | 
|  | // Handle injecting the python root. Assume that the user used the | 
|  | // exact string "$python_root$". | 
|  | labels := strings.ReplaceAll(directiveArg, "$python_root$", config.PythonProjectRoot()) | 
|  | config.SetDefaultVisibility(strings.Split(labels, ",")) | 
|  | } | 
|  | case pythonconfig.Visibility: | 
|  | labels := strings.ReplaceAll(strings.TrimSpace(d.Value), "$python_root$", config.PythonProjectRoot()) | 
|  | config.AppendVisibility(labels) | 
|  | case pythonconfig.TestFilePattern: | 
|  | value := strings.TrimSpace(d.Value) | 
|  | if value == "" { | 
|  | log.Fatal("directive 'python_test_file_pattern' requires a value") | 
|  | } | 
|  | globStrings := strings.Split(value, ",") | 
|  | for _, g := range globStrings { | 
|  | if !doublestar.ValidatePattern(g) { | 
|  | log.Fatalf("invalid glob pattern '%s'", g) | 
|  | } | 
|  | } | 
|  | config.SetTestFilePattern(globStrings) | 
|  | case pythonconfig.LabelConvention: | 
|  | value := strings.TrimSpace(d.Value) | 
|  | if value == "" { | 
|  | log.Fatalf("directive '%s' requires a value", pythonconfig.LabelConvention) | 
|  | } | 
|  | config.SetLabelConvention(value) | 
|  | case pythonconfig.LabelNormalization: | 
|  | switch directiveArg := strings.ToLower(strings.TrimSpace(d.Value)); directiveArg { | 
|  | case "pep503": | 
|  | config.SetLabelNormalization(pythonconfig.Pep503LabelNormalizationType) | 
|  | case "none": | 
|  | config.SetLabelNormalization(pythonconfig.NoLabelNormalizationType) | 
|  | case "snake_case": | 
|  | config.SetLabelNormalization(pythonconfig.SnakeCaseLabelNormalizationType) | 
|  | default: | 
|  | config.SetLabelNormalization(pythonconfig.DefaultLabelNormalizationType) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | gazelleManifestPath := filepath.Join(c.RepoRoot, rel, gazelleManifestFilename) | 
|  | gazelleManifest, err := py.loadGazelleManifest(gazelleManifestPath) | 
|  | if err != nil { | 
|  | log.Fatal(err) | 
|  | } | 
|  | if gazelleManifest != nil { | 
|  | config.SetGazelleManifest(gazelleManifest) | 
|  | } | 
|  | } | 
|  |  | 
|  | func (py *Configurer) loadGazelleManifest(gazelleManifestPath string) (*manifest.Manifest, error) { | 
|  | if _, err := os.Stat(gazelleManifestPath); err != nil { | 
|  | if os.IsNotExist(err) { | 
|  | return nil, nil | 
|  | } | 
|  | return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err) | 
|  | } | 
|  | manifestFile := new(manifest.File) | 
|  | if err := manifestFile.Decode(gazelleManifestPath); err != nil { | 
|  | return nil, fmt.Errorf("failed to load Gazelle manifest at %q: %w", gazelleManifestPath, err) | 
|  | } | 
|  | return manifestFile.Manifest, nil | 
|  | } |