changeset 55712:6076dc9cff3f string-tapas

Multiline String Literal branch
author jlaskey
date Fri, 05 Apr 2019 10:41:17 -0300
parents 13c02cc7a6e5
children 2b7308743d64
files src/java.base/share/classes/java/lang/String.java src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java src/jdk.compiler/share/classes/com/sun/tools/javac/code/Source.java src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavaTokenizer.java src/jdk.compiler/share/classes/com/sun/tools/javac/parser/UnicodeReader.java src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties test/langtools/tools/javac/MultilineStringLiteralAPI.java test/langtools/tools/javac/MultilineStringLiteralLang.java
diffstat 8 files changed, 463 insertions(+), 12 deletions(-) [+]
line wrap: on
line diff
--- a/src/java.base/share/classes/java/lang/String.java	Thu Apr 04 21:29:46 2019 +0200
+++ b/src/java.base/share/classes/java/lang/String.java	Fri Apr 05 10:41:17 2019 -0300
@@ -2874,6 +2874,114 @@
     }
 
     /**
+     * Removes vertical and horizontal white space margins from around the
+     * essential body of a multi-line string, while preserving relative
+     * indentation.
+     * <p>
+     * This string is first conceptually separated into lines as if by
+     * {@link String#lines()}.
+     * <p>
+     * Then, the <i>minimum indentation</i> (min) is determined as follows. For
+     * each non-blank line (as defined by {@link String#isBlank()}), the
+     * leading {@link Character#isWhitespace(int) white space} characters are
+     * counted. The <i>min</i> value is the smallest of these counts.
+     * <p>
+     * For each non-blank line, <i>min</i> leading white space characters are
+     * removed. Each white space character is treated as a single character. In
+     * particular, the tab character {@code "\t"} (U+0009) is considered a
+     * single character; it is not expanded.
+     * <p>
+     * Leading and trailing blank lines, if any, are removed. Trailing spaces are
+     * preserved.
+     * <p>
+     * Each line is suffixed with a line feed character {@code "\n"} (U+000A).
+     * <p>
+     * Finally, the lines are concatenated into a single string and returned.
+     *
+     * @apiNote
+     * This method's primary purpose is to shift a block of lines as far as
+     * possible to the left, while preserving relative indentation. Lines
+     * that were indented the least will thus have no leading white space.
+     *
+     * Example:
+     * <blockquote><pre>
+     * """
+     *      This is the first line
+     *          This is the second line
+     * """.align();
+     *
+     * returns
+     * This is the first line
+     *     This is the second line
+     * </pre></blockquote>
+     *
+     * @return string with margins removed and line terminators normalized
+     *
+     * @see String#lines()
+     * @see String#isBlank()
+     * @see String#indent(int)
+     * @see Character#isWhitespace(int)
+     *
+     * @since 13
+     */
+    public String align() {
+        return align(0);
+    }
+
+    /**
+     * Removes vertical and horizontal white space margins from around the
+     * essential body of a multi-line string, while preserving relative
+     * indentation and with optional indentation adjustment.
+     * <p>
+     * Invoking this method is equivalent to:
+     * <blockquote>
+     *  {@code this.align().indent(n)}
+     * </blockquote>
+     *
+     * @apiNote
+     * Examples:
+     * <blockquote><pre>
+     * """
+     *      This is the first line
+     *          This is the second line
+     * """.align(0);
+     *
+     * returns
+     * This is the first line
+     *     This is the second line
+     *
+     *
+     * """
+     *    This is the first line
+     *       This is the second line
+     * """.align(4);
+     * returns
+     *     This is the first line
+     *         This is the second line
+     * </pre></blockquote>
+     *
+     * @param n  number of leading white space characters
+     *           to add or remove
+     *
+     * @return string with margins removed, indentation adjusted and
+     *         line terminators normalized
+     *
+     * @see String#align()
+     *
+     * @since 13
+     */
+    public String align(int n) {
+        if (isEmpty()) {
+            return "";
+        }
+        int outdent = lines().filter(not(String::isBlank))
+            .mapToInt(String::indexOfNonWhitespace)
+            .min()
+            .orElse(0);
+        return indent(n - outdent, true);
+    }
+
+    /**
      * This method allows the application of a function to {@code this}
      * string. The function should expect a single String argument
      * and produce an {@code R} result.
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java	Thu Apr 04 21:29:46 2019 +0200
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java	Fri Apr 05 10:41:17 2019 -0300
@@ -167,7 +167,8 @@
     public boolean isPreview(Feature feature) {
         if (feature == Feature.SWITCH_EXPRESSION ||
             feature == Feature.SWITCH_MULTIPLE_CASE_LABELS ||
-            feature == Feature.SWITCH_RULE)
+            feature == Feature.SWITCH_RULE ||
+            feature == Feature.MULTILINE_STRING_LITERALS)
             return true;
         //Note: this is a backdoor which allows to optionally treat all features as 'preview' (for testing).
         //When real preview features will be added, this method can be implemented to return 'true'
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Source.java	Thu Apr 04 21:29:46 2019 +0200
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Source.java	Fri Apr 05 10:41:17 2019 -0300
@@ -188,7 +188,8 @@
         IMPORT_ON_DEMAND_OBSERVABLE_PACKAGES(JDK1_2, JDK8),
         SWITCH_MULTIPLE_CASE_LABELS(JDK13, Fragments.FeatureMultipleCaseLabels, DiagKind.PLURAL),
         SWITCH_RULE(JDK13, Fragments.FeatureSwitchRules, DiagKind.PLURAL),
-        SWITCH_EXPRESSION(JDK13, Fragments.FeatureSwitchExpressions, DiagKind.PLURAL);
+        SWITCH_EXPRESSION(JDK13, Fragments.FeatureSwitchExpressions, DiagKind.PLURAL),
+        MULTILINE_STRING_LITERALS(JDK12, Fragments.FeatureMultilineStringLiterals, DiagKind.PLURAL);
 
         enum DiagKind {
             NORMAL,
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavaTokenizer.java	Thu Apr 04 21:29:46 2019 +0200
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavaTokenizer.java	Fri Apr 05 10:41:17 2019 -0300
@@ -459,6 +459,59 @@
         }
     }
 
+    /** Scan a string literal.
+     */
+    private void scanStringLiteral(int pos) {
+        int firstEOLN = -1;
+        int rescan = reader.bp;
+        int openCount = countChar('\"', 3);
+        if (openCount == 2) {
+            reader.reset(rescan);
+            openCount = countChar('\"', 1);
+        }
+        while (reader.bp < reader.buflen) {
+            if (reader.ch == '\"') {
+                int closeCount = countChar('\"', openCount);
+                rescan = reader.bp;
+                if (openCount == closeCount) {
+                    tk = Tokens.TokenKind.STRINGLITERAL;
+                    return;
+                }
+                reader.repeat('\"', closeCount);
+                reader.reset(rescan);
+            } else if (reader.ch == LF || reader.ch == CR) {
+                if (openCount == 1) {
+                    break;
+                }
+                int start = reader.bp;
+                if (firstEOLN == -1) {
+                    firstEOLN = start;
+                }
+                if (reader.ch == CR && reader.peekChar() == LF) {
+                    reader.scanChar();
+                }
+                reader.putChar('\n', true);
+                processLineTerminator(start, reader.bp);
+            } else {
+                scanLitChar(pos);
+            }
+        }
+        if (firstEOLN  != -1) {
+            reader.reset(firstEOLN);
+        }
+        lexError(pos, Errors.UnclosedStrLit);
+    }
+
+    /** Count and skip repeated occurances of the specified character.
+     */
+    private int countChar(char ch, int max) {
+        int count = 0;
+        for ( ; count < max && reader.bp < reader.buflen && reader.ch == ch; count++) {
+            reader.scanChar();
+        }
+        return count;
+    }
+
     /** Read token.
      */
     public Token readToken() {
@@ -636,17 +689,9 @@
                     }
                     break loop;
                 case '\"':
-                    reader.scanChar();
-                    while (reader.ch != '\"' && reader.ch != CR && reader.ch != LF && reader.bp < reader.buflen)
-                        scanLitChar(pos);
-                    if (reader.ch == '\"') {
-                        tk = TokenKind.STRINGLITERAL;
-                        reader.scanChar();
-                    } else {
-                        lexError(pos, Errors.UnclosedStrLit);
-                    }
+                    scanStringLiteral(pos);
                     break loop;
-               default:
+                default:
                     if (isSpecial(reader.ch)) {
                         scanOperator();
                     } else {
@@ -716,6 +761,7 @@
                     comments.prepend(comment);
         }
 
+
     /** Return the position where a lexical error occurred;
      */
     public int errPos() {
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/UnicodeReader.java	Thu Apr 04 21:29:46 2019 +0200
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/UnicodeReader.java	Fri Apr 05 10:41:17 2019 -0300
@@ -154,6 +154,17 @@
         return new String(sbuf, 0, sp);
     }
 
+    protected void repeat(char ch, int count) {
+        for ( ; 0 < count; count--) {
+            putChar(ch, false);
+        }
+    }
+
+    protected void reset(int pos) {
+        bp = pos - 1;
+        scanChar();
+    }
+
     /** Convert unicode escape; bp points to initial '\' character
      *  (Spec 3.3).
      */
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties	Thu Apr 04 21:29:46 2019 +0200
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties	Fri Apr 05 10:41:17 2019 -0300
@@ -2838,6 +2838,9 @@
 compiler.misc.feature.private.intf.methods=\
     private interface methods
 
+compiler.misc.feature.multiline.string.literals=\
+    multi-line string literals
+
 compiler.misc.feature.multiple.case.labels=\
     multiple case labels
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/langtools/tools/javac/MultilineStringLiteralAPI.java	Fri Apr 05 10:41:17 2019 -0300
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Unit tests for Multiline String Literal language changes
+ * @library /tools/lib
+ * @modules jdk.compiler/com.sun.tools.javac.api
+ *          jdk.compiler/com.sun.tools.javac.main
+ * @build toolbox.ToolBox toolbox.JavacTask
+ * @run main MultilineStringLiteralAPI
+ */
+
+import toolbox.JavacTask;
+import toolbox.JavaTask;
+import toolbox.Task;
+import toolbox.ToolBox;
+
+public class MultilineStringLiteralAPI {
+    private static ToolBox TOOLBOX = new ToolBox();
+
+    public static void main(String... args) {
+        test1();
+        test2();
+        test3();
+        test4();
+    }
+
+    /*
+     * Check that correct/incorrect syntax is properly detected
+     */
+    static void test1() {
+        String[] s = new String[] { "a", "ab", "abc", "\u2022", "*".repeat(1000), "*".repeat(10000) };
+        for (String a : s)  {
+            String code =
+                    "public class CorrectTest {\n" +
+                            "    public static void main(String... args) {\n" +
+                            "        String xxx = " +
+                            "\"\"\"" + a + "\"\"\";\n" +
+                            "    }\n" +
+                            "}\n";
+            compPass(code);
+        }
+    }
+
+    /*
+     * Check that use of \u0022 is properly detected
+     */
+    static void test2() {
+        compPass("public class UnicodeDelimiterTest {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \\u0022\\u0022\\u0022abc\\u0022\\u0022\\u0022;\n" +
+                "    }\n" +
+                "}\n");
+    }
+
+    /*
+     * Check edge cases of multiline string literal as last token
+     */
+    static void test3() {
+        compFail("public class EndTest {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"abc\"\"\"");
+        compFail("public class MultilineStringLiteralTest {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"abc\"\"");
+        compFail("public class MultilineStringLiteralTest {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"abc\"");
+        compFail("public class MultilineStringLiteralTest {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"abc");
+        compFail("public class MultilineStringLiteralTest {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"abc\\u0000");
+        compFail("public class MultilineStringLiteralTest {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"abc\\u001A");
+    }
+
+    /*
+     * Check line terminator translation
+     */
+    static void test4() {
+        String[] terminators = new String[] { "\n", "\r\n", "\r" };
+        for (String terminator : terminators) {
+            String code = "public class LineTerminatorTest {" + terminator +
+                          "    public static void main(String... args) {" + terminator +
+                          "        String s =" + terminator +
+                          "\"\"\"" + terminator +
+                          "abc" + terminator +
+                          "\"\"\";" + terminator +
+                          "        System.out.println(s.equals(\"\\nabc\\n\"));" + terminator +
+                          "    }" + terminator +
+                          "}" + terminator;
+            new JavacTask(TOOLBOX)
+                    .sources(code)
+                    .classpath(".")
+                    // .options("--enable-preview", "-source", "13")
+                    .run();
+            String output = new JavaTask(TOOLBOX)
+                    // .vmOptions("--enable-preview")
+                    .classpath(".")
+                    .classArgs("LineTerminatorTest")
+                    .run()
+                    .writeAll()
+                    .getOutput(Task.OutputKind.STDOUT);
+
+            if (!output.contains("true")) {
+                throw new RuntimeException("Error detected");
+            }
+        }
+    }
+
+    /*
+     * Test source for successful compile.
+     */
+    static void compPass(String source) {
+        String output = new JavacTask(TOOLBOX)
+                .sources(source)
+                .classpath(".")
+                // .options("--enable-preview", "-source", "13", "-encoding", "utf8")
+                .options("-encoding", "utf8")
+                .run()
+                .writeAll()
+                .getOutput(Task.OutputKind.DIRECT);
+
+        if (output.contains("compiler.err")) {
+            throw new RuntimeException("Error detected");
+        }
+    }
+
+    /*
+     * Test source for unsuccessful compile and specific error.
+     */
+    static void compFail(String source)  {
+        String errors = new JavacTask(TOOLBOX)
+                .sources(source)
+                .classpath(".")
+                // .options("-XDrawDiagnostics", "--enable-preview", "-source", "13", "-encoding", "utf8")
+                .options("-XDrawDiagnostics", "-encoding", "utf8")
+                .run(Task.Expect.FAIL)
+                .writeAll()
+                .getOutput(Task.OutputKind.DIRECT);
+
+        if (!errors.contains("compiler.err")) {
+            throw new RuntimeException("No error detected");
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/langtools/tools/javac/MultilineStringLiteralLang.java	Fri Apr 05 10:41:17 2019 -0300
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Unit tests for Multiline String Literal language changes
+ * @compile -encoding utf8 MultilineStringLiteralLang.java
+ * @run main/othervm MultilineStringLiteralLang
+ */
+
+/*
+ * @compile --enable-preview -source 13 -encoding utf8 MultilineStringLiteralLang.java
+ * @run main/othervm --enable-preview MultilineStringLiteralLang
+ */
+
+public class MultilineStringLiteralLang {
+    public static void main(String... args) {
+        test1();
+        test2();
+    }
+
+    /*
+     * Test basic string functionality.
+     */
+    static void test1() {
+        EQ("""abc""", "abc");
+        EQ("""abc"def""", "abc\"def");
+        EQ("""abc""def""", "abc\"\"def");
+        EQ("""abc\"""def""", "abc\"\"\"def");
+        EQ("""abc"\""def""", "abc\"\"\"def");
+        EQ("""abc""\"def""", "abc\"\"\"def");
+        EQ("""abc\ndef""", "abc\ndef");
+        EQ("""abc\u0020def""", "abc\u0020def");
+        EQ("""abc•def""", "abc•def");
+
+        LENGTH("""abc""", 3);
+    }
+
+    /*
+     * Test multiline string functionality.
+     */
+    static void test2() {
+        EQ(
+"""abc
+def
+ghi""", "abc\ndef\nghi");
+        EQ(
+"""abc
+def
+ghi
+""", "abc\ndef\nghi\n");
+        EQ(
+"""
+abc
+def
+ghi""", "\nabc\ndef\nghi");
+        EQ(
+"""
+abc
+def
+ghi
+""", "\nabc\ndef\nghi\n");
+    }
+
+    /*
+     * Raise an exception if the string is not the expected length.
+     */
+    static void LENGTH(String string, int length) {
+        if (string == null || string.length() != length) {
+            System.err.println("Failed LENGTH");
+            System.err.println(string + " " + length);
+            throw new RuntimeException("Failed LENGTH");
+        }
+    }
+
+    /*
+     * Raise an exception if the two input strings are not equal.
+     */
+    static void EQ(String input, String expected) {
+        if (input == null || expected == null || !expected.equals(input)) {
+            System.err.println("Failed EQ");
+            System.err.println();
+            System.err.println("Input:");
+            System.err.println(input.replaceAll(" ", "."));
+            System.err.println();
+            System.err.println("Expected:");
+            System.err.println(expected.replaceAll(" ", "."));
+            throw new RuntimeException();
+        }
+    }
+}