Skip to content

Commit

Permalink
feat(gazelle): generate kt_jvm_binary targets (#2990)
Browse files Browse the repository at this point in the history
GitOrigin-RevId: 762169dfe192a8c5ac31f269fe59908f335ea078
  • Loading branch information
jbedard authored and alexeagle committed Aug 14, 2023
1 parent 3f712fe commit dde3bbd
Show file tree
Hide file tree
Showing 25 changed files with 229 additions and 48 deletions.
1 change: 1 addition & 0 deletions gazelle/kotlin/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ go_library(
"@bazel_gazelle//resolve:go_default_library",
"@bazel_gazelle//rule:go_default_library",
"@com_github_bazel_contrib_rules_jvm//java/gazelle/javaconfig",
"@com_github_emirpasic_gods//maps/treemap",
"@com_github_emirpasic_gods//sets/treeset",
"@com_github_emirpasic_gods//utils",
],
Expand Down
50 changes: 43 additions & 7 deletions gazelle/kotlin/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"math"
"os"
"path"
"strings"
"sync"

gazelle "aspect.build/cli/gazelle/common"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/bazelbuild/bazel-gazelle/language"
"github.com/bazelbuild/bazel-gazelle/resolve"
"github.com/bazelbuild/bazel-gazelle/rule"
"github.com/emirpasic/gods/maps/treemap"
"github.com/emirpasic/gods/sets/treeset"
)

Expand All @@ -34,13 +36,25 @@ func (kt *kotlinLang) GenerateRules(args language.GenerateArgs) language.Generat
// Collect all source files.
sourceFiles := kt.collectSourceFiles(cfg, args)

// TODO: multiple targets (lib, test, ...)
target := NewKotlinTarget()
// TODO: multiple library targets (lib, test, ...)
libTarget := NewKotlinLibTarget()
binTargets := treemap.NewWithStringComparator()

// Parse all source files and group information into target(s)
for p := range kt.parseFiles(args, sourceFiles) {
target.Files.Add(p.File)
target.Packages.Add(p.Package)
var target *KotlinTarget

if p.HasMain {
binTarget := NewKotlinBinTarget(p.File, p.Package)
binTargets.Put(p.File, binTarget)

target = &binTarget.KotlinTarget
} else {
libTarget.Files.Add(p.File)
libTarget.Packages.Add(p.Package)

target = &libTarget.KotlinTarget
}

for _, impt := range p.Imports {
target.Imports.Add(ImportStatement{
Expand All @@ -55,14 +69,19 @@ func (kt *kotlinLang) GenerateRules(args language.GenerateArgs) language.Generat

var result language.GenerateResult

targetName := gazelle.ToDefaultTargetName(args, "root")
libTargetName := gazelle.ToDefaultTargetName(args, "root")
kt.addLibraryRule(libTargetName, libTarget, args, false, &result)

kt.addLibraryRule(targetName, target, args, false, &result)
for _, v := range binTargets.Values() {
binTarget := v.(*KotlinBinTarget)
binTargetName := toBinaryTargetName(binTarget.File)
kt.addBinaryRule(binTargetName, binTarget, args, &result)
}

return result
}

func (kt *kotlinLang) addLibraryRule(targetName string, target *KotlinTarget, args language.GenerateArgs, isTestRule bool, result *language.GenerateResult) {
func (kt *kotlinLang) addLibraryRule(targetName string, target *KotlinLibTarget, args language.GenerateArgs, isTestRule bool, result *language.GenerateResult) {
// TODO: check for rule collisions

// Generate nothing if there are no source files. Remove any existing rules.
Expand Down Expand Up @@ -96,6 +115,23 @@ func (kt *kotlinLang) addLibraryRule(targetName string, target *KotlinTarget, ar
BazelLog.Infof("add rule '%s' '%s:%s'", ktLibrary.Kind(), args.Rel, ktLibrary.Name())
}

func (kt *kotlinLang) addBinaryRule(targetName string, target *KotlinBinTarget, args language.GenerateArgs, result *language.GenerateResult) {
main_class := strings.TrimSuffix(target.File, ".kt")
if target.Package != "" {
main_class = target.Package + "." + main_class
}

ktBinary := rule.NewRule(KtJvmBinary, targetName)
ktBinary.SetAttr("srcs", []string{target.File})
ktBinary.SetAttr("main_class", main_class)
ktBinary.SetPrivateAttr(packagesKey, target)

result.Gen = append(result.Gen, ktBinary)
result.Imports = append(result.Imports, target)

BazelLog.Infof("add rule '%s' '%s:%s'", ktBinary.Kind(), args.Rel, ktBinary.Name())
}

// TODO: put in common?
func (kt *kotlinLang) parseFiles(args language.GenerateArgs, sources *treeset.Set) chan *parser.ParseResult {
// The channel of all files to parse.
Expand Down
64 changes: 49 additions & 15 deletions gazelle/kotlin/kotlin.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,73 @@
package gazelle

import "strings"
import (
"path"
"strings"
)

import "github.com/emirpasic/gods/sets/treeset"

func IsNativeImport(impt string) bool {
return strings.HasPrefix(impt, "kotlin.") || strings.HasPrefix(impt, "kotlinx.") || strings.HasPrefix(impt, "java.") || strings.HasPrefix(impt, "javax.")
}

type KotlinTarget struct {
Imports *treeset.Set
}

/**
* Information for kotlin library target including:
* - BUILD package name
* - kotlin files
* - kotlin import statements from all files
* - kotlin packages implemented
* - kotlin files with main() methods
*/
type KotlinTarget struct {
Name string
type KotlinLibTarget struct {
KotlinTarget

Imports *treeset.Set
Packages *treeset.Set

Mains *treeset.Set

Files *treeset.Set
Files *treeset.Set
}

func NewKotlinTarget() *KotlinTarget {
return &KotlinTarget{
Imports: treeset.NewWith(importStatementComparator),
func NewKotlinLibTarget() *KotlinLibTarget {
return &KotlinLibTarget{
KotlinTarget: KotlinTarget{
Imports: treeset.NewWith(importStatementComparator),
},
Packages: treeset.NewWithStringComparator(),
Mains: treeset.NewWithStringComparator(),
Files: treeset.NewWithStringComparator(),
}
}

/**
* Information for kotlin binary (main() method) including:
* - kotlin import statements from all files
* - the package
* - the file
*/
type KotlinBinTarget struct {
KotlinTarget

File string
Package string
}

func NewKotlinBinTarget(file, pkg string) *KotlinBinTarget {
return &KotlinBinTarget{
KotlinTarget: KotlinTarget{
Imports: treeset.NewWith(importStatementComparator),
},
File: file,
Package: pkg,
}
}

// packagesKey is the name of a private attribute set on generated kt_library
// rules. This attribute contains the KotlinTarget for the target.
const packagesKey = "_java_packages"
const packagesKey = "_kotlin_package"

func toBinaryTargetName(mainFile string) string {
base := strings.ToLower(strings.TrimSuffix(path.Base(mainFile), path.Ext(mainFile)))

// TODO: move target name template to directive
return base + "_bin"
}
13 changes: 13 additions & 0 deletions gazelle/kotlin/language.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const LanguageName = "kotlin"

const (
KtJvmLibrary = "kt_jvm_library"
KtJvmBinary = "kt_jvm_binary"
RulesKotlinRepositoryName = "io_bazel_rules_kotlin"
)

Expand Down Expand Up @@ -61,13 +62,25 @@ var kotlinKinds = map[string]rule.KindInfo{
"deps": true,
},
},

KtJvmBinary: {
MatchAny: false,
NonEmptyAttrs: map[string]bool{
"srcs": true,
"main_class": true,
},
SubstituteAttrs: map[string]bool{},
MergeableAttrs: map[string]bool{},
ResolveAttrs: map[string]bool{},
},
}

var kotlinLoads = []rule.LoadInfo{
{
Name: "@" + RulesKotlinRepositoryName + "//kotlin:jvm.bzl",
Symbols: []string{
KtJvmLibrary,
KtJvmBinary,
},
},
}
Expand Down
6 changes: 6 additions & 0 deletions gazelle/kotlin/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type ParseResult struct {
File string
Imports []string
Package string
HasMain bool
}

type Parser interface {
Expand Down Expand Up @@ -93,6 +94,11 @@ func (p *treeSitterParser) Parse(filePath, source string) (*ParseResult, []error
}

result.Package = readIdentifier(getLoneChild(nodeI, "identifier"), sourceCode, false)
} else if nodeI.Type() == "function_declaration" {
nodeJ := getLoneChild(nodeI, "simple_identifier")
if nodeJ.Content(sourceCode) == "main" {
result.HasMain = true
}
}
}

Expand Down
24 changes: 24 additions & 0 deletions gazelle/kotlin/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,30 @@ func TestTreesitterParser(t *testing.T) {
}
})
}

t.Run("main detection", func(t *testing.T) {
res, _ := NewParser().Parse("main.kt", "fun main() {}")
if !res.HasMain {
t.Errorf("main method should be detected")
}

res, _ = NewParser().Parse("x.kt", `
package my.demo
fun main() {}
`)
if !res.HasMain {
t.Errorf("main method should be detected with package")
}

res, _ = NewParser().Parse("x.kt", `
package my.demo
import kotlin.text.*
fun main() {}
`)
if !res.HasMain {
t.Errorf("main method should be detected with imports")
}
})
}

func equal[T comparable](a, b []T) bool {
Expand Down
40 changes: 25 additions & 15 deletions gazelle/kotlin/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,24 @@ func (*Resolver) Name() string {
func (kt *Resolver) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec {
BazelLog.Debugf("Imports: '%s:%s'", f.Pkg, r.Name())

target := r.PrivateAttr(packagesKey).(*KotlinTarget)

provides := make([]resolve.ImportSpec, 0, target.Packages.Size())
for _, pkg := range target.Packages.Values() {
provides = append(provides, resolve.ImportSpec{
Lang: LanguageName,
Imp: pkg.(string),
})
}
if r.PrivateAttr(packagesKey) != nil {
target, isLib := r.PrivateAttr(packagesKey).(*KotlinLibTarget)
if isLib {
provides := make([]resolve.ImportSpec, 0, target.Packages.Size())
for _, pkg := range target.Packages.Values() {
provides = append(provides, resolve.ImportSpec{
Lang: LanguageName,
Imp: pkg.(string),
})
}

// TODO: why nil instead of just returning empty?
if len(provides) == 0 {
return nil
if len(provides) > 0 {
return provides
}
}
}

return provides
return nil
}

func (kt *Resolver) Embeds(r *rule.Rule, from label.Label) []label.Label {
Expand All @@ -73,8 +75,16 @@ func (kt *Resolver) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.Re
start := time.Now()
BazelLog.Infof("Resolve '%s' dependencies", from.String())

if r.Kind() == KtJvmLibrary {
deps, err := kt.resolveImports(c, ix, importData.(*KotlinTarget).Imports, from)
if r.Kind() == KtJvmLibrary || r.Kind() == KtJvmBinary {
var target KotlinTarget

if r.Kind() == KtJvmLibrary {
target = importData.(*KotlinLibTarget).KotlinTarget
} else {
target = importData.(*KotlinBinTarget).KotlinTarget
}

deps, err := kt.resolveImports(c, ix, target.Imports, from)
if err != nil {
log.Fatal("Resolution Error: ", err)
os.Exit(1)
Expand Down
Empty file.
19 changes: 19 additions & 0 deletions gazelle/kotlin/tests/bin/BUILD.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_binary", "kt_jvm_library")

kt_jvm_library(
name = "bin",
srcs = ["lib.kt"],
)

kt_jvm_binary(
name = "hello_bin",
srcs = ["Hello.kt"],
main_class = "Hello",
)

kt_jvm_binary(
name = "pkghello_bin",
srcs = ["PkgHello.kt"],
main_class = "foo.pkg.PkgHello",
deps = [":bin"],
)
3 changes: 3 additions & 0 deletions gazelle/kotlin/tests/bin/Hello.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fun main() {
println("Hello world!")
}
7 changes: 7 additions & 0 deletions gazelle/kotlin/tests/bin/PkgHello.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package foo.pkg

import test.lib.*

fun main() {
println("Hello world from ", Constants.NAME)
}
2 changes: 2 additions & 0 deletions gazelle/kotlin/tests/bin/WORKSPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This is a Bazel workspace for the Gazelle test data.
workspace(name = "bin")
5 changes: 5 additions & 0 deletions gazelle/kotlin/tests/bin/lib.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package test.lib

object Constants {
const val NAME = "bin test"
}
1 change: 1 addition & 0 deletions gazelle/kotlin/tests/local_deps/BUILD.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package(default_visibility = ["//visibility:public"])
4 changes: 3 additions & 1 deletion gazelle/kotlin/tests/local_deps/BUILD.out
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")

package(default_visibility = ["//visibility:public"])

kt_jvm_library(
name = "local_deps",
srcs = ["main.kt"],
srcs = ["root.kt"],
deps = [
"//impt",
"//impt-star",
Expand Down
Loading

0 comments on commit dde3bbd

Please sign in to comment.