8223967: Implement Text Blocks (Preview) in the Java compiler
authorjlaskey
Thu, 06 Jun 2019 12:24:44 -0300
changeset 55263 830ca7b43b95
parent 55262 7d83cf1cfa74
child 55264 b7ad292e686c
8223967: Implement Text Blocks (Preview) in the Java compiler Reviewed-by: vromero, jlahoda, abuckley
src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.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/ScannerFactory.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
src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties
test/langtools/tools/javac/TextBlockAPI.java
test/langtools/tools/javac/TextBlockLang.java
test/langtools/tools/javac/diags/examples/TextBlockCloseDelimiter.java
test/langtools/tools/javac/diags/examples/TextBlockOpenDelimiter.java
test/langtools/tools/javac/diags/examples/TextBlockWhitespace.java
test/langtools/tools/javap/WhitespaceTest.java
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java	Thu Jun 06 12:24:44 2019 -0300
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java	Thu Jun 06 12:24:44 2019 -0300
@@ -275,6 +275,11 @@
         STATIC("static"),
 
         /**
+         * Warn about issues relating to use of text blocks
+         */
+        TEXT_BLOCKS("text-blocks"),
+
+        /**
          * Warn about issues relating to use of try blocks (i.e. try-with-resources)
          */
         TRY("try"),
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java	Thu Jun 06 12:24:44 2019 -0300
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java	Thu Jun 06 12:24:44 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.TEXT_BLOCKS)
             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 Jun 06 12:24:44 2019 -0300
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Source.java	Thu Jun 06 12:24:44 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),
+        TEXT_BLOCKS(JDK13, Fragments.FeatureTextBlocks, DiagKind.PLURAL);
 
         enum DiagKind {
             NORMAL,
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavaTokenizer.java	Thu Jun 06 12:24:44 2019 -0300
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavaTokenizer.java	Thu Jun 06 12:24:44 2019 -0300
@@ -25,15 +25,22 @@
 
 package com.sun.tools.javac.parser;
 
+import com.sun.tools.javac.code.Lint;
+import com.sun.tools.javac.code.Lint.LintCategory;
 import com.sun.tools.javac.code.Preview;
 import com.sun.tools.javac.code.Source;
 import com.sun.tools.javac.code.Source.Feature;
 import com.sun.tools.javac.parser.Tokens.Comment.CommentStyle;
 import com.sun.tools.javac.resources.CompilerProperties.Errors;
+import com.sun.tools.javac.resources.CompilerProperties.Warnings;
 import com.sun.tools.javac.util.*;
-import com.sun.tools.javac.util.JCDiagnostic.DiagnosticFlag;
+import com.sun.tools.javac.util.JCDiagnostic.*;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.nio.CharBuffer;
+import java.util.HashSet;
+import java.util.Set;
 
 import static com.sun.tools.javac.parser.Tokens.*;
 import static com.sun.tools.javac.util.LayoutCharacters.*;
@@ -84,8 +91,21 @@
      */
     protected UnicodeReader reader;
 
+    /** Should the string stripped of indentation?
+     */
+    protected boolean shouldStripIndent;
+
+    /** Should the string's escapes be translated?
+     */
+    protected boolean shouldTranslateEscapes;
+
     protected ScannerFactory fac;
 
+    // The set of lint options currently in effect. It is initialized
+    // from the context, and then is set/reset as needed by Attr as it
+    // visits all the various parts of the trees during attribution.
+    protected Lint lint;
+
     private static final boolean hexFloatsWork = hexFloatsWork();
     private static boolean hexFloatsWork() {
         try {
@@ -121,6 +141,7 @@
         this.source = fac.source;
         this.preview = fac.preview;
         this.reader = reader;
+        this.lint = fac.lint;
     }
 
     protected void checkSourceLevel(int pos, Feature feature) {
@@ -150,6 +171,11 @@
         errPos = pos;
     }
 
+    protected void lexWarning(LintCategory lc, int pos, JCDiagnostic.Warning key) {
+        DiagnosticPosition dp = new SimpleDiagnosticPosition(pos) ;
+        log.warning(lc, dp, key);
+    }
+
     /** Read next character in character or string literal and copy into sbuf.
      */
     private void scanLitChar(int pos) {
@@ -200,6 +226,309 @@
         }
     }
 
+    /** Read next character in character or string literal and copy into sbuf
+     *  without translating escapes. Used by text blocks to preflight verify
+     *  escapes sequences.
+     */
+    private void scanLitCharRaw(int pos) {
+        if (reader.ch == '\\') {
+            if (reader.peekChar() == '\\' && !reader.isUnicode()) {
+                reader.skipChar();
+                reader.putChar('\\', false);
+                reader.putChar('\\', true);
+            } else {
+                reader.putChar('\\', true);
+                switch (reader.ch) {
+                case '0': case '1': case '2': case '3':
+                case '4': case '5': case '6': case '7':
+                    char leadch = reader.ch;
+                    reader.putChar(true);
+                    if ('0' <= reader.ch && reader.ch <= '7') {
+                        reader.putChar(true);
+                        if (leadch <= '3' && '0' <= reader.ch && reader.ch <= '7') {
+                            reader.putChar(true);
+                        }
+                    }
+                    break;
+                // Effectively list of valid escape sequences.
+                case 'b':
+                case 't':
+                case 'n':
+                case 'f':
+                case 'r':
+                case '\'':
+                case '\"':
+                case '\\':
+                    reader.putChar(true); break;
+                default:
+                    lexError(reader.bp, Errors.IllegalEscChar);
+                }
+            }
+        } else if (reader.bp != reader.buflen) {
+            reader.putChar(true);
+        }
+    }
+
+    /** Interim access to String methods used to support text blocks.
+     *  Required to handle bootstrapping with pre-text block jdks.
+     *  Could be reworked in the 'next' jdk.
+     */
+    static class TextBlockSupport {
+        /** Reflection method to remove incidental indentation.
+         */
+        private static final Method stripIndent;
+
+        /** Reflection method to translate escape sequences.
+         */
+        private static final Method translateEscapes;
+
+        /** true if stripIndent and translateEscapes are available in the bootstrap jdk.
+         */
+        private static final boolean hasSupport;
+
+        /** Get a string method via refection or null if not available.
+         */
+        private static Method getStringMethodOrNull(String name) {
+            try {
+                return String.class.getMethod(name);
+            } catch (Exception ex) {
+                // Method not available, return null.
+            }
+            return null;
+        }
+
+        static {
+            // Get text block string methods.
+            stripIndent = getStringMethodOrNull("stripIndent");
+            translateEscapes = getStringMethodOrNull("translateEscapes");
+            // true if stripIndent and translateEscapes are available in the bootstrap jdk.
+            hasSupport = stripIndent != null && translateEscapes != null;
+        }
+
+        /** Return true if stripIndent and translateEscapes are available in the bootstrap jdk.
+         */
+        static boolean hasSupport() {
+            return hasSupport;
+        }
+
+        /** Return the leading whitespace count (indentation) of the line.
+         */
+        private static int indent(String line) {
+            return line.length() - line.stripLeading().length();
+        }
+
+        enum WhitespaceChecks {
+            INCONSISTENT,
+            TRAILING
+        };
+
+        /** Check that the use of white space in content is not problematic.
+         */
+        static Set<WhitespaceChecks> checkWhitespace(String string) {
+            // Start with empty result set.
+            Set<WhitespaceChecks> checks = new HashSet<>();
+            // No need to check empty strings.
+            if (string.isEmpty()) {
+                return checks;
+            }
+            // Maximum common indentation.
+            int outdent = 0;
+            // No need to check indentation if opting out (last line is empty.)
+            char lastChar = string.charAt(string.length() - 1);
+            boolean optOut = lastChar == '\n' || lastChar == '\r';
+            // Split string based at line terminators.
+            String[] lines = string.split("\\R");
+            int length = lines.length;
+            // Extract last line.
+            String lastLine = lines[length - 1];
+            if (!optOut) {
+                // Prime with the last line indentation (may be blank.)
+                outdent = indent(lastLine);
+                for (String line : lines) {
+                    // Blanks lines have no influence (last line accounted for.)
+                    if (!line.isBlank()) {
+                        outdent = Integer.min(outdent, indent(line));
+                        if (outdent == 0) {
+                            break;
+                        }
+                    }
+                }
+            }
+            // Last line is representative.
+            String start = lastLine.substring(0, outdent);
+            for (String line : lines) {
+                // Fail if a line does not have the same indentation.
+                if (!line.isBlank() && !line.startsWith(start)) {
+                    // Mix of different white space
+                    checks.add(WhitespaceChecks.INCONSISTENT);
+                }
+                // Line has content even after indent is removed.
+                if (outdent < line.length()) {
+                    // Is the last character a white space.
+                    lastChar = line.charAt(line.length() - 1);
+                    if (Character.isWhitespace(lastChar)) {
+                        // Has trailing white space.
+                        checks.add(WhitespaceChecks.TRAILING);
+                    }
+                }
+            }
+            return checks;
+        }
+
+        /** Invoke String::stripIndent through reflection.
+         */
+        static String stripIndent(String string) {
+            try {
+                string = (String)stripIndent.invoke(string);
+            } catch (InvocationTargetException | IllegalAccessException ex) {
+                throw new RuntimeException(ex);
+            }
+            return string;
+        }
+
+        /** Invoke String::translateEscapes through reflection.
+         */
+        static String translateEscapes(String string) {
+            try {
+                string = (String)translateEscapes.invoke(string);
+            } catch (InvocationTargetException | IllegalAccessException ex) {
+                throw new RuntimeException(ex);
+            }
+            return string;
+        }
+    }
+
+    /** Test for EOLN.
+     */
+    private boolean isEOLN() {
+        return reader.ch == LF || reader.ch == CR;
+    }
+
+    /** Test for CRLF.
+     */
+    private boolean isCRLF() {
+        return reader.ch == CR && reader.peekChar() == LF;
+    }
+
+    /** 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;
+    }
+
+    /** Scan a string literal or text block.
+     */
+    private void scanString(int pos) {
+        // Clear flags.
+        shouldStripIndent = false;
+        shouldTranslateEscapes = false;
+        // Check if text block string methods are present.
+        boolean hasTextBlockSupport = TextBlockSupport.hasSupport();
+        // Track the end of first line for error recovery.
+        int firstEOLN = -1;
+        // Attempt to scan for up to 3 double quotes.
+        int openCount = countChar('\"', 3);
+        switch (openCount) {
+        case 1: // Starting a string literal.
+            break;
+        case 2: // Starting an empty string literal.
+            // Start again but only consume one quote.
+            reader.reset(pos);
+            openCount = countChar('\"', 1);
+            break;
+        case 3: // Starting a text block.
+            // Check if preview feature is enabled for text blocks.
+            checkSourceLevel(pos, Feature.TEXT_BLOCKS);
+            // Only proceed if text block string methods are present.
+            if (hasTextBlockSupport) {
+                // Indicate that the final string should have incidental indentation removed.
+                shouldStripIndent = true;
+                // Verify the open delimiter sequence.
+                boolean hasOpenEOLN = false;
+                while (reader.bp < reader.buflen && Character.isWhitespace(reader.ch)) {
+                    hasOpenEOLN = isEOLN();
+                    if (hasOpenEOLN) {
+                        break;
+                    }
+                    reader.scanChar();
+                }
+                // Error if the open delimiter sequence not is """<Whitespace>*<LineTerminator>.
+                if (!hasOpenEOLN) {
+                    lexError(reader.bp, Errors.IllegalTextBlockOpen);
+                    return;
+                }
+                // Skip line terminator.
+                int start = reader.bp;
+                if (isCRLF()) {
+                    reader.scanChar();
+                }
+                reader.scanChar();
+                processLineTerminator(start, reader.bp);
+            } else {
+                // No text block string methods are present, so reset and treat like string literal.
+                reader.reset(pos);
+                openCount = countChar('\"', 1);
+            }
+            break;
+        }
+        // While characters are available.
+        while (reader.bp < reader.buflen) {
+            // If possible close delimiter sequence.
+            if (reader.ch == '\"') {
+                // Check to see if enough double quotes are present.
+                int closeCount = countChar('\"', openCount);
+                if (openCount == closeCount) {
+                    // Good result.
+                    tk = Tokens.TokenKind.STRINGLITERAL;
+                    return;
+                }
+                // False alarm, add double quotes to string buffer.
+                reader.repeat('\"', closeCount);
+            } else if (isEOLN()) {
+                // Line terminator in string literal is an error.
+                // Fall out to unclosed string literal error.
+                if (openCount == 1) {
+                    break;
+                }
+                 // Add line terminator to string buffer.
+                int start = reader.bp;
+                if (isCRLF()) {
+                    reader.scanChar();
+                }
+                reader.putChar('\n', true);
+                processLineTerminator(start, reader.bp);
+                // Record first line terminator for error recovery.
+                if (firstEOLN == -1) {
+                    firstEOLN = reader.bp;
+                }
+            } else if (reader.ch == '\\') {
+                // Handle escape sequences.
+                if (hasTextBlockSupport) {
+                    // Indicate that the final string should have escapes translated.
+                    shouldTranslateEscapes = true;
+                    // Validate escape sequence and add to string buffer.
+                    scanLitCharRaw(pos);
+                } else {
+                    // Translate escape sequence and add result to string buffer.
+                    scanLitChar(pos);
+                }
+            } else {
+                // Add character to string buffer.
+                reader.putChar(true);
+            }
+        }
+        // String ended without close delimiter sequence.
+        lexError(pos, openCount == 1 ? Errors.UnclosedStrLit : Errors.UnclosedTextBlock);
+        if (firstEOLN  != -1) {
+            // Reset recovery position to point after open delimiter sequence.
+            reader.reset(firstEOLN);
+        }
+    }
+
     private void scanDigits(int pos, int digitRadix) {
         char saveCh;
         int savePos;
@@ -624,7 +953,7 @@
                         lexError(pos, Errors.EmptyCharLit);
                         reader.scanChar();
                     } else {
-                        if (reader.ch == CR || reader.ch == LF)
+                        if (isEOLN())
                             lexError(pos, Errors.IllegalLineEndInCharLit);
                         scanLitChar(pos);
                         if (reader.ch == '\'') {
@@ -636,17 +965,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);
-                    }
+                    scanString(pos);
                     break loop;
-               default:
+                default:
                     if (isSpecial(reader.ch)) {
                         scanOperator();
                     } else {
@@ -695,7 +1016,34 @@
             switch (tk.tag) {
                 case DEFAULT: return new Token(tk, pos, endPos, comments);
                 case NAMED: return new NamedToken(tk, pos, endPos, name, comments);
-                case STRING: return new StringToken(tk, pos, endPos, reader.chars(), comments);
+                case STRING: {
+                    // Get characters from string buffer.
+                    String string = reader.chars();
+                    // If a text block.
+                    if (shouldStripIndent) {
+                        // Verify that the incidental indentation is consistent.
+                        if (lint.isEnabled(LintCategory.TEXT_BLOCKS)) {
+                            Set<TextBlockSupport.WhitespaceChecks> checks =
+                                    TextBlockSupport.checkWhitespace(string);
+                            if (checks.contains(TextBlockSupport.WhitespaceChecks.INCONSISTENT)) {
+                                lexWarning(LintCategory.TEXT_BLOCKS, pos,
+                                        Warnings.InconsistentWhiteSpaceIndentation);
+                            }
+                            if (checks.contains(TextBlockSupport.WhitespaceChecks.TRAILING)) {
+                                lexWarning(LintCategory.TEXT_BLOCKS, pos,
+                                        Warnings.TrailingWhiteSpaceWillBeRemoved);
+                            }
+                        }
+                        // Remove incidental indentation.
+                        string = TextBlockSupport.stripIndent(string);
+                    }
+                    // Translate escape sequences if present.
+                    if (shouldTranslateEscapes) {
+                        string = TextBlockSupport.translateEscapes(string);
+                    }
+                    // Build string token.
+                    return new StringToken(tk, pos, endPos, string, comments);
+                }
                 case NUMERIC: return new NumericToken(tk, pos, endPos, reader.chars(), radix, comments);
                 default: throw new AssertionError();
             }
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/ScannerFactory.java	Thu Jun 06 12:24:44 2019 -0300
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/ScannerFactory.java	Thu Jun 06 12:24:44 2019 -0300
@@ -27,6 +27,7 @@
 
 import java.nio.CharBuffer;
 
+import com.sun.tools.javac.code.Lint;
 import com.sun.tools.javac.code.Preview;
 import com.sun.tools.javac.code.Source;
 import com.sun.tools.javac.util.Context;
@@ -59,6 +60,7 @@
     final Source source;
     final Preview preview;
     final Tokens tokens;
+    final Lint lint;
 
     /** Create a new scanner factory. */
     protected ScannerFactory(Context context) {
@@ -68,6 +70,7 @@
         this.source = Source.instance(context);
         this.preview = Preview.instance(context);
         this.tokens = Tokens.instance(context);
+        this.lint = Lint.instance(context);
     }
 
     public Scanner newScanner(CharSequence input, boolean keepDocComments) {
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/UnicodeReader.java	Thu Jun 06 12:24:44 2019 -0300
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/UnicodeReader.java	Thu Jun 06 12:24:44 2019 -0300
@@ -154,6 +154,21 @@
         return new String(sbuf, 0, sp);
     }
 
+    /** Add 'count' copies of the character 'ch' to the string buffer.
+     */
+    protected void repeat(char ch, int count) {
+        for ( ; 0 < count; count--) {
+            putChar(ch, false);
+        }
+    }
+
+    /** Reset the scan buffer pointer to 'pos'.
+     */
+    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 Jun 06 12:24:44 2019 -0300
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties	Thu Jun 06 12:24:44 2019 -0300
@@ -623,6 +623,15 @@
 compiler.err.illegal.line.end.in.char.lit=\
     illegal line end in character literal
 
+compiler.err.illegal.text.block.open=\
+    illegal text block open delimiter sequence, missing line terminator
+
+compiler.warn.inconsistent.white.space.indentation=\
+    inconsistent white space indentation
+
+compiler.warn.trailing.white.space.will.be.removed=\
+    trailing white space will be removed
+
 compiler.err.illegal.nonascii.digit=\
     illegal non-ASCII digit
 
@@ -1244,6 +1253,9 @@
 compiler.err.unclosed.str.lit=\
     unclosed string literal
 
+compiler.err.unclosed.text.block=\
+    unclosed text block
+
 # 0: string
 compiler.err.unsupported.encoding=\
     unsupported encoding: {0}
@@ -2859,6 +2871,9 @@
 compiler.misc.feature.private.intf.methods=\
     private interface methods
 
+compiler.misc.feature.text.blocks=\
+    text blocks
+
 compiler.misc.feature.multiple.case.labels=\
     multiple case labels
 
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties	Thu Jun 06 12:24:44 2019 -0300
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties	Thu Jun 06 12:24:44 2019 -0300
@@ -243,6 +243,9 @@
 javac.opt.Xlint.desc.static=\
     Warn about accessing a static member using an instance.
 
+javac.opt.Xlint.desc.text-blocks=\
+    Warn about inconsistent white space characters in text block indentation.
+
 javac.opt.Xlint.desc.try=\
     Warn about issues relating to use of try blocks (i.e. try-with-resources).
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/langtools/tools/javac/TextBlockAPI.java	Thu Jun 06 12:24:44 2019 -0300
@@ -0,0 +1,173 @@
+/*
+ * 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
+ * @bug 8223967
+ * @summary Unit tests for Text Block 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 TextBlockAPI
+ */
+
+import toolbox.JavacTask;
+import toolbox.JavaTask;
+import toolbox.Task;
+import toolbox.ToolBox;
+
+public class TextBlockAPI {
+    private static ToolBox TOOLBOX = new ToolBox();
+    private final static String JDK_VERSION = Integer.toString(Runtime.version().feature());
+
+    public static void main(String... args) {
+        test1();
+        test2();
+        test3();
+        test4();
+    }
+
+    /*
+     * Check that correct/incorrect syntax is properly detected
+     */
+    static void test1() {
+        for (String lineterminators : new String[] { "\n", "\r", "\r\n" })
+        for (String whitespace : new String[] { "", "   ", "\t", "\u2002" })
+        for (String content : new String[] { "a", "ab", "abc", "\u2022", "*".repeat(1000), "*".repeat(10000) })  {
+            String code =
+                    "public class CorrectTest {\n" +
+                            "    public static void main(String... args) {\n" +
+                            "        String xxx = " +
+                            "\"\"\"" + whitespace + lineterminators +
+                            content +
+                            "\"\"\";\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\\u0022\nabc\n\\u0022\\u0022\\u0022;\n" +
+                "    }\n" +
+                "}\n");
+    }
+
+    /*
+     * Check edge cases of text blocks as last token
+     */
+    static void test3() {
+        compFail("public class EndTest {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"\nabc\"\"\"");
+        compFail("public class TwoQuoteClose {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"\nabc\"\"");
+        compFail("public class OneQuoteClose {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"\nabc\"");
+        compFail("public class NoClose {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"\nabc");
+        compFail("public class ZeroTerminator {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"\nabc\\u0000");
+        compFail("public class NonBreakingSpace {\n" +
+                "    public static void main(String... args) {\n" +
+                "        String xxx = \"\"\"\nabc\\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(\"abc\\n\"));" + terminator +
+                          "    }" + terminator +
+                          "}" + terminator;
+            new JavacTask(TOOLBOX)
+                    .sources(code)
+                    .classpath(".")
+                    .options("--enable-preview", "-source", JDK_VERSION, "-encoding", "utf8")
+                    .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", JDK_VERSION, "-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", JDK_VERSION, "-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/TextBlockLang.java	Thu Jun 06 12:24:44 2019 -0300
@@ -0,0 +1,105 @@
+/*
+ * 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
+ * @bug 8223967
+ * @summary Unit tests for Text Block language changes
+ * @compile --enable-preview -source ${jdk.version} -encoding utf8 TextBlockLang.java
+ * @run main/othervm --enable-preview TextBlockLang
+ */
+
+public class TextBlockLang {
+    public static void main(String... args) {
+        test1();
+    }
+
+    /*
+     * Test basic string functionality.
+     */
+    static void test1() {
+        EQ("""
+           """, "");
+        EQ("""
+            abc
+            """, "abc\n");
+        EQ("""
+
+           """, "\n");
+        EQ("""
+            "
+            """, "\"\n");
+        EQ("""
+            ""
+            """, "\"\"\n");
+        EQ("""
+           \"""
+           """, "\"\"\"\n");
+        EQ("""
+           "\""
+           """, "\"\"\"\n");
+        EQ("""
+           ""\"
+           """, "\"\"\"\n");
+        EQ("""
+            \r
+            """, "\r\n");
+        EQ("""
+            \u2022
+            """, "\u2022\n");
+        EQ("""
+            •
+            """, "\u2022\n");
+        LENGTH("""
+            abc
+            """, 4);
+    }
+
+
+    /*
+     * 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();
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/langtools/tools/javac/diags/examples/TextBlockCloseDelimiter.java	Thu Jun 06 12:24:44 2019 -0300
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+ // key: compiler.warn.preview.feature.use.plural
+ // key: compiler.misc.feature.text.blocks
+ // key: compiler.err.unclosed.text.block
+ // options: --enable-preview -source ${jdk.version} -Xlint:preview
+
+class TextBlockCloseDelimiter {
+    String m() {
+        return """
+               ;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/langtools/tools/javac/diags/examples/TextBlockOpenDelimiter.java	Thu Jun 06 12:24:44 2019 -0300
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+ // key: compiler.warn.preview.feature.use.plural
+ // key: compiler.misc.feature.text.blocks
+ // key: compiler.err.illegal.text.block.open
+ // options: --enable-preview -source ${jdk.version} -Xlint:preview
+
+class TextBlockOpenDelimiter {
+    String m() {
+        return """xxxx
+               """;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/langtools/tools/javac/diags/examples/TextBlockWhitespace.java	Thu Jun 06 12:24:44 2019 -0300
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+ // key: compiler.warn.preview.feature.use.plural
+ // key: compiler.misc.feature.text.blocks
+ // key: compiler.warn.inconsistent.white.space.indentation
+ // key: compiler.warn.trailing.white.space.will.be.removed
+ // options: --enable-preview -source ${jdk.version} -Xlint:preview,text-blocks
+
+class TextBlockWhitespace {
+    String m() {
+        return """
+\u0009\u0009\u0009\u0009tab indentation
+\u0020\u0020\u0020\u0020space indentation and trailing space\u0020
+\u0020\u0020\u0020\u0020""";
+    }
+}
--- a/test/langtools/tools/javap/WhitespaceTest.java	Thu Jun 06 12:24:44 2019 -0300
+++ b/test/langtools/tools/javap/WhitespaceTest.java	Thu Jun 06 12:24:44 2019 -0300
@@ -37,8 +37,8 @@
     }
 
     void run() throws Exception {
-        test("-v", "java.lang.String");
-        test("-XDtab:1", "-v", "java.lang.String");
+        test("-v", "java.lang.Object");
+        test("-XDtab:1", "-v", "java.lang.Object");
 
         String testClasses = System.getProperty("test.classes");
         for (int i = 10; i < 40; i++)