8202941: GenModuleInfoSource build tool does not detect missing semicolons
authormchung
Wed, 08 Aug 2018 14:40:02 -0700
changeset 51339 554bb4e2d10d
parent 51338 aa3bfacc912c
child 51340 818768cd1c6c
8202941: GenModuleInfoSource build tool does not detect missing semicolons Reviewed-by: erikj
make/gensrc/Gensrc-jdk.internal.vm.compiler.gmk
make/jdk/src/classes/build/tools/module/GenModuleInfoSource.java
make/jdk/src/classes/build/tools/module/ModuleInfoExtraTest.java
src/java.base/share/classes/module-info.java
--- a/make/gensrc/Gensrc-jdk.internal.vm.compiler.gmk	Wed Aug 08 15:24:21 2018 -0400
+++ b/make/gensrc/Gensrc-jdk.internal.vm.compiler.gmk	Wed Aug 08 14:40:02 2018 -0700
@@ -123,6 +123,7 @@
 $(GENSRC_DIR)/module-info.java.extra: $(GENSRC_DIR)/_gensrc_proc_done
 	($(CD) $(GENSRC_DIR)/META-INF/providers && \
 	    p=""; \
+	    impl=""; \
 	    for i in $$($(LS) | $(SORT)); do \
 	      c=$$($(CAT) $$i | $(TR) -d '\n\r'); \
 	      if test x$$p != x$$c; then \
@@ -131,15 +132,27 @@
 	        fi; \
 	        $(ECHO) "provides $$c with" >> $@; \
                 p=$$c; \
+	        impl=""; \
 	      fi; \
-	      $(ECHO) "    $$i," >> $@; \
+              if test x$$impl != x; then \
+	        $(ECHO) "  , $$i" >> $@; \
+              else \
+	        $(ECHO) "    $$i" >> $@; \
+              fi; \
+              impl=$$i; \
 	    done); \
             $(ECHO) "    ;" >> $@; \
 	$(ECHO) "uses org.graalvm.compiler.options.OptionDescriptors;" >> $@; \
 	$(ECHO) "provides org.graalvm.compiler.options.OptionDescriptors with" >> $@; \
+        impl=""; \
 	for i in $$($(FIND) $(GENSRC_DIR) -name '*_OptionDescriptors.java' | $(SORT)); do \
 	    c=$$($(ECHO) $$i | $(SED) 's:.*/jdk\.internal\.vm\.compiler/\(.*\)\.java:\1:' | $(TR) '/' '.'); \
-	    $(ECHO) "    $$c," >> $@; \
+            if test x$$impl != x; then \
+	      $(ECHO) "  , $$c" >> $@; \
+            else \
+	      $(ECHO) "    $$c" >> $@; \
+            fi; \
+            impl=$$c; \
 	done; \
 	$(ECHO) "    ;" >> $@;
 
--- a/make/jdk/src/classes/build/tools/module/GenModuleInfoSource.java	Wed Aug 08 15:24:21 2018 -0400
+++ b/make/jdk/src/classes/build/tools/module/GenModuleInfoSource.java	Wed Aug 08 14:40:02 2018 -0700
@@ -37,7 +37,9 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import static java.util.stream.Collectors.*;
 
@@ -131,7 +133,7 @@
         // parse module-info.java.extra
         this.extras = new ModuleInfo();
         for (Path file : extraFiles) {
-            extras.parse(file);
+            extras.parseExtra(file);
         }
 
         // merge with module-info.java.extra
@@ -177,6 +179,7 @@
         final Map<String, Statement> provides = new HashMap<>();
 
         Statement getStatement(String directive, String name) {
+            Objects.requireNonNull(name);
             switch (directive) {
                 case "exports":
                     if (moduleInfo.exports.containsKey(name) &&
@@ -223,49 +226,49 @@
             extraFiles.exports.entrySet()
                 .stream()
                 .filter(e -> exports.containsKey(e.getKey()) &&
-                                e.getValue().filter(modules))
+                    e.getValue().filter(modules))
                 .forEach(e -> mergeExportsOrOpens(exports.get(e.getKey()),
-                                                  e.getValue(),
-                                                  modules));
+                    e.getValue(),
+                    modules));
 
             // add exports that are not defined in the original module-info.java
             extraFiles.exports.entrySet()
                 .stream()
                 .filter(e -> !exports.containsKey(e.getKey()) &&
-                                e.getValue().filter(modules))
+                    e.getValue().filter(modules))
                 .forEach(e -> addTargets(getStatement("exports", e.getKey()),
-                                         e.getValue(),
-                                         modules));
+                    e.getValue(),
+                    modules));
 
             // API package opened in the original module-info.java
             extraFiles.opens.entrySet()
                 .stream()
                 .filter(e -> opens.containsKey(e.getKey()) &&
-                                e.getValue().filter(modules))
+                    e.getValue().filter(modules))
                 .forEach(e -> mergeExportsOrOpens(opens.get(e.getKey()),
-                                                  e.getValue(),
-                                                  modules));
+                    e.getValue(),
+                    modules));
 
             // add opens that are not defined in the original module-info.java
             extraFiles.opens.entrySet()
                 .stream()
                 .filter(e -> !opens.containsKey(e.getKey()) &&
-                                e.getValue().filter(modules))
+                    e.getValue().filter(modules))
                 .forEach(e -> addTargets(getStatement("opens", e.getKey()),
-                                         e.getValue(),
-                                         modules));
+                    e.getValue(),
+                    modules));
 
             // provides
             extraFiles.provides.keySet()
                 .stream()
                 .filter(service -> provides.containsKey(service))
                 .forEach(service -> mergeProvides(service,
-                                                  extraFiles.provides.get(service)));
+                    extraFiles.provides.get(service)));
             extraFiles.provides.keySet()
                 .stream()
                 .filter(service -> !provides.containsKey(service))
                 .forEach(service -> provides.put(service,
-                                                 extraFiles.provides.get(service)));
+                    extraFiles.provides.get(service)));
 
             // uses
             extraFiles.uses.keySet()
@@ -280,8 +283,8 @@
                                 Set<String> modules)
         {
             extra.targets.stream()
-                 .filter(mn -> modules.contains(mn))
-                 .forEach(mn -> statement.addTarget(mn));
+                .filter(mn -> modules.contains(mn))
+                .forEach(mn -> statement.addTarget(mn));
         }
 
         private void mergeExportsOrOpens(Statement statement,
@@ -319,7 +322,7 @@
             }
 
             extra.targets.stream()
-                 .forEach(mn -> statement.addTarget(mn));
+                .forEach(mn -> statement.addTarget(mn));
         }
 
 
@@ -358,189 +361,173 @@
                 .forEach(e -> writer.println(e.getValue()));
         }
 
-        private void parse(Path sourcefile) throws IOException {
-            List<String> lines = Files.readAllLines(sourcefile);
-            Statement statement = null;
-            boolean hasTargets = false;
 
-            for (int lineNumber = 1; lineNumber <= lines.size(); ) {
-                String l = lines.get(lineNumber-1).trim();
-                int index = 0;
-
-                if (l.isEmpty()) {
-                    lineNumber++;
-                    continue;
-                }
-
-                // comment block starts
-                if (l.startsWith("/*")) {
-                    while (l.indexOf("*/") == -1) { // end comment block
-                        l = lines.get(lineNumber++).trim();
-                    }
-                    index = l.indexOf("*/") + 2;
-                    if (index >= l.length()) {
-                        lineNumber++;
-                        continue;
-                    } else {
-                        // rest of the line
-                        l = l.substring(index, l.length()).trim();
-                        index = 0;
-                    }
-                }
-
-                // skip comment and annotations
-                if (l.startsWith("//") || l.startsWith("@")) {
-                    lineNumber++;
-                    continue;
-                }
-
-                int current = lineNumber;
-                int count = 0;
-                while (index < l.length()) {
-                    if (current == lineNumber && ++count > 20)
-                        throw new Error("Fail to parse line " + lineNumber + " " + sourcefile);
+        private void parse(Path file) throws IOException {
+            Parser parser = new Parser(file);
+            parser.run();
+            if (verbose) {
+                parser.dump();
+            }
+            process(parser, false);
+        }
 
-                    int end = l.indexOf(';');
-                    if (end == -1)
-                        end = l.length();
-                    String content = l.substring(0, end).trim();
-                    if (content.isEmpty()) {
-                        index = end+1;
-                        if (index < l.length()) {
-                            // rest of the line
-                            l = l.substring(index, l.length()).trim();
-                            index = 0;
-                        }
-                        continue;
-                    }
-
-                    String[] s = content.split("\\s+");
-                    String keyword = s[0].trim();
+        private void parseExtra(Path file) throws IOException {
+            Parser parser = new Parser(file);
+            parser.run();
+            if (verbose) {
+                parser.dump();
+            }
+            process(parser, true);
+        }
 
-                    String name = s.length > 1 ? s[1].trim() : null;
-                    trace("%d: %s index=%d len=%d%n", lineNumber, l, index, l.length());
-                    switch (keyword) {
-                        case "module":
-                        case "requires":
-                        case "}":
-                            index = l.length();  // skip to the end
-                            continue;
 
-                        case "exports":
-                        case "opens":
-                        case "provides":
-                        case "uses":
-                            // assume name immediately after exports, opens, provides, uses
-                            statement = getStatement(keyword, name);
-                            hasTargets = false;
-
-                            int i = l.indexOf(name, keyword.length()+1) + name.length() + 1;
-                            l = i < l.length() ? l.substring(i, l.length()).trim() : "";
-                            index = 0;
-
-                            if (s.length >= 3) {
-                                if (!s[2].trim().equals(statement.qualifier)) {
-                                    throw new RuntimeException(sourcefile + ", line " +
-                                        lineNumber + ", is malformed: " + s[2]);
-                                }
-                            }
-
-                            break;
+        private void process(Parser parser, boolean extraFile) throws IOException {
+            // no duplicate statement local in each file
+            Map<String, Statement> exports = new HashMap<>();
+            Map<String, Statement> opens = new HashMap<>();
+            Map<String, Statement> uses = new HashMap<>();
+            Map<String, Statement> provides = new HashMap<>();
 
-                        case "to":
-                        case "with":
-                            if (statement == null) {
-                                throw new RuntimeException(sourcefile + ", line " +
-                                    lineNumber + ", is malformed");
-                            }
-
-                            hasTargets = true;
-                            String qualifier = statement.qualifier;
-                            i = l.indexOf(qualifier, index) + qualifier.length() + 1;
-                            l = i < l.length() ? l.substring(i, l.length()).trim() : "";
-                            index = 0;
-                            break;
+            String token = null;
+            boolean hasCurlyBracket = false;
+            while ((token = parser.nextToken()) != null) {
+                if (token.equals("module")) {
+                    String modulename = nextIdentifier(parser);
+                    if (extraFile) {
+                        throw parser.newError("cannot declare module in " + parser.sourceFile);
                     }
-
-                    if (index >= l.length()) {
-                        // skip to next line
-                        continue;
+                    skipTokenOrThrow(parser, "{", "missing {");
+                    hasCurlyBracket = true;
+                } else if (token.equals("requires")) {
+                    token = nextIdentifier(parser);
+                    if (token.equals("transitive")) {
+                        token = nextIdentifier(parser);
+                    }
+                    if (extraFile) {
+                        throw parser.newError("cannot declare requires in " + parser.sourceFile);
                     }
-
-                        // comment block starts
-                    if (l.startsWith("/*")) {
-                        while (l.indexOf("*/") == -1) { // end comment block
-                            l = lines.get(lineNumber++).trim();
+                    skipTokenOrThrow(parser, ";", "missing semicolon");
+                } else if (isExportsOpensProvidesUses(token)) {
+                    // new statement
+                    String keyword = token;
+                    String name = nextIdentifier(parser);
+                    Statement statement = getStatement(keyword, name);
+                    switch (keyword) {
+                        case "exports":
+                            if (exports.containsKey(name)) {
+                                throw parser.newError("multiple " + keyword + " " + name);
+                            }
+                            exports.put(name, statement);
+                            break;
+                        case "opens":
+                            if (opens.containsKey(name)) {
+                                throw parser.newError("multiple " + keyword + " " + name);
+                            }
+                            opens.put(name, statement);
+                            break;
+                        case "uses":
+                            if (uses.containsKey(name)) {
+                                throw parser.newError("multiple " + keyword + " " + name);
+                            }
+                            uses.put(name, statement);
+                            break;
+                        /*  Disable this check until jdk.internal.vm.compiler generated file is fixed.
+                        case "provides":
+                            if (provides.containsKey(name)) {
+                                throw parser.newError("multiple " + keyword + " " + name);
+                            }
+                            provides.put(name, statement);
+                            break;
+                        */
+                    }
+                    String lookAhead = lookAhead(parser);
+                    if (lookAhead.equals(statement.qualifier)) {
+                        parser.nextToken(); // skip qualifier
+                        while ((lookAhead = parser.peekToken()) != null) {
+                            // add target name
+                            name = nextIdentifier(parser);
+                            statement.addTarget(name);
+                            lookAhead = lookAhead(parser);
+                            if (lookAhead.equals(",") || lookAhead.equals(";")) {
+                                parser.nextToken();
+                            } else {
+                                throw parser.newError("missing semicolon");
+                            }
+                            if (lookAhead.equals(";")) {
+                                break;
+                            }
                         }
-                        index = l.indexOf("*/") + 2;
-                        if (index >= l.length()) {
-                            continue;
-                        } else {
-                            // rest of the line
-                            l = l.substring(index, l.length()).trim();
-                            index = 0;
-                        }
+                    } else {
+                        skipTokenOrThrow(parser, ";", "missing semicolon");
                     }
-
-                    if (l.startsWith("//")) {
-                        index = l.length();
-                        continue;
+                } else if (token.equals(";")) {
+                    continue;
+                } else if (hasCurlyBracket && token.equals("}")) {
+                    hasCurlyBracket = false;
+                    if (parser.peekToken() != null) {  // must be EOF
+                        throw parser.newError("is malformed");
                     }
-
-                    if (statement == null) {
-                        throw new RuntimeException(sourcefile + ", line " +
-                            lineNumber + ": missing keyword?");
-                    }
+                } else {
+                    throw parser.newError("missing keyword");
+                }
+            }
+            if (hasCurlyBracket) {
+                parser.newError("missing }");
+            }
+        }
 
-                    if (!hasTargets) {
-                        continue;
-                    }
+        private boolean isExportsOpensProvidesUses(String word) {
+            switch (word) {
+                case "exports":
+                case "opens":
+                case "provides":
+                case "uses":
+                    return true;
+                default:
+                    return false;
+            }
+        }
 
-                    if (index >= l.length()) {
-                        throw new RuntimeException(sourcefile + ", line " +
-                            lineNumber + ": " + l);
-                    }
-
-                    // parse the target module of exports, opens, or provides
-                    Statement stmt = statement;
+        private String lookAhead(Parser parser) {
+            String lookAhead = parser.peekToken();
+            if (lookAhead == null) { // EOF
+                throw parser.newError("reach end of file");
+            }
+            return lookAhead;
+        }
 
-                    int terminal = l.indexOf(';', index);
-                    // determine up to which position to parse
-                    int pos = terminal != -1 ? terminal : l.length();
-                    // parse up to comments
-                    int pos1 = l.indexOf("//", index);
-                    if (pos1 != -1 && pos1 < pos) {
-                        pos = pos1;
-                    }
-                    int pos2 = l.indexOf("/*", index);
-                    if (pos2 != -1 && pos2 < pos) {
-                        pos = pos2;
-                    }
-                    // target module(s) for qualitifed exports or opens
-                    // or provider implementation class(es)
-                    String rhs = l.substring(index, pos).trim();
-                    index += rhs.length();
-                    trace("rhs: index=%d [%s] [line: %s]%n", index, rhs, l);
+        private String nextIdentifier(Parser parser) {
+            String lookAhead = parser.peekToken();
+            boolean maybeIdentifier = true;
+            switch (lookAhead) {
+                case "module":
+                case "requires":
+                case "exports":
+                case "opens":
+                case "provides":
+                case "uses":
+                case "to":
+                case "with":
+                case ",":
+                case ";":
+                case "{":
+                case "}":
+                    maybeIdentifier = false;
+            }
+            if (lookAhead == null || !maybeIdentifier) {
+                throw parser.newError("<identifier> missing");
+            }
 
-                    String[] targets = rhs.split(",");
-                    for (String t : targets) {
-                        String n = t.trim();
-                        if (n.length() > 0)
-                            stmt.addTarget(n);
-                    }
+            return parser.nextToken();
+        }
 
-                    // start next statement
-                    if (pos == terminal) {
-                        statement = null;
-                        hasTargets = false;
-                        index = terminal + 1;
-                    }
-                    l = index < l.length() ? l.substring(index, l.length()).trim() : "";
-                    index = 0;
-                }
-
-                lineNumber++;
+        private String skipTokenOrThrow(Parser parser, String token, String msg) {
+            // look ahead to report the proper line number
+            String lookAhead = parser.peekToken();
+            if (!token.equals(lookAhead)) {
+                throw parser.newError(msg);
             }
+            return parser.nextToken();
         }
     }
 
@@ -620,4 +607,175 @@
             System.out.format(fmt, params);
         }
     }
+
+    static class Parser {
+        private static final List<String> EMPTY = List.of();
+
+        private final Path sourceFile;
+        private boolean inCommentBlock = false;
+        private List<List<String>> tokens = new ArrayList<>();
+        private int lineNumber = 1;
+        private int index = 0;
+
+        Parser(Path file) {
+            this.sourceFile = file;
+        }
+
+        void run() throws IOException {
+            List<String> lines = Files.readAllLines(sourceFile);
+            for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
+                String l = lines.get(lineNumber - 1).trim();
+                tokenize(l);
+            }
+        }
+
+        /*
+         * Tokenize the given string.  Comments are skipped.
+         */
+        List<String> tokenize(String l) {
+            while (!l.isEmpty()) {
+                if (inCommentBlock) {
+                    int comment = l.indexOf("*/");
+                    if (comment == -1)
+                        return emptyTokens();
+
+                    // end comment block
+                    inCommentBlock = false;
+                    if ((comment + 2) >= l.length()) {
+                        return emptyTokens();
+                    }
+                    l = l.substring(comment + 2, l.length()).trim();
+                }
+
+                // skip comment
+                int comment = l.indexOf("//");
+                if (comment >= 0) {
+                    l = l.substring(0, comment).trim();
+                    if (l.isEmpty()) return emptyTokens();
+                }
+
+                if (l.isEmpty()) {
+                    return emptyTokens();
+                }
+
+                int beginComment = l.indexOf("/*");
+                int endComment = l.indexOf("*/");
+                if (beginComment == -1)
+                    return tokens(l);
+
+                String s1 = l.substring(0, beginComment).trim();
+                if (endComment > 0) {
+                    String s2 = l.substring(endComment + 2, l.length()).trim();
+                    if (s1.isEmpty()) {
+                        l = s2;
+                    } else if (s2.isEmpty()) {
+                        l = s1;
+                    } else {
+                        l = s1 + " " + s2;
+                    }
+                } else {
+                    inCommentBlock = true;
+                    return tokens(s1);
+                }
+            }
+            return tokens(l);
+        }
+
+        private List<String> emptyTokens() {
+            this.tokens.add(EMPTY);
+            return EMPTY;
+        }
+        private List<String> tokens(String l) {
+            List<String> tokens = new ArrayList<>();
+            for (String s : l.split("\\s+")) {
+                int pos=0;
+                s = s.trim();
+                if (s.isEmpty())
+                     continue;
+
+                int i = s.indexOf(',', pos);
+                int j = s.indexOf(';', pos);
+                while ((i >= 0 && i < s.length()) || (j >= 0 && j < s.length())) {
+                    if (j == -1 || (i >= 0 && i < j)) {
+                        String n = s.substring(pos, i).trim();
+                        if (!n.isEmpty()) {
+                            tokens.add(n);
+                        }
+                        tokens.add(s.substring(i, i + 1));
+                        pos = i + 1;
+                        i = s.indexOf(',', pos);
+                    } else {
+                        String n = s.substring(pos, j).trim();
+                        if (!n.isEmpty()) {
+                            tokens.add(n);
+                        }
+                        tokens.add(s.substring(j, j + 1));
+                        pos = j + 1;
+                        j = s.indexOf(';', pos);
+                    }
+                }
+
+                String n = s.substring(pos).trim();
+                if (!n.isEmpty()) {
+                    tokens.add(n);
+                }
+            }
+            this.tokens.add(tokens);
+            return tokens;
+        }
+
+        /*
+         * Returns next token.
+         */
+        String nextToken() {
+            while (lineNumber <= tokens.size()) {
+                List<String> l = tokens.get(lineNumber-1);
+                if (index < l.size()) {
+                    return l.get(index++);
+                } else {
+                    lineNumber++;
+                    index = 0;
+                }
+            }
+            return null;
+        }
+
+        /*
+         * Peeks next token.
+         */
+        String peekToken() {
+            int ln = lineNumber;
+            int i = index;
+            while (ln <= tokens.size()) {
+                List<String> l = tokens.get(ln-1);
+                if (i < l.size()) {
+                    return l.get(i++);
+                } else {
+                    ln++;
+                    i = 0;
+                }
+            }
+            return null;
+        }
+
+        Error newError(String msg) {
+            if (lineNumber <= tokens.size()) {
+                throw new Error(sourceFile + ", line " +
+                    lineNumber + ", " + msg + " \"" + lineAt(lineNumber) + "\"");
+            } else {
+                throw new Error(sourceFile + ", line " + lineNumber + ", " + msg);
+            }
+        }
+
+        void dump() {
+            for (int i = 1; i <= tokens.size(); i++) {
+                System.out.format("%d: %s%n", i, lineAt(i));
+            }
+        }
+
+        private String lineAt(int i) {
+            return tokens.get(i-1).stream().collect(Collectors.joining(" "));
+        }
+    }
 }
+
--- a/make/jdk/src/classes/build/tools/module/ModuleInfoExtraTest.java	Wed Aug 08 15:24:21 2018 -0400
+++ b/make/jdk/src/classes/build/tools/module/ModuleInfoExtraTest.java	Wed Aug 08 14:40:02 2018 -0700
@@ -28,6 +28,7 @@
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.io.UncheckedIOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -35,6 +36,7 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import build.tools.module.GenModuleInfoSource.Statement;
 
@@ -42,29 +44,36 @@
  * Sanity test for GenModuleInfoSource tool
  */
 public class ModuleInfoExtraTest {
-    private static final Path DIR = Paths.get("test");
+    private static final Path DIR = Paths.get("gen-module-info-test");
+    private static boolean verbose = false;
     public static void main(String... args) throws Exception {
         if (args.length != 0)
-            GenModuleInfoSource.verbose = true;
+            verbose = true;
 
+        GenModuleInfoSource.verbose = verbose;
         ModuleInfoExtraTest test = new ModuleInfoExtraTest("m", "m1", "m2", "m3");
-        test.run();
+        test.testModuleInfo();
+        test.errorCases();
     }
 
     String[] moduleInfo = new String[] {
-        "exports p",
-        "to",
-        "   // comment",
-        "   /* comment */ m1",
+        "module m {",
+        "    requires m1;",
+        "    requires transitive m2;",
+        "    exports p",
+        "    to",
+        "               // comment ... ",
+        "    /* comment */ m1",
         ",",
-        "m2,m3",
-        "   ;",
-        "exports q to m1;",
-        "provides s with /* ",
-        "  comment */ impl     ;    // comment",
-        "provides s1",
-        "    with  ",
-        "    impl1, impl2;"
+        "       m2,m3",
+        "  ;",
+        "    exports q to m1;",
+        "    provides s with /*   ",
+        "    comment */ impl     ;    // comment",
+        "    provides s1",
+        "       with  ",
+        "       impl1, impl2;",
+        "}"
     };
 
     String[] moduleInfoExtra = new String[] {
@@ -76,33 +85,8 @@
         "opens p.q ",
         "   to /* comment */ m3",
         "   , // m1",
-        "   /* comment */, m4;",
-        "provides s1 with impl3;"
-    };
-
-    String[] test1 = new String[] {
-        "exports p1 to m1;",
-        "exports p2"
-    };
-
-    String[] test2 = new String[] {
-        "exports to m1;"
-    };
-
-    String[] test3 = new String[]{
-        "exports p3 to m1;",
-        "    m2, m3;"
-    };
-
-    String[] test4 = new String[]{
-        "provides s with impl1;",   // typo ; should be ,
-        "   impl2, impl3;"
-    };
-
-    String[] test5 = new String[]{
-        "uses s3",
-        "provides s3 with impl1,",
-        "   impl2, impl3;"
+        "   /* comment */ m4; uses p.I",
+        ";   provides s1 with impl3;"
     };
 
     final Builder builder;
@@ -110,11 +94,6 @@
         this.builder = new Builder(name).modules(modules);
     }
 
-    void run() throws IOException {
-        testModuleInfo();
-        errorCases();
-    }
-
 
     void testModuleInfo() throws IOException {
         GenModuleInfoSource source = builder.sourceFile(moduleInfo).build();
@@ -155,7 +134,9 @@
                Set<String> opensPQ,
                Set<String> providerS,
                Set<String> providerS1) {
-        source.moduleInfo.print(new PrintWriter(System.out, true));
+        if (verbose)
+            source.moduleInfo.print(new PrintWriter(System.out, true));
+
         Statement export = source.moduleInfo.exports.get("p");
         if (!export.targets.equals(targetsP)) {
             throw new Error("unexpected: " + export);
@@ -177,24 +158,112 @@
         }
     }
 
-
+    final Map<String[], String> badModuleInfos = Map.of(
+        new String[] {
+            "module x {",
+            "   exports p1 to ",
+            "           m1",
+            "}"
+        },                      ".*, line .*, missing semicolon.*",
+        new String[] {
+            "module x ",
+            "   exports p1;"
+        },                      ".*, line .*, missing \\{.*",
+        new String[] {
+            "module x {",
+            "   requires m1;",
+            "   requires",
+            "}"
+        },                      ".*, line .*, <identifier> missing.*",
+        new String[] {
+            "module x {",
+            "   requires transitive m1",
+            "}"
+        },                      ".*, line .*, missing semicolon.*",
+        new String[] {
+            "module x {",
+            "   exports p1 to m1;",
+            "   exports p1 to m2;",
+            "}"
+        },                      ".*, line .*, multiple exports p1.*"
+    );
 
-    void errorCases() throws IOException {
-        fail(test1);
-        fail(test2);
-        fail(test3);
-        fail(test4);
-        fail(test5);
+    final Map<String[], String> badExtraFiles = Map.of(
+            new String[] {
+                "requires m2;"     // not allowed
+            },                      ".*, line .*, cannot declare requires .*",
+            new String[] {
+                "exports p1 to m1;",
+                "exports p2"            // missing semicolon
+            },                      ".*, line .*, reach end of file.*",
+            new String[] {
+                "exports to m1;"        // missing <identifier>
+            },                      ".*, line .*, <identifier> missing.*",
+            new String[] {
+                "exports p3 to m1;",
+                "    m2, m3;"           // missing keyword
+            },                      ".*, line .*, missing keyword.*",
+            new String[] {
+                "provides s with impl1;",   // typo ; should be ,
+                "   impl2, impl3;"
+            },                      ".*, line .*, missing keyword.*",
+            new String[] {
+                "uses s3",                  // missing semicolon
+                "provides s3 with impl1,",
+                "   impl2, impl3;"
+            },                      ".*, line .*, missing semicolon.*",
+            new String[] {
+                "opens p1 to m1,, m2;"     // missing identifier
+            },                      ".*, line .*, <identifier> missing.*"
+    );
+
+    final Map<String[], String> duplicates = Map.of(
+            new String[] {
+                "   exports p1 to m1, m2;",
+                "   exports p1 to m3;",
+            },                      ".*, line .*, multiple exports p1.*",
+            new String[] {
+                "   opens p1 to m1, m2;",
+                "   exports p1 to m3;",
+                "   opens p1 to m3;"
+            },                      ".*, line .*, multiple opens p1.*",
+            new String[] {
+                "   uses s;",
+                "   uses s;"
+            },                      ".*, line .*, multiple uses s.*"
+    );
+
+    void errorCases() {
+        badModuleInfos.entrySet().stream().forEach(e -> badModuleInfoFile(e.getKey(), e.getValue()));
+        badExtraFiles.entrySet().stream().forEach(e -> badExtraFile(e.getKey(), e.getValue()));
+        duplicates.entrySet().stream().forEach(e -> badExtraFile(e.getKey(), e.getValue()));
     }
 
-    void fail(String... extras) throws IOException {
-        Path file = DIR.resolve("test1");
-        Files.write(file, Arrays.asList(extras));
+    void badModuleInfoFile(String[] lines, String regex)  {
+        Builder builder = new Builder("x").modules("m1", "m2", "m3");
         try {
+            GenModuleInfoSource source = builder.sourceFile(lines).build();
+            throw new RuntimeException("Expected error: " + Arrays.toString(lines));
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        } catch (Error e) {
+            if (!e.getMessage().matches(regex)) {
+                throw e;
+            }
+        }
+    }
+
+    void badExtraFile(String[] extras, String regex)  {
+        Path file = DIR.resolve("test1");
+        try {
+            Files.write(file, Arrays.asList(extras));
             builder.build(file);
-        } catch (RuntimeException e) {
-            if (!e.getMessage().matches("test/test1, line .* is malformed.*") &&
-                !e.getMessage().matches("test/test1, line .* missing keyword.*")) {
+            Files.deleteIfExists(file);
+            throw new RuntimeException("Expected error: " + Arrays.toString(extras));
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        } catch (Error e) {
+            if (!e.getMessage().matches(regex)) {
                 throw e;
             }
         }
@@ -218,11 +287,9 @@
             Files.createDirectories(sourceFile.getParent());
             try (BufferedWriter bw = Files.newBufferedWriter(sourceFile);
                  PrintWriter writer = new PrintWriter(bw)) {
-                writer.format("module %s {%n", moduleName);
                 for (String l : lines) {
                     writer.println(l);
                 }
-                writer.println("}");
             }
             return this;
         }
--- a/src/java.base/share/classes/module-info.java	Wed Aug 08 15:24:21 2018 -0400
+++ b/src/java.base/share/classes/module-info.java	Wed Aug 08 14:40:02 2018 -0700
@@ -304,9 +304,9 @@
         jdk.security.auth,
         jdk.security.jgss;
     exports sun.security.util.math to
-        jdk.crypto.ec
+        jdk.crypto.ec;
     exports sun.security.util.math.intpoly to
-        jdk.crypto.ec
+        jdk.crypto.ec;
     exports sun.security.x509 to
         jdk.crypto.ec,
         jdk.crypto.cryptoki,