All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.diffplug.selfie.guts.Literals.kt Maven / Gradle / Ivy

There is a newer version: 2.4.0
Show newest version
/*
 * Copyright (C) 2023-2024 DiffPlug
 *
 * 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
 *
 *     https://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 com.diffplug.selfie.guts

import kotlin.math.abs

internal expect fun jreVersion(): Int

enum class Language {
  JAVA,
  JAVA_PRE15,
  KOTLIN,
  GROOVY,
  SCALA;

  companion object {
    fun fromFilename(filename: String): Language {
      return when (filename.substringAfterLast('.')) {
        "java" -> if (jreVersion() < 15) JAVA_PRE15 else JAVA
        "kt" -> KOTLIN
        "groovy",
        "gvy",
        "gy" -> GROOVY
        "scala",
        "sc" -> SCALA
        else -> throw IllegalArgumentException("Unknown language for file $filename")
      }
    }
  }
}

class LiteralValue(val expected: T?, val actual: T, val format: LiteralFormat)

abstract class LiteralFormat {
  internal abstract fun encode(
      value: T,
      language: Language,
      encodingPolicy: EscapeLeadingWhitespace
  ): String
  internal abstract fun parse(str: String, language: Language): T
}

private const val MAX_RAW_NUMBER = 1000
private const val PADDING_SIZE = MAX_RAW_NUMBER.toString().length - 1
private fun encodeUnderscores(
    buffer: StringBuilder,
    value: Long,
    language: Language
): StringBuilder {
  return if (value >= MAX_RAW_NUMBER) {
    val mod = value % MAX_RAW_NUMBER
    val leftPadding = PADDING_SIZE - mod.toString().length
    encodeUnderscores(buffer, value / MAX_RAW_NUMBER, language)
    buffer.append("_")
    for (i in leftPadding downTo 1) {
      buffer.append('0')
    }
    buffer.append(mod)
    buffer
  } else if (value < 0) {
    buffer.append('-')
    encodeUnderscores(buffer, abs(value), language)
  } else {
    buffer.append(value)
  }
}

internal object LiteralInt : LiteralFormat() {
  override fun encode(
      value: Int,
      language: Language,
      encodingPolicy: EscapeLeadingWhitespace
  ): String {
    return encodeUnderscores(StringBuilder(), value.toLong(), language).toString()
  }
  override fun parse(str: String, language: Language): Int {
    return str.replace("_", "").toInt()
  }
}

internal object LiteralLong : LiteralFormat() {
  override fun encode(
      value: Long,
      language: Language,
      encodingPolicy: EscapeLeadingWhitespace
  ): String {
    val buffer = encodeUnderscores(StringBuilder(), value, language)
    buffer.append('L')
    return buffer.toString()
  }
  override fun parse(str: String, language: Language): Long {
    var longStr = str.replace("_", "")
    if (longStr.endsWith("L")) {
      longStr = longStr.substring(0, longStr.length - 1)
    }
    return longStr.toLong()
  }
}

private const val TRIPLE_QUOTE = "\"\"\""
private const val KOTLIN_DOLLAR = "\${'\$'}"
private const val KOTLIN_DOLLARQUOTE = "\${'\"'}"

internal object LiteralString : LiteralFormat() {
  override fun encode(
      value: String,
      language: Language,
      encodingPolicy: EscapeLeadingWhitespace
  ): String =
      if (value.indexOf('\n') == -1)
          when (language) {
            Language.SCALA, // scala only does $ substitution for s" and f" strings
            Language.JAVA_PRE15,
            Language.GROOVY,
            Language.JAVA -> encodeSingleJava(value)
            Language.KOTLIN -> encodeSingleJavaWithDollars(value)
          }
      else
          when (language) {
            // TODO: support triple-quoted strings in scala
            // https://github.com/diffplug/selfie/issues/106
            Language.SCALA,
            // TODO: support triple-quoted strings in groovy
            // https://github.com/diffplug/selfie/issues/105
            Language.GROOVY,
            Language.JAVA_PRE15 -> encodeMultiAsSingleJava(value)
            Language.JAVA -> encodeMultiJava(value, encodingPolicy)
            Language.KOTLIN -> encodeMultiKotlin(value, encodingPolicy)
          }
  override fun parse(str: String, language: Language): String =
      if (!str.startsWith(TRIPLE_QUOTE))
          when (language) {
            Language.SCALA,
            Language.JAVA_PRE15,
            Language.JAVA -> parseSingleJava(SourceFile.commaDelimitedParseCleanup(str))
            Language.GROOVY,
            Language.KOTLIN -> parseSingleJavaWithDollars(str)
          }
      else
          when (language) {
            Language.SCALA ->
                throw UnsupportedOperationException(
                    "Selfie doesn't support triple-quoted strings in Scala, yet - help wanted: https://github.com/diffplug/selfie/issues/106")
            Language.GROOVY ->
                throw UnsupportedOperationException(
                    "Selfie doesn't support triple-quoted strings in Groovy, yet - help wanted: https://github.com/diffplug/selfie/issues/105")
            Language.JAVA_PRE15,
            Language.JAVA -> parseMultiJava(str)
            Language.KOTLIN -> parseMultiKotlin(str)
          }
  fun encodeSingleJava(value: String): String = encodeSingleJavaish(value, false)
  fun encodeSingleJavaWithDollars(value: String) = encodeSingleJavaish(value, true)
  private fun encodeSingleJavaish(value: String, escapeDollars: Boolean): String {
    val source = StringBuilder()
    source.append("\"")
    for (char in value) {
      when (char) {
        '\b' -> source.append("\\b")
        '\n' -> source.append("\\n")
        '\r' -> source.append("\\r")
        '\t' -> source.append("\\t")
        '\"' -> source.append("\\\"")
        '\\' -> source.append("\\\\")
        '$' -> if (escapeDollars) source.append(KOTLIN_DOLLAR) else source.append('$')
        else ->
            if (isControlChar(char)) {
              source.append("\\u")
              source.append(char.code.toString(16).padStart(4, '0'))
            } else {
              source.append(char)
            }
      }
    }
    source.append("\"")
    return source.toString()
  }
  private fun isControlChar(c: Char): Boolean {
    return c in '\u0000'..'\u001F' || c == '\u007F'
  }
  fun parseSingleJava(sourceWithQuotes: String) = parseSingleJavaish(sourceWithQuotes, false)
  fun parseSingleJavaWithDollars(sourceWithQuotes: String) =
      parseSingleJavaish(sourceWithQuotes, true)
  private fun parseSingleJavaish(sourceWithQuotes: String, removeDollars: Boolean): String {
    check(sourceWithQuotes.startsWith('"'))
    check(sourceWithQuotes.endsWith('"'))
    val source = sourceWithQuotes.substring(1, sourceWithQuotes.length - 1)
    val toUnescape = if (removeDollars) inlineDollars(source) else source
    return unescapeJava(toUnescape)
  }
  fun encodeMultiAsSingleJava(arg: String): String =
      arg.lines().joinToString(",\n") { encodeSingleJava(it) }
  fun encodeMultiKotlin(arg: String, escapeLeadingWhitespace: EscapeLeadingWhitespace): String {
    val escapeDollars = arg.replace("$", KOTLIN_DOLLAR)
    val escapeTripleQuotes =
        escapeDollars.replace(
            TRIPLE_QUOTE, "$KOTLIN_DOLLARQUOTE$KOTLIN_DOLLARQUOTE$KOTLIN_DOLLARQUOTE")
    val protectWhitespace =
        escapeTripleQuotes.lines().joinToString("\n") { line ->
          val protectTrailingWhitespace =
              if (line.endsWith(" ")) {
                line.dropLast(1) + "\${' '}"
              } else if (line.endsWith("\t")) {
                line.dropLast(1) + "\${'\\t'}"
              } else line
          escapeLeadingWhitespace.escapeLine(protectTrailingWhitespace, "\${' '}", "\${'\\t'}")
        }
    return "$TRIPLE_QUOTE$protectWhitespace$TRIPLE_QUOTE"
  }
  fun encodeMultiJava(arg: String, escapeLeadingWhitespace: EscapeLeadingWhitespace): String {
    val escapeBackslashes = arg.replace("\\", "\\\\")
    val escapeTripleQuotes = escapeBackslashes.replace(TRIPLE_QUOTE, "\\\"\\\"\\\"")
    var protectWhitespace =
        escapeTripleQuotes.lines().joinToString("\n") { line ->
          val protectTrailingWhitespace =
              if (line.endsWith(" ")) {
                line.dropLast(1) + "\\s"
              } else if (line.endsWith("\t")) {
                line.dropLast(1) + "\\t"
              } else line
          escapeLeadingWhitespace.escapeLine(protectTrailingWhitespace, "\\s", "\\t")
        }
    val commonPrefix =
        protectWhitespace
            .lines()
            .mapNotNull { line ->
              if (line.isNotBlank()) line.takeWhile { it.isWhitespace() } else null
            }
            .minOrNull() ?: ""
    if (commonPrefix.isNotEmpty()) {
      val lines = protectWhitespace.lines()
      val last = lines.last()
      protectWhitespace =
          lines.joinToString("\n") { line ->
            if (line === last) {
              if (line.startsWith(" ")) "\\s${line.drop(1)}"
              else if (line.startsWith("\t")) "\\t${line.drop(1)}"
              else
                  throw UnsupportedOperationException(
                      "How did it end up with a common whitespace prefix?")
            } else line
          }
    }
    return "$TRIPLE_QUOTE\n$protectWhitespace$TRIPLE_QUOTE"
  }
  private val charLiteralRegex = """\$\{'(\\?.)'\}""".toRegex()
  private fun inlineDollars(source: String): String {
    if (source.indexOf('$') == -1) {
      return source
    }
    return charLiteralRegex.replace(source) { matchResult ->
      val charLiteral = matchResult.groupValues[1]
      when {
        charLiteral.length == 1 -> charLiteral
        charLiteral.length == 2 && charLiteral[0] == '\\' ->
            when (charLiteral[1]) {
              't' -> "\t"
              'b' -> "\b"
              'n' -> "\n"
              'r' -> "\r"
              '\'' -> "'"
              '\\' -> "\\"
              '$' -> "$"
              else -> charLiteral
            }
        else -> throw IllegalArgumentException("Unknown character literal $charLiteral")
      }
    }
  }
  private fun unescapeJava(source: String): String {
    val firstEscape = source.indexOf('\\')
    if (firstEscape == -1) {
      return source
    }
    val value = StringBuilder()
    value.append(source.substring(0, firstEscape))
    var i = firstEscape
    while (i < source.length) {
      var c = source[i]
      if (c == '\\') {
        i++
        c = source[i]
        when (c) {
          '\"' -> value.append('\"')
          '\\' -> value.append('\\')
          'b' -> value.append('\b')
          'f' -> value.append('\u000c')
          'n' -> value.append('\n')
          'r' -> value.append('\r')
          's' -> value.append(' ')
          't' -> value.append('\t')
          'u' -> {
            val code = source.substring(i + 1, i + 5).toInt(16)
            value.append(code.toChar())
            i += 4
          }
          else -> throw IllegalArgumentException("Unknown escape sequence $c")
        }
      } else {
        value.append(c)
      }
      i++
    }
    return value.toString()
  }
  fun parseMultiJava(sourceWithQuotes: String): String {
    check(sourceWithQuotes.startsWith("$TRIPLE_QUOTE\n"))
    check(sourceWithQuotes.endsWith(TRIPLE_QUOTE))
    val source =
        sourceWithQuotes.substring(
            TRIPLE_QUOTE.length + 1, sourceWithQuotes.length - TRIPLE_QUOTE.length)
    val lines = source.lines()
    val commonPrefix =
        lines
            .mapNotNull { line ->
              if (line.isNotBlank()) line.takeWhile { it.isWhitespace() } else null
            }
            .minOrNull() ?: ""
    return lines.joinToString("\n") { line ->
      if (line.isBlank()) {
        ""
      } else {
        val removedPrefix = if (commonPrefix.isEmpty()) line else line.removePrefix(commonPrefix)
        val removeTrailingWhitespace = removedPrefix.trimEnd()
        val handleEscapeSequences = unescapeJava(removeTrailingWhitespace)
        handleEscapeSequences
      }
    }
  }
  fun parseMultiKotlin(sourceWithQuotes: String): String {
    check(sourceWithQuotes.startsWith(TRIPLE_QUOTE))
    check(sourceWithQuotes.endsWith(TRIPLE_QUOTE))
    val source =
        sourceWithQuotes.substring(
            TRIPLE_QUOTE.length, sourceWithQuotes.length - TRIPLE_QUOTE.length)
    return inlineDollars(source)
  }
}

internal object LiteralBoolean : LiteralFormat() {
  override fun encode(
      value: Boolean,
      language: Language,
      encodingPolicy: EscapeLeadingWhitespace
  ): String {
    return value.toString()
  }
  override fun parse(str: String, language: Language): Boolean {
    return str.toBooleanStrict()
  }
}

/** Some kinds of _TODO don't change the argument at all. */
enum class TodoStub {
  toMatchDisk,
  toBeFile;
  fun createLiteral() = LiteralValue(null, this, LiteralTodoStub)
}

internal object LiteralTodoStub : LiteralFormat() {
  override fun encode(
      value: TodoStub,
      language: Language,
      encodingPolicy: EscapeLeadingWhitespace
  ) = throw UnsupportedOperationException()
  override fun parse(str: String, language: Language) = throw UnsupportedOperationException()
  fun createLiteral(kind: TodoStub) = LiteralValue(null, kind, LiteralTodoStub)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy