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