blob: a369a64b8e44514bf8e4e874b98502c1f8a939f8 [file] [log] [blame]
// 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
}