@@ -2,6 +2,8 @@ package graphql.kickstart.tools
22
33import graphql.language.Definition
44import graphql.language.Document
5+ import graphql.language.SourceLocation
6+ import graphql.parser.InvalidSyntaxException
57import graphql.parser.MultiSourceReader
68import graphql.parser.Parser
79import graphql.parser.ParserEnvironment
@@ -11,6 +13,7 @@ import graphql.schema.idl.RuntimeWiring
1113import graphql.schema.idl.SchemaDirectiveWiring
1214import org.antlr.v4.runtime.RecognitionException
1315import org.antlr.v4.runtime.misc.ParseCancellationException
16+ import java.text.NumberFormat
1417import kotlin.Int.Companion.MAX_VALUE
1518import kotlin.reflect.KClass
1619
@@ -20,7 +23,7 @@ import kotlin.reflect.KClass
2023class 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
214246class 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)
0 commit comments