blob: 5b961a26b0e5ba19ee5fa48a263523a22fe0a50f [file]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package bazel2gn
import (
"errors"
"fmt"
"strings"
"go.starlark.net/syntax"
)
// Parse takes a path to a starlark file and returns a parsed AST.
//
// This delegates to the starlark parser library we are using.
// Create a function wrapper here to capture the settings and modes we use during parsing.
func Parse(path string) (*syntax.File, error) {
opts := syntax.FileOptions{
// Empty means default file-level settings for parsing.
}
return opts.Parse(path, nil, syntax.RetainComments)
}
// indent indents input lines by input levels.
func indent(lines []string, level int) []string {
var indented []string
prefix := strings.Repeat(indentPrefix, level)
for _, l := range lines {
indented = append(indented, prefix+l)
}
return indented
}
// unwrapParenExpr removes any surrounding parentheses from an expression.
func unwrapParenExpr(expr syntax.Expr) syntax.Expr {
for {
paren, ok := expr.(*syntax.ParenExpr)
if !ok {
return expr
}
expr = paren.X
}
}
// StmtToGN converts a Bazel statement [0] to GN.
//
// [0] https://github.com/bazelbuild/starlark/blob/master/spec.md#statements
func StmtToGN(stmt syntax.Stmt) ([]string, error) {
// Skip the statement if it is mark for skipping by users.
shouldSkip, err := hasSkipAnnotation(stmt)
if err != nil {
return nil, fmt.Errorf("failed to check skip annotation: %v", err)
}
if shouldSkip {
return nil, nil
}
switch v := stmt.(type) {
case *syntax.LoadStmt:
// Load statements are ignored during conversion.
return nil, nil
case *syntax.ExprStmt:
return exprToGN(v.X, nil)
case *syntax.AssignStmt:
return assignStmtToGN(v)
default:
return nil, fmt.Errorf("statement of type %T is not supported to be converted to GN, node details: %#v", stmt, stmt)
}
}
// exprToGN converts a Bazel expression [0] to GN.
//
// It applies input transformers first before delegating to more specific
// conversion functions based on expression type.
//
// [0] https://github.com/bazelbuild/starlark/blob/master/spec.md#expressions
func exprToGN(expr syntax.Expr, transformers []transformer) ([]string, error) {
for _, ts := range transformers {
var err error
expr, err = ts(expr)
if err != nil {
return nil, fmt.Errorf("applying special handler before converting expression: %v", err)
}
}
switch v := expr.(type) {
case *syntax.CallExpr:
// NOTE: I'm not sure whether we need to plumb transformers here, so far it
// is not necessary. callExprToGN should be a top-level entry point for
// macro and rules.
return callExprToGN(v)
case *syntax.BinaryExpr:
return binaryExprToGN(v, transformers)
case *syntax.Ident:
return identToGN(v)
case *syntax.Literal:
return []string{v.Raw}, nil
case *syntax.ListExpr:
return listExprToGN(v, transformers)
case *syntax.DictExpr:
return dictExprToGN(v, transformers)
case *syntax.ParenExpr:
return exprToGN(v.X, transformers)
default:
return nil, fmt.Errorf("expression of type %T is not supported when converting to GN, node details: %#v", expr, expr)
}
}
// identToGN converts a Bazel identifier to GN.
func identToGN(ident *syntax.Ident) ([]string, error) {
val, ok := specialIdentifiers[ident.Name]
if !ok {
val = ident.Name
}
return []string{val}, nil
}
// assignStmtToGN converts a Bazel assignment statement [0] to GN.
//
// [0] https://github.com/bazelbuild/starlark/blob/master/spec.md#assignments
func assignStmtToGN(stmt *syntax.AssignStmt) ([]string, error) {
lhs, err := exprToGN(stmt.LHS, nil)
if err != nil {
return nil, fmt.Errorf("converting lhs of assignment statement: %v", err)
}
if len(lhs) == 0 {
return nil, errors.New("lhs of assignment statement is unexpectedly empty")
}
// Apply the visibility transformation to assignments, enabling the use of
// file-level variables for visibility.
var transformers = []transformer{bazelVisibilityToGN}
statement_transformers, err := transformersFromComments(stmt.Comments())
if err != nil {
return nil, err
}
transformers = append(transformers, statement_transformers...)
var ret []string
if hasBranching(stmt.RHS) {
lc, err := listConcatWithSelectToGN(lhs[0], stmt.RHS, transformers)
if err != nil {
return nil, err
}
if stmt.Op == syntax.EQ {
ret = append(ret, fmt.Sprintf("%s = []", lhs[0]))
}
ret = append(ret, lc...)
} else {
rhs, err := exprToGN(stmt.RHS, transformers)
if err != nil {
return nil, fmt.Errorf("converting rhs of assignment statement: %v", err)
}
if len(rhs) == 0 {
return nil, errors.New("rhs of assignment statement is unexpectedly empty")
}
ret = append(ret, fmt.Sprintf("%s %s %s", lhs[0], opToGN(stmt.Op), rhs[0]))
ret = append(ret, rhs[1:]...)
}
ret = append(ret, []string{
"",
`# To avoid "Assignment had no effect" from GN.`,
`# It's possible this variable is only used in if conditions (e.g. is_host).`,
fmt.Sprintf(`not_needed([ "%s" ])`, lhs[0]),
"",
}...)
return ret, nil
}
// targetCompatibleWithToGNConditions converts a Bazel `target_compatible_with`
// expression to GN conditions.
//
// It supports identifiers representing a list of constraints and lists of
// constraints containing literals and identifiers.
// It does not support concatenating multiple lists of constraints.
func targetCompatibleWithToGNConditions(expr syntax.Expr) ([]string, error) {
switch v := expr.(type) {
case *syntax.Ident:
// An identifier not in a list. It must be a variable representing a list.
gnCondition, ok := bazelConstraintListVarsToGNConditions[v.Name]
if !ok {
return nil, fmt.Errorf("unsupported target_compatible_with variable: %v", v.Name)
}
return []string{gnCondition}, nil
case *syntax.ListExpr:
var gnConditions []string
for _, elm := range v.List {
switch elm := elm.(type) {
case *syntax.Literal:
val, ok := elm.Value.(string)
if !ok {
return nil, fmt.Errorf("unexpected literal type in target_compatible_with: %T", elm.Value)
}
gnCondition, ok := bazelConstraintsToGNConditions[val]
if !ok {
return nil, fmt.Errorf("unsupported target_compatible_with variable: %v", val)
}
gnConditions = append(gnConditions, gnCondition)
case *syntax.Ident:
gnCondition, ok := bazelConstraintsToGNConditions[elm.Name]
if !ok {
return nil, fmt.Errorf("unsupported target_compatible_with variable: %v", elm.Name)
}
gnConditions = append(gnConditions, gnCondition)
default:
return nil, fmt.Errorf("unsupported expression type in target_compatible_with list: %T", elm)
}
}
return gnConditions, nil
default:
return nil, fmt.Errorf("unsupported type %T as value to target_compatible_with in Bazel, node details: %#v", expr, expr)
}
}
// callExprToGN converts a Bazel call expression [0] to GN. These calls should
// be macro or Bazel rules known to the converter.
//
// [0] https://github.com/bazelbuild/starlark/blob/master/spec.md#function-and-method-calls
func callExprToGN(expr *syntax.CallExpr) ([]string, error) {
fn := expr.Fn.(*syntax.Ident)
bazelRule := fn.Name
gnTemplateName, ok := bazelRuleToGNTemplate[bazelRule]
if !ok {
return nil, fmt.Errorf("%s is not a known Bazel rule to convert to GN", bazelRule)
}
if gnTemplateName == "__NO_GN_EQUIVALENT__" {
return nil, nil
}
// TODO(jayzhuang): Handle package level settings, e.g. visibility.
if bazelRule == "package" {
return nil, nil
}
attrsToOmit := attrsToOmitByRules[bazelRule]
// Loops through all arguments to handle special ones first.
var name string
var remainingArgs []*syntax.BinaryExpr
var wrappingConditions []string
for _, arg := range expr.Args {
binaryExpr, ok := arg.(*syntax.BinaryExpr)
if !ok || binaryExpr.Op != syntax.EQ {
return nil, fmt.Errorf("only attribute assignments (e.g. `attr = value`) are allowed in Bazel targets to be converted to GN, got %#v", arg)
}
// Skip the binary expression if it is marked for skipping.
shouldSkip, err := hasSkipAnnotation(binaryExpr)
if err != nil {
return nil, fmt.Errorf("failed to check skip annotation: %v", err)
}
if shouldSkip {
continue
}
ident, ok := binaryExpr.X.(*syntax.Ident)
if !ok {
return nil, fmt.Errorf("unexpected node type on the left hand side of binary expression in target definition, want syntax.Ident, got %T", binaryExpr.X)
}
if attrsToOmit[ident.Name] {
continue
}
if ident.Name == "name" {
lines, err := exprToGN(binaryExpr.Y, nil)
if err != nil {
return nil, fmt.Errorf("converting target name: %v", err)
}
name = strings.Join(lines, "\n")
continue
}
if ident.Name == "target_compatible_with" {
var err error
wrappingConditions, err = targetCompatibleWithToGNConditions(binaryExpr.Y)
if err != nil {
return nil, fmt.Errorf("converting Bazel target_compatible_with to GN conditions: %v", err)
}
continue
}
isCCLibrary := bazelRule == "cc_library" || bazelRule == "fx_cc_library"
if isCCLibrary && ident.Name == "alwayslink" {
// Convert `cc_library` or `fx_cc_library` with `alwayslink = True` to `source_set`.
if rhsIdent, ok := binaryExpr.Y.(*syntax.Ident); ok && rhsIdent.Name == "True" {
gnTemplateName = "source_set"
}
// Regardless of its value, `alwayslink` is not field in GN, so ignore it.
continue
}
remainingArgs = append(remainingArgs, binaryExpr)
}
if name == "" {
return nil, errors.New("missing `name` attribute in Bazel target")
}
ret := []string{fmt.Sprintf("%s(%s) {", gnTemplateName, name)}
// Loop through all args again to actually build the content of this target.
for _, arg := range remainingArgs {
lines, err := attrAssignmentToGN(arg, bazelRule)
if err != nil {
return nil, fmt.Errorf("converting Bazel attribute to GN: %v", err)
}
ret = append(ret, indent(lines, 1)...)
}
if extra, ok := extraGnExpressionByRules[bazelRule]; ok {
ret = append(ret, indent([]string{extra}, 1)...)
}
ret = append(ret, "}")
if len(wrappingConditions) > 0 {
ret = append([]string{
fmt.Sprintf("if (%s) {", strings.Join(wrappingConditions, " && ")),
}, indent(ret, 1)...)
ret = append(ret, "}")
}
return ret, nil
}
// convertAttrName converts input Bazel attrName to the corresponding parameter name in GN.
//
// This function takes the current Bazel rule being converted into consideration.
func convertAttrName(attrName, bazelRule string) string {
ret, ok := commonAttrMap[attrName]
if ok {
return ret
}
ruleAttrMap, ok := attrMapsByRules[bazelRule]
if !ok {
return attrName
}
ret, ok = ruleAttrMap[attrName]
if ok {
return ret
}
return attrName
}
// hasClearAnnotation returns true if the input expr has a clear annotation comment.
//
// This function only picks up suffix comments, for example:
//
// ```
//
// # This comment is NOT considered
// copts = [] # This comment is considered
//
// copts = [ # This comment is NOT considered
// ] # This comment is considered
//
// ```
func hasClearAnnotation(expr syntax.Expr) bool {
comments := expr.Comments()
if comments != nil {
for _, c := range comments.Suffix {
if c.Text == clearAnnotation {
return true
}
}
}
return false
}
// attrAssignmentToGN converts a Bazel assignment [0] to GN. These assignments
// are used to assign values to fields during target definitions in Bazel.
//
// This function handles the special cases in attribute assignments, e.g. when
// select calls are involved. This is done through applying node transformers
// funcs during the traversal.
//
// NOTE: Assignment is a special binary expression with operator =.
//
// [0] https://github.com/bazelbuild/starlark/blob/master/spec.md#assignments
func attrAssignmentToGN(expr *syntax.BinaryExpr, bazelRule string) ([]string, error) {
lhs, ok := expr.X.(*syntax.Ident)
if !ok {
return nil, fmt.Errorf("expecting an identifier on the left hand side of attribute assignment, got %T", expr.X)
}
attrName := convertAttrName(lhs.Name, bazelRule)
// Intercept genrule cmd assignment and convert it directly.
//
// NOTE: This means bazel2gn does NOT support select calls in `cmd` currently.
if bazelRule == "genrule" && attrName == "cmd" {
return genruleCmdToGN(expr.Y)
}
// Intercept tags attribute to sync assert_no_deps.
if attrName == "tags" {
return tagsToGN(expr.Y)
}
// Intercept rustenv attribute to convert dict to list of strings.
if attrName == "rustenv" {
return rustenvToGN(expr.Y)
}
op, ok := attrGNAssignmentOps[attrName]
if !ok {
op = "="
}
if idkFuchsiaSpecificAttrs[lhs.Name] || idkNonFuchsiaSpecificAttrs[lhs.Name] {
op = "+="
}
// By default "configs" uses += to concatenate values set in Bazel with =.
// Check for clear annotation to explicitly clear configs in GN, which
// requires using =.
//
// TODO(https://fxbug.dev/430953918): Figure out a better way to handle configs and public_configs conversion.
if attrName == "configs" && hasClearAnnotation(expr) {
op = "="
}
transformers, err := transformersFromComments(expr.Comments())
if err != nil {
return nil, err
}
switch attrName {
case "visibility":
transformers = append(transformers, bazelVisibilityToGN)
case "deps", "public_deps", "test_deps", "proc_macro_deps", "args_deps", "plugin_deps":
transformers = append(transformers, bazelDepToGN)
case "configs":
transformers = append(transformers, bazelCOptToGNConfig)
case "api", "outputs", "sources", "inputs", "args_sources":
transformers = append(transformers, bazelFilePathsToGN)
case "ldflags":
transformers = append(transformers, bazelLdflagsToGN)
}
rhsY := unwrapParenExpr(expr.Y)
// This is a simple `attr = select(...)`, convert in-place.
if isSelectCall(rhsY) {
return selectToGN(attrName, op, rhsY.(*syntax.CallExpr), transformers)
}
if condExpr, ok := rhsY.(*syntax.CondExpr); ok {
return condExprToGN(attrName, op, condExpr, transformers)
}
// It is not a simple `select` call on the RHS, and `select`s are found in
// subtree, so assume this is list concatenation with `select`s in them.
//
// NOTE: Currently `select`s and `if`s are only supported in list concatenation when
// they are used in binary expressions. Other usages will fail this call.
if hasBranching(rhsY) {
lc, err := listConcatWithSelectToGN(attrName, rhsY, transformers)
if err != nil {
return nil, err
}
// Start with an empty list so it's easy to += new elements from later
// conversions.
return append([]string{fmt.Sprintf("%s %s []", attrName, op)}, lc...), nil
}
rhs, err := exprToGN(expr.Y, transformers)
if err != nil {
return nil, fmt.Errorf("converting rhs of binary expression: %v", err)
}
if len(rhs) == 0 {
return nil, errors.New("rhs hand side of binary expression is unexpectedly empty")
}
ret := []string{fmt.Sprintf("%s %s %s", attrName, op, rhs[0])}
ret = append(ret, rhs[1:]...)
// Handle any additional work necessary for specific assignments.
if attrName == "sdk_headers_for_internal_use" {
ret = handle_sdk_headers_for_internal_use(ret, rhs, 0)
}
// Wrap the entire assignment in a condition if appropriate.
// This should be the last thing before returning the result.
switch {
case idkFuchsiaSpecificAttrs[lhs.Name]:
ret = append([]string{"if (is_fuchsia) {"}, indent(ret, 1)...)
ret = append(ret, "}")
case idkNonFuchsiaSpecificAttrs[lhs.Name]:
ret = append([]string{"if (!is_fuchsia) {"}, indent(ret, 1)...)
ret = append(ret, "}")
}
return ret, nil
}
// binaryExprToGN converts a general Bazel binary expression [0] to GN.
//
// [0] https://github.com/bazelbuild/starlark/blob/master/spec.md#binary-operators
func binaryExprToGN(expr *syntax.BinaryExpr, transformers []transformer) ([]string, error) {
lhs, err := exprToGN(expr.X, transformers)
if err != nil {
return nil, fmt.Errorf("converting lhs of binary expression: %v", err)
}
if len(lhs) == 0 {
return nil, errors.New("lhs of binary expression is unexpectedly empty")
}
rhs, err := exprToGN(expr.Y, transformers)
if err != nil {
return nil, fmt.Errorf("converting rhs of binary expression: %v", err)
}
if len(rhs) == 0 {
return nil, errors.New("rhs hand side of binary expression is unexpectedly empty")
}
ret := lhs[:len(lhs)-1]
ret = append(ret, fmt.Sprintf("%s %s %s", lhs[len(lhs)-1], opToGN(expr.Op), rhs[0]))
ret = append(ret, rhs[1:]...)
return ret, nil
}
func opToGN(op syntax.Token) string {
ret, ok := specialTokens[op]
if !ok {
return op.String()
}
return ret
}
// listExprToGN converts a Bazel list expression [0] to GN.
//
// [0] https://github.com/bazelbuild/starlark/blob/master/spec.md#lists
func listExprToGN(expr *syntax.ListExpr, transformers []transformer) ([]string, error) {
ret := []string{"["}
for _, elm := range expr.List {
shouldSkip, err := hasSkipAnnotation(elm)
if err != nil {
return nil, fmt.Errorf("failed to check skip annotation: %v", err)
}
if shouldSkip {
continue
}
elmLines, err := exprToGN(elm, transformers)
if err != nil {
return nil, fmt.Errorf("converting list element: %v", err)
}
if len(elmLines) == 0 {
continue
}
elmLines[len(elmLines)-1] = elmLines[len(elmLines)-1] + ","
ret = append(ret, indent(elmLines, 1)...)
}
ret = append(ret, "]")
return ret, nil
}
// dictExprToGN converts a Bazel dictionary expression to a GN scope.
func dictExprToGN(expr *syntax.DictExpr, transformers []transformer) ([]string, error) {
ret := []string{"{"}
for _, entry := range expr.List {
entryDictEntry, ok := entry.(*syntax.DictEntry)
if !ok {
return nil, fmt.Errorf("unexpected node type in dictionary entry: %T", entry)
}
key, err := exprToGN(entryDictEntry.Key, transformers)
if err != nil {
return nil, fmt.Errorf("converting dictionary key: %v", err)
}
value, err := exprToGN(entryDictEntry.Value, transformers)
if err != nil {
return nil, fmt.Errorf("converting dictionary value: %v", err)
}
// In GN, keys are identifiers, so they should not be quoted.
// Starlark dictionary keys are strings, so they are quoted.
// We need to remove the quotes from the key.
key[0] = strings.Trim(key[0], `"`)
lines := []string{fmt.Sprintf("%s = %s", key[0], value[0])}
if len(value) > 1 {
lines = append(lines, value[1:]...)
}
ret = append(ret, indent(lines, 1)...)
}
ret = append(ret, "}")
return ret, nil
}
// hasSkipAnnotation returns true if the given node has a skip annotation.
//
// A skip annotation is a comment that exactly matches `@bazel2gn:skip`, and
// right above or after the node on the same line. The node will be skipped
// during conversion if the annotation is found.
//
// For example, these are skip annotations:
//
// ```
//
// # Some other comment
// # @bazel2gn:skip
// go_library(
// ...
// # @bazel2gn:skip
// attr = val, # @bazel2gn:skip
// )
//
// ```
//
// And these are NOT skip annotations:
//
// ```
//
// # @bazel2gn:skip
// # Skip annotation should be on the same line or right above the node.
// go_library(
// ...
// )
// # @bazel2gn:skip
//
// ```
func hasSkipAnnotation(node syntax.Node) (bool, error) {
comments := node.Comments()
if comments == nil {
return false, nil
}
if len(comments.Before) > 0 {
// For comments before, match the last line.
if comments.Before[len(comments.Before)-1].Text == skipAnnotation {
return true, nil
}
}
if len(comments.Suffix) > 0 {
// For suffixes, match the comment immediately following.
if comments.Suffix[0].Text == skipAnnotation {
return true, nil
}
}
allComments := [][]syntax.Comment{
comments.Before,
comments.After,
}
for _, c := range allComments {
if hasAnnotation(c, skipAnnotation) {
return false, fmt.Errorf("found skip annotation in unexpected location, it should be on the line right above the target definition to take effect")
}
}
return false, nil
}
func hasAnnotation(comments []syntax.Comment, annotation string) bool {
for _, comment := range comments {
if comment.Text == annotation {
fmt.Printf("found skip annotation %v %v\n", comment, annotation)
return true
}
}
return false
}
// transformersFromAnnotations returns the list of transformers to apply to a
// target definition based on the annotations found in the comments.
func transformersFromAnnotations(comments []syntax.Comment) ([]transformer, error) {
transformers := []transformer{}
for _, comment := range comments {
if strings.HasPrefix(comment.Text, transformerAnnotationPrefix) {
n := strings.TrimPrefix(comment.Text, transformerAnnotationPrefix)
transformer, ok := transformerAnnotationNames[n]
if !ok {
return nil, fmt.Errorf("unknown transformer: %s", n)
}
transformers = append(transformers, transformer)
}
}
return transformers, nil
}
func transformersFromComments(comments *syntax.Comments) ([]transformer, error) {
if comments == nil {
return nil, nil
}
before, err := transformersFromAnnotations(comments.Before)
if err != nil {
return nil, err
}
suffix, err := transformersFromAnnotations(comments.Suffix)
if err != nil {
return nil, err
}
return append(before, suffix...), nil
}