Skip to content

Commit

Permalink
Implement py code generation handling
Browse files Browse the repository at this point in the history
It is possible to write Bazel rules that generate Python code and
act as a `py_library`. The plugin is augmented with this change to
have a means of detecting these sort of rules and be able to work
with them.
  • Loading branch information
andponlin-canva committed Oct 7, 2024
1 parent 66f5130 commit 6788b8e
Show file tree
Hide file tree
Showing 22 changed files with 1,230 additions and 28 deletions.
56 changes: 56 additions & 0 deletions aspect/intellij_info_impl.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ PY2 = 1

PY3 = 2

TARGET_TAG_PY_CODE_GENERATOR = "intellij-py-code-generator"

# PythonCompatVersion enum; must match PyIdeInfo.PythonSrcsVersion
SRC_PY2 = 1

Expand Down Expand Up @@ -338,6 +340,60 @@ def collect_py_info(target, ctx, semantics, ide_info, ide_info_file, output_grou
args = _do_starlark_string_expansion(ctx, "args", args, data_deps)
imports = getattr(ctx.rule.attr, "imports", [])

# If there are apparently no sources found from `srcs` and the target has the tag
# for code-generation then use the run-files as the sources and take the imports
# from the PyInfo.

if 0 == len(sources) and TARGET_TAG_PY_CODE_GENERATOR in getattr(ctx.rule.attr, "tags", []):
def provider_import_to_attr_import(provider_import):
"""\
Remaps the imports from PyInfo
The imports that are supplied on the `PyInfo` are relative to the runfiles and so are
not the same as those which might be supplied on an attribute of `py_library`. This
function will remap those back so they look as if they were `imports` attributes on
the rule. The form of the runfiles import is `<workspace_name>/<package_dir>/<import>`.
The actual `workspace_name` is not interesting such that the first part can be simply
stripped. Next the package to the Label is stripped leaving a path that would have been
supplied on an `imports` attribute to a Rule.
"""

# Other code in this file appears to assume *NIX path component separators?

provider_import_parts = [p for p in provider_import.split("/")]
package_parts = [p for p in ctx.label.package.split("/")]

if 0 == len(provider_import_parts):
return None

scratch_parts = provider_import_parts[1:] # remove the workspace name or _main

for p in package_parts:
if 0 != len(provider_import_parts) and scratch_parts[0] == p:
scratch_parts = scratch_parts[1:]
else:
return None

return "/".join(scratch_parts)

def provider_imports_to_attr_imports():
result = []

for provider_import in target[PyInfo].imports.to_list():
attr_import = provider_import_to_attr_import(provider_import)
if attr_import:
result.append(attr_import)

return result

if target[PyInfo].imports:
imports.extend(provider_imports_to_attr_imports())

runfiles = target[DefaultInfo].default_runfiles

if runfiles and runfiles.files:
sources.extend([artifact_location(f) for f in runfiles.files.to_list()])

ide_info["py_ide_info"] = struct_omit_none(
launcher = py_launcher,
python_version = _get_python_version(ctx),
Expand Down
4 changes: 3 additions & 1 deletion base/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ java_library(
"//shared:vcs",
"//third_party/auto_value",
"@error_prone_annotations//jar",
"@gson//jar"
"@gson//jar",
],
)

Expand Down Expand Up @@ -125,6 +125,7 @@ java_library(
name = "label_api",
srcs = [
"src/com/google/idea/blaze/base/ideinfo/ArtifactLocation.java",
"src/com/google/idea/blaze/base/ideinfo/Tags.java",
"src/com/google/idea/blaze/base/model/primitives/InvalidTargetException.java",
"src/com/google/idea/blaze/base/model/primitives/Kind.java",
"src/com/google/idea/blaze/base/model/primitives/Label.java",
Expand Down Expand Up @@ -246,6 +247,7 @@ java_library(
java_library(
name = "workspace_language_checker_api",
srcs = [
"src/com/google/idea/blaze/base/ideinfo/Tags.java",
"src/com/google/idea/blaze/base/model/primitives/LanguageClass.java",
"src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageChecker.java",
],
Expand Down
2 changes: 2 additions & 0 deletions base/src/META-INF/blaze-base.xml
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@
<extensionPoint qualifiedName="com.google.idea.blaze.BuildSystemVersionChecker" interface="com.google.idea.blaze.base.plugin.BuildSystemVersionChecker"/>
<extensionPoint qualifiedName="com.google.idea.blaze.BlazeIssueParserProvider" interface="com.google.idea.blaze.base.issueparser.BlazeIssueParserProvider"/>
<extensionPoint qualifiedName="com.google.idea.blaze.DirectoryToTargetProvider" interface="com.google.idea.blaze.base.dependencies.DirectoryToTargetProvider"/>
<extensionPoint qualifiedName="com.google.idea.blaze.TargetTagFilter" interface="com.google.idea.blaze.base.dependencies.TargetTagFilter"/>
<extensionPoint qualifiedName="com.google.idea.blaze.SourceToTargetProvider" interface="com.google.idea.blaze.base.dependencies.SourceToTargetProvider"/>
<extensionPoint qualifiedName="com.google.idea.blaze.SourceToTargetFilteringStrategy" interface="com.google.idea.blaze.base.dependencies.SourceToTargetFilteringStrategy"/>
<extensionPoint qualifiedName="com.google.idea.blaze.MacroTargetProvider"
Expand Down Expand Up @@ -637,6 +638,7 @@
<TargetFinder implementation="com.google.idea.blaze.base.run.targetfinder.ProjectTargetFinder"/>
<SourceToTargetFinder implementation="com.google.idea.blaze.base.run.testmap.ProjectSourceToTargetFinder"/>
<DirectoryToTargetProvider implementation="com.google.idea.blaze.base.dependencies.BlazeQueryDirectoryToTargetProvider" order="last"/>
<TargetTagFilter implementation="com.google.idea.blaze.base.dependencies.BlazeQueryTargetTagFilter" order="last"/>
<SourceToTargetProvider implementation="com.google.idea.blaze.base.dependencies.BlazeQuerySourceToTargetProvider" order="last"/>
<SourceToTargetFilteringStrategy implementation="com.google.idea.blaze.base.dependencies.SourceToTargetFilteringStrategy$IgnoredRules"/>
<SourceToTargetFilteringStrategy implementation="com.google.idea.blaze.base.dependencies.SourceToTargetFilteringStrategy$SupportedLanguages"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright 2024 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 com.google.idea.blaze.base.dependencies;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.idea.blaze.base.bazel.BuildSystem;
import com.google.idea.blaze.base.bazel.BuildSystem.BuildInvoker;
import com.google.idea.blaze.base.command.BlazeCommand;
import com.google.idea.blaze.base.command.BlazeCommandName;
import com.google.idea.blaze.base.command.BlazeFlags;
import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
import com.google.idea.blaze.base.model.primitives.Label;
import com.google.idea.blaze.base.model.primitives.TargetExpression;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.google.idea.blaze.base.settings.Blaze;
import com.google.idea.blaze.exception.BuildException;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.SystemInfo;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;

/**
* This class is able to filter a list of targets selecting those that are
* tagged with one or more of a number of tags.
*/

public class BlazeQueryTargetTagFilter implements TargetTagFilter {

private static final Logger logger =
Logger.getInstance(BlazeQueryTargetTagFilter.class);

/**
* Tags are checked against this {@link Pattern} to ensure they can be used with
* the Bazel queries run in this logic. See
* {@link com.google.idea.blaze.base.ideinfo.Tags} for example Tags that would
* be used inside the Plugin; all of which conform to this {@link Pattern}.
*/
private static final Pattern PATTERN_TAG = Pattern.compile("^[a-zA-Z0-9_-]+$");

@Nullable
@Override
public List<TargetExpression> doFilterCodeGen(
Project project,
BlazeContext context,
List<TargetExpression> targets,
Set<String> tags) {

if (targets.isEmpty() || tags.isEmpty()) {
return ImmutableList.of();
}

return runQuery(project, getQueryString(targets, tags), context);
}

@VisibleForTesting
public static String getQueryString(List<TargetExpression> targets, Set<String> tags) {
Preconditions.checkArgument(null != targets && !targets.isEmpty(), "the targets must be supplied");
Preconditions.checkArgument(null != tags && !tags.isEmpty(), "the tags must be supplied");

for (String tag : tags) {
if (!PATTERN_TAG.matcher(tag).matches()) {
throw new IllegalStateException("the tag [" + tag + "] is not able to be used for filtering");
}
}

String targetsExpression = targets.stream().map(Object::toString).collect(Collectors.joining(" + "));
String matchExpression = String.format("[\\[ ](%s)[,\\]]", String.join("|", ImmutableList.sortedCopyOf(tags)));

if (SystemInfo.isWindows) {
return String.format("attr('tags', '%s', %s)", matchExpression, targetsExpression);
}

return String.format("attr(\"tags\", \"%s\", %s)", matchExpression, targetsExpression);
}

@javax.annotation.Nullable
private static List<TargetExpression> runQuery(
Project project,
String query,
BlazeContext context) {

BuildSystem buildSystem = Blaze.getBuildSystemProvider(project).getBuildSystem();

BlazeCommand.Builder command =
BlazeCommand.builder(
buildSystem.getDefaultInvoker(project, context), BlazeCommandName.QUERY)
.addBlazeFlags("--output=label")
.addBlazeFlags(BlazeFlags.KEEP_GOING)
.addBlazeFlags(query);

BuildInvoker invoker = buildSystem.getDefaultInvoker(project, context);

try (BuildResultHelper helper = invoker.createBuildResultHelper();
InputStream queryResultStream = invoker.getCommandRunner()
.runQuery(project, command, helper, context)) {

return new BufferedReader(new InputStreamReader(queryResultStream, UTF_8))
.lines()
.map(Label::createIfValid)
.collect(Collectors.toList());

} catch (IOException | BuildException e) {
logger.error(e.getMessage(), e);
return null;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2024 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 com.google.idea.blaze.base.dependencies;

import com.google.common.collect.ImmutableList;
import com.google.idea.blaze.base.model.primitives.TargetExpression;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;

/**
* Implementations of this interface are able to filter targets for having
* one of a set of tags.
*/

public interface TargetTagFilter {

ExtensionPointName<TargetTagFilter> EP_NAME =
ExtensionPointName.create("com.google.idea.blaze.TargetTagFilter");

static boolean hasProvider() {
return EP_NAME.getExtensions().length != 0;
}

/**
* This method will run a Bazel query to select for those targets having a tag
* which matches one of the supplied {@code tags}.
*
* @param tags is a list of tags that targets are expected to have configured in
* order to be filtered in.
* @param targets is a list of Bazel targets to filter.
* @return a subset of the supplied targets that include one of the supplied {code tags}.
*/
static List<TargetExpression> filterCodeGen(
Project project,
BlazeContext context,
List<TargetExpression> targets,
Set<String> tags) {
return Arrays.stream(EP_NAME.getExtensions())
.map(p -> p.doFilterCodeGen(project, context, targets, tags))
.filter(Objects::nonNull)
.findFirst()
.orElse(ImmutableList.of());
}

/**
* {@see #filterCodeGen}
*/
@Nullable
List<TargetExpression> doFilterCodeGen(
Project project,
BlazeContext context,
List<TargetExpression> targets,
Set<String> tags);

}
7 changes: 6 additions & 1 deletion base/src/com/google/idea/blaze/base/ideinfo/Tags.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016 The Bazel Authors. All rights reserved.
* Copyright 2016-2024 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.
Expand Down Expand Up @@ -30,4 +30,9 @@ public final class Tags {

/** Ignores the target. */
public static final String TARGET_TAG_EXCLUDE_TARGET = "intellij-exclude-target";

/**
* Signals to the IDE that a rule produces Python code rather than has code as input.
*/
public static final String TARGET_TAG_PY_CODE_GENERATOR = "intellij-py-code-generator";
}
Loading

0 comments on commit 6788b8e

Please sign in to comment.