diff --git a/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/JSON.kt b/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/JSON.kt new file mode 100644 index 00000000000..441fc8197b9 --- /dev/null +++ b/js/js.parser/src/org/jetbrains/kotlin/js/parser/sourcemaps/JSON.kt @@ -0,0 +1,364 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.js.parser.sourcemaps + +import java.io.Reader +import java.io.StringReader +import java.io.StringWriter +import java.io.Writer + +sealed class JsonNode { + abstract fun write(writer: Writer) + + override fun toString(): String = StringWriter().also { write(it) }.toString() +} + +object JsonNull : JsonNode() { + override fun write(writer: Writer) { + writer.append("null") + } +} + +class JsonBoolean private constructor(val value: Boolean) : JsonNode() { + private val stringValue = value.toString() + + override fun write(writer: Writer) { + writer.append(stringValue) + } + + companion object { + val TRUE = JsonBoolean(true) + val FALSE = JsonBoolean(false) + + fun of(value: Boolean): JsonBoolean = if (value) TRUE else FALSE + } +} + +data class JsonObject(val properties: MutableMap) : JsonNode() { + constructor(vararg properties: Pair) : this(properties.toMap().toMutableMap()) + + override fun write(writer: Writer) { + writer.append('{') + var first = true + for ((key, value) in properties) { + if (!first) { + writer.append(',') + } + first = false + JsonString(key).write(writer) + writer.append(':') + value.write(writer) + } + writer.append('}') + } + + override fun toString(): String = super.toString() +} + +data class JsonArray(val elements: MutableList) : JsonNode() { + constructor(vararg elements: JsonNode) : this(elements.toMutableList()) + + override fun write(writer: Writer) { + writer.append('[') + var first = true + for (element in elements) { + if (!first) { + writer.append(',') + } + first = false + element.write(writer) + } + writer.append(']') + } +} + +data class JsonString(val value: String) : JsonNode() { + override fun write(writer: Writer) { + writer.append('"') + for (c in value) { + when (c) { + '\\' -> writer.append("\\\\") + '"' -> writer.append("\\\"") + '\r' -> writer.append("\\r") + '\n' -> writer.append("\\n") + '\t' -> writer.append("\\t") + '\b' -> writer.append("\\b") + '\u000C' -> writer.append("\\f") + in ' '..126.toChar() -> writer.append(c) + else -> { + writer.append("\\u") + var shift = 16 + repeat(4) { + shift -= 4 + val digit = (c.toInt() ushr shift) and 0xF + writer.append(if (digit < 10) (digit + '0'.toInt()).toChar() else (digit - 10 + 'a'.toInt()).toChar()) + } + } + } + } + writer.append('"') + } + + override fun toString(): String = super.toString() +} + +data class JsonNumber(val value: Double) : JsonNode() { + override fun write(writer: Writer) { + if (value.toLong().toDouble() == value) { + writer.append(value.toLong().toString()) + } + else { + writer.append(value.toString()) + } + } + + override fun toString(): String = super.toString() +} + +class JsonSyntaxException(val offset: Int, val line: Int, val column: Int, val text: String) : + RuntimeException("JSON syntax error at ${line + 1}, ${column + 1}: $text") + +fun parseJson(reader: Reader): JsonNode = JsonParser(reader).parse() + +fun parseJson(text: String): JsonNode = parseJson(StringReader(text)) + +private class JsonParser(val reader: Reader) { + private var charCode = reader.read() + private var offset = 0 + private var line = 0 + private var col = 0 + private var wasCR = false + + fun parse(): JsonNode { + val result = parseNode() + skipSpaces() + if (charCode != -1) throw error("End of input expected") + return result + } + + private fun skipSpaces() { + while (true) { + when (charCode) { + ' '.toInt(), '\t'.toInt(), '\n'.toInt(), '\r'.toInt() -> advance() + else -> return + } + } + } + + private fun parseNode(): JsonNode { + skipSpaces() + return when (charCode) { + '['.toInt() -> parseArray() + '{'.toInt() -> parseObject() + '"'.toInt() -> JsonString(parseString()) + 'n'.toInt() -> { expectString("null"); JsonNull } + 'f'.toInt() -> { expectString("false"); JsonBoolean.FALSE } + 't'.toInt() -> { expectString("true"); JsonBoolean.TRUE } + in '0'.toInt()..'9'.toInt() -> JsonNumber(parseNumber()) + '-'.toInt() -> { advance(); JsonNumber(-parseNumber()) + } + else -> error("Unexpected char") + } + } + + private fun parseArray(): JsonArray { + advance() + val result = JsonArray() + while (true) { + skipSpaces() + if (charCode == ']'.toInt()) { + advance() + break + } + else { + if (result.elements.isNotEmpty()) { + expectCharAndAdvance(',') + } + result.elements += parseNode() + } + } + return result + } + + private fun parseObject(): JsonObject { + advance() + val result = JsonObject() + while (true) { + skipSpaces() + if (charCode == '}'.toInt()) { + advance() + break + } + else { + if (result.properties.isNotEmpty()) { + expectCharAndAdvance(',') + } + + skipSpaces() + val key = parseString() + if (key in result.properties) { + error("Duplicate property name: $key") + } + + skipSpaces() + expectCharAndAdvance(':') + + result.properties[key] = parseNode() + } + } + return result + } + + private fun parseString(): String { + expectCharAndAdvance('"') + + val sb = StringBuilder() + while (true) { + when (charCode) { + '"'.toInt() -> { advance(); return sb.toString() } + '\\'.toInt() -> sb.append(parseEscapeSequence()) + else -> { + if (charCode >= ' '.toInt()) { + sb.append(charCode.toChar()) + advance() + } + else { + error("Invalid character in string literal") + } + } + } + } + } + + private fun parseEscapeSequence(): Char { + advance() + return when (charCode) { + '"'.toInt() -> advanceAndThen { '"' } + '\\'.toInt() -> advanceAndThen { '\\' } + '/'.toInt() -> advanceAndThen { '/' } + 'b'.toInt() -> advanceAndThen { '\b' } + 'n'.toInt() -> advanceAndThen { '\n' } + 'r'.toInt() -> advanceAndThen { '\r' } + 'f'.toInt() -> advanceAndThen { '\u000C' } + 't'.toInt() -> advanceAndThen { '\t' } + 'u'.toInt() -> parseHexEscapeSequence() + else -> error("Invalid escape sequence") + } + } + + private fun parseHexEscapeSequence(): Char { + advance() + var value = 0 + repeat(4) { + value *= 16 + value += when (charCode) { + in '0'.toInt()..'9'.toInt() -> charCode - '0'.toInt() + in 'a'.toInt()..'f'.toInt() -> charCode - 'a'.toInt() + 10 + in 'A'.toInt()..'F'.toInt() -> charCode - 'A'.toInt() + 10 + else -> error("Invalid escape sequence, hexadecimal char expected") + } + advance() + } + return value.toChar() + } + + private fun parseNumber(): Double { + val sb = StringBuilder() + takeIntegerDigitsTo(sb) + if (sb.startsWith('0') && sb.length > 1) error("Number must not start with zero") + + return when (charCode) { + '.'.toInt() -> { + sb.append('.') + advance() + takeIntegerDigitsTo(sb) + if (charCode == 'e'.toInt() || charCode == 'E'.toInt()) { + takeExponentTo(sb) + } + sb.toString().toDouble() + } + 'e'.toInt(), 'E'.toInt() -> { + takeExponentTo(sb) + sb.toString().toDouble() + } + else -> return sb.toString().toDouble() + } + } + + private fun takeIntegerDigitsTo(buffer: StringBuilder) { + var size = 0 + while (charCode in '0'.toInt()..'9'.toInt()) { + buffer.append(charCode.toChar()) + advance() + size++ + } + if (size == 0) error("Invalid char, decimal digit expected") + } + + private fun takeExponentTo(buffer: StringBuilder) { + buffer.append('e') + advance() + if (charCode == '-'.toInt() || charCode == '+'.toInt()) { + buffer.append(charCode.toChar()) + advance() + } + takeIntegerDigitsTo(buffer) + } + + private fun expectString(expected: String) { + for (c in expected) { + if (charCode != c.toInt()) error("Unexpected char, '$expected' expected here") + advance() + } + } + + private fun expectCharAndAdvance(expected: Char) { + if (charCode != expected.toInt()) error("Unexpected char, '$expected' expected here") + advance() + } + + private inline fun advanceAndThen(f: () -> T): T { + advance() + return f() + } + + private fun advance() { + if (charCode == -1) return + when (charCode) { + '\r'.toInt() -> { + line++ + col = 0 + wasCR = true + } + '\n'.toInt() -> { + if (!wasCR) { + line++ + col = 0 + wasCR = false + } + } + else -> { + col++ + wasCR = false + } + } + charCode = reader.read() + offset++ + } + + private fun error(text: String): Nothing = throw JsonSyntaxException(offset, line, col, text) +} diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/JsonTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/JsonTest.kt new file mode 100644 index 00000000000..00cad33b39e --- /dev/null +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/JsonTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.js.test + +import org.jetbrains.kotlin.js.parser.sourcemaps.JsonArray +import org.jetbrains.kotlin.js.parser.sourcemaps.JsonBoolean +import org.jetbrains.kotlin.js.parser.sourcemaps.JsonNode +import org.jetbrains.kotlin.js.parser.sourcemaps.JsonNull +import org.jetbrains.kotlin.js.parser.sourcemaps.JsonNumber +import org.jetbrains.kotlin.js.parser.sourcemaps.JsonObject +import org.jetbrains.kotlin.js.parser.sourcemaps.JsonString +import org.junit.Assert.assertEquals +import org.junit.Test + +class JsonTest { + @Test + fun complex() { + val input = """ + { + "a": [ 1, 2, { "x" : 3 }, [ 5 ] ], + "b": 1, + "c": { "foo": null, "bar": true, "baz": false }, + "d": null, + "e": "qwe", + "f": 1.2, + "g": [ [], {} ] + } + """.trimIndent() + + val first = org.jetbrains.kotlin.js.parser.sourcemaps.parseJson(input) + val second = org.jetbrains.kotlin.js.parser.sourcemaps.parseJson(first.toString()) + + val expected = JsonObject( + "a" to JsonArray( + JsonNumber(1.0), + JsonNumber(2.0), + JsonObject("x" to JsonNumber(3.0)), + JsonArray(JsonNumber(5.0)) + ), + "b" to JsonNumber(1.0), + "c" to JsonObject( + "foo" to JsonNull, + "bar" to JsonBoolean.TRUE, + "baz" to JsonBoolean.FALSE + ), + "d" to JsonNull, + "e" to JsonString("qwe"), + "f" to JsonNumber(1.2), + "g" to JsonArray(JsonArray(), JsonObject()) + ) + + assertJsonEquals(expected, first) + assertJsonEquals(expected, second) + } + + @Test + fun numbers() { + assertJsonEquals(JsonNumber(1.0), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("1")) + assertJsonEquals(JsonNumber(1.2), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("1.2")) + assertJsonEquals(JsonNumber(10.2), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("10.2")) + assertJsonEquals(JsonNumber(10.02), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("10.02")) + assertJsonEquals(JsonNumber(0.2), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("0.2")) + assertJsonEquals(JsonNumber(1000.0), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("1E3")) + assertJsonEquals(JsonNumber(0.001), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("1E-3")) + assertJsonEquals(JsonNumber(0.0012), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("1.2e-3")) + assertJsonEquals(JsonNumber(1200.0), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("1.2e+3")) + assertJsonEquals(JsonNumber(-1200.0), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("-1.2e+3")) + } + + @Test + fun strings() { + assertJsonEquals(JsonString("foo"), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("\"foo\"")) + assertJsonEquals(JsonString(""), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("\"\"")) + assertJsonEquals(JsonString("\r\n\t\b\u000C\u0012 Й"), org.jetbrains.kotlin.js.parser.sourcemaps.parseJson("\"\\r\\n\\t\\b\\f\\u0012 Й\"")) + } + + private fun assertJsonEquals(expected: JsonNode, actual: JsonNode) { + assertEquals(expected, actual) + assertEquals(expected.toString(), actual.toString()) + } +} \ No newline at end of file