From 01a097bf6d4a9edeafd6f009189e6556fec9c6f5 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Tue, 18 Jul 2023 20:00:09 +0200 Subject: [PATCH] Apollo AST: add start/end instead of endColumn/endLine (#5103) * replace endLine/endColumn by start/end * update API dump * Update libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/SourceLocation.kt Co-authored-by: Benoit Lubek --------- Co-authored-by: Benoit Lubek --- libraries/apollo-ast/api/apollo-ast.api | 4 +- .../apollo3/ast/SourceLocation.kt | 35 +++- .../com/apollographql/apollo3/ast/api.kt | 51 +++++- .../com/apollographql/apollo3/ast/gql.kt | 2 +- .../apollo3/ast/internal/Lexer.kt | 163 ++++++++++-------- .../apollo3/ast/internal/LexerException.kt | 2 +- .../apollo3/ast/internal/Parser.kt | 10 +- .../apollo3/ast/internal/Token.kt | 48 +++--- .../introspection/introspection_to_schema.kt | 8 +- .../apollo3/graphql/ast/test/ParserTest.kt | 8 +- .../validation/OperationValidationTest.kt | 11 +- .../apollo3/ast/internal/antlrParse.kt | 15 +- .../apollo3/ast/internal/antlr_to_gql.kt | 4 +- 13 files changed, 231 insertions(+), 130 deletions(-) diff --git a/libraries/apollo-ast/api/apollo-ast.api b/libraries/apollo-ast/api/apollo-ast.api index 149f396dd5a..dfe3c1bcb5d 100644 --- a/libraries/apollo-ast/api/apollo-ast.api +++ b/libraries/apollo-ast/api/apollo-ast.api @@ -910,11 +910,11 @@ public final class com/apollographql/apollo3/ast/SourceLocation { public static final field Companion Lcom/apollographql/apollo3/ast/SourceLocation$Companion; public fun (IIIILjava/lang/String;)V public final fun getColumn ()I - public final fun getEndColumn ()I - public final fun getEndLine ()I + public final fun getEnd ()I public final fun getFilePath ()Ljava/lang/String; public final fun getLine ()I public final fun getPosition ()I + public final fun getStart ()I public final fun pretty ()Ljava/lang/String; public fun toString ()Ljava/lang/String; } diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/SourceLocation.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/SourceLocation.kt index 5f167d18760..3a90958d653 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/SourceLocation.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/SourceLocation.kt @@ -3,22 +3,26 @@ package com.apollographql.apollo3.ast import com.apollographql.apollo3.annotations.ApolloDeprecatedSince /** + * @param start the offset where the symbol starts, inclusive, starting at 0 + * Note that because the parser works on UTF16 Strings, the offset is in terms of UTF16 chars and not unicode Chars + * + * @param end the offset where the symbol ends, exclusive + * Because [end] is exclusive, you can use str.substring(start, end) to get the symbol text + * Note that because the parser works on UTF16 Strings, the offset is in terms of UTF16 chars and not unicode Chars + * * @param line the line where the symbol starts, starting at 1 * * @param column the column where the symbol starts, starting at 1 + * Note that because the parser works on UTF16 Strings, the column is in terms of UTF16 chars and not unicode Chars * - * @param endLine the line where the symbol ends, inclusive, starting at 1 - * - * @param endColumn the column where the symbol ends, inclusive, starting at 1 - * * * @param filePath The path to the document containing the node - * Might be null if the document origin is not known + * Might be null if the document origin is not known (parsing from a String for an example) */ class SourceLocation( + val start: Int, + val end: Int, val line: Int, val column: Int, - val endLine: Int, - val endColumn: Int, val filePath: String? ) { @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v4_0_0) @@ -35,7 +39,22 @@ class SourceLocation( companion object { @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v4_0_0) @Deprecated("SourceLocation is now nullable and this is replaced by null", ReplaceWith("null"), level = DeprecationLevel.ERROR) - val UNKNOWN = SourceLocation(-1, -1, -1, -1, null) + val UNKNOWN = SourceLocation.forPath(null) + + /** + * Constructs a [SourceLocation] that only contains a filePath for the moments when we're constructing nodes programmatically + * but still want to carry around the path of the original nodes for debugging purposes. + * TODO: I'm not sure how much this is helping vs confusing. We might want to remove that and just set a null sourceLocation in those cases + */ + internal fun forPath(filePath: String?): SourceLocation { + return SourceLocation( + start = 0, + end = 1, + line = -1, + column = -1, + filePath = filePath + ) + } } } diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/api.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/api.kt index 84ce9575f34..73750adb36e 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/api.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/api.kt @@ -27,25 +27,57 @@ fun BufferedSource.toSchema(filePath: String? = null): Schema = parseAsGQLDocume * See [parseAsGQLDocument] and [validateAsExecutable] for more granular error reporting */ @ApolloExperimental -fun BufferedSource.toExecutableDefinitions(schema: Schema, filePath: String? = null, fieldsOnDisjointTypesMustMerge: Boolean = true): List = parseAsGQLDocument(filePath) +fun BufferedSource.toExecutableDefinitions( + schema: Schema, + filePath: String? = null, + fieldsOnDisjointTypesMustMerge: Boolean = true, +): List = parseAsGQLDocument(filePath) .getOrThrow() .validateAsExecutable(schema, fieldsOnDisjointTypesMustMerge) .getOrThrow() -private fun BufferedSource.parseInternal(filePath: String?, withSourceLocation: Boolean, block: Parser.() -> T): GQLResult { +private fun BufferedSource.parseInternal(filePath: String?, withSourceLocation: Boolean, block: Parser.() -> T): GQLResult { return try { GQLResult(Parser(this.use { it.readUtf8() }, withSourceLocation, filePath).block(), emptyList()) } catch (e: ParserException) { - GQLResult(null, listOf(Issue.ParsingError(e.message, SourceLocation(e.token.line, e.token.column, -1, -1, filePath)))) + GQLResult( + null, + listOf( + Issue.ParsingError( + e.message, + SourceLocation( + start = e.token.start, + end = e.token.end, + line = e.token.line, + column = e.token.column, + filePath = filePath + ) + ) + ) + ) } catch (e: LexerException) { - GQLResult(null, listOf(Issue.ParsingError(e.message, SourceLocation(e.line, e.column, -1, -1, filePath)))) + GQLResult( + null, + listOf( + Issue.ParsingError( + e.message, + SourceLocation( + start = e.pos, + end = e.pos + 1, + line = e.line, + column = e.column, + filePath = filePath + ) + ) + ) + ) } } class ParserOptions( val useAntlr: Boolean = false, val allowEmptyDocuments: Boolean = true, - val withSourceLocation: Boolean = true + val withSourceLocation: Boolean = true, ) { companion object { val Default = ParserOptions() @@ -96,7 +128,7 @@ fun BufferedSource.parseAsGQLValue(filePath: String? = null, options: ParserOpti * Closes [BufferedSource] */ @ApolloExperimental -fun BufferedSource.parseAsGQLType(filePath: String? = null, options: ParserOptions = ParserOptions.Default): GQLResult { +fun BufferedSource.parseAsGQLType(filePath: String? = null, options: ParserOptions = ParserOptions.Default): GQLResult { return if (options.useAntlr) { parseTypeWithAntlr(this, filePath) } else { @@ -110,7 +142,10 @@ fun BufferedSource.parseAsGQLType(filePath: String? = null, options: ParserOptio * Closes [BufferedSource] */ @ApolloExperimental -fun BufferedSource.parseAsGQLSelections(filePath: String? = null, options: ParserOptions = ParserOptions.Default): GQLResult> { +fun BufferedSource.parseAsGQLSelections( + filePath: String? = null, + options: ParserOptions = ParserOptions.Default, +): GQLResult> { return if (options.useAntlr) { parseSelectionsWithAntlr(this, filePath) } else { @@ -164,7 +199,7 @@ fun GQLDocument.validateAsExecutable(schema: Schema, fieldsOnDisjointTypesMustMe fun GQLFragmentDefinition.inferVariables( schema: Schema, fragments: Map, - fieldsOnDisjointTypesMustMerge: Boolean + fieldsOnDisjointTypesMustMerge: Boolean, ) = ExecutableValidationScope(schema, fragments, fieldsOnDisjointTypesMustMerge).inferFragmentVariables(this) diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gql.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gql.kt index 16d54b833e9..6bd7354f7d6 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gql.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/gql.kt @@ -131,7 +131,7 @@ class GQLDocument( val definitions: List, override val sourceLocation: SourceLocation?, ) : GQLNode { - constructor(definitions: List, filePath: String?): this(definitions, SourceLocation(0, 0, -1, -1, filePath)) + constructor(definitions: List, filePath: String?): this(definitions, SourceLocation.forPath(filePath)) override val children = definitions diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Lexer.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Lexer.kt index a678f3e298e..16f16eba03d 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Lexer.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Lexer.kt @@ -1,22 +1,7 @@ package com.apollographql.apollo3.ast.internal -import okio.BufferedSource - /** - * A GraphQL lexer that emits [Token]s from a [BufferedSource] - * - * The source must contain utf-8 content. Other encodings are not supported. - * - * The code is inspired by the Moshi Json reader and works at the byte level. As most of GraphQL (names, alias, etc...) is plain ASCII - * this saves decoding utf-8 for a good chunk of the source. Because we need to track the position and line/column, we can't use the - * same logic as Moshi and create String directly from the bytes. Instead, we need to read each code point individually. - * - * The [Lexer] must be closed after use - * - * Throws - * - [LexerException] on malformed input - * - [okio.IOException] on I/O error - * - most likely [kotlin.IndexOutOfBoundsException] if trying to lex a large file + * A GraphQL lexer that emits [Token]s from a [String] */ internal class Lexer(val src: String) { /** @@ -115,28 +100,28 @@ internal class Lexer(val src: String) { discardComment() } - '!' -> return Token.ExclamationPoint(line, column(start)) - '$' -> return Token.Dollar(line, column(start)) - '&' -> return Token.Ampersand(line, column(start)) - '(' -> return Token.LeftParenthesis(line, column(start)) - ')' -> return Token.RightParenthesis(line, column(start)) + '!' -> return Token.ExclamationPoint(start, line, column(start)) + '$' -> return Token.Dollar(start, line, column(start)) + '&' -> return Token.Ampersand(start, line, column(start)) + '(' -> return Token.LeftParenthesis(start, line, column(start)) + ')' -> return Token.RightParenthesis(start, line, column(start)) '.' -> { if (pos + 1 < len && src[pos] == '.' && src[pos + 1] == '.') { pos += 2 - return Token.Spread(line, column(start)) + return Token.Spread(start, line, column(start)) } else { - throw LexerException("Unterminated spread operator", line, column(start), null) + throw LexerException("Unterminated spread operator", start, line, column(start), null) } } - ':' -> return Token.Colon(line, column(start)) - '=' -> return Token.Equals(line, column(start)) - '@' -> return Token.At(line, column(start)) - '[' -> return Token.LeftBracket(line, column(start)) - ']' -> return Token.RightBracket(line, column(start)) - '{' -> return Token.LeftBrace(line, column(start)) - '}' -> return Token.RightBrace(line, column(start)) - '|' -> return Token.Pipe(line, column(start)) + ':' -> return Token.Colon(start, line, column(start)) + '=' -> return Token.Equals(start, line, column(start)) + '@' -> return Token.At(start, line, column(start)) + '[' -> return Token.LeftBracket(start, line, column(start)) + ']' -> return Token.RightBracket(start, line, column(start)) + '{' -> return Token.LeftBrace(start, line, column(start)) + '}' -> return Token.RightBrace(start, line, column(start)) + '|' -> return Token.Pipe(start, line, column(start)) '"' -> { return if (pos + 1 < len && src[pos] == '"' && src[pos + 1] == '"') { pos += 2 @@ -147,18 +132,18 @@ internal class Lexer(val src: String) { } else -> { - throw LexerException("Unexpected symbol '${c}' (0x${c.code.toString(16)})", line, column(start), null) + throw LexerException("Unexpected symbol '${c}' (0x${c.code.toString(16)})", start, line, column(start), null) } } } - return Token.EndOfFile(line, column(pos)) + return Token.EndOfFile(pos, line, column(pos)) } // we are just after "\u" private fun readUnicodeEscape(): Int { if (pos == len) { - throw LexerException("Unterminated Unicode escape", line, column(pos), null) + throw LexerException("Unterminated Unicode escape", pos, line, column(pos), null) } when (src[pos]) { @@ -191,18 +176,18 @@ internal class Lexer(val src: String) { } } - throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", line, column(start), null) + throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", start, line, column(start), null) } } } private fun Int.isLeadingSurrogate(): Boolean { - return this in 0xd800..0xdbff; + return this in 0xd800..0xdbff } private fun Int.isTrailingSurrogate(): Boolean { - return this in 0xdc00..0xdfff; + return this in 0xdc00..0xdfff } private fun Int.isUnicodeScalar(): Boolean { @@ -217,7 +202,7 @@ internal class Lexer(val src: String) { // An int32 has 8 hex digits max while (i < 9) { if (pos == len) { - throw LexerException("Unterminated Unicode escape", line, column(pos), null) + throw LexerException("Unterminated Unicode escape", pos, line, column(pos), null) } val c = src[pos++] @@ -225,12 +210,12 @@ internal class Lexer(val src: String) { if (i == 0) { val start = pos - i - 4 // empty unicode escape? - throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", line, column(start), null) + throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", start, line, column(start), null) } if (!result.isUnicodeScalar()) { val start = pos - i - 4 - throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", line, column(start), null) + throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", start, line, column(start), null) } // Verify that the code point is valid? @@ -240,7 +225,7 @@ internal class Lexer(val src: String) { val h = c.decodeHex() if (h == -1) { val start = pos - i - 4 - throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", line, column(start), null) + throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", start, line, column(start), null) } result = result.shl(4).or(h) @@ -248,7 +233,7 @@ internal class Lexer(val src: String) { } val start = pos - i - 3 - throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", line, column(start), null) + throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", start, line, column(start), null) } private fun Char.decodeHex(): Int { @@ -271,7 +256,7 @@ internal class Lexer(val src: String) { private fun readFixedUnicodeEscape(): Int { if (pos + 4 >= len) { - throw LexerException("Unterminated Unicode escape", line, column(pos), null) + throw LexerException("Unterminated Unicode escape", pos, line, column(pos), null) } var result = 0 @@ -279,7 +264,7 @@ internal class Lexer(val src: String) { val h = src[pos++].decodeHex() if (h == -1) { val start = pos - i - 3 - throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", line, column(start), null) + throw LexerException("Invalid Unicode escape '${src.substring(start, pos)}'", start, line, column(start), null) } result = result.shl(4).or(h) } @@ -289,7 +274,7 @@ internal class Lexer(val src: String) { private fun readEscapeCharacter(): Int { if (pos == len) { - throw LexerException("Unterminated escape", line, column(pos), null) + throw LexerException("Unterminated escape", pos, line, column(pos), null) } val c = src[pos++] @@ -303,7 +288,7 @@ internal class Lexer(val src: String) { 'r' -> '\r'.code 't' -> '\t'.code 'u' -> readUnicodeEscape() - else -> throw LexerException("Invalid escape character '\\${c}'", line, column(pos - 2), null) + else -> throw LexerException("Invalid escape character '\\${c}'", pos - 2, line, column(pos - 2), null) } } @@ -313,14 +298,21 @@ internal class Lexer(val src: String) { while (true) { if (pos == len) { - throw LexerException("Unterminated string", line, column(pos), null) + throw LexerException("Unterminated string", pos, line, column(pos), null) } val c = src[pos++] when (c) { '\\' -> builder.appendCodePointMpp(readEscapeCharacter()) - '\"' -> return Token.String(line, column(start), line, column(pos - 1), builder.toString()) - '\r', '\n' -> throw LexerException("Unterminated string", line, column(pos - 1), null) + '\"' -> return Token.String( + start = start, + end = pos, + line = line, + column = column(start), + value = builder.toString() + ) + + '\r', '\n' -> throw LexerException("Unterminated string", pos - 1, line, column(pos - 1), null) else -> { // TODO: we are lenient here and allow potentially invalid chars like invalid surrogate pairs builder.append(c) @@ -338,7 +330,7 @@ internal class Lexer(val src: String) { while (true) { if (pos == len) { - throw LexerException("Unterminated block string", line, column(pos), null) + throw LexerException("Unterminated block string", pos, line, column(pos), null) } val c = src[pos++] @@ -383,11 +375,11 @@ internal class Lexer(val src: String) { blockLines.add(currentLine.toString()) return Token.String( - startLine, - startColumn, - line, - column(pos - 1), - blockLines.dedentBlockStringLines().joinToString("\n") + start = start, + end = pos, + line = startLine, + column = startColumn, + value = blockLines.dedentBlockStringLines().joinToString("\n") ) } else { currentLine.append(c) @@ -432,14 +424,16 @@ internal class Lexer(val src: String) { pos++ state = STATE_ZERO } + else -> { state = STATE_ZERO } } } + STATE_ZERO -> { var c = src[pos] - when { + when { c == '0' -> { pos++ state = STATE_DOT_EXP @@ -449,18 +443,21 @@ internal class Lexer(val src: String) { } c = src[pos] if (pos < len && c.isDigit()) { - throw LexerException("Invalid number, unexpected digit after 0: '${c}'", line, column(pos), null) + throw LexerException("Invalid number, unexpected digit after 0: '${c}'", pos, line, column(pos), null) } } + c.isDigit() -> { pos++ state = STATE_INTEGER_DIGIT } + else -> { - throw LexerException("Invalid number, expected digit but got '${c}'", line, column(pos), null) + throw LexerException("Invalid number, expected digit but got '${c}'", pos, line, column(pos), null) } } } + STATE_INTEGER_DIGIT -> { if (src[pos].isDigit()) { pos++ @@ -468,64 +465,71 @@ internal class Lexer(val src: String) { state = STATE_DOT_EXP } } + STATE_DOT_EXP -> { - when(src[pos]) { + when (src[pos]) { '.' -> { isFloat = true pos++ if (pos == len) { - throw LexerException("Unterminated number", line, column(start), null) + throw LexerException("Unterminated number", start, line, column(start), null) } val c = src[pos] if (!c.isDigit()) { - throw LexerException("Invalid number, expected digit but got '${c}'", line, column(pos), null) + throw LexerException("Invalid number, expected digit but got '${c}'", pos, line, column(pos), null) } pos++ state = STATE_FRACTIONAL_DIGIT } + else -> { state = STATE_EXP } } } + STATE_EXP -> { - when(src[pos]) { + when (src[pos]) { 'e', 'E' -> { isFloat = true pos++ if (pos == len) { - throw LexerException("Unterminated number", line, column(start), null) + throw LexerException("Unterminated number", start, line, column(start), null) } state = STATE_SIGN } + else -> break } } + STATE_SIGN -> { var c = src[pos] when (c) { '-', '+' -> { pos++ if (pos == len) { - throw LexerException("Unterminated number", line, column(start), null) + throw LexerException("Unterminated number", start, line, column(start), null) } c = src[pos] if (!c.isDigit()) { - throw LexerException("Invalid number, expected digit but got '${c}'", line, column(pos), null) + throw LexerException("Invalid number, expected digit but got '${c}'", pos, line, column(pos), null) } pos++ state = STATE_EXP_DIGIT } + else -> { if (!c.isDigit()) { - throw LexerException("Invalid number, expected digit but got '${c}'", line, column(pos), null) + throw LexerException("Invalid number, expected digit but got '${c}'", pos, line, column(pos), null) } pos++ state = STATE_EXP_DIGIT } } } + STATE_EXP_DIGIT -> { if (src[pos].isDigit()) { pos++ @@ -533,6 +537,7 @@ internal class Lexer(val src: String) { break } } + STATE_FRACTIONAL_DIGIT -> { if (src[pos].isDigit()) { pos++ @@ -545,15 +550,27 @@ internal class Lexer(val src: String) { // Numbers cannot be followed by . or NameStart if (pos < len && (src[pos] == '.' || src[pos].isNameStart())) { - throw LexerException("Invalid number, expected digit but got '${src[pos]}'", line, column(pos), null) + throw LexerException("Invalid number, expected digit but got '${src[pos]}'", pos, line, column(pos), null) } val asString = src.substring(start, pos) return if (isFloat) { - Token.Float(line, column(start), column(pos), asString.toDouble()) + Token.Float( + start = start, + end = pos, + line = line, + column = column(start), + value = asString.toDouble() + ) } else { - Token.Int(line, column(start), column(pos), asString.toInt()) + Token.Int( + start = start, + end = pos, + line = line, + column = column(start), + value = asString.toInt() + ) } } @@ -583,7 +600,13 @@ internal class Lexer(val src: String) { break } } - return Token.Name(line = line, column = column(start), endColumn = column(pos - 1), value = src.substring(start, pos)) + return Token.Name( + start = start, + end = pos, + line = line, + column = column(start), + value = src.substring(start, pos) + ) } private fun column(pos: Int): Int { diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/LexerException.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/LexerException.kt index ae0e70781cd..617de2b5e26 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/LexerException.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/LexerException.kt @@ -1,3 +1,3 @@ package com.apollographql.apollo3.ast.internal -internal class LexerException(override val message: String, val line: Int, val column: Int, cause: Throwable?) : Exception(message, cause) +internal class LexerException(override val message: String, val pos: Int, val line: Int, val column: Int, cause: Throwable?) : Exception(message, cause) diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Parser.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Parser.kt index 5781098f2af..c4cecd55ef2 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Parser.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Parser.kt @@ -271,7 +271,11 @@ internal class Parser(src: String, private val withSourceLocation: Boolean, val if (!withSourceLocation) return null return SourceLocation( - from.line, from.column, lastToken.endLine, lastToken.endColumn, filePath + start = from.start, + end = lastToken.end, + line = from.line, + column = from.column, + filePath = filePath ) } @@ -893,10 +897,10 @@ internal class Parser(src: String, private val withSourceLocation: Boolean, val if (!withSourceLocation) return null return SourceLocation( + token.start, + token.end, token.line, token.column, - token.endLine, - token.endColumn, filePath ) } diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Token.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Token.kt index aa1d71b5866..da4162c3677 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Token.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/internal/Token.kt @@ -1,83 +1,89 @@ package com.apollographql.apollo3.ast.internal -internal sealed class Token(val line: kotlin.Int, val column: kotlin.Int, val endLine: kotlin.Int, val endColumn: kotlin.Int) { - object StartOfFile : Token(1, 1, 1, 1) { +/** + * @param start: the 0-indexed offset in the string where the token starts (inclusive) + * @param end: the 0-indexed offset in the string where the token ends (exclusive) + * @param line: the 1-indexed line in the string where the token starts (inclusive) + * @param column: the 1-indexed column in the string where the token starts (inclusive) + */ +internal sealed class Token(val start: kotlin.Int, val end: kotlin.Int, val line: kotlin.Int, val column: kotlin.Int) { + object StartOfFile : Token(0, 0, 1, 1) { override fun toString() = "SOF" } - class EndOfFile(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class EndOfFile(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start, line, column) { override fun toString() = "EOF" } - class ExclamationPoint(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class ExclamationPoint(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = "!" } - class Dollar(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class Dollar(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = "$" } - class Ampersand(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class Ampersand(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = "&" } - class LeftParenthesis(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class LeftParenthesis(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = "(" } - class RightParenthesis(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class RightParenthesis(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = ")" } - class Spread(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column + 3) { + class Spread(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 3, line, column ) { override fun toString() = "..." } - class Colon(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class Colon(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = ":" } - class Equals(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class Equals(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = "=" } - class At(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class At(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = "@" } - class LeftBracket(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class LeftBracket(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = "[" } - class RightBracket(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class RightBracket(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = "]" } - class LeftBrace(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class LeftBrace(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = "{" } - class RightBrace(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class RightBrace(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = "}" } - class Pipe(line: kotlin.Int, column: kotlin.Int) : Token(line, column, line, column) { + class Pipe(start: kotlin.Int, line: kotlin.Int, column: kotlin.Int) : Token(start, start + 1, line, column) { override fun toString() = "|" } - class Name(line: kotlin.Int, column: kotlin.Int, endColumn: kotlin.Int, val value: kotlin.String) : Token(line, column, line, endColumn) { + class Name(start: kotlin.Int, end: kotlin.Int, line: kotlin.Int, column: kotlin.Int, val value: kotlin.String) : Token(start, end, line, column) { override fun toString() = "name: $value" } - class Int(line: kotlin.Int, column: kotlin.Int, endColumn: kotlin.Int, val value: kotlin.Int) : Token(line, column, line, endColumn) { + class Int(start: kotlin.Int, end: kotlin.Int, line: kotlin.Int, column: kotlin.Int, val value: kotlin.Int) : Token(start, end, line, column) { override fun toString() = "int: $value" } - class Float(line: kotlin.Int, column: kotlin.Int, endColumn: kotlin.Int, val value: Double) : Token(line, column, line, endColumn) { + class Float(start: kotlin.Int, end: kotlin.Int, line: kotlin.Int, column: kotlin.Int, val value: Double) : Token(start, end, line, column) { override fun toString() = "float: $value" } - class String(line: kotlin.Int, column: kotlin.Int, endLine: kotlin.Int, endColumn: kotlin.Int, val value: kotlin.String) : Token(line, column, endLine, endColumn) { + class String(start: kotlin.Int, end: kotlin.Int, line: kotlin.Int, column: kotlin.Int, val value: kotlin.String) : Token(start, end, line, column) { override fun toString() = "string: \"$value\"" } } diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/introspection/introspection_to_schema.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/introspection/introspection_to_schema.kt index d9dc3206f7a..69d079cc34a 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/introspection/introspection_to_schema.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo3/ast/introspection/introspection_to_schema.kt @@ -47,13 +47,7 @@ import kotlin.jvm.JvmName private class GQLDocumentBuilder(private val introspectionSchema: IntrospectionSchema, filePath: String?) { - private val sourceLocation = SourceLocation( - filePath = filePath, - line = -1, - column = -1, - endLine = -1, - endColumn = -1, - ) + private val sourceLocation = SourceLocation.forPath(filePath) fun toGQLDocument(): GQLDocument { return with(introspectionSchema.__schema) { diff --git a/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/ParserTest.kt b/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/ParserTest.kt index ff2a53b6f26..451191241fc 100644 --- a/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/ParserTest.kt +++ b/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/ParserTest.kt @@ -87,10 +87,10 @@ query Test { .cast() .sourceLocation!! .apply { + assertEquals(55, start) + assertEquals(82, end) assertEquals(4, line) - assertEquals(6, endLine) assertEquals(3, column) - assertEquals(5, endColumn) } } @@ -112,10 +112,10 @@ query Test { .cast() .sourceLocation!! .apply { + assertEquals(20, start) + assertEquals(34, end) assertEquals(2, line) - assertEquals(2, endLine) assertEquals(3, column) - assertEquals(16, endColumn) } } } diff --git a/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/OperationValidationTest.kt b/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/OperationValidationTest.kt index 211638ddeea..0ce6f86255e 100644 --- a/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/OperationValidationTest.kt +++ b/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/OperationValidationTest.kt @@ -25,6 +25,15 @@ class OperationValidationTest { val operationIssues = operations.validateAsExecutable(schema).issues assertEquals(1, operationIssues.size) assertEquals("Use of deprecated input field `deprecatedParameter`", operationIssues[0].message) - assertEquals(SourceLocation(12, 41, -1, -1, null).pretty(), operationIssues[0].sourceLocation.pretty()) + assertEquals( + SourceLocation( + start = 0, + end = 1, + line = 12, + column = 41, + null + ).pretty(), + operationIssues[0].sourceLocation.pretty() + ) } } diff --git a/libraries/apollo-ast/src/jvmMain/kotlin/com/apollographql/apollo3/ast/internal/antlrParse.kt b/libraries/apollo-ast/src/jvmMain/kotlin/com/apollographql/apollo3/ast/internal/antlrParse.kt index b2b1033e810..f887dd6eb9c 100644 --- a/libraries/apollo-ast/src/jvmMain/kotlin/com/apollographql/apollo3/ast/internal/antlrParse.kt +++ b/libraries/apollo-ast/src/jvmMain/kotlin/com/apollographql/apollo3/ast/internal/antlrParse.kt @@ -60,7 +60,13 @@ internal fun antlrParse( ) { issues.add(Issue.ParsingError( message = "Unsupported token `${(offendingSymbol as? Token)?.text ?: offendingSymbol.toString()}`", - sourceLocation = SourceLocation(line, position, -1, -1, filePath) + sourceLocation = SourceLocation( + start = 0, + end = 1, + line = line, + column = position + 1, + filePath = filePath + ) )) } } @@ -74,7 +80,12 @@ internal fun antlrParse( issues.add( Issue.ParsingError( "Extra token at end of file `${currentToken.text}`", - SourceLocation(currentToken.line, currentToken.charPositionInLine, -1, -1, filePath) + SourceLocation( + start = 0, + end = 1, + line = currentToken.line, + column = currentToken.charPositionInLine, + filePath = filePath) ) ) } diff --git a/libraries/apollo-ast/src/jvmMain/kotlin/com/apollographql/apollo3/ast/internal/antlr_to_gql.kt b/libraries/apollo-ast/src/jvmMain/kotlin/com/apollographql/apollo3/ast/internal/antlr_to_gql.kt index 64bc50efb4e..90d61305fb2 100644 --- a/libraries/apollo-ast/src/jvmMain/kotlin/com/apollographql/apollo3/ast/internal/antlr_to_gql.kt +++ b/libraries/apollo-ast/src/jvmMain/kotlin/com/apollographql/apollo3/ast/internal/antlr_to_gql.kt @@ -18,10 +18,10 @@ internal fun GraphQLParser.TypeContext.toGQLType(filePath: String? = null) = Ant private class AntlrToGQLScope(val filePath: String?) { private fun sourceLocation(token: Token) = SourceLocation( + start = token.startIndex, + end = token.stopIndex + 1, line = token.line, column = token.charPositionInLine + 1, - endLine = -1, - endColumn = -1, filePath = filePath )