diff --git a/shared/src/main/scala/io/github/apexdevtools/oparser/OutlineParser.scala b/shared/src/main/scala/io/github/apexdevtools/oparser/OutlineParser.scala index 74b45fe..8bda9cf 100644 --- a/shared/src/main/scala/io/github/apexdevtools/oparser/OutlineParser.scala +++ b/shared/src/main/scala/io/github/apexdevtools/oparser/OutlineParser.scala @@ -514,6 +514,10 @@ final class OutlineParser[TypeDecl <: IMutableTypeDeclaration, Ctx]( charOffset + 1 < length && contents(charOffset + 1) == peek } + private def peekMatch(offset: Int, peek: Char): Boolean = { + charOffset + offset < length && contents(charOffset + offset) == peek + } + private val byteBuffer = new mutable.StringBuilder(4) private def consumeCharacter(capture: Boolean = true): Unit = { @@ -647,20 +651,20 @@ final class OutlineParser[TypeDecl <: IMutableTypeDeclaration, Ctx]( } } - private def consumeNewline(): Boolean = { + private def consumeNewline(capture: Boolean = false): Boolean = { if (currentChar == Tokens.CarriageReturn && peekMatch(Tokens.Newline)) { - consumeCharacter(false) - consumeCharacter(false) + consumeCharacter(capture) + consumeCharacter(capture) line += 1 lineOffset = 0 true } else if (currentChar == Tokens.Newline) { - consumeCharacter(false) + consumeCharacter(capture) line += 1 lineOffset = 0 true } else { - consumeCharacter(false) + consumeCharacter(capture) false } } @@ -709,6 +713,12 @@ final class OutlineParser[TypeDecl <: IMutableTypeDeclaration, Ctx]( private def consumeStringLiteral(capture: Boolean = false): Unit = { buffer.clear() + + if (isAtMultilineStringLiteral) { + consumeMultilineStringLiteral(capture) + return + } + consumeCharacter(capture) var continue = true @@ -737,6 +747,48 @@ final class OutlineParser[TypeDecl <: IMutableTypeDeclaration, Ctx]( } } + private def isAtMultilineStringLiteral: Boolean = { + peekMatch(1, Tokens.SingleQuote) && + peekMatch(2, Tokens.SingleQuote) && + (peekMatch(3, Tokens.Newline) || peekMatch(3, Tokens.CarriageReturn)) + } + + private def consumeMultilineStringLiteral(capture: Boolean): Unit = { + consumeCharacter(capture) + consumeCharacter(capture) + consumeCharacter(capture) + + var continue = true + while (continue) { + val currentOffset = charOffset + + currentChar match { + case Tokens.SingleQuote + if peekMatch(1, Tokens.SingleQuote) && peekMatch(2, Tokens.SingleQuote) => + consumeCharacter(capture) + consumeCharacter(capture) + consumeCharacter(capture) + continue = false + case Tokens.BackSlash => + consumeCharacter(capture) + if (currentChar == Tokens.SingleQuote || currentChar == Tokens.BackSlash) { + consumeCharacter(capture) + } + case Tokens.Newline | Tokens.CarriageReturn => + consumeNewline(capture) + case _ => + consumeCharacter(capture) + } + + if (continue) { + if (atEnd()) { + throw new Exception("End of file") + } else if (currentOffset == charOffset) + throw new Exception("No Progress") + } + } + } + /** Read a token and return it * * @return diff --git a/shared/src/test/scala/io/github/apexdevtools/oparser/SmokeTest.scala b/shared/src/test/scala/io/github/apexdevtools/oparser/SmokeTest.scala index 992dd7d..f9ffeb9 100644 --- a/shared/src/test/scala/io/github/apexdevtools/oparser/SmokeTest.scala +++ b/shared/src/test/scala/io/github/apexdevtools/oparser/SmokeTest.scala @@ -153,6 +153,92 @@ class SmokeTest extends AnyFunSpec { assert(method.modifiers sameElements Array(Modifier("public"))) } + it("parses multi-line string literals") { + val content = + """public class Dummy { + | public void func() { + | String a = ''' + |{ + | "name": "John" + |}'''; + | } + |} + |""".stripMargin + val td = parse(content) + + assert(td.methods.length == 1) + td.methods.head.bodyLocation.foreach(bl => + assert( + extractLocation(content, bl) == + """public void func() { + | String a = ''' + |{ + | "name": "John" + |}'''; + | } + |""".stripMargin + ) + ) + } + + it("parses multi-line string literals with odd single quotes in the body") { + val content = + """public class Dummy { + | public void func() { + | String a = ''' + |can't + |'''; + | } + |} + |""".stripMargin + val td = parse(content) + + assert(td.methods.length == 1) + } + + it("parses multi-line string literals with doubled single quotes in the body") { + val content = + """public class Dummy { + | public void func() { + | String a = ''' + |''two'' + |'''; + | } + |} + |""".stripMargin + val td = parse(content) + + assert(td.methods.length == 1) + } + + it("parses multi-line string literals with unbalanced braces in the body") { + val content = + """public class Dummy { + | public void func() { + | String a = ''' + |{ + |'''; + | } + |} + |""".stripMargin + val td = parse(content) + + assert(td.methods.length == 1) + } + + it("falls back to legacy string literal parsing for malformed multi-line string literals") { + val content = + """public class Dummy { + | public void func() { + | String a = '''abc'''; + | } + |} + |""".stripMargin + val td = parse(content) + + assert(td.methods.length == 1) + } + it("errors on constructor without body terminated with semi-colon") { val content = """public class Dummy {