Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve output of AttributeSpec to accept CodeBlocks. #91

Merged
merged 1 commit into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/main/java/io/outfoxx/swiftpoet/AttributeSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ class AttributeSpec internal constructor(
out.emitCode(identifier)
if (arguments.isNotEmpty()) {
out.emit("(")
out.emit(arguments.joinToString())
out.emitCode(
codeBlock = arguments.joinToCode(),
isConstantContext = true,
)
out.emit(")")
}
return out
Expand All @@ -37,18 +40,22 @@ class AttributeSpec internal constructor(
class Builder internal constructor(
val identifier: CodeBlock
) : Taggable.Builder<Builder>() {
internal val arguments = mutableListOf<String>()
internal val arguments = mutableListOf<CodeBlock>()

fun addArgument(code: String): Builder = apply {
arguments += CodeBlock.of(code)
}

fun addArgument(code: CodeBlock): Builder = apply {
arguments += code
}

fun addArguments(codes: List<String>): Builder = apply {
arguments += codes
arguments += codes.map(CodeBlock.Companion::of)
}

fun addArguments(vararg codes: String): Builder = apply {
arguments += codes
arguments += codes.map(CodeBlock.Companion::of)
}

fun build(): AttributeSpec =
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/io/outfoxx/swiftpoet/CodeWriter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,16 @@ internal class CodeWriter constructor(

fun emitCode(format: String, vararg args: Any?) = emitCode(CodeBlock.of(format, *args))

fun emitCode(codeBlock: CodeBlock) = apply {
fun emitCode(
codeBlock: CodeBlock,
isConstantContext: Boolean = false,
dnkoutso marked this conversation as resolved.
Show resolved Hide resolved
) = apply {
var a = 0
val partIterator = codeBlock.formatParts.listIterator()
while (partIterator.hasNext()) {
val part = partIterator.next()
when (part) {
"%L" -> emitLiteral(codeBlock.args[a++])
"%L" -> emitLiteral(codeBlock.args[a++], isConstantContext)

"%N" -> emit(codeBlock.args[a++] as String)

Expand All @@ -213,7 +216,11 @@ internal class CodeWriter constructor(
// Emit null as a literal null: no quotes.
emit(
if (string != null)
stringLiteralWithQuotes(string) else
stringLiteralWithQuotes(
string,
isInsideRawString = false,
isConstantContext = isConstantContext,
) else
"null"
)
}
Expand Down Expand Up @@ -253,11 +260,11 @@ internal class CodeWriter constructor(
out.wrappingSpace(indentLevel + 2)
}

private fun emitLiteral(o: Any?) {
private fun emitLiteral(o: Any?, isConstantContext: Boolean) {
when (o) {
is AnyTypeSpec -> o.emit(this)
is PropertySpec -> o.emit(this, emptySet())
is CodeBlock -> emitCode(o)
is CodeBlock -> emitCode(o, isConstantContext = isConstantContext)
else -> emit(o.toString())
}
}
Expand Down
18 changes: 10 additions & 8 deletions src/main/java/io/outfoxx/swiftpoet/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ internal fun <T> Collection<T>.containsAnyOf(vararg t: T) = t.any(this::contains

// see https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.10.6
internal fun characterLiteralWithoutSingleQuotes(c: Char) = when {
c == '\b' -> "\\b" // \u0008: backspace (BS)
c == '\t' -> "\\t" // \u0009: horizontal tab (HT)
c == '\n' -> "\\n" // \u000a: linefeed (LF)
c == '\r' -> "\\r" // \u000d: carriage return (CR)
c == '\"' -> "\"" // \u0022: double quote (")
c == '\'' -> "\\'" // \u0027: single quote (')
c == '\\' -> "\\\\" // \u005c: backslash (\)
c.isIsoControl -> String.format("\\u%04x", c.code)
c == '\b' -> "\\u{8}" // \u{0008}: backspace (BS)
c == '\t' -> "\\t" // \u{0009}: horizontal tab (HT)
c == '\n' -> "\\n" // \u{000a}: linefeed (LF)
c == '\r' -> "\\r" // \u{000d}: carriage return (CR)
c == '\"' -> "\\\"" // \u{0022}: double quote (")
c == '\'' -> "\\'" // \u{0027}: single quote (')
c == '\\' -> "\\\\" // \u{005c}: backslash (\)
c.isIsoControl -> String.format("\\u{%x}", c.code)
else -> Character.toString(c)
}

Expand Down Expand Up @@ -113,6 +113,8 @@ internal fun stringLiteralWithQuotes(
return result.toString()
} else {
val result = StringBuilder(value.length + 32)
// Using pre-formatted strings allows us to get away with not escaping symbols that would
// normally require escaping, e.g. "foo ${"bar"} baz".
if (isInsideRawString) result.append("\"\"\"") else result.append('"')
for (c in value) {
// Trivial case: single quote must not be escaped.
Expand Down
63 changes: 63 additions & 0 deletions src/test/java/io/outfoxx/swiftpoet/test/AttributeSpecTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2018 Outfox, Inc.
dnkoutso marked this conversation as resolved.
Show resolved Hide resolved
*
* 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 io.outfoxx.swiftpoet.test

import io.outfoxx.swiftpoet.AttributeSpec
import io.outfoxx.swiftpoet.CodeBlock
import io.outfoxx.swiftpoet.CodeWriter
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import java.io.StringWriter

@DisplayName("AttributeSpec Tests")
class AttributeSpecTests {
@Test
@DisplayName("Generates simple attribute")
fun testSimpleAttribute() {
val testAttr =
AttributeSpec.builder("objc")
.build()

val out = StringWriter()
testAttr.emit(CodeWriter(out))

assertThat(
out.toString(),
equalTo("@objc")
)
}

@Test
@DisplayName("Generates attribute with argument")
fun testAttributeWithArgument() {
val value = CodeBlock.of("%S", "someValue")
val testAttr =
AttributeSpec.builder("CustomAttribute")
.addArgument(CodeBlock.of("value: %L", value))
.build()

val out = StringWriter()
testAttr.emit(CodeWriter(out))

assertThat(
out.toString(),
equalTo("@CustomAttribute(value: \"someValue\")")
)
}
}
85 changes: 85 additions & 0 deletions src/test/java/io/outfoxx/swiftpoet/test/UtilTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2018 Outfox, Inc.
dnkoutso marked this conversation as resolved.
Show resolved Hide resolved
*
* 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 io.outfoxx.swiftpoet.test

import io.outfoxx.swiftpoet.characterLiteralWithoutSingleQuotes
import io.outfoxx.swiftpoet.escapeIfNecessary
import io.outfoxx.swiftpoet.stringLiteralWithQuotes
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class UtilTests {
@Test fun characterLiteral() {
assertEquals("a", characterLiteralWithoutSingleQuotes('a'))
assertEquals("b", characterLiteralWithoutSingleQuotes('b'))
assertEquals("c", characterLiteralWithoutSingleQuotes('c'))
assertEquals("%", characterLiteralWithoutSingleQuotes('%'))
// common escapes
assertEquals("\\u{8}", characterLiteralWithoutSingleQuotes('\b'))
assertEquals("\\t", characterLiteralWithoutSingleQuotes('\t'))
assertEquals("\\n", characterLiteralWithoutSingleQuotes('\n'))
assertEquals("\\u{c}", characterLiteralWithoutSingleQuotes('\u000c'))
assertEquals("\\r", characterLiteralWithoutSingleQuotes('\r'))
assertEquals("\\\"", characterLiteralWithoutSingleQuotes('"'))
assertEquals("\\'", characterLiteralWithoutSingleQuotes('\''))
assertEquals("\\\\", characterLiteralWithoutSingleQuotes('\\'))
// octal escapes
assertEquals("\\u{0}", characterLiteralWithoutSingleQuotes('\u0000'))
assertEquals("\\u{7}", characterLiteralWithoutSingleQuotes('\u0007'))
assertEquals("?", characterLiteralWithoutSingleQuotes('\u003f'))
assertEquals("\\u{7f}", characterLiteralWithoutSingleQuotes('\u007f'))
assertEquals("¿", characterLiteralWithoutSingleQuotes('\u00bf'))
assertEquals("ÿ", characterLiteralWithoutSingleQuotes('\u00ff'))
// unicode escapes
assertEquals("\\u{0}", characterLiteralWithoutSingleQuotes('\u0000'))
assertEquals("\\u{1}", characterLiteralWithoutSingleQuotes('\u0001'))
assertEquals("\\u{2}", characterLiteralWithoutSingleQuotes('\u0002'))
assertEquals("€", characterLiteralWithoutSingleQuotes('\u20AC'))
assertEquals("☃", characterLiteralWithoutSingleQuotes('\u2603'))
assertEquals("♠", characterLiteralWithoutSingleQuotes('\u2660'))
assertEquals("♣", characterLiteralWithoutSingleQuotes('\u2663'))
assertEquals("♥", characterLiteralWithoutSingleQuotes('\u2665'))
assertEquals("♦", characterLiteralWithoutSingleQuotes('\u2666'))
assertEquals("✵", characterLiteralWithoutSingleQuotes('\u2735'))
assertEquals("✺", characterLiteralWithoutSingleQuotes('\u273A'))
assertEquals("/", characterLiteralWithoutSingleQuotes('\uFF0F'))
}

@Test fun stringLiteral() {
stringLiteral("abc")
stringLiteral("♦♥♠♣")
stringLiteral("€\\t@\\t\${\'\$\'}", "€\t@\t$")
assertThat(stringLiteralWithQuotes("abc();\ndef();"), equalTo("\"\"\"\n|abc();\n|def();\n\"\"\".trimMargin()"))
stringLiteral("This is \\\"quoted\\\"!", "This is \"quoted\"!")
stringLiteral("😀", "😀")
stringLiteral("e^{i\\\\pi}+1=0", "e^{i\\pi}+1=0")
assertThat(stringLiteralWithQuotes("abc();\ndef();", isConstantContext = true), equalTo("\"abc();\\ndef();\""))
}

@Test fun escapeNonJavaIdentifiers() {
assertThat(escapeIfNecessary("8startWithNumber"), equalTo("`8startWithNumber`"))
assertThat(escapeIfNecessary("with-hyphen"), equalTo("`with-hyphen`"))
assertThat(escapeIfNecessary("with space"), equalTo("`with space`"))
assertThat(escapeIfNecessary("with_unicode_punctuation\\u2026"), equalTo("`with_unicode_punctuation\\u2026`"))
}

private fun stringLiteral(string: String) = stringLiteral(string, string)
private fun stringLiteral(expected: String, value: String) =
assertEquals("\"$expected\"", stringLiteralWithQuotes(value))
}