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

Fix bug with inline comment #168

Merged
merged 4 commits into from
Dec 13, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,6 @@ package com.akuleshov7.ktoml.parsers

import com.akuleshov7.ktoml.exceptions.ParseException

/**
* Takes only the text before a comment, searching for a comment after the specified
* [startIndex].
*
* @param startIndex The index to start the comment search from.
* @return The text before a comment, i.e.
* ```kotlin
* "a = 0 # Comment".takeBeforeComment() == "a = 0"
* ```
*/
internal fun String.takeBeforeComment(startIndex: Int) =
when (val hashIndex = indexOf('#', startIndex)) {
-1 -> trim()
else -> take(hashIndex).trim()
}

/**
* Trims a comment of any text before it and its hash token.
*
* @return The comment text, i.e.
* ```kotlin
* "a = 0 # Comment".trimComment() == "Comment"
* ```
*/
internal fun String.trimComment() =
when (val hashIndex = indexOf('#')) {
-1 -> ""
else -> drop(hashIndex + 1).trim()
}

/**
* Splitting dot-separated string to the list of tokens:
* a.b.c -> [a, b, c]; a."b.c".d -> [a, "b.c", d];
Expand Down Expand Up @@ -120,6 +90,44 @@ internal fun String.trimBrackets(): String = trimSymbols(this, "[", "]")
*/
internal fun String.trimDoubleBrackets(): String = trimSymbols(this, "[[", "]]")

/**
* Takes only the text before a comment
*
* @param allowEscapedQuotesInLiteralStrings value from TomlInputConfig
* @return The text before a comment, i.e.
* ```kotlin
* "a = 0 # Comment".takeBeforeComment() == "a = 0"
* ```
*/
internal fun String.takeBeforeComment(allowEscapedQuotesInLiteralStrings: Boolean): String {
val commentStartIndex = getCommentStartIndex(allowEscapedQuotesInLiteralStrings)

return if (commentStartIndex == -1) {
this.trim()
} else {
this.substring(0, commentStartIndex).trim()
}
}

/**
* Trims a comment of any text before it and its hash token.
*
* @param allowEscapedQuotesInLiteralStrings value from TomlInputConfig
* @return The comment text, i.e.
* ```kotlin
* "a = 0 # Comment".trimComment() == "Comment"
* ```
*/
internal fun String.trimComment(allowEscapedQuotesInLiteralStrings: Boolean): String {
val commentStartIndex = getCommentStartIndex(allowEscapedQuotesInLiteralStrings)

return if (commentStartIndex == -1) {
""
} else {
drop(commentStartIndex + 1).trim()
}
}

private fun String.validateSpaces(lineNo: Int, fullKey: String) {
if (this.trim().count { it == ' ' } > 0 && this.isNotQuoted()) {
throw ParseException(
Expand Down Expand Up @@ -167,6 +175,41 @@ private fun String.validateSymbols(lineNo: Int) {
}
}

private fun String.getCommentStartIndex(allowEscapedQuotesInLiteralStrings: Boolean): Int {
val isEscapingDisabled = if (allowEscapedQuotesInLiteralStrings) {
// escaping is disabled when the config option is true AND we have a literal string
val firstQuoteLetter = this.firstOrNull { it == '\"' || it == '\'' }
firstQuoteLetter == '\''
} else {
false
}

val chars = if (!isEscapingDisabled) {
this.replace("\\\"", "__")
.replace("\\\'", "__")
} else {
this
}.toCharArray()
var currentQuoteChar: Char? = null

chars.forEachIndexed { idx, symbol ->
// take hash index if it's not enclosed in quotation marks
if (symbol == '#' && currentQuoteChar == null) {
return idx
}

if (symbol == '\"' || symbol == '\'') {
if (currentQuoteChar == null) {
currentQuoteChar = symbol
} else if (currentQuoteChar == symbol) {
currentQuoteChar = null
}
}
}

return -1
}

private fun Char.isLetterOrDigit() = CharRange('A', 'Z').contains(this) ||
CharRange('a', 'z').contains(this) ||
CharRange('0', '9').contains(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ public value class TomlParser(private val config: TomlInputConfig) {
// comments and empty lines can easily be ignored in the TomlTree, but we cannot filter them out in mutableTomlLines
// because we need to calculate and save lineNo
if (line.isComment()) {
comments += line.trimComment()
comments += line.trimComment(config.allowEscapedQuotesInLiteralStrings)
} else if (!line.isEmptyLine()) {
// Parse the inline comment if any
val inlineComment = line.trimComment()
val inlineComment = line.trimComment(config.allowEscapedQuotesInLiteralStrings)

if (line.isTableNode()) {
if (line.isArrayOfTables()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,7 @@ public fun String.parseList(lineNo: Int, config: TomlInputConfig): TomlArray = T
* @throws ParseException
*/
public fun String.splitKeyValue(lineNo: Int, config: TomlInputConfig = TomlInputConfig()): Pair<String, String> {
// finding the index of the last quote, if no quotes are found, then use the length of the string
val closingQuoteIndex = listOf(
this.lastIndexOf("\""),
this.lastIndexOf("\'"),
this.lastIndexOf("\"\"\"")
).filterNot { it == -1 }.maxOrNull() ?: 0

// removing the commented part of the string
// search starts goes from the closingQuoteIndex to the end of the string
val pair = takeBeforeComment(closingQuoteIndex)
val pair = takeBeforeComment(config.allowEscapedQuotesInLiteralStrings)

// searching for an equals sign that should be placed main part of the string (not in the comment)
val firstEqualsSign = pair.indexOfFirst { it == '=' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ public class TomlArrayOfTables(
throw ParseException("Invalid Array of Tables provided: $content." +
" It has missing closing brackets: ']]'", lineNo)
}

// getting the content inside brackets ([a.b] -> a.b)
val sectionFromContent = content.takeBeforeComment(lastIndexOfBrace).trimDoubleBrackets()
val sectionFromContent = content
.takeBeforeComment(config.allowEscapedQuotesInLiteralStrings)
.trimDoubleBrackets()
.trim()

if (sectionFromContent.isBlank()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ public class TomlTablePrimitive(
throw ParseException("Invalid Tables provided: $content." +
" It has missing closing bracket: ']'", lineNo)
}

// getting the content inside brackets ([a.b] -> a.b)
val sectionFromContent = content.takeBeforeComment(lastIndexOfBrace).trim().trimBrackets()
val sectionFromContent = content
.takeBeforeComment(config.allowEscapedQuotesInLiteralStrings)
.trimBrackets()
.trim()

if (sectionFromContent.isBlank()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.akuleshov7.ktoml.parsers

import kotlin.test.Test
import kotlin.test.assertEquals

class StringUtilsTest {

@Test
fun testForTakeBeforeComment() {
var lineWithoutComment = "test_key = \"test_value\" # \" some comment".takeBeforeComment(false)
assertEquals("test_key = \"test_value\"", lineWithoutComment)

lineWithoutComment = "key = \"\"\"value\"\"\" # \"".takeBeforeComment(false)
assertEquals("key = \"\"\"value\"\"\"", lineWithoutComment)

lineWithoutComment = "key = 123 # \"\"\"abc".takeBeforeComment(false)
assertEquals("key = 123", lineWithoutComment)

lineWithoutComment = "key = \"ab\\\"#cdef\"#123".takeBeforeComment(false)
assertEquals("key = \"ab\\\"#cdef\"", lineWithoutComment)

lineWithoutComment = " \t#123".takeBeforeComment(false)
assertEquals("", lineWithoutComment)

lineWithoutComment = "key = \"ab\'c\" # ".takeBeforeComment(false)
assertEquals("key = \"ab\'c\"", lineWithoutComment)

lineWithoutComment = """
a = 'C:\some\path\' #\abc
""".trimIndent().takeBeforeComment(true)
assertEquals("""a = 'C:\some\path\'""", lineWithoutComment)
}

@Test
fun testForTrimComment() {
var comment = "a = \"here#hash\" # my comment".trimComment(false)
assertEquals("my comment", comment)

comment = "a = \"here#\\\"hash\" # my comment".trimComment(false)
assertEquals("my comment", comment)

comment = " # my comment".trimComment(false)
assertEquals("my comment", comment)

comment = """
a = 'C:\some\path\' #\abc
""".trimIndent().trimComment(true)
assertEquals("\\abc", comment)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ class ValueParserTest {
assertEquals("1 = 2", TomlKeyValuePrimitive(pairTest, 0).value.content)
}

@Test
fun symbolsAfterComment() {
val keyValue = "test_key = \"test_value\" # \" some comment".splitKeyValue(0)
assertEquals("test_value", TomlKeyValuePrimitive(keyValue, 0).value.content)
}

@Test
fun parsingIssueValue() {
assertFailsWith<ParseException> { " = false".splitKeyValue(0) }
Expand Down