From 1dc5fcac04e06beadd8c69f6a4116d70de40506d Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sun, 3 Apr 2022 19:39:26 -0700 Subject: [PATCH] Port text block support toolargs supports multi tools Tests need args for scalac, javac and also to specify test conditions such as required jvm version. --- .../tools/dotc/config/CommandLineParser.scala | 14 +- .../tools/dotc/parsing/JavaScanners.scala | 246 ++++++++++++++---- .../transform/PatmatExhaustivityTest.scala | 2 +- compiler/test/dotty/tools/repl/ReplTest.scala | 2 +- compiler/test/dotty/tools/utils.scala | 67 +++-- .../dotty/tools/vulpix/ParallelTesting.scala | 38 ++- .../t12290/SCALA_ONLY_Invalid1.java | 7 + .../t12290/SCALA_ONLY_Invalid2.java | 6 + tests/run-with-compiler/t12290.check | 66 +++++ tests/run-with-compiler/t12290/Test.scala | 33 +++ .../run-with-compiler/t12290/TextBlocks.java | 84 ++++++ 11 files changed, 475 insertions(+), 90 deletions(-) create mode 100644 tests/neg-with-compiler/t12290/SCALA_ONLY_Invalid1.java create mode 100644 tests/neg-with-compiler/t12290/SCALA_ONLY_Invalid2.java create mode 100644 tests/run-with-compiler/t12290.check create mode 100644 tests/run-with-compiler/t12290/Test.scala create mode 100644 tests/run-with-compiler/t12290/TextBlocks.java diff --git a/compiler/src/dotty/tools/dotc/config/CommandLineParser.scala b/compiler/src/dotty/tools/dotc/config/CommandLineParser.scala index 7ff93c392425..f7c4b825c244 100644 --- a/compiler/src/dotty/tools/dotc/config/CommandLineParser.scala +++ b/compiler/src/dotty/tools/dotc/config/CommandLineParser.scala @@ -95,19 +95,17 @@ object CommandLineParser: def tokenize(line: String): List[String] = tokenize(line, x => throw new ParseException(x)) - /** - * Expands all arguments starting with @ to the contents of the - * file named like each argument. + /** Expands all arguments starting with @ to the contents of the file named like each argument. */ def expandArg(arg: String): List[String] = - def stripComment(s: String) = s takeWhile (_ != '#') - val path = Paths.get(arg stripPrefix "@") - if (!Files.exists(path)) + val path = Paths.get(arg.stripPrefix("@")) + if !Files.exists(path) then System.err.nn.println(s"Argument file ${path.nn.getFileName} could not be found") Nil else - val lines = Files.readAllLines(path).nn // default to UTF-8 encoding - val params = lines.asScala map stripComment mkString " " + def stripComment(s: String) = s.indexOf('#') match { case -1 => s case i => s.substring(0, i) } + val lines = Files.readAllLines(path).nn + val params = lines.asScala.map(stripComment).filter(!_.nn.isEmpty).mkString(" ") tokenize(params) class ParseException(msg: String) extends RuntimeException(msg) diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala b/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala index d7178bd411e7..7ed584c9a03b 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala @@ -7,8 +7,9 @@ import core.Names.SimpleName import Scanners._ import util.SourceFile import JavaTokens._ -import scala.annotation.{ switch, tailrec } +import scala.annotation.{switch, tailrec} import util.Chars._ +import PartialFunction.cond object JavaScanners { @@ -31,23 +32,29 @@ object JavaScanners { // Get next token ------------------------------------------------------------ def nextToken(): Unit = - if (next.token == EMPTY) { + if next.token == EMPTY then lastOffset = lastCharOffset fetchToken() - } - else { - this copyFrom next + else + this.copyFrom(next) next.token = EMPTY - } - def lookaheadToken: Int = { - prev copyFrom this - nextToken() + def lookaheadToken: Int = + lookAhead() val t = token - next copyFrom this - this copyFrom prev + reset() t - } + + def lookAhead() = + prev.copyFrom(this) + nextToken() + + def reset() = + next.copyFrom(this) + this.copyFrom(prev) + + class LookaheadScanner extends JavaScanner(source, startFrom = charOffset - 1): + override protected def initialize(): Unit = nextChar() /** read next token */ @@ -93,15 +100,23 @@ object JavaScanners { case '\"' => nextChar() - while (ch != '\"' && (isUnicodeEscape || ch != CR && ch != LF && ch != SU)) - getlitch() - if (ch == '\"') { - token = STRINGLIT - setStrVal() - nextChar() - } + if ch != '\"' then // "..." non-empty string literal + while ch != '\"' && (isUnicodeEscape || ch != CR && ch != LF && ch != SU) do + getlitch() + if ch == '\"' then + token = STRINGLIT + setStrVal() + nextChar() + else + error("unclosed string literal") else - error("unclosed string literal") + nextChar() + if ch != '\"' then // "" empty string literal + token = STRINGLIT + setStrVal() + else + nextChar() + getTextBlock() case '\'' => nextChar() @@ -399,46 +414,177 @@ object JavaScanners { // Literals ----------------------------------------------------------------- - /** read next character in character or string literal: + /** Read next character in character or string literal. */ - protected def getlitch(): Unit = - if (ch == '\\') { + protected def getlitch(): Unit = getlitch(scanOnly = false, inTextBlock = false) + + /** Read next character in character or string literal. + * + * @param scanOnly skip emitting errors or adding to the literal buffer + * @param inTextBlock is this for a text block? + */ + def getlitch(scanOnly: Boolean, inTextBlock: Boolean): Unit = + def octal: Char = + val leadch: Char = ch + var oct: Int = digit2int(ch, 8) nextChar() if ('0' <= ch && ch <= '7') { - val leadch: Char = ch - var oct: Int = digit2int(ch, 8) + oct = oct * 8 + digit2int(ch, 8) nextChar() - if ('0' <= ch && ch <= '7') { + if (leadch <= '3' && '0' <= ch && ch <= '7') { oct = oct * 8 + digit2int(ch, 8) nextChar() - if (leadch <= '3' && '0' <= ch && ch <= '7') { - oct = oct * 8 + digit2int(ch, 8) - nextChar() + } + } + oct.asInstanceOf[Char] + end octal + def greatEscape: Char = + nextChar() + if '0' <= ch && ch <= '7' then octal + else + val x = ch match + case 'b' => '\b' + case 's' => ' ' + case 't' => '\t' + case 'n' => '\n' + case 'f' => '\f' + case 'r' => '\r' + case '\"' => '\"' + case '\'' => '\'' + case '\\' => '\\' + case CR | LF if inTextBlock => + if !scanOnly then nextChar() + 0 + case _ => + if !scanOnly then error("invalid escape character", charOffset - 1) + ch + if x != 0 then nextChar() + x + end greatEscape + + // begin getlitch + val c: Char = + if ch == '\\' then greatEscape + else + val res = ch + nextChar() + res + if c != 0 && !scanOnly then putChar(c) + end getlitch + + /** Read a triple-quote delimited text block, starting after the first three double quotes. + */ + private def getTextBlock(): Unit = { + // Open delimiter is followed by optional space, then a newline + while (ch == ' ' || ch == '\t' || ch == FF) { + nextChar() + } + if (ch != LF && ch != CR) { // CR-LF is already normalized into LF by `JavaCharArrayReader` + error("illegal text block open delimiter sequence, missing line terminator") + return + } + nextChar() + + /* Do a lookahead scan over the full text block to: + * - compute common white space prefix + * - find the offset where the text block ends + */ + var commonWhiteSpacePrefix = Int.MaxValue + var blockEndOffset = 0 + var blockClosed = false + var lineWhiteSpacePrefix = 0 + var lineIsOnlyWhitespace = true + val in = LookaheadScanner() + while (!blockClosed && (isUnicodeEscape || ch != SU)) { + if (in.ch == '\"') { // Potential end of the block + in.nextChar() + if (in.ch == '\"') { + in.nextChar() + if (in.ch == '\"') { + blockClosed = true + commonWhiteSpacePrefix = commonWhiteSpacePrefix min lineWhiteSpacePrefix + blockEndOffset = in.charOffset - 2 } } - putChar(oct.asInstanceOf[Char]) + + // Not the end of the block - just a single or double " character + if (!blockClosed) { + lineIsOnlyWhitespace = false + } + } else if (in.ch == CR || in.ch == LF) { // new line in the block + in.nextChar() + if (!lineIsOnlyWhitespace) { + commonWhiteSpacePrefix = commonWhiteSpacePrefix min lineWhiteSpacePrefix + } + lineWhiteSpacePrefix = 0 + lineIsOnlyWhitespace = true + } else if (lineIsOnlyWhitespace && Character.isWhitespace(in.ch)) { // extend white space prefix + in.nextChar() + lineWhiteSpacePrefix += 1 + } else { + lineIsOnlyWhitespace = false + in.getlitch(scanOnly = true, inTextBlock = true) } - else { - ch match { - case 'b' => putChar('\b') - case 't' => putChar('\t') - case 'n' => putChar('\n') - case 'f' => putChar('\f') - case 'r' => putChar('\r') - case '\"' => putChar('\"') - case '\'' => putChar('\'') - case '\\' => putChar('\\') - case _ => - error("invalid escape character", charOffset - 1) - putChar(ch) + } + + // Bail out if the block never did have an end + if (!blockClosed) { + error("unclosed text block") + return + } + + // Second pass: construct the literal string value this time + while (charOffset < blockEndOffset) { + // Drop the line's leading whitespace + var remainingPrefix = commonWhiteSpacePrefix + while (remainingPrefix > 0 && ch != CR && ch != LF && charOffset < blockEndOffset) { + nextChar() + remainingPrefix -= 1 + } + + var trailingWhitespaceLength = 0 + var escapedNewline = false // Does the line end with `\`? + while (ch != CR && ch != LF && charOffset < blockEndOffset && !escapedNewline) { + if (Character.isWhitespace(ch)) { + trailingWhitespaceLength += 1 + } else { + trailingWhitespaceLength = 0 } + + // Detect if the line is about to end with `\` + if ch == '\\' && cond(lookaheadChar()) { case CR | LF => true } then + escapedNewline = true + + getlitch(scanOnly = false, inTextBlock = true) + } + + // Remove the last N characters from the buffer */ + def popNChars(n: Int): Unit = + if n > 0 then + val text = litBuf.toString + litBuf.clear() + val trimmed = text.substring(0, text.length - (n min text.length)) + trimmed.nn.foreach(litBuf.append) + + // Drop the line's trailing whitespace + popNChars(trailingWhitespaceLength) + + // Normalize line terminators + if ((ch == CR || ch == LF) && !escapedNewline) { nextChar() + putChar('\n') } } - else { - putChar(ch) - nextChar() - } + + token = STRINGLIT + setStrVal() + + // Trailing """ + nextChar() + nextChar() + nextChar() + } + end getTextBlock /** read fractional part and exponent of floating point number * if one is present. @@ -585,8 +731,10 @@ object JavaScanners { } /* Initialization: read first char, then first token */ - nextChar() - nextToken() + protected def initialize(): Unit = + nextChar() + nextToken() + initialize() } private val (lastKeywordStart, kwArray) = buildKeywordArray(keywords) diff --git a/compiler/test/dotty/tools/dotc/transform/PatmatExhaustivityTest.scala b/compiler/test/dotty/tools/dotc/transform/PatmatExhaustivityTest.scala index 5b846b6daf21..eb6ab8e8fb5f 100644 --- a/compiler/test/dotty/tools/dotc/transform/PatmatExhaustivityTest.scala +++ b/compiler/test/dotty/tools/dotc/transform/PatmatExhaustivityTest.scala @@ -23,7 +23,7 @@ class PatmatExhaustivityTest { val options = List("-pagewidth", "80", "-color:never", "-Ystop-after:explicitSelf", "-classpath", TestConfiguration.basicClasspath) private def compile(files: List[JPath]): Seq[String] = { - val opts = toolArgsFor(files) + val opts = toolArgsFor(files).get(ToolName.Scalac).getOrElse(Nil) val stringBuffer = new StringWriter() val printWriter = new PrintWriter(stringBuffer) val reporter = TestReporter.simplifiedReporter(printWriter) diff --git a/compiler/test/dotty/tools/repl/ReplTest.scala b/compiler/test/dotty/tools/repl/ReplTest.scala index b4f69d4e9b11..1af1f68d6533 100644 --- a/compiler/test/dotty/tools/repl/ReplTest.scala +++ b/compiler/test/dotty/tools/repl/ReplTest.scala @@ -69,7 +69,7 @@ extends ReplDriver(options, new PrintStream(out, true, StandardCharsets.UTF_8.na val expectedOutput = lines.filter(nonBlank) val actualOutput = { - val opts = toolArgsParse(lines.take(1)) + val opts = toolArgsFor(ToolName.Scalac)(lines.take(1)) val (optsLine, inputLines) = if opts.isEmpty then ("", lines) else (lines.head, lines.drop(1)) resetToInitial(opts) diff --git a/compiler/test/dotty/tools/utils.scala b/compiler/test/dotty/tools/utils.scala index f51747bbf54d..9d56b28a2c41 100644 --- a/compiler/test/dotty/tools/utils.scala +++ b/compiler/test/dotty/tools/utils.scala @@ -15,8 +15,10 @@ import scala.util.Using.resource import scala.util.chaining.given import scala.util.control.{ControlThrowable, NonFatal} +import dotc.config.CommandLineParser + def scripts(path: String): Array[File] = { - val dir = new File(getClass.getResource(path).getPath) + val dir = new File(this.getClass.getResource(path).getPath) assert(dir.exists && dir.isDirectory, "Couldn't load scripts dir") dir.listFiles.filter { f => val path = if f.isDirectory then f.getPath + "/" else f.getPath @@ -48,24 +50,49 @@ def assertThrows[T <: Throwable: ClassTag](p: T => Boolean)(body: => Any): Unit case NonFatal(other) => throw AssertionError(s"Wrong exception: expected ${implicitly[ClassTag[T]]} but was ${other.getClass.getName}").tap(_.addSuppressed(other)) end assertThrows -def toolArgsFor(files: List[JPath], charset: Charset = UTF_8): List[String] = - files.flatMap(path => toolArgsParse(resource(Files.lines(path, charset))(_.limit(10).toScala(List)))) +/** Famous tool names in the ecosystem. Used for tool args in test files. */ +enum ToolName: + case Scala, Scalac, Java, Javac, Test +object ToolName: + def named(s: String): ToolName = values.find(_.toString.equalsIgnoreCase(s)).getOrElse(throw IllegalArgumentException(s)) + +/** Take a prefix of each file, extract tool args, parse, and combine. + * Arg parsing respects quotation marks. Result is a map from ToolName to the combined tokens. + */ +def toolArgsFor(files: List[JPath], charset: Charset = UTF_8): Map[ToolName, List[String]] = + files.foldLeft(Map.empty[ToolName, List[String]]) { (res, path) => + val toolargs = toolArgsParse(resource(Files.lines(path, charset))(_.limit(10).toScala(List))) + toolargs.foldLeft(res) { + case (acc, (tool, args)) => + val name = ToolName.named(tool) + val tokens = CommandLineParser.tokenize(args) + acc.updatedWith(name)(v0 => v0.map(_ ++ tokens).orElse(Some(tokens))) + } + } + +def toolArgsFor(tool: ToolName)(lines: List[String]): List[String] = + toolArgsParse(lines).collectFirst { case (name, args) if tool eq ToolName.named(name) => CommandLineParser.tokenize(args) }.getOrElse(Nil) -// Inspect the first 10 of the given lines for compiler options of the form +// scalac: arg1 arg2, with alternative opening, optional space, alt names, text that is not */ up to end. +// groups are (name, args) +private val toolArg = raw"(?://|/\*| \*) ?(?i:(${ToolName.values.mkString("|")})):((?:[^*]|\*(?!/))*)".r.unanchored + +// Inspect the lines for compiler options of the form // `// scalac: args`, `/* scalac: args`, ` * scalac: args`. -// If args string ends in close comment, drop the `*` `/`. -// If split, parse the args string as a command line. -// (from scala.tools.partest.nest.Runner#toolArgsFor) -def toolArgsParse(lines: List[String]): List[String] = { - val tag = "scalac:" - val endc = "*" + "/" // be forgiving of /* scalac: ... */ - def stripped(s: String) = s.substring(s.indexOf(tag) + tag.length).stripSuffix(endc) - val args = lines.to(LazyList).take(10).filter { s => - s.contains("//" + tag) - || s.contains("// " + tag) - || s.contains("/* " + tag) - || s.contains(" * " + tag) - // but avoid picking up comments like "% scalac ./a.scala" and "$ scalac a.scala" - }.map(stripped).headOption - args.map(dotc.config.CommandLineParser.tokenize).getOrElse(Nil) -} \ No newline at end of file +// If args string ends in close comment, stop at the `*` `/`. +// Returns all the matches by the regex. +def toolArgsParse(lines: List[String]): List[(String,String)] = + lines.flatMap { case toolArg(name, args) => List((name, args)) case _ => Nil } + +import org.junit.Test +import org.junit.Assert._ + +class ToolArgsTest: + @Test def `missing toolarg is absent`: Unit = assertEquals(Nil, toolArgsParse(List(""))) + @Test def `toolarg is present`: Unit = assertEquals(("test", " -hey") :: Nil, toolArgsParse("// test: -hey" :: Nil)) + @Test def `tool is present`: Unit = assertEquals("-hey" :: Nil, toolArgsFor(ToolName.Test)("// test: -hey" :: Nil)) + @Test def `missing tool is absent`: Unit = assertEquals(Nil, toolArgsFor(ToolName.Javac)("// test: -hey" :: Nil)) + @Test def `multitool is present`: Unit = + assertEquals("-hey" :: Nil, toolArgsFor(ToolName.Test)("// test: -hey" :: "// javac: -d /tmp" :: Nil)) + assertEquals("-d" :: "/tmp" :: Nil, toolArgsFor(ToolName.Javac)("// test: -hey" :: "// javac: -d /tmp" :: Nil)) +end ToolArgsTest diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index 6df8c734ad3e..618525d6e23f 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -448,14 +448,27 @@ trait ParallelTesting extends RunnerOrchestration { self => throw e protected def compile(files0: Array[JFile], flags0: TestFlags, suppressErrors: Boolean, targetDir: JFile): TestReporter = { + import scala.util.Properties.* + def flattenFiles(f: JFile): Array[JFile] = if (f.isDirectory) f.listFiles.flatMap(flattenFiles) else Array(f) val files: Array[JFile] = files0.flatMap(flattenFiles) + val toolArgs = toolArgsFor(files.toList.map(_.toPath), getCharsetFromEncodingOpt(flags0)) + + val spec = raw"(\d+)(\+)?".r + val testFilter = toolArgs.get(ToolName.Test) match + case Some("-jvm" :: spec(n, more) :: Nil) => + if more == "+" then isJavaAtLeast(n) else javaSpecVersion == n + case Some(args) => throw new IllegalStateException(args.mkString("unknown test option: ", ", ", "")) + case None => true + + def scalacOptions = toolArgs.get(ToolName.Scalac).getOrElse(Nil) + val flags = flags0 - .and(toolArgsFor(files.toList.map(_.toPath), getCharsetFromEncodingOpt(flags0)): _*) + .and(scalacOptions: _*) .and("-d", targetDir.getPath) .withClasspath(targetDir.getPath) @@ -493,18 +506,21 @@ trait ParallelTesting extends RunnerOrchestration { self => val allArgs = flags.all - // If a test contains a Java file that cannot be parsed by Dotty's Java source parser, its - // name must contain the string "JAVA_ONLY". - val dottyFiles = files.filterNot(_.getName.contains("JAVA_ONLY")).map(_.getPath) - driver.process(allArgs ++ dottyFiles, reporter = reporter) + if testFilter then + // If a test contains a Java file that cannot be parsed by Dotty's Java source parser, its + // name must contain the string "JAVA_ONLY". + val dottyFiles = files.filterNot(_.getName.contains("JAVA_ONLY")).map(_.getPath) + driver.process(allArgs ++ dottyFiles, reporter = reporter) - val javaFiles = files.filter(_.getName.endsWith(".java")).map(_.getPath) - val javaErrors = compileWithJavac(javaFiles) + // todo a better mechanism than ONLY. test: -scala-only? + val javaFiles = files.filter(_.getName.endsWith(".java")).filterNot(_.getName.contains("SCALA_ONLY")).map(_.getPath) + val javaErrors = compileWithJavac(javaFiles) - if (javaErrors.isDefined) { - echo(s"\njava compilation failed: \n${ javaErrors.get }") - fail(failure = JavaCompilationFailure(javaErrors.get)) - } + if (javaErrors.isDefined) { + echo(s"\njava compilation failed: \n${ javaErrors.get }") + fail(failure = JavaCompilationFailure(javaErrors.get)) + } + end if reporter } diff --git a/tests/neg-with-compiler/t12290/SCALA_ONLY_Invalid1.java b/tests/neg-with-compiler/t12290/SCALA_ONLY_Invalid1.java new file mode 100644 index 000000000000..726bcad7e936 --- /dev/null +++ b/tests/neg-with-compiler/t12290/SCALA_ONLY_Invalid1.java @@ -0,0 +1,7 @@ +// test: -jvm 15+ +class SCALA_ONLY_Invalid1 { + + public static final String badOpeningDelimiter = """non-whitespace // error + foo + """; // error +} diff --git a/tests/neg-with-compiler/t12290/SCALA_ONLY_Invalid2.java b/tests/neg-with-compiler/t12290/SCALA_ONLY_Invalid2.java new file mode 100644 index 000000000000..b6a38d15c42f --- /dev/null +++ b/tests/neg-with-compiler/t12290/SCALA_ONLY_Invalid2.java @@ -0,0 +1,6 @@ +class SCALA_ONLY_Invalid2 { + + // Closing delimiter is first three eligible `"""`, not last + public static final String closingDelimiterIsNotScalas = """ + foo""""; // error +} // anypos-error diff --git a/tests/run-with-compiler/t12290.check b/tests/run-with-compiler/t12290.check new file mode 100644 index 000000000000..c6ce23a28ef2 --- /dev/null +++ b/tests/run-with-compiler/t12290.check @@ -0,0 +1,66 @@ +==== +A text + +==== + + +

Hello, world

+ + + +==== +SELECT "EMP_ID", "LAST_NAME" FROM "EMPLOYEE_TB" +WHERE "CITY" = 'INDIANAPOLIS' +ORDER BY "EMP_ID", "LAST_NAME"; + +==== + + +

Hello, world

+ + + +==== + + +

Hello, world

+ + + +==== + + +

Hello, world

+ + + + +==== + + +

Hello , world

+ + + +==== + this line has 4 tabs before it + this line has 5 spaces before it and space after it + this line has 2 tabs and 3 spaces before it +  this line has 6 spaces before it + +==== +String text = """ + A text block inside a text block +"""; + +==== +foo bar +baz +==== + +==== +XY + +==== +X Y +==== diff --git a/tests/run-with-compiler/t12290/Test.scala b/tests/run-with-compiler/t12290/Test.scala new file mode 100644 index 000000000000..e6c96573f032 --- /dev/null +++ b/tests/run-with-compiler/t12290/Test.scala @@ -0,0 +1,33 @@ +/* Using `valueOf` is a way to check that the Java string literals were properly + * parsed, since the parsed value is what the Scala compiler will use when + * resolving the singleton types + */ +object Test extends App { + println("====") + println(valueOf[TextBlocks.aText.type]) + println("====") + println(valueOf[TextBlocks.html1.type]) + println("====") + println(valueOf[TextBlocks.query.type]) + println("====") + println(valueOf[TextBlocks.html2.type]) + println("====") + println(valueOf[TextBlocks.html3.type]) + println("====") + println(valueOf[TextBlocks.html4.type]) + println("====") + println(valueOf[TextBlocks.html5.type]) + println("====") + println(valueOf[TextBlocks.mixedIndents.type]) + println("====") + println(valueOf[TextBlocks.code.type]) + println("====") + println(valueOf[TextBlocks.simpleString.type]) + println("====") + println(valueOf[TextBlocks.emptyString.type]) + println("====") + println(valueOf[TextBlocks.XY.type]) + println("====") + println(valueOf[TextBlocks.Octal.type]) + println("====") +} diff --git a/tests/run-with-compiler/t12290/TextBlocks.java b/tests/run-with-compiler/t12290/TextBlocks.java new file mode 100644 index 000000000000..6a827923a052 --- /dev/null +++ b/tests/run-with-compiler/t12290/TextBlocks.java @@ -0,0 +1,84 @@ +// test: -jvm 15+ +class TextBlocks { + + final static String aText = """ + A text + """; + + final static String html1 = """ + + +

Hello, world

+ + + """; + + // quote characters are unescaped + final static String query = """ + SELECT "EMP_ID", "LAST_NAME" FROM "EMPLOYEE_TB" + WHERE "CITY" = 'INDIANAPOLIS' + ORDER BY "EMP_ID", "LAST_NAME"; + """; + + // incidental trailing spaces + final static String html2 = """ + + +

Hello, world

+ + + """; + + // trailing delimiter influences + final static String html3 = """ + + +

Hello, world

+ + + """; + + // blank line does not affect + final static String html4 = """ + + +

Hello, world

+ + + + """; + + // escape sequences + final static String html5 = """ + \n + \ +

Hello\s,\tworld

+ + + """; + // mixed indentation + final static String mixedIndents = """ + \s this line has 4 tabs before it + this line has 5 spaces before it and space after it \u0020 \u000C\u0020 \u001E + this line has 2 tabs and 3 spaces before it +\u0020 \u000C\u0020 \u001E this line has 6 spaces before it + """; + + final static String code = + """ + String text = \""" + A text block inside a text block + \"""; + """; + + final static String simpleString = "foo\tbar\nbaz"; + + final static String emptyString = ""; + + final static String XY = """ +X\ +Y +"""; + + final static String Octal = "X\040Y"; +}