Skip to content

Commit

Permalink
Add gazelle:resolve_file_symbol_name directive (#109)
Browse files Browse the repository at this point in the history
* Add gazelle:resolve_file_symbol_name directive
* integrate scala_rule with ShouldResolveFileSymbolName
* fix tests]
* docs
  • Loading branch information
pcj authored Dec 3, 2023
1 parent 7bd32ba commit 021c30d
Show file tree
Hide file tree
Showing 12 changed files with 227 additions and 35 deletions.
109 changes: 100 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@
- [`gazelle:resolve`](#gazelleresolve)
- [`gazelle:resolve_with`](#gazelleresolve_with)
- [`gazelle:resolve_kind_rewrite_name`](#gazelleresolve_kind_rewrite_name)
- [`gazelle:resolve_file_symbol_name`](#gazelleresolve_file_symbol_name)
- [`gazelle:annotate`](#gazelleannotate)
- [`imports`](#imports)
- [`exports`](#exports)
- [Import Resolution Procedure](#import-resolution-procedure)
- [How Required Imports are Calculated](#how-required-imports-are-calculated)
- [Rule](#rule)
Expand Down Expand Up @@ -832,6 +834,70 @@ This tells the extension _"if you find a rule with kind `my_scala_app`, rewrite
the label name to name + `"_lib"`, using the magic token `%{name}` as a
placeholder."_

### `gazelle:resolve_file_symbol_name`

This directive can be used to resolve free names listed in a scala file against
the current file symbol scope. To inspect the `names` of a file, take a look at the file parse cache. For example:

```json
{
"label": "//common/utils/logging/scala",
"kind": "scala_library",
"files": [
{
"filename": "src/LogField.scala",
"imports": [
"com.typesafe.scalalogging.LazyLogging",
"net.logstash.logback.marker.MapEntriesAppendingMarker",
"net.logstash.logback.marker.ObjectAppendingMarker",
"scala.jdk.CollectionConverters._"
],
"packages": [
"common.utils.logging"
],
"objects": [
"common.utils.logging.LogField",
"common.utils.logging.LogFields"
],
"traits": [
"common.utils.logging.LogField"
],
"names": [
"LazyLogging",
"LogField",
"LogFields",
"MapEntriesAppendingMarker",
"ObjectAppendingMarker",
"String",
"apply",
"fieldName",
"fieldValue",
"fieldValue.toString",
"name",
],
"extends": {
"object trumid.common.utils.logging.LogField": {
"classes": [
"com.typesafe.scalalogging.LazyLogging"
]
}
}
}
],
"sha256": "3ee80930372ea846ebb48e55eb76d55fed89b6af5f05d08f98b38045eb0464d6",
"parseTimeMillis": "3"
},
```

In this case, if a dependency was missing from the `deps` list, but would be
corrected by resolving `ObjectAppendingMarker` (but not
`MapEntriesAppendingMarker`, for example purposes), one could instruct the
resolver to try and resolve it selectively via:

```
# gazelle:resolve_file_symbol_name LogField.scala +ObjectAppendingMarker -MapEntriesAppendingMarker
```


### `gazelle:annotate`

Expand All @@ -852,26 +918,38 @@ Generates:
```bazel
scala_binary(
name = "app",
# ❌ AbstractServiceBase<ERROR> symbol not found (EXTENDS of foo.allocation.Main)
# ✅ akka.NotUsed<CLASS> @maven//:com_typesafe_akka_akka_actor_2_12<jarindex> (DIRECT of BusinessFlows.scala)
# ✅ java.time.format.DateTimeFormatter<CLASS> NO-LABEL<java> (DIRECT of RequestHandler.scala)
# ✅ scala.concurrent.ExecutionContext<PACKAGE> @maven//:org_scala_lang_scala_library<maven> (DIRECT of RequestHandler.scala)
# import: ❌ AbstractServiceBase<ERROR> symbol not found (EXTENDS of foo.allocation.Main)
# import: ✅ akka.NotUsed<CLASS> @maven//:com_typesafe_akka_akka_actor_2_12<jarindex> (DIRECT of BusinessFlows.scala)
# import: ✅ java.time.format.DateTimeFormatter<CLASS> NO-LABEL<java> (DIRECT of RequestHandler.scala)
# import: ✅ scala.concurrent.ExecutionContext<PACKAGE> @maven//:org_scala_lang_scala_library<maven> (DIRECT of RequestHandler.scala)
srcs = glob(["src/main/**/*.scala"]),
main_class = "foo.allocation.Main",
)
```

#### `exports`

This adds a list of comments to the `srcs` attribute detailing the provided
exports and how they resolved. Example:

```
# gazelle:annotate exports
```

# Import Resolution Procedure

## How Required Imports are Calculated

### Rule

If the rule has `main_class` attribute, that name is added to the imports (type `MAIN_CLASS`).
If the rule has `main_class` attribute, that name is added to the imports (type
`MAIN_CLASS`).

The remainder of rule imports are collected from file imports for all `.scala` source files in the rule.
The remainder of rule imports are collected from file imports for all `.scala`
source files in the rule.

Once this initial set of imports are gathered, the transitive set of required symbol are collected from:
Once this initial set of imports are gathered, the transitive set of required
symbol are collected from:

- `extends` clauses (type `EXTENDS`)
- imports matching a `gazelle:resolve_with` directive (type `IMPLICIT`).
Expand All @@ -885,7 +963,8 @@ The imports for a file are collected as follows:
The `.scala` file is parsed:

- Import statements are collected, including nested imports.
- a set of *names* are collected by traversing the body of the AST. Some of these names are function calls, some of them are types, etc.
- a set of *names* are collected by traversing the body of the AST. Some of
these names are function calls, some of them are types, etc.

Symbols named in import statements are added to imports (type `DIRECT`).

Expand All @@ -896,7 +975,19 @@ A trie of the symbols in scope for the file is built from:
- the file package(s)
- wildcard imports

Then, all *names* in the file are tested against the file scope. Matching symbols are added to the imports (type `RESOLVED_NAME`).
"Names" represent a variety of things in a scala file. It might be a class
instatiation (e.g `new Foo()`), a static method call `doSomething()`, and other
similar names.

In an earlier implementation of scala-gazelle, all *names* in the file were
tested against the file scope and matching symbols are added to the imports
list (of type `RESOLVED_NAME`).

The drawback of that approach is that it was imprecise, potentially leading to
false positive import resolutions (and unnecessary and/or incorrect `deps`
list).

Now, name resolution is an opt-in feature using the gazelle directive `gazelle:resolve_file_symbol_name` directive.

## How Required Imports are Resolved

Expand Down
2 changes: 2 additions & 0 deletions language/scala/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ go_library(
"@bazel_gazelle//rule:go_default_library",
"@build_stack_rules_proto//pkg/protoc",
"@com_github_bazelbuild_buildtools//build:go_default_library",
"@com_github_bmatcuk_doublestar_v4//:doublestar",
"@com_github_pcj_mobyprogress//:mobyprogress",
],
)
Expand Down Expand Up @@ -83,6 +84,7 @@ go_test(
"language_test.go",
"loads_test.go",
"scala_config_test.go",
"scala_package_test.go",
"scala_rule_test.go",
],
data = [":gazelle"] + glob(["testdata/**"]),
Expand Down
7 changes: 4 additions & 3 deletions language/scala/existing_scala_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ func init() {
mustRegister("@io_bazel_rules_scala//scala:scala.bzl", "scala_test")
}

// existingScalaRuleProvider implements RuleResolver for scala-like rules that are
// already in the build file. It does not create any new rules. This rule
// implementation is used to parse files named in 'srcs' and update 'deps'.
// existingScalaRuleProvider implements RuleResolver for scala-like rules that
// are already in the build file. It does not create any new rules. This rule
// implementation is used to parse files named in 'srcs' and update 'deps' (and
// optionally, exports).
type existingScalaRuleProvider struct {
load, name string
}
Expand Down
7 changes: 4 additions & 3 deletions language/scala/language.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,13 @@ func (sl *scalaLang) Name() string { return scalaLangName }
// KnownDirectives implements part of the language.Language interface
func (*scalaLang) KnownDirectives() []string {
return []string{
scalaRuleDirective,
resolveConflictsDirective,
resolveFileSymbolName,
resolveGlobDirective,
resolveKindRewriteNameDirective,
resolveWithDirective,
resolveConflictsDirective,
scalaAnnotateDirective,
resolveKindRewriteNameDirective,
scalaRuleDirective,
}
}

Expand Down
7 changes: 4 additions & 3 deletions language/scala/language_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ func ExampleLanguage_KnownDirectives() {
fmt.Println(d)
}
// output:
// scala_rule
// resolve_conflicts
// resolve_file_symbol_name
// resolve_glob
// resolve_kind_rewrite_name
// resolve_with
// resolve_conflicts
// scala_annotate
// resolve_kind_rewrite_name
// scala_rule
}
67 changes: 58 additions & 9 deletions language/scala/scala_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/bazelbuild/bazel-gazelle/resolve"
"github.com/bazelbuild/bazel-gazelle/rule"
"github.com/bazelbuild/buildtools/build"
"github.com/bmatcuk/doublestar/v4"

"github.com/stackb/scala-gazelle/pkg/collections"
"github.com/stackb/scala-gazelle/pkg/resolver"
Expand All @@ -31,20 +32,22 @@ const (
resolveGlobDirective = "resolve_glob"
resolveConflictsDirective = "resolve_conflicts"
resolveWithDirective = "resolve_with"
resolveFileSymbolName = "resolve_file_symbol_name"
resolveKindRewriteNameDirective = "resolve_kind_rewrite_name"
)

// scalaConfig represents the config extension for the a scala package.
type scalaConfig struct {
config *config.Config
rel string
universe resolver.Universe
overrides []*overrideSpec
implicitImports []*implicitImportSpec
rules map[string]*scalarule.Config
labelNameRewrites map[string]resolver.LabelNameRewriteSpec
annotations map[annotation]interface{}
conflictResolvers []resolver.ConflictResolver
config *config.Config
rel string
universe resolver.Universe
overrides []*overrideSpec
implicitImports []*implicitImportSpec
resolveFileSymbolNames []*resolveFileSymbolNameSpec
rules map[string]*scalarule.Config
labelNameRewrites map[string]resolver.LabelNameRewriteSpec
annotations map[annotation]interface{}
conflictResolvers []resolver.ConflictResolver
}

// newScalaConfig initializes a new scalaConfig.
Expand Down Expand Up @@ -102,6 +105,9 @@ func (c *scalaConfig) clone(config *config.Config, rel string) *scalaConfig {
if c.conflictResolvers != nil {
clone.conflictResolvers = c.conflictResolvers[:]
}
if c.resolveFileSymbolNames != nil {
clone.resolveFileSymbolNames = c.resolveFileSymbolNames[:]
}
return clone
}

Expand Down Expand Up @@ -149,6 +155,8 @@ func (c *scalaConfig) parseDirectives(directives []rule.Directive) (err error) {
c.parseResolveGlobDirective(d)
case resolveWithDirective:
c.parseResolveWithDirective(d)
case resolveFileSymbolName:
c.parseResolveFileSymbolNames(d)
case resolveKindRewriteNameDirective:
c.parseResolveKindRewriteNameDirective(d)
case resolveConflictsDirective:
Expand Down Expand Up @@ -215,6 +223,23 @@ func (c *scalaConfig) parseResolveWithDirective(d rule.Directive) {
})
}

func (c *scalaConfig) parseResolveFileSymbolNames(d rule.Directive) {
parts := strings.Fields(d.Value)
if len(parts) < 2 {
log.Printf("invalid gazelle:%s directive: expected [FILENAME_PATTERN [+|-]SYMBOLS...], got %v", resolveKindRewriteNameDirective, parts)
return
}
pattern := parts[0]

for _, part := range parts[1:] {
intent := collections.ParseIntent(part)
c.resolveFileSymbolNames = append(c.resolveFileSymbolNames, &resolveFileSymbolNameSpec{
pattern: pattern,
symbolName: *intent,
})
}
}

func (c *scalaConfig) parseResolveKindRewriteNameDirective(d rule.Directive) {
parts := strings.Fields(d.Value)
if len(parts) != 3 {
Expand Down Expand Up @@ -318,6 +343,23 @@ func (c *scalaConfig) shouldAnnotateExports() bool {
return ok
}

// ShouldResolveFileSymbolName tests whether the given symbol name pattern
// should be resolved within the scope of the given filename pattern.
// resolveFileSymbolNameSpecs represent a whitelist; if no patterns match, false
// is returned.
func (c *scalaConfig) ShouldResolveFileSymbolName(filename, name string) bool {
for _, spec := range c.resolveFileSymbolNames {
if ok, _ := doublestar.Match(spec.pattern, filename); !ok {
continue
}
if ok, _ := doublestar.Match(spec.symbolName.Value, name); !ok {
continue
}
return spec.symbolName.Want
}
return false
}

func (c *scalaConfig) Comment() build.Comment {
return build.Comment{Token: "# " + c.String()}
}
Expand Down Expand Up @@ -487,6 +529,13 @@ type implicitImportSpec struct {
deps []string
}

type resolveFileSymbolNameSpec struct {
// pattern is the filename glob pattern to test
pattern string
// symbol is the symbol name to resolve
symbolName collections.Intent
}

func parseAnnotation(val string) annotation {
switch val {
case "imports":
Expand Down
Loading

0 comments on commit 021c30d

Please sign in to comment.