blob: 89310267c3f1164dc68e8907c5fd975a1794518e [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 (
"bufio"
"context"
_ "embed"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
"strings"
"sync"
"github.com/emirpasic/gods/sets/treeset"
godsutils "github.com/emirpasic/gods/utils"
)
var (
parserCmd *exec.Cmd
parserStdin io.WriteCloser
parserStdout io.Reader
parserMutex sync.Mutex
)
func startParserProcess(ctx context.Context) {
// due to #691, we need a system interpreter to boostrap, part of which is
// to locate the hermetic interpreter.
parserCmd = exec.CommandContext(ctx, "python3", helperPath, "parse")
parserCmd.Stderr = os.Stderr
stdin, err := parserCmd.StdinPipe()
if err != nil {
log.Printf("failed to initialize parser: %v\n", err)
os.Exit(1)
}
parserStdin = stdin
stdout, err := parserCmd.StdoutPipe()
if err != nil {
log.Printf("failed to initialize parser: %v\n", err)
os.Exit(1)
}
parserStdout = stdout
if err := parserCmd.Start(); err != nil {
log.Printf("failed to initialize parser: %v\n", err)
os.Exit(1)
}
}
func shutdownParserProcess() {
if err := parserStdin.Close(); err != nil {
fmt.Fprintf(os.Stderr, "error closing parser: %v", err)
}
if err := parserCmd.Wait(); err != nil {
log.Printf("failed to wait for parser: %v\n", err)
}
}
// python3Parser implements a parser for Python files that extracts the modules
// as seen in the import statements.
type python3Parser struct {
// The value of language.GenerateArgs.Config.RepoRoot.
repoRoot string
// The value of language.GenerateArgs.Rel.
relPackagePath string
// The function that determines if a dependency is ignored from a Gazelle
// directive. It's the signature of pythonconfig.Config.IgnoresDependency.
ignoresDependency func(dep string) bool
}
// newPython3Parser constructs a new python3Parser.
func newPython3Parser(
repoRoot string,
relPackagePath string,
ignoresDependency func(dep string) bool,
) *python3Parser {
return &python3Parser{
repoRoot: repoRoot,
relPackagePath: relPackagePath,
ignoresDependency: ignoresDependency,
}
}
// parseSingle parses a single Python file and returns the extracted modules
// from the import statements as well as the parsed comments.
func (p *python3Parser) parseSingle(pyFilename string) (*treeset.Set, error) {
pyFilenames := treeset.NewWith(godsutils.StringComparator)
pyFilenames.Add(pyFilename)
return p.parse(pyFilenames)
}
// parse parses multiple Python files and returns the extracted modules from
// the import statements as well as the parsed comments.
func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, error) {
parserMutex.Lock()
defer parserMutex.Unlock()
modules := treeset.NewWith(moduleComparator)
req := map[string]interface{}{
"repo_root": p.repoRoot,
"rel_package_path": p.relPackagePath,
"filenames": pyFilenames.Values(),
}
encoder := json.NewEncoder(parserStdin)
if err := encoder.Encode(&req); err != nil {
return nil, fmt.Errorf("failed to parse: %w", err)
}
reader := bufio.NewReader(parserStdout)
data, err := reader.ReadBytes(0)
if err != nil {
return nil, fmt.Errorf("failed to parse: %w", err)
}
data = data[:len(data)-1]
var allRes []parserResponse
if err := json.Unmarshal(data, &allRes); err != nil {
return nil, fmt.Errorf("failed to parse: %w", err)
}
for _, res := range allRes {
annotations, err := annotationsFromComments(res.Comments)
if err != nil {
return nil, fmt.Errorf("failed to parse annotations: %w", err)
}
for _, m := range res.Modules {
// Check for ignored dependencies set via an annotation to the Python
// module.
if annotations.ignores(m.Name) || annotations.ignores(m.From) {
continue
}
// Check for ignored dependencies set via a Gazelle directive in a BUILD
// file.
if p.ignoresDependency(m.Name) || p.ignoresDependency(m.From) {
continue
}
modules.Add(m)
}
}
return modules, nil
}
// parserResponse represents a response returned by the parser.py for a given
// parsed Python module.
type parserResponse struct {
// The modules depended by the parsed module.
Modules []module `json:"modules"`
// The comments contained in the parsed module. This contains the
// annotations as they are comments in the Python module.
Comments []comment `json:"comments"`
}
// module represents a fully-qualified, dot-separated, Python module as seen on
// the import statement, alongside the line number where it happened.
type module struct {
// The fully-qualified, dot-separated, Python module name as seen on import
// statements.
Name string `json:"name"`
// The line number where the import happened.
LineNumber uint32 `json:"lineno"`
// The path to the module file relative to the Bazel workspace root.
Filepath string `json:"filepath"`
// If this was a from import, e.g. from foo import bar, From indicates the module
// from which it is imported.
From string `json:"from"`
}
// moduleComparator compares modules by name.
func moduleComparator(a, b interface{}) int {
return godsutils.StringComparator(a.(module).Name, b.(module).Name)
}
// annotationKind represents Gazelle annotation kinds.
type annotationKind string
const (
// The Gazelle annotation prefix.
annotationPrefix string = "gazelle:"
// The ignore annotation kind. E.g. '# gazelle:ignore <module_name>'.
annotationKindIgnore annotationKind = "ignore"
)
// comment represents a Python comment.
type comment string
// asAnnotation returns an annotation object if the comment has the
// annotationPrefix.
func (c *comment) asAnnotation() (*annotation, error) {
uncomment := strings.TrimLeft(string(*c), "# ")
if !strings.HasPrefix(uncomment, annotationPrefix) {
return nil, nil
}
withoutPrefix := strings.TrimPrefix(uncomment, annotationPrefix)
annotationParts := strings.SplitN(withoutPrefix, " ", 2)
if len(annotationParts) < 2 {
return nil, fmt.Errorf("`%s` requires a value", *c)
}
return &annotation{
kind: annotationKind(annotationParts[0]),
value: annotationParts[1],
}, nil
}
// annotation represents a single Gazelle annotation parsed from a Python
// comment.
type annotation struct {
kind annotationKind
value string
}
// annotations represent the collection of all Gazelle annotations parsed out of
// the comments of a Python module.
type annotations struct {
// The parsed modules to be ignored by Gazelle.
ignore map[string]struct{}
}
// annotationsFromComments returns all the annotations parsed out of the
// comments of a Python module.
func annotationsFromComments(comments []comment) (*annotations, error) {
ignore := make(map[string]struct{})
for _, comment := range comments {
annotation, err := comment.asAnnotation()
if err != nil {
return nil, err
}
if annotation != nil {
if annotation.kind == annotationKindIgnore {
modules := strings.Split(annotation.value, ",")
for _, m := range modules {
if m == "" {
continue
}
m = strings.TrimSpace(m)
ignore[m] = struct{}{}
}
}
}
}
return &annotations{
ignore: ignore,
}, nil
}
// ignored returns true if the given module was ignored via the ignore
// annotation.
func (a *annotations) ignores(module string) bool {
_, ignores := a.ignore[module]
return ignores
}