Introduce grammar to parse task references
Uses antlr4 to parse the task reference syntax. Also,
adds a missing test case to ensure functionality.
Include 'maven' in the zuul config to ensure it's installed on the host
which fixes the error:
antlr4_runtime requires mvn as a dependency. Please check your PATH.
Change-Id: I439d813f63639558109f547f0e769d6833d4c680
diff --git a/.zuul.yaml b/.zuul.yaml
index 52c12ce..6e83fbf 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -2,6 +2,7 @@
name: plugins-task-build
parent: gerrit-plugin-build
pre-run:
+ - tools/playbooks/install_maven.yaml
- tools/playbooks/install_docker.yaml
- tools/playbooks/install_python3-distutils.yaml
vars:
diff --git a/BUILD b/BUILD
index ec0e964..8782740 100644
--- a/BUILD
+++ b/BUILD
@@ -8,6 +8,7 @@
load("//tools/js:eslint.bzl", "eslint")
load("//tools/bzl:junit.bzl", "junit_tests")
load("@rules_java//java:defs.bzl", "java_library", "java_plugin")
+load("@rules_antlr//antlr:antlr4.bzl", "antlr")
plugin_name = "task"
@@ -29,6 +30,19 @@
exports = ["@auto-value//jar"],
)
+antlr(
+ name = "task_reference",
+ srcs = ["src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4"],
+ package = "com.googlesource.gerrit.plugins.task",
+ visibility = ["//visibility:public"],
+)
+
+java_library(
+ name = "task_reference_parser",
+ srcs = [":task_reference"],
+ deps = ["@antlr4_runtime//jar"],
+)
+
gerrit_plugin(
name = plugin_name,
srcs = glob(["src/main/java/**/*.java"]),
@@ -40,7 +54,11 @@
],
resource_jars = [":gr-task-plugin"],
resources = glob(["src/main/resources/**/*"]),
- deps = [":auto-value"],
+ deps = [
+ ":auto-value",
+ ":task_reference_parser",
+ "@antlr4_runtime//jar",
+ ],
javacopts = [ "-Werror", "-Xlint:all", "-Xlint:-classfile", "-Xlint:-processing"],
)
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index fdf9c1c..901cd0f 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -1,4 +1,5 @@
load("@bazel_tools//tools/build_defs/repo:maven_rules.bzl", "maven_jar")
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
def external_plugin_deps():
AUTO_VALUE_VERSION = "1.7.4"
@@ -14,3 +15,42 @@
artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
sha1 = "eff48ed53995db2dadf0456426cc1f8700136f86",
)
+
+ http_archive(
+ name = "rules_antlr",
+ sha256 = "26e6a83c665cf6c1093b628b3a749071322f0f70305d12ede30909695ed85591",
+ strip_prefix = "rules_antlr-0.5.0",
+ urls = ["https://github.com/marcohu/rules_antlr/archive/0.5.0.tar.gz"],
+ )
+
+ maven_jar(
+ name = "antlr3_runtime",
+ artifact = "org.antlr:antlr-runtime:3.5.2",
+ sha1 = "cd9cd41361c155f3af0f653009dcecb08d8b4afd",
+ )
+
+ ANTLR_VERSION = "4.9.3"
+
+ maven_jar(
+ name = "antlr4_runtime",
+ artifact = "org.antlr:antlr4-runtime:" + ANTLR_VERSION,
+ sha1 = "81befc16ebedb8b8aea3e4c0835dd5ca7e8523a8",
+ )
+
+ maven_jar(
+ name = "antlr4_tool",
+ artifact = "org.antlr:antlr4:" + ANTLR_VERSION,
+ sha1 = "9d47afaa75d70903b5b77413b034d6b201d7d5d6",
+ )
+
+ maven_jar(
+ name = "stringtemplate4",
+ artifact = "org.antlr:ST4:4.3.1",
+ sha1 = "9c61ac6d17b7f450b4048742c2cc73787972518e",
+ )
+
+ maven_jar(
+ name = "javax_json",
+ artifact = "org.glassfish:javax.json:1.0.4",
+ sha1 = "3178f73569fd7a1e5ffc464e680f7a8cc784b85a",
+ )
diff --git a/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4 b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
new file mode 100644
index 0000000..5631e07
--- /dev/null
+++ b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
@@ -0,0 +1,89 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// 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.
+
+/**
+ *
+ * This file defines the grammar used for Task Reference
+ *
+ * TASK_REF = [ [ TASK_FILE_PATH ] '^' ] TASK_NAME
+ *
+ * Examples:
+ *
+ * file: All-Projects:refs/meta/config:task.config
+ * reference: foo.config^sample
+ * Implied task:
+ * file: All-Projects:refs/meta/config:task/foo.config task: sample
+ *
+ * file: All-Projects:refs/meta/config:task/dir/bar.config
+ * reference: /foo.config^sample
+ * Implied task:
+ * file: All-Projects:refs/meta/config:task/foo.config task: sample
+ *
+ * file: All-Projects:refs/meta/config:task/dir/bar.config
+ * reference: sub-dir/foo.config^sample
+ * Implied task:
+ * file: All-Projects:refs/meta/config:task/dir/sub-dir/foo.config task: sample
+ *
+ * file: All-Projects:refs/meta/config:task/dir/bar.config
+ * reference: ^sample
+ * Implied task:
+ * file: All-Projects:refs/meta/config:task.config task: sample
+ *
+ */
+
+grammar TaskReference;
+
+options {
+ language = Java;
+}
+
+reference
+ : file_path? TASK
+ ;
+
+file_path
+ : (absolute| relative)? TASK_DELIMETER
+ ;
+
+absolute
+ : '/' relative
+ ;
+
+relative
+ : dir* NAME
+ ;
+
+dir
+ : (NAME '/')
+ ;
+
+TASK
+ : (~'^')+ EOF
+ ;
+
+NAME
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH+
+ ;
+
+fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH
+ : ':' | '?' | '#' | '[' | ']' | '@'
+ |'!' | '$' | '&' | '\'' | '(' | ')'
+ | '*' | '+' | ',' | ';' | '=' | '%'
+ | 'A'..'Z' | 'a'..'z' | '0'..'9'
+ | '_' | '.' | '\\' | '-' | '~'
+ ;
+
+TASK_DELIMETER
+ : '^'
+ ;
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
index 2791d38..3361464 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
@@ -129,6 +129,8 @@
return task;
}
}
+ } catch (RuntimeConfigInvalidException e) {
+ throw e.checkedException;
} catch (NoSuchElementException e) {
// expression was not optional but we ran out of names to try
throw new ConfigInvalidException("task not defined");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java b/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java
new file mode 100644
index 0000000..cc0ed94
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// 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.googlesource.gerrit.plugins.task;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+public class RuntimeConfigInvalidException extends RuntimeException {
+ protected static final long serialVersionUID = 1L;
+ protected ConfigInvalidException checkedException;
+
+ public RuntimeConfigInvalidException(ConfigInvalidException e) {
+ super(e);
+ this.checkedException = e;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
index e47e880..759caba 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
@@ -18,18 +18,21 @@
import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
/**
* A TaskExpression represents a config string pointing to an expression which includes zero or more
- * task names separated by a '|', and potentially termintated by a '|'. If the expression is not
- * terminated by a '|' it indicates that task resolution of at least one task is required. Task
+ * task references separated by a '|', and potentially termintated by a '|'. If the expression is
+ * not terminated by a '|' it indicates that task resolution of at least one task is required. Task
* selection priority is from left to right. This can be expressed as:
*
* <pre>
- * TASK_REF = [ [ TASK_FILE_PATH ] '^' ] TASK_NAME
- * TASK_EXPR = TASK_REF [ WHITE_SPACE * '|' [ WHITE_SPACE * TASK_EXPR ] ]
+ * TASK_EXPR = TASK_REFERENCE [ WHITE_SPACE * '|' [ WHITE_SPACE * TASK_EXPR ] ]
* </pre>
*
+ * <a href="file:../../../../../../antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4">See
+ * this for Task Reference</a>
+ *
* <p>Example expressions to prioritized names and requirements:
*
* <ul>
@@ -41,12 +44,6 @@
* <pre> "shadenfreud |" -> ("shadenfreud") optional</pre>
* <li>
* <pre> "foo | bar |" -> ("foo", "bar") optional</pre>
- * <li>
- * <pre> "/foo^bar | baz |" -> ("task/foo^bar", "baz") optional</pre>
- * <li>
- * <pre> "foo^bar | baz |" -> ("cur_dir/foo^bar", "baz") optional</pre>
- * <li>
- * <pre> "^bar | baz |" -> ("task.config^bar", "baz") optional</pre>
* </ul>
*/
public class TaskExpression implements Iterable<TaskKey> {
@@ -86,7 +83,11 @@
throw new NoSuchElementException("No more names, yet expression was not optional");
}
hasNext = null;
- return new TaskReference(key.file(), m.group(1)).getTaskKey();
+ try {
+ return new TaskReference(key.file(), m.group(1)).getTaskKey();
+ } catch (ConfigInvalidException e) {
+ throw new RuntimeConfigInvalidException(e);
+ }
}
};
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
index f21e732..6acab3e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
@@ -15,7 +15,11 @@
package com.googlesource.gerrit.plugins.task;
import com.google.auto.value.AutoValue;
+import com.google.common.base.Preconditions;
import com.google.gerrit.entities.BranchNameKey;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.eclipse.jgit.errors.ConfigInvalidException;
/** An immutable reference to a task in task config file. */
@AutoValue
@@ -49,4 +53,67 @@
public boolean isTasksFactoryGenerated() {
return subSection().section().equals(CONFIG_TASKS_FACTORY);
}
+
+ public static class Builder {
+ protected final FileKey relativeTo;
+ protected String file;
+ protected String task;
+
+ Builder(FileKey relativeTo) {
+ this.relativeTo = relativeTo;
+ }
+
+ public TaskKey buildTaskKey() {
+ return TaskKey.create(
+ isRelativePath() ? relativeTo : FileKey.create(relativeTo.branch(), file), task);
+ }
+
+ public void setAbsolute() {
+ file = TaskFileConstants.TASK_DIR;
+ }
+
+ public void setPath(Path path) throws ConfigInvalidException {
+ Path parentDir = Paths.get(relativeTo.file()).getParent();
+ if (parentDir == null) {
+ parentDir = Paths.get(TaskFileConstants.TASK_DIR);
+ }
+
+ file =
+ isRelativePath()
+ ? parentDir.resolve(path).toString()
+ : Paths.get(file).resolve(path).toString();
+ throwIfInvalidPath();
+ }
+
+ public void setRefRootFile() throws ConfigInvalidException {
+ Preconditions.checkState(!isFileAlreadySet());
+ file = TaskFileConstants.TASK_CFG;
+ }
+
+ public void setTaskName(String task) {
+ this.task = task;
+ }
+
+ protected void throwIfInvalidPath() throws ConfigInvalidException {
+ Path path = Paths.get(file);
+ if (!path.startsWith(TaskFileConstants.TASK_DIR)
+ && !path.equals(Paths.get(TaskFileConstants.TASK_CFG))) {
+ throw new ConfigInvalidException(
+ "Invalid config location, path should be "
+ + TaskFileConstants.TASK_CFG
+ + " or under "
+ + TaskFileConstants.TASK_DIR
+ + " directory");
+ }
+ }
+
+ /** Returns true when the path implies relative or same file. */
+ protected boolean isRelativePath() {
+ return file == null;
+ }
+
+ protected boolean isFileAlreadySet() {
+ return file != null;
+ }
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
index 2463a0c..139b27d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
@@ -14,9 +14,17 @@
package com.googlesource.gerrit.plugins.task;
-import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.NoSuchElementException;
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.Lexer;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.ParseTreeWalker;
+import org.eclipse.jgit.errors.ConfigInvalidException;
/** This class is used by TaskExpression to decode the task from task reference. */
public class TaskReference {
@@ -31,37 +39,78 @@
}
}
- public TaskKey getTaskKey() {
- String[] referenceSplit = reference.split("\\^");
- switch (referenceSplit.length) {
- case 1:
- return TaskKey.create(currentFile, referenceSplit[0]);
- case 2:
- return TaskKey.create(getFileKey(referenceSplit[0]), referenceSplit[1]);
- default:
- throw new NoSuchElementException();
+ public TaskKey getTaskKey() throws ConfigInvalidException {
+ TaskKey.Builder builder = new TaskKey.Builder(currentFile);
+ ParseTreeWalker walker = new ParseTreeWalker();
+ try {
+ walker.walk(new TaskReferenceListener(builder), parse());
+ } catch (RuntimeConfigInvalidException e) {
+ throw e.checkedException;
+ }
+ return builder.buildTaskKey();
+ }
+
+ protected ParseTree parse() {
+ Lexer lexer = new TaskReferenceLexer(CharStreams.fromString(reference));
+ lexer.removeErrorListeners();
+ lexer.addErrorListener(TaskReferenceErrorListener.INSTANCE);
+ return new TaskReferenceParser(new CommonTokenStream(lexer)).reference();
+ }
+
+ protected static class TaskReferenceErrorListener extends BaseErrorListener {
+ protected static final TaskReferenceErrorListener INSTANCE = new TaskReferenceErrorListener();
+
+ @Override
+ public void syntaxError(
+ Recognizer<?, ?> recognizer,
+ Object offendingSymbol,
+ int line,
+ int charPositionInLine,
+ String msg,
+ RecognitionException e) {
+ throw new NoSuchElementException();
}
}
- protected FileKey getFileKey(String referenceFile) {
- return FileKey.create(currentFile.branch(), getFile(referenceFile));
- }
+ protected class TaskReferenceListener extends TaskReferenceBaseListener {
+ TaskKey.Builder builder;
- protected String getFile(String referencedFile) {
- if (referencedFile.isEmpty()) { // Implies a task from root task.config
- return TaskFileConstants.TASK_CFG;
+ TaskReferenceListener(TaskKey.Builder builder) {
+ this.builder = builder;
}
- if (referencedFile.startsWith("/")) { // Implies absolute path to the config is provided
- return Paths.get(TaskFileConstants.TASK_DIR, referencedFile).toString();
+ @Override
+ public void enterAbsolute(TaskReferenceParser.AbsoluteContext ctx) {
+ builder.setAbsolute();
}
- // Implies a relative path to sub-directory
- Path dir = Paths.get(currentFile.file()).getParent();
- if (dir == null) { // Relative path in root task.config should refer to files under task dir
- return Paths.get(TaskFileConstants.TASK_DIR, referencedFile).toString();
- } else {
- return Paths.get(dir.toString(), referencedFile).toString();
+ @Override
+ public void enterRelative(TaskReferenceParser.RelativeContext ctx) {
+ try {
+ builder.setPath(
+ ctx.dir().stream()
+ .map(dir -> Paths.get(dir.NAME().getText()))
+ .reduce(Paths.get(""), (a, b) -> a.resolve(b))
+ .resolve(ctx.NAME().getText()));
+ } catch (ConfigInvalidException e) {
+ throw new RuntimeConfigInvalidException(e);
+ }
+ }
+
+ @Override
+ public void enterReference(TaskReferenceParser.ReferenceContext ctx) {
+ builder.setTaskName(ctx.TASK().getText());
+ }
+
+ @Override
+ public void enterFile_path(TaskReferenceParser.File_pathContext ctx) {
+ if (ctx.absolute() == null && ctx.relative() == null) {
+ try {
+ builder.setRefRootFile();
+ } catch (ConfigInvalidException e) {
+ throw new RuntimeConfigInvalidException(e);
+ }
+ }
}
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
index 8d079ff..5f309d0 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
@@ -18,6 +18,7 @@
import com.google.gerrit.entities.Project;
import java.util.NoSuchElementException;
import junit.framework.TestCase;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.junit.Test;
public class TaskReferenceTest extends TestCase {
@@ -53,13 +54,19 @@
}
@Test
- public void testReferencingRelativeTask() {
+ public void testReferencingRelativeDirTask() {
String reference = " dir/common.config^" + SIMPLE;
assertEquals(
createTaskKey(SUB_COMMON_CFG, SIMPLE), getTaskFromReference(COMMON_CFG, reference));
}
@Test
+ public void testReferencingRelativeFileTask() {
+ String reference = "common.config^" + SIMPLE;
+ assertEquals(createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(COMMON_CFG, reference));
+ }
+
+ @Test
public void testReferencingAbsoluteTask() {
String reference = " /common.config^" + SIMPLE;
assertEquals(
@@ -79,7 +86,11 @@
}
protected static TaskKey getTaskFromReference(FileKey file, String expression) {
- return new TaskReference(file, expression).getTaskKey();
+ try {
+ return new TaskReference(file, expression).getTaskKey();
+ } catch (ConfigInvalidException e) {
+ throw new NoSuchElementException();
+ }
}
protected static TaskKey createTaskKey(FileKey file, String task) {
diff --git a/tools/playbooks/install_maven.yaml b/tools/playbooks/install_maven.yaml
new file mode 100644
index 0000000..ae1690d
--- /dev/null
+++ b/tools/playbooks/install_maven.yaml
@@ -0,0 +1,8 @@
+- hosts: all
+ tasks:
+ - name: Install maven
+ become: true
+ package:
+ name:
+ - maven
+ state: present