diff --git a/kotlinpoet/src/jvmMain/kotlin/com/squareup/kotlinpoet/KModifier.kt b/kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/KModifier.kt similarity index 95% rename from kotlinpoet/src/jvmMain/kotlin/com/squareup/kotlinpoet/KModifier.kt rename to kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/KModifier.kt index 81d03570f..0c0ad73fd 100644 --- a/kotlinpoet/src/jvmMain/kotlin/com/squareup/kotlinpoet/KModifier.kt +++ b/kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/KModifier.kt @@ -19,7 +19,6 @@ import com.squareup.kotlinpoet.KModifier.INTERNAL import com.squareup.kotlinpoet.KModifier.PRIVATE import com.squareup.kotlinpoet.KModifier.PROTECTED import com.squareup.kotlinpoet.KModifier.PUBLIC -import java.util.EnumSet public enum class KModifier( internal val keyword: String, @@ -89,4 +88,4 @@ public enum class KModifier( } } -internal val VISIBILITY_MODIFIERS: Set = EnumSet.of(PUBLIC, INTERNAL, PROTECTED, PRIVATE) +internal val VISIBILITY_MODIFIERS: Set = setOf(PUBLIC, INTERNAL, PROTECTED, PRIVATE) diff --git a/kotlinpoet/src/jvmMain/kotlin/com/squareup/kotlinpoet/Util.kt b/kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/Util.kt similarity index 83% rename from kotlinpoet/src/jvmMain/kotlin/com/squareup/kotlinpoet/Util.kt rename to kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/Util.kt index 53257db02..e500d66d2 100644 --- a/kotlinpoet/src/jvmMain/kotlin/com/squareup/kotlinpoet/Util.kt +++ b/kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/Util.kt @@ -15,23 +15,17 @@ */ package com.squareup.kotlinpoet -import com.squareup.kotlinpoet.CodeBlock.Companion.isPlaceholder -import java.util.Collections - internal object NullAppendable : Appendable { - override fun append(charSequence: CharSequence) = this - override fun append(charSequence: CharSequence, start: Int, end: Int) = this - override fun append(c: Char) = this + override fun append(value: CharSequence?) = this + override fun append(value: CharSequence?, startIndex: Int, endIndex: Int) = this + override fun append(value: Char) = this } -internal fun Map.toImmutableMap(): Map = - Collections.unmodifiableMap(LinkedHashMap(this)) +internal expect fun Map.toImmutableMap(): Map -internal fun Collection.toImmutableList(): List = - Collections.unmodifiableList(ArrayList(this)) +internal expect fun Collection.toImmutableList(): List -internal fun Collection.toImmutableSet(): Set = - Collections.unmodifiableSet(LinkedHashSet(this)) +internal expect fun Collection.toImmutableSet(): Set internal inline fun > Collection.toEnumSet(): Set = enumValues().filterTo(mutableSetOf(), this::contains) @@ -63,10 +57,16 @@ internal fun characterLiteralWithoutSingleQuotes(c: Char) = when { c == '\"' -> "\"" // \u0022: double quote (") c == '\'' -> "\\'" // \u0027: single quote (') c == '\\' -> "\\\\" // \u005c: backslash (\) - c.isIsoControl -> String.format("\\u%04x", c.code) + c.isIsoControl -> formatIsoControlCode(c.code) else -> c.toString() } +internal fun formatIsoControlCode(code: Int): String = + "\\u${code.toHexStr().padStart(4, '0')}" + +internal fun Int.toHexStr(): String = + toUInt().toString(16) + internal fun escapeCharacterLiterals(s: String) = buildString { for (c in s) append(characterLiteralWithoutSingleQuotes(c)) } @@ -138,34 +138,27 @@ internal fun stringLiteralWithQuotes( } } -internal fun CodeBlock.ensureEndsWithNewLine() = trimTrailingNewLine('\n') - -internal fun CodeBlock.trimTrailingNewLine(replaceWith: Char? = null) = if (isEmpty()) { - this -} else { - with(toBuilder()) { - val lastFormatPart = trim().formatParts.last() - if (lastFormatPart.isPlaceholder && args.isNotEmpty()) { - val lastArg = args.last() - if (lastArg is String) { - val trimmedArg = lastArg.trimEnd('\n') - args[args.size - 1] = if (replaceWith != null) { - trimmedArg + replaceWith - } else { - trimmedArg - } - } - } else { - formatParts[formatParts.lastIndexOf(lastFormatPart)] = lastFormatPart.trimEnd('\n') - if (replaceWith != null) { - formatParts += "$replaceWith" - } - } - return@with build() - } -} +// TODO Waiting for `CodeBlock` migration. +// internal fun CodeBlock.ensureEndsWithNewLine() -private val IDENTIFIER_REGEX = +// TODO Waiting for `CodeBlock` migration. +// internal fun CodeBlock.trimTrailingNewLine(replaceWith: Char? = null) + +/** + * Will crash if used `IDENTIFIER_REGEX_VALUE.toRegex()` directly in WasmJs: + * `PatternSyntaxException: No such character class`. + * + * It works in JS and JVM. + * + * For now: + * - Keep the use of `Regex` in JVM and JS. + * - And use `RegExp` directly in WasmJs for matching, + * using it in a similar way as in JS. + * + * See also: [KT-71003](https://youtrack.jetbrains.com/issue/KT-71003) + */ +internal const val IDENTIFIER_REGEX_VALUE = + // language=regexp ( "((\\p{gc=Lu}+|\\p{gc=Ll}+|\\p{gc=Lt}+|\\p{gc=Lm}+|\\p{gc=Lo}+|\\p{gc=Nl}+)+" + "\\d*" + @@ -173,9 +166,8 @@ private val IDENTIFIER_REGEX = "|" + "(`[^\n\r`]+`)" ) - .toRegex() -internal val String.isIdentifier get() = IDENTIFIER_REGEX.matches(this) +internal expect val String.isIdentifier: Boolean // https://kotlinlang.org/docs/reference/keyword-reference.html internal val KEYWORDS = setOf( @@ -317,7 +309,7 @@ internal fun String.escapeAsAlias(validate: Boolean = true): String { val newAlias = StringBuilder("") - if (!Character.isJavaIdentifierStart(first())) { + if (!first().isJavaIdentifierStart()) { newAlias.append('_') } @@ -327,8 +319,8 @@ internal fun String.escapeAsAlias(validate: Boolean = true): String { continue } - if (!Character.isJavaIdentifierPart(ch)) { - newAlias.append("_U").append(Integer.toHexString(ch.code).padStart(4, '0')) + if (!ch.isJavaIdentifierPart()) { + newAlias.append("_U").append(ch.code.toHexStr().padStart(4, '0')) continue } @@ -348,8 +340,8 @@ private fun String.escapeIfAllCharactersAreUnderscore() = if (allCharactersAreUn private fun String.escapeIfNotJavaIdentifier(): String { return if (( - !Character.isJavaIdentifierStart(first()) || - drop(1).any { !Character.isJavaIdentifierPart(it) } + !first().isJavaIdentifierStart() || + drop(1).any { !it.isJavaIdentifierPart() } ) && !alreadyEscaped() ) { @@ -362,3 +354,7 @@ private fun String.escapeIfNotJavaIdentifier(): String { internal fun String.escapeSegmentsIfNecessary(delimiter: Char = '.') = split(delimiter) .filter { it.isNotEmpty() } .joinToString(delimiter.toString()) { it.escapeIfNecessary() } + +internal expect fun Char.isJavaIdentifierStart(): Boolean + +internal expect fun Char.isJavaIdentifierPart(): Boolean diff --git a/kotlinpoet/src/jsMain/kotlin/com/squareup/kotlinpoet/Util.js.kt b/kotlinpoet/src/jsMain/kotlin/com/squareup/kotlinpoet/Util.js.kt new file mode 100644 index 000000000..b2cc2dd49 --- /dev/null +++ b/kotlinpoet/src/jsMain/kotlin/com/squareup/kotlinpoet/Util.js.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * 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.squareup.kotlinpoet + +private val IDENTIFIER_REGEX = IDENTIFIER_REGEX_VALUE.toRegex() + +internal actual val String.isIdentifier: Boolean get() = IDENTIFIER_REGEX.matches(this) diff --git a/kotlinpoet/src/jvmMain/kotlin/com/squareup/kotlinpoet/Util.jvm.kt b/kotlinpoet/src/jvmMain/kotlin/com/squareup/kotlinpoet/Util.jvm.kt new file mode 100644 index 000000000..aa3cf8a2c --- /dev/null +++ b/kotlinpoet/src/jvmMain/kotlin/com/squareup/kotlinpoet/Util.jvm.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * 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.squareup.kotlinpoet + +import com.squareup.kotlinpoet.CodeBlock.Companion.isPlaceholder +import java.util.Collections + +internal actual fun Map.toImmutableMap(): Map = + Collections.unmodifiableMap(LinkedHashMap(this)) + +internal actual fun Collection.toImmutableList(): List = + Collections.unmodifiableList(ArrayList(this)) + +internal actual fun Collection.toImmutableSet(): Set = + Collections.unmodifiableSet(LinkedHashSet(this)) + +// TODO Waiting for `CodeBlock` migration. +internal fun CodeBlock.ensureEndsWithNewLine() = trimTrailingNewLine('\n') + +// TODO Waiting for `CodeBlock` migration. +internal fun CodeBlock.trimTrailingNewLine(replaceWith: Char? = null) = if (isEmpty()) { + this +} else { + with(toBuilder()) { + val lastFormatPart = trim().formatParts.last() + if (lastFormatPart.isPlaceholder && args.isNotEmpty()) { + val lastArg = args.last() + if (lastArg is String) { + val trimmedArg = lastArg.trimEnd('\n') + args[args.size - 1] = if (replaceWith != null) { + trimmedArg + replaceWith + } else { + trimmedArg + } + } + } else { + formatParts[formatParts.lastIndexOf(lastFormatPart)] = lastFormatPart.trimEnd('\n') + if (replaceWith != null) { + formatParts += "$replaceWith" + } + } + return@with build() + } +} + +private val IDENTIFIER_REGEX = IDENTIFIER_REGEX_VALUE.toRegex() + +internal actual val String.isIdentifier: Boolean + get() = IDENTIFIER_REGEX.matches(this) + +internal actual fun Char.isJavaIdentifierStart(): Boolean = + Character.isJavaIdentifierStart(this) + +internal actual fun Char.isJavaIdentifierPart(): Boolean = + Character.isJavaIdentifierPart(this) diff --git a/kotlinpoet/src/nonJvmMain/kotlin/com/squareup/kotlinpoet/Util.nonJvm.kt b/kotlinpoet/src/nonJvmMain/kotlin/com/squareup/kotlinpoet/Util.nonJvm.kt new file mode 100644 index 000000000..7e52926fa --- /dev/null +++ b/kotlinpoet/src/nonJvmMain/kotlin/com/squareup/kotlinpoet/Util.nonJvm.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * 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.squareup.kotlinpoet + +internal actual fun Map.toImmutableMap(): Map = + toMap() + +internal actual fun Collection.toImmutableList(): List = + toList() + +internal actual fun Collection.toImmutableSet(): Set = + toSet() + +internal actual fun Char.isJavaIdentifierStart(): Boolean { + return isLetter() || + this in CharCategory.LETTER_NUMBER || + this == '$' || + this == '_' +} + +internal actual fun Char.isJavaIdentifierPart(): Boolean { + // TODO + // A character may be part of a Java identifier if any of the following conditions are true: + // - it is a letter + // - it is a currency symbol (such as '$') + // - it is a connecting punctuation character (such as '_') + // - it is a digit + // - it is a numeric letter (such as a Roman numeral character) + // - it is a combining mark + // - it is a non-spacing mark + // isIdentifierIgnorable returns true for the character. + // Also missing here: + // - a combining mark + return isLetter() || + isDigit() || + this in CharCategory.LETTER_NUMBER || + this in CharCategory.NON_SPACING_MARK || + this == '_' || + this == '$' || + isIdentifierIgnorable() + // +} + +internal fun Char.isIdentifierIgnorable(): Boolean { + // The following Unicode characters are ignorable in a Java identifier or a Unicode identifier: + // - ISO control characters that are not whitespace + // - '\u0000' through '\u0008' + // - '\u000E' through '\u001B' + // - '\u007F' through '\u009F' + // - all characters that have the FORMAT general category value + return ( + isISOControl() && ( + this in '\u0000'..'\u0008' || + this in '\u000E'..'\u001B' || + this in '\u007F'..'\u009F' + ) + ) || this in CharCategory.FORMAT +} diff --git a/kotlinpoet/src/wasmJsMain/kotlin/com/squareup/kotlinpoet/Util.wasmJs.kt b/kotlinpoet/src/wasmJsMain/kotlin/com/squareup/kotlinpoet/Util.wasmJs.kt new file mode 100644 index 000000000..81e2c2966 --- /dev/null +++ b/kotlinpoet/src/wasmJsMain/kotlin/com/squareup/kotlinpoet/Util.wasmJs.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * 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.squareup.kotlinpoet + +internal actual val String.isIdentifier: Boolean + get() { + val regExp = RegExp(IDENTIFIER_REGEX_VALUE, "gu") + regExp.reset() + + val match = regExp.exec(this) ?: return false + return match.index == 0 && regExp.lastIndex == length + } + +internal external interface RegExpMatch { + val index: Int + val length: Int +} + +internal external class RegExp(pattern: String, flags: String? = definedExternally) : JsAny { + fun exec(str: String): RegExpMatch? + override fun toString(): String + + var lastIndex: Int +} + +internal fun RegExp.reset() { + lastIndex = 0 +}