Skip to content

Commit 4d85da7

Browse files
authored
Merge pull request #838 from graphql-java-kickstart/bugfix/392
Fix line numbers in schema syntax errors and name the source
2 parents 603d40e + 6f63923 commit 4d85da7

3 files changed

Lines changed: 119 additions & 13 deletions

File tree

‎src/main/kotlin/graphql/kickstart/tools/SchemaParserBuilder.kt‎

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package graphql.kickstart.tools
22

33
import graphql.language.Definition
44
import graphql.language.Document
5+
import graphql.language.SourceLocation
6+
import graphql.parser.InvalidSyntaxException
57
import graphql.parser.MultiSourceReader
68
import graphql.parser.Parser
79
import graphql.parser.ParserEnvironment
@@ -11,6 +13,7 @@ import graphql.schema.idl.RuntimeWiring
1113
import graphql.schema.idl.SchemaDirectiveWiring
1214
import org.antlr.v4.runtime.RecognitionException
1315
import org.antlr.v4.runtime.misc.ParseCancellationException
16+
import java.text.NumberFormat
1417
import kotlin.Int.Companion.MAX_VALUE
1518
import kotlin.reflect.KClass
1619

@@ -20,7 +23,7 @@ import kotlin.reflect.KClass
2023
class SchemaParserBuilder {
2124

2225
private val dictionary = SchemaParserDictionary()
23-
private val schemaString = StringBuilder()
26+
private val schemaStrings = mutableListOf<Source>()
2427
private val files = mutableListOf<String>()
2528
private val resolvers = mutableListOf<GraphQLResolver<*>>()
2629
private val scalars = mutableListOf<GraphQLScalarType>()
@@ -49,10 +52,14 @@ class SchemaParserBuilder {
4952
* Add a GraphQL schema string directly.
5053
*/
5154
fun schemaString(string: String) = this.apply {
52-
if (schemaString.isNotEmpty()) {
53-
schemaString.append("\n")
54-
}
55-
schemaString.append(string)
55+
schemaStrings.add(Source(string, null))
56+
}
57+
58+
/**
59+
* Add a GraphQL schema string directly, naming its source in parse errors and source locations.
60+
*/
61+
fun schemaString(string: String, sourceName: String) = this.apply {
62+
schemaStrings.add(Source(string, sourceName))
5663
}
5764

5865
/**
@@ -172,10 +179,10 @@ class SchemaParserBuilder {
172179

173180
private fun parseDocuments(): List<Document> {
174181
try {
175-
val documents = files.map { parseDocument(readFile(it), it) }.toMutableList()
182+
val documents = files.map { parseDocument(listOf(Source(readFile(it), it))) }.toMutableList()
176183

177-
if (schemaString.isNotBlank()) {
178-
documents.add(parseDocument(schemaString.toString()))
184+
if (schemaStrings.any { it.text.isNotBlank() }) {
185+
documents.add(parseDocument(schemaStrings))
179186
}
180187

181188
return documents
@@ -189,16 +196,37 @@ class SchemaParserBuilder {
189196
}
190197
}
191198

192-
private fun parseDocument(input: String, sourceName: String? = null): Document {
193-
val sourceReader = MultiSourceReader
194-
.newMultiSourceReader()
195-
.string(input, sourceName)
199+
private fun parseDocument(sources: List<Source>): Document {
200+
// graphql-java's MultiSourceReader numbers the last line of the last source from the start of the first one.
201+
// Ending every source with a line break leaves only the end of input on that line, see syntaxErrorInSource.
202+
val separated = if (sources.size > 1) sources.map { it.endingWithLineBreak() } else sources
203+
val sourceReader = MultiSourceReader.newMultiSourceReader()
204+
.apply { separated.forEach { string(it.text, it.name) } }
196205
.trackData(true).build()
197206
val environment = ParserEnvironment
198207
.newParserEnvironment()
199208
.document(sourceReader)
200209
.parserOptions(parserOptions).build()
201-
return parser.parseDocument(environment)
210+
try {
211+
return parser.parseDocument(environment)
212+
} catch (e: InvalidSyntaxException) {
213+
throw syntaxErrorInSource(e, separated.last())
214+
}
215+
}
216+
217+
/**
218+
* Makes the line of an unexpected end of input relative to the last source and adds the source name to the message.
219+
*/
220+
private fun syntaxErrorInSource(e: InvalidSyntaxException, lastSource: Source): InvalidSyntaxException {
221+
val location = e.location ?: return e
222+
val line = if (e.offendingToken == "<EOF>") lastSource.text.lines().size else location.line
223+
if (line == location.line && location.sourceName == null) return e
224+
225+
// graphql-java formats the line for the default locale, e.g. "1,103"
226+
val number = NumberFormat.getInstance()
227+
val message = e.message.orEmpty().replaceFirst(" ${number.format(location.line)} ", " ${number.format(line)} ")
228+
val inSource = location.sourceName?.let { " in $it" }.orEmpty()
229+
return SchemaSyntaxException(message + inSource, SourceLocation(line, location.column, location.sourceName), e)
202230
}
203231

204232
private fun readFile(filename: String) =
@@ -209,6 +237,10 @@ class SchemaParserBuilder {
209237
* Build the parser with the supplied schema and dictionary.
210238
*/
211239
fun build() = SchemaParser(scan(), options, runtimeWiringBuilder.build())
240+
241+
private data class Source(val text: String, val name: String?) {
242+
fun endingWithLineBreak() = if (text.endsWith("\n")) this else copy(text = "$text\n")
243+
}
212244
}
213245

214246
class InvalidSchemaError(
@@ -218,3 +250,6 @@ class InvalidSchemaError(
218250
override val message: String
219251
get() = "Invalid schema provided (${recognitionException.javaClass.name}) at: ${recognitionException.offendingToken}"
220252
}
253+
254+
internal class SchemaSyntaxException(message: String, location: SourceLocation, e: InvalidSyntaxException) :
255+
InvalidSyntaxException(message, location, e.offendingToken, e.sourcePreview, e)

‎src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package graphql.kickstart.tools
33
import graphql.ExecutionResult
44
import graphql.GraphQL
55
import graphql.kickstart.tools.resolver.FieldResolverError
6+
import graphql.parser.InvalidSyntaxException
67
import graphql.schema.*
78
import graphql.schema.idl.SchemaDirectiveWiring
89
import graphql.schema.idl.SchemaDirectiveWiringEnvironment
@@ -294,6 +295,73 @@ class SchemaParserTest {
294295
assertEquals(sourceLocation?.sourceName, "Test.graphqls")
295296
}
296297

298+
@Test
299+
fun `parser should report syntax error line relative to the schema string containing it`() {
300+
val error = assertThrows(InvalidSyntaxException::class.java) {
301+
SchemaParser.newParser()
302+
.schemaString(
303+
"""
304+
|type Query {
305+
| id: ID!
306+
|}
307+
""".trimMargin())
308+
.schemaString("type Foo { bar: String!! }")
309+
.build()
310+
}
311+
312+
assertEquals(error.message, "Invalid syntax with offending token '!' at line 1 column 24")
313+
assertEquals(error.location?.line, 1)
314+
}
315+
316+
@Test
317+
fun `parser should include file name in syntax error`() {
318+
val error = assertThrows(InvalidSyntaxException::class.java) {
319+
SchemaParser.newParser()
320+
.file("InvalidSyntax.graphqls")
321+
.build()
322+
}
323+
324+
assertEquals(error.message, "Invalid syntax with offending token '!' at line 2 column 15 in InvalidSyntax.graphqls")
325+
assertEquals(error.location?.sourceName, "InvalidSyntax.graphqls")
326+
}
327+
328+
@Test
329+
fun `parser should report unexpected end of the last schema string relative to it`() {
330+
val error = assertThrows(InvalidSyntaxException::class.java) {
331+
SchemaParser.newParser()
332+
// over 1000 lines, so graphql-java's message groups the thousands in the overall line
333+
.schemaString((1..1100).joinToString("\n") { "type Type$it { id: ID! }" }, "Types.graphqls")
334+
.schemaString("type Foo {\n bar: String\n", "Foo.graphqls")
335+
.build()
336+
}
337+
338+
assertEquals(error.message, "Invalid syntax with offending token '<EOF>' at line 3 column 1 in Foo.graphqls")
339+
assertEquals(error.location?.line, 3)
340+
}
341+
342+
@Test
343+
fun `parser should include source location for field definition in named schema string`() {
344+
val schema = SchemaParser.newParser()
345+
.schemaString(
346+
"""
347+
|schema {
348+
| query: Query
349+
|}
350+
""".trimMargin(), "Schema.graphqls")
351+
.schemaString("type Query { id: ID! }", "Query.graphqls")
352+
.resolvers(QueryWithIdResolver())
353+
.build()
354+
.makeExecutableSchema()
355+
356+
val sourceLocation = schema.getObjectType("Query")!!
357+
.getFieldDefinition("id")
358+
.definition!!.sourceLocation
359+
assertNotNull(sourceLocation)
360+
assertEquals(sourceLocation?.line, 1)
361+
assertEquals(sourceLocation?.column, 14)
362+
assertEquals(sourceLocation?.sourceName, "Query.graphqls")
363+
}
364+
297365
@Test
298366
fun `support enum types if only used as input type`() {
299367
SchemaParser.newParser()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type Foo {
2+
bar: String!!
3+
}

0 commit comments

Comments
 (0)