src/main/scala/lang/Parser.scala (159 lines of code) (raw):
package cql.lang
import scala.util.Try
import scala.util.Success
import TokenType.*
case class ParseError(position: Int, message: String) extends Error(message)
class Parser(tokens: List[Token]):
var current: Int = 0;
val skipTypes = List()
def parse(): Try[QueryList] =
Try { queryList }
// program -> statement* EOF
private def queryList =
var queries = List.empty[CqlBinary | CqlField | QueryOutputModifier]
while (peek().tokenType != EOF) {
queries = queries :+ query
}
QueryList(queries)
val startOfCqlField = List(TokenType.CHIP_KEY, TokenType.PLUS)
val startOfQueryOutputModifier =
List(TokenType.QUERY_OUTPUT_MODIFIER_KEY, TokenType.AT)
val startOfQueryValue =
List(TokenType.CHIP_VALUE, TokenType.COLON)
private def query: CqlBinary | CqlField | QueryOutputModifier =
if (startOfCqlField.contains(peek().tokenType)) queryField
else if (startOfQueryOutputModifier.contains(peek().tokenType))
queryOutputModifier
else if (startOfQueryValue.contains(peek().tokenType))
throw ParseError(
peek().start,
"I found an unexpected ':'. Did you intend to search for a tag, section or similar, e.g. tag:news? If you would like to add a search phrase containing a ':' character, please surround it in double quotes."
)
else queryBinary
private def queryBinary: CqlBinary =
val left = queryContent
peek().tokenType match {
case TokenType.AND =>
val andToken = consume(TokenType.AND)
guardAgainstCqlField("after 'AND'.")
if (isAtEnd) {
throw error(
"There must be a query following 'AND', e.g. this AND that."
)
}
CqlBinary(left, Some((andToken), queryBinary))
case TokenType.OR =>
val orToken = consume(TokenType.OR)
guardAgainstCqlField("after 'OR'.")
if (isAtEnd) {
error(
"There must be a query following 'OR', e.g. this OR that."
)
}
CqlBinary(left, Some((orToken, queryBinary)))
case _ => CqlBinary(left)
}
private def queryContent: QueryExpr =
val content: CqlGroup | CqlStr | CqlBinary = peek().tokenType match
case TokenType.LEFT_BRACKET => queryGroup
case TokenType.STRING => queryStr
case token if List(TokenType.AND, TokenType.OR).contains(token) =>
throw error(
s"An ${token.toString()} keyword must have a search term before and after it, e.g. this ${token.toString} that."
)
case _ =>
throw error(s"I didn't expect what I found after '${previous().lexeme}'")
QueryExpr(content)
private def queryGroup: CqlGroup =
consume(TokenType.LEFT_BRACKET, "Groups should start with a left bracket")
if (isAtEnd || peek().tokenType == TokenType.RIGHT_BRACKET) {
throw error(
"Groups must contain some content. Put a search term between the brackets!"
)
}
guardAgainstCqlField(
"within a group. Try putting this query outside of the brackets!"
)
val binary = queryBinary
consume(TokenType.RIGHT_BRACKET, "Groups must end with a right bracket.")
CqlGroup(binary)
private def queryStr: CqlStr =
val token = consume(TokenType.STRING, "Expected a string")
CqlStr(token.literal.getOrElse(""))
private def queryField: CqlField =
val key = Try {
consume(TokenType.CHIP_KEY, "Expected a search key, e.g. +tag")
}.recover { _ =>
consume(TokenType.PLUS, "Expected at least a +")
}.get
val value = Try {
consume(TokenType.CHIP_VALUE, s"Expected a search value, e.g. +tag:news")
}.recoverWith { _ =>
Try {
consume(TokenType.COLON, "Expected at least a :")
}
}.toOption
CqlField(key, value)
private def queryOutputModifier: QueryOutputModifier =
val key = Try {
consume(
TokenType.QUERY_OUTPUT_MODIFIER_KEY,
"Expected a query modifier key, e.g. @show-fields"
)
}.recover { _ =>
consume(TokenType.AT, "Expected at least an @")
}.get
val value = Try {
consume(
TokenType.CHIP_VALUE,
"Expected a value for the query modifier, e.g. @show-fields:all"
)
}.recoverWith { _ =>
Try {
consume(TokenType.COLON, "Expected at least a :")
}
}.toOption
QueryOutputModifier(key, value)
private def matchTokens(tokens: TokenType*) =
tokens.exists(token =>
if (check(token)) {
advance()
true
} else false
)
/** Throw a sensible parse error when a query field or output modifier is
* found in the wrong place.
*/
private def guardAgainstCqlField(errorLocation: String) =
peek().tokenType match {
case TokenType.AT =>
throw error(
s"You cannot put output modifiers (e.g. @show-fields:all) ${errorLocation}"
)
case TokenType.PLUS =>
throw error(
s"You cannot put queries for tags, sections etc. ${errorLocation}"
)
case TokenType.CHIP_KEY =>
val queryFieldNode = queryField
throw error(
s"You cannot query for ${queryFieldNode.key.literal.getOrElse("")}s ${errorLocation}"
)
case TokenType.QUERY_OUTPUT_MODIFIER_KEY =>
val queryFieldNode = queryOutputModifier
throw error(
s"You cannot add an output modifier for ${queryFieldNode.key.literal
.getOrElse("")}s ${errorLocation}"
)
case _ => ()
}
private def check(tokenType: TokenType) =
if (isAtEnd) false else peek().tokenType == tokenType
private def isAtEnd = peek().tokenType == EOF
private def peek() = tokens(current)
private def advance() =
if (!isAtEnd) current = current + 1
previous()
private def consume(tokenType: TokenType, message: String = "") = {
if (check(tokenType)) advance()
else throw error(message)
}
private def previous() = tokens(current - 1)
private def error(message: String) =
new ParseError(peek().start, message)