2020-06-26 15:04:12 +00:00
/ * Copyright 2020 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 .
* /
2020-08-20 17:14:55 +00:00
// Package bzl generates a `bzl_library` target for every `.bzl` file in
2020-06-26 15:04:12 +00:00
// each package.
//
// The `bzl_library` rule is provided by
// https://github.com/bazelbuild/bazel-skylib.
//
// This extension is experimental and subject to change. It is not included
// in the default Gazelle binary.
2020-08-20 17:14:55 +00:00
package bzl
2020-06-26 15:04:12 +00:00
import (
"flag"
"fmt"
"io/ioutil"
"log"
"path/filepath"
"sort"
"strings"
"github.com/bazelbuild/bazel-gazelle/config"
"github.com/bazelbuild/bazel-gazelle/label"
"github.com/bazelbuild/bazel-gazelle/language"
"github.com/bazelbuild/bazel-gazelle/pathtools"
"github.com/bazelbuild/bazel-gazelle/repo"
"github.com/bazelbuild/bazel-gazelle/resolve"
"github.com/bazelbuild/bazel-gazelle/rule"
"github.com/bazelbuild/buildtools/build"
)
const languageName = "starlark"
const fileType = ".bzl"
var ignoreSuffix = suffixes {
"_tests.bzl" ,
"_test.bzl" ,
}
type suffixes [ ] string
func ( s suffixes ) Matches ( test string ) bool {
for _ , v := range s {
if strings . HasSuffix ( test , v ) {
return true
}
}
return false
}
type bzlLibraryLang struct { }
// NewLanguage is called by Gazelle to install this language extension in a binary.
func NewLanguage ( ) language . Language {
return & bzlLibraryLang { }
}
// Name returns the name of the language. This should be a prefix of the
// kinds of rules generated by the language, e.g., "go" for the Go extension
// since it generates "go_library" rules.
func ( * bzlLibraryLang ) Name ( ) string { return languageName }
// The following methods are implemented to satisfy the
// https://pkg.go.dev/github.com/bazelbuild/bazel-gazelle/resolve?tab=doc#Resolver
// interface, but are otherwise unused.
func ( * bzlLibraryLang ) RegisterFlags ( fs * flag . FlagSet , cmd string , c * config . Config ) { }
func ( * bzlLibraryLang ) CheckFlags ( fs * flag . FlagSet , c * config . Config ) error { return nil }
func ( * bzlLibraryLang ) KnownDirectives ( ) [ ] string { return nil }
func ( * bzlLibraryLang ) Configure ( c * config . Config , rel string , f * rule . File ) { }
// Kinds returns a map of maps rule names (kinds) and information on how to
// match and merge attributes that may be found in rules of those kinds. All
// kinds of rules generated for this language may be found here.
func ( * bzlLibraryLang ) Kinds ( ) map [ string ] rule . KindInfo {
return kinds
}
// Loads returns .bzl files and symbols they define. Every rule generated by
// GenerateRules, now or in the past, should be loadable from one of these
// files.
func ( * bzlLibraryLang ) Loads ( ) [ ] rule . LoadInfo {
return [ ] rule . LoadInfo { {
Name : "@bazel_skylib//:bzl_library.bzl" ,
Symbols : [ ] string { "bzl_library" } ,
} }
}
// Fix repairs deprecated usage of language-specific rules in f. This is
// called before the file is indexed. Unless c.ShouldFix is true, fixes
// that delete or rename rules should not be performed.
func ( * bzlLibraryLang ) Fix ( c * config . Config , f * rule . File ) { }
// Imports returns a list of ImportSpecs that can be used to import the rule
// r. This is used to populate RuleIndex.
//
// If nil is returned, the rule will not be indexed. If any non-nil slice is
// returned, including an empty slice, the rule will be indexed.
func ( b * bzlLibraryLang ) Imports ( c * config . Config , r * rule . Rule , f * rule . File ) [ ] resolve . ImportSpec {
srcs := r . AttrStrings ( "srcs" )
imports := make ( [ ] resolve . ImportSpec , len ( srcs ) )
for _ , src := range srcs {
spec := resolve . ImportSpec {
// Lang is the language in which the import string appears (this should
// match Resolver.Name).
Lang : languageName ,
// Imp is an import string for the library.
Imp : fmt . Sprintf ( "//%s:%s" , f . Pkg , src ) ,
}
imports = append ( imports , spec )
}
return imports
}
// Embeds returns a list of labels of rules that the given rule embeds. If
// a rule is embedded by another importable rule of the same language, only
// the embedding rule will be indexed. The embedding rule will inherit
// the imports of the embedded rule.
// Since SkyLark doesn't support embedding this should always return nil.
func ( * bzlLibraryLang ) Embeds ( r * rule . Rule , from label . Label ) [ ] label . Label { return nil }
// Resolve translates imported libraries for a given rule into Bazel
// dependencies. Information about imported libraries is returned for each
// rule generated by language.GenerateRules in
// language.GenerateResult.Imports. Resolve generates a "deps" attribute (or
// the appropriate language-specific equivalent) for each import according to
// language-specific rules and heuristics.
func ( * bzlLibraryLang ) Resolve ( c * config . Config , ix * resolve . RuleIndex , rc * repo . RemoteCache , r * rule . Rule , importsRaw interface { } , from label . Label ) {
imports := importsRaw . ( [ ] string )
r . DelAttr ( "deps" )
if len ( imports ) == 0 {
return
}
deps := make ( [ ] string , 0 , len ( imports ) )
for _ , imp := range imports {
2020-09-07 21:29:39 +00:00
impLabel , err := label . Parse ( imp )
if err != nil {
log . Printf ( "%s: import of %q is invalid: %v" , from . String ( ) , imp , err )
continue
}
// the index only contains absolute labels, not relative
impLabel = impLabel . Abs ( from . Repo , from . Pkg )
2020-10-19 16:49:17 +00:00
if impLabel . Repo == "bazel_tools" {
// The @bazel_tools repo is tricky because it is a part of the "shipped
// with bazel" core library for interacting with the outside world.
// This means that it can not depend on skylib. Fortunately there is a
// fairly simple workaround for this, which is that you can add those
// bzl files as `deps` entries.
deps = append ( deps , imp )
continue
}
2020-09-07 21:29:39 +00:00
if impLabel . Repo != "" || ! c . IndexLibraries {
2020-06-26 15:04:12 +00:00
// This is a dependency that is external to the current repo, or indexing
// is disabled so take a guess at what hte target name should be.
deps = append ( deps , strings . TrimSuffix ( imp , fileType ) )
2020-10-19 16:49:17 +00:00
continue
}
res := resolve . ImportSpec {
Lang : languageName ,
Imp : impLabel . String ( ) ,
}
matches := ix . FindRulesByImport ( res , languageName )
if len ( matches ) == 0 {
log . Printf ( "%s: %q (%s) was not found in dependency index. Skipping. This may result in an incomplete deps section and require manual BUILD file intervention.\n" , from . String ( ) , imp , impLabel . String ( ) )
}
for _ , m := range matches {
depLabel := m . Label
depLabel = depLabel . Rel ( from . Repo , from . Pkg )
deps = append ( deps , depLabel . String ( ) )
2020-06-26 15:04:12 +00:00
}
}
sort . Strings ( deps )
if len ( deps ) > 0 {
r . SetAttr ( "deps" , deps )
}
}
var kinds = map [ string ] rule . KindInfo {
"bzl_library" : {
NonEmptyAttrs : map [ string ] bool { "srcs" : true , "deps" : true } ,
MergeableAttrs : map [ string ] bool { "srcs" : true } ,
} ,
}
// GenerateRules extracts build metadata from source files in a directory.
// GenerateRules is called in each directory where an update is requested
// in depth-first post-order.
//
// args contains the arguments for GenerateRules. This is passed as a
// struct to avoid breaking implementations in the future when new
// fields are added.
//
// A GenerateResult struct is returned. Optional fields may be added to this
// type in the future.
//
// Any non-fatal errors this function encounters should be logged using
// log.Print.
func ( * bzlLibraryLang ) GenerateRules ( args language . GenerateArgs ) language . GenerateResult {
var rules [ ] * rule . Rule
var imports [ ] interface { }
for _ , f := range append ( args . RegularFiles , args . GenFiles ... ) {
if ! isBzlSourceFile ( f ) {
continue
}
name := strings . TrimSuffix ( f , fileType )
r := rule . NewRule ( "bzl_library" , name )
r . SetAttr ( "srcs" , [ ] string { f } )
2020-10-22 05:52:14 +00:00
shouldSetVisibility := args . File == nil || ! args . File . HasDefaultVisibility ( )
if shouldSetVisibility {
vis := checkInternalVisibility ( args . Rel , "//visibility:public" )
r . SetAttr ( "visibility" , [ ] string { vis } )
2020-06-26 15:04:12 +00:00
}
fullPath := filepath . Join ( args . Dir , f )
loads , err := getBzlFileLoads ( fullPath )
if err != nil {
log . Printf ( "%s: contains syntax errors: %v" , fullPath , err )
// Don't `continue` since it is reasonable to create a target even
// without deps.
}
rules = append ( rules , r )
imports = append ( imports , loads )
}
return language . GenerateResult {
Gen : rules ,
Imports : imports ,
Empty : generateEmpty ( args ) ,
}
}
func getBzlFileLoads ( path string ) ( [ ] string , error ) {
f , err := ioutil . ReadFile ( path )
if err != nil {
return nil , fmt . Errorf ( "ioutil.ReadFile(%q) error: %v" , path , err )
}
ast , err := build . ParseBuild ( path , f )
if err != nil {
return nil , fmt . Errorf ( "build.Parse(%q) error: %v" , f , err )
}
var loads [ ] string
build . WalkOnce ( ast , func ( expr * build . Expr ) {
n := * expr
if l , ok := n . ( * build . LoadStmt ) ; ok {
loads = append ( loads , l . Module . Value )
}
} )
sort . Strings ( loads )
return loads , nil
}
func isBzlSourceFile ( f string ) bool {
return strings . HasSuffix ( f , fileType ) && ! ignoreSuffix . Matches ( f )
}
// generateEmpty generates the list of rules that don't need to exist in the
// BUILD file any more.
// For each bzl_library rule in args.File that only has srcs that aren't in
// args.RegularFiles or args.GenFiles, add a bzl_library with no srcs or deps.
// That will let Gazelle delete bzl_library rules after the corresponding .bzl
// files are deleted.
func generateEmpty ( args language . GenerateArgs ) [ ] * rule . Rule {
var ret [ ] * rule . Rule
if args . File == nil {
return ret
}
for _ , r := range args . File . Rules {
if r . Kind ( ) != "bzl_library" {
continue
}
name := r . AttrString ( "name" )
exists := make ( map [ string ] bool )
for _ , f := range args . RegularFiles {
exists [ f ] = true
}
for _ , f := range args . GenFiles {
exists [ f ] = true
}
for _ , r := range args . File . Rules {
srcsExist := false
for _ , f := range r . AttrStrings ( "srcs" ) {
if exists [ f ] {
srcsExist = true
break
}
}
if ! srcsExist {
ret = append ( ret , rule . NewRule ( "bzl_library" , name ) )
}
}
}
return ret
}
type srcsList [ ] string
func ( s srcsList ) Contains ( m string ) bool {
for _ , e := range s {
if e == m {
return true
}
}
return false
}
2020-10-22 05:52:14 +00:00
// checkInternalVisibility overrides the given visibility if the package is
// internal.
func checkInternalVisibility ( rel , visibility string ) string {
if i := pathtools . Index ( rel , "internal" ) ; i > 0 {
visibility = fmt . Sprintf ( "//%s:__subpackages__" , rel [ : i - 1 ] )
} else if i := pathtools . Index ( rel , "private" ) ; i > 0 {
visibility = fmt . Sprintf ( "//%s:__subpackages__" , rel [ : i - 1 ] )
} else if pathtools . HasPrefix ( rel , "internal" ) || pathtools . HasPrefix ( rel , "private" ) {
visibility = "//:__subpackages__"
}
return visibility
}