Skip to content

Commit

Permalink
Merge pull request #1728 from aml-org/W-12521486/exception-in-type-ex…
Browse files Browse the repository at this point in the history
…pression

W-12521486: fixed wrong type expression parsing
  • Loading branch information
tomsfernandez authored Feb 15, 2023
2 parents 5c48bac + 0e7afdb commit 98aedf3
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#%RAML 1.0
title: Something
version: 1.0

/test:
post:
body:
multipart/form-data:
type: file[] | []
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#%RAML 1.0
title: Something
version: 1.0

/test:
post:
body:
multipart/form-data:
type: []

types:
EmptyArray: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ModelId: file://amf-cli/shared/src/test/resources/validations/raml/empty-array-in-union-type-expression.raml
Profile:
Conforms: false
Number of results: 1

Level: Violation

- Constraint: http://a.ml/vocabularies/amf/parser#invalid-type-expression
Message: Syntax error, generating empty array
Severity: Violation
Target:
Property:
Range: [(9,23)-(9,24)]
Location: file://amf-cli/shared/src/test/resources/validations/raml/empty-array-in-union-type-expression.raml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
ModelId: file://amf-cli/shared/src/test/resources/validations/raml/standalone-array-as-type.raml
Profile:
Conforms: false
Number of results: 2

Level: Violation

- Constraint: http://a.ml/vocabularies/amf/parser#empty-type-expression-array
Message: [] is not a valid type
Severity: Violation
Target:
Property:
Range: [(9,14)-(9,16)]
Location: file://amf-cli/shared/src/test/resources/validations/raml/standalone-array-as-type.raml

- Constraint: http://a.ml/vocabularies/amf/parser#empty-type-expression-array
Message: [] is not a valid type
Severity: Violation
Target:
Property:
Range: [(12,14)-(12,16)]
Location: file://amf-cli/shared/src/test/resources/validations/raml/standalone-array-as-type.raml
Original file line number Diff line number Diff line change
Expand Up @@ -712,4 +712,15 @@ class RamlModelUniquePlatformReportTest extends UniquePlatformReportGenTest {
test("nested JSON Schema reference by id in draft 4") {
validate("/raml/nested-json-schema-ref/api.raml", Some("raml/nested-json-schema-ref.report"))
}

test("empty array expression in union should be invalid") {
validate(
"/raml/empty-array-in-union-type-expression.raml",
Some("raml/empty-array-in-union-type-expression.report")
)
}

test("Standalone array as type") {
validate("/raml/standalone-array-as-type.raml", Some("raml/standalone-array-as-type.report"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,18 @@ case class RamlTypeDetector(
)(implicit ctx: ShapeParserContext)
extends RamlTypeSyntax {

def detect(node: YNode): Option[TypeDef] = node.tagType match {
def detect(node: YNode): Option[TypeDef] = node.value match {

case YType.Seq =>
val sequence = node.as[Seq[YNode]]
InheritsTypeDetecter(collectTypeDefs(sequence), node).orElse(Some(ObjectType)) // type expression type?
case sequence: YSequence if sequence.isEmpty =>
ctx.eh.violation(EmptyTypeExpressionArray, "", "[] is not a valid type", node.location)
Some(ObjectType) // keep compatibility with non empty case

case YType.Map => detectOrInferType(node)
case sequence: YSequence =>
InheritsTypeDetecter(collectTypeDefs(sequence.nodes), node).orElse(Some(ObjectType)) // type expression type?

case YType.Null => Some(defaultType.typeDef)
case _: YMap => detectOrInferType(node)

case _ if node.tagType == YType.Null => Some(defaultType.typeDef)

case _ =>
val scalar = node.as[YScalar]
Expand Down Expand Up @@ -123,7 +126,7 @@ case class RamlTypeDetector(

private def parseAndMatchTypeExpression(node: YNode, text: String) = {
RamlExpressionParser
.check(shape => shape.withId("/"), text)
.check(shape => shape.withId("/"), text, node)
.flatMap(s => ShapeClassTypeDefMatcher(s, node, recursive))
.map {
case TypeDef.UnionType | TypeDef.ArrayType if !recursive => TypeExpressionType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,31 @@ private[expression] trait AbstractParser {
previous()
}

protected def check(token: String) = {
protected def expect(token: String) = {
if (isAtEnd) false
else peek().token == token
else peek().exists(_.token == token)
}

protected def peek() = tokens(current)
protected def peek() = tokens.lift(current)

protected def previous(): Token = tokens(current - 1)

protected def expectPrevious(token: String) = tokens.lift(current - 2).exists(_.token == token)

protected def consume(token: String) = {
if (check(token)) Some(advance())
if (expect(token)) Some(advance())
else None
}

protected def consumeToEnd(): Seq[Token] = {
val rest = tokens.splitAt(current)._2
val (_, rest) = tokens.splitAt(current)
current = tokens.size
rest
}

protected def consumeUntil(token: String): Seq[Token] = {
var accumulated = Seq[Token]()
while (!check(token) && !isAtEnd) {
while (!expect(token) && !isAtEnd) {
accumulated = accumulated :+ advance()
}
accumulated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ private[expression] trait AnnotationHelper {
}

protected def setShapeAnnotation(shapeToAnnotate: Shape, leftShape: Shape, rightShape: Shape): Shape = {
val leftLexical = leftShape.annotations.find(classOf[LexicalInformation]).get
val rightLexical = rightShape.annotations.find(classOf[LexicalInformation]).get
val nextAnnotations = annotations.copy() += LexicalInformation(leftLexical.range.start, rightLexical.range.`end`)
val leftLexical = leftShape.annotations.lexical()
val rightLexical = rightShape.annotations.lexical()
val nextAnnotations = annotations.copy() += LexicalInformation(leftLexical.start, rightLexical.`end`)
shapeToAnnotate.annotations ++= nextAnnotations
shapeToAnnotate
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,53 +22,56 @@ private[expression] class RamlExpressionASTBuilder(
extends AbstractParser
with AnnotationHelper {

private val parsedShapes = mutable.Stack[Shape]()

def build(): Option[Shape] = {
if (tokens.isEmpty) return None
def build(previous: Option[Shape] = None): Option[Shape] = {
var result = previous
if (tokens.isEmpty) return result
while (!isAtEnd) {
val current = advance()
val maybeShape = current.token match {
result = current.token match {
case SYMBOL => Some(parseSymbol(current))
case START_ARRAY => Some(parseArray(current))
case UNION => Some(parseUnion(current))
case START_GROUP => parseGroup(current)
case _ => None
case START_ARRAY => Some(parseArray(current, result))
case UNION => Some(parseUnion(current, result))
case START_GROUP => parseGroup(current, result)
case _ => result
}
maybeShape.foreach(parsedShapes.push)
}
parsedShapes.headOption
result
}

private def parseGroup(token: Token): Option[Shape] = {
private def parseGroup(token: Token, previous: Option[Shape]): Option[Shape] = {
val tokens = consumeUntil(END_GROUP)
val maybeEndGroup = consume(END_GROUP)
val optionalShape =
new RamlExpressionASTBuilder(tokens, declarationFinder, annotations, unresolvedRegister).build()
val optionalShape = parse(tokens, previous)
optionalShape.foreach(s => s.annotations += GroupedTypeExpression())
optionalShape.foreach(s => setShapeAnnotation(s, token, maybeEndGroup))
optionalShape
}

private def parseUnion(token: Token): Shape = {
private def parseUnion(token: Token, previous: Option[Shape]): Shape = {
val union = UnionShape()
if (parsedShapes.isEmpty) {
throwError("Syntax error, cannot create empty Union", token)
return union
}
val tokens = consumeToEnd()
val optionalShape =
new RamlExpressionASTBuilder(tokens, declarationFinder, annotations, unresolvedRegister).build()
val previousShape = parsedShapes.pop()
optionalShape match {
case Some(shape) =>
val nextAnyOf = calculateAnyOf(previousShape, shape)
union.setWithoutId(UnionShapeModel.AnyOf, AmfArray(nextAnyOf), Annotations(SingleExpression()))
setShapeAnnotation(union, previousShape, shape)
case None => union
previous match {
case None =>
throwError("Syntax error, cannot create empty Union", token)
union
case Some(previousShape) =>
val tokens = consumeToEnd()
val optionalShape = parse(tokens)
optionalShape match {
case Some(shape) =>
val nextAnyOf = calculateAnyOf(previousShape, shape)
union.setWithoutId(UnionShapeModel.AnyOf, AmfArray(nextAnyOf), Annotations(SingleExpression()))
setShapeAnnotation(union, previousShape, shape)
case None => union
}
}
}

private def parse(tokens: Seq[Token]): Option[Shape] = parse(tokens, None)

private def parse(tokens: Seq[Token], previous: Option[Shape]): Option[Shape] = {
new RamlExpressionASTBuilder(tokens, declarationFinder, annotations, unresolvedRegister).build(previous)
}

private def calculateAnyOf(previousShape: Shape, nextShape: Shape): Seq[Shape] = {
val atLeastOneIsDeclaredElement = linksToDeclaredElement(previousShape) || linksToDeclaredElement(nextShape)
if (atLeastOneIsDeclaredElement) return Seq(previousShape, nextShape)
Expand All @@ -80,26 +83,27 @@ private[expression] class RamlExpressionASTBuilder(
}
}

private def parseArray(token: Token): Shape = {
private def parseArray(token: Token, previous: Option[Shape]): Shape = {
val array = ArrayShape()
if (parsedShapes.isEmpty) {
throwError("Syntax error, generating empty array", token)
return array
}
val maybeEndToken = consume(END_ARRAY)
if (maybeEndToken.isEmpty) {
throwError("Syntax error, expected ]", token)
return array
}
val previousShape = parsedShapes.pop()
val finalShape = previousShape match {
case _: ArrayShape =>
ArrayShape()
.setWithoutId(ArrayShapeModel.Items, previousShape, Annotations(SingleExpression()))
.toMatrixShapeWithoutId
case _ => ArrayShape().setWithoutId(ArrayShapeModel.Items, previousShape, Annotations(SingleExpression()))
previous match {
case None =>
throwError("Syntax error, generating empty array", token)
array
case Some(previousShape) =>
val maybeEndToken = consume(END_ARRAY)
if (maybeEndToken.isEmpty) {
throwError("Syntax error, expected ]", token)
return array
}
val finalShape = previousShape match {
case _: ArrayShape =>
ArrayShape()
.setWithoutId(ArrayShapeModel.Items, previousShape, Annotations(SingleExpression()))
.toMatrixShapeWithoutId
case _ => ArrayShape().setWithoutId(ArrayShapeModel.Items, previousShape, Annotations(SingleExpression()))
}
setShapeAnnotation(finalShape, token, maybeEndToken)
}
setShapeAnnotation(finalShape, token, maybeEndToken)
}

private def parseSymbol(token: Token): Shape = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ import org.yaml.model._
object RamlExpressionParser {

def parse(adopt: Shape => Unit, expression: String, part: YPart)(implicit ctx: ShapeParserContext): Option[Shape] = {
val node = getValue(part)
val annotations = Annotations(node).reject(a => a.isInstanceOf[LexicalInformation])
val position = Position(node.location.lineFrom, node.location.columnFrom)
val tokens = new RamlExpressionLexer(expression, position).lex()
val node = getValue(part)
val position = Position(node.location.lineFrom, node.location.columnFrom)
val tokens = new RamlExpressionLexer(expression, position).lex()
val builder =
new RamlExpressionASTBuilder(
tokens,
ContextDeclarationFinder(ctx),
annotations,
annotations(node),
ContextRegister(ctx, Some(node))
)(ctx.eh)
val result = builder
Expand All @@ -29,15 +28,27 @@ object RamlExpressionParser {
result
}

def check(adopt: Shape => Unit, expression: String)(implicit ctx: ShapeParserContext): Option[Shape] = {
val tokens = new RamlExpressionLexer(expression, Position.ZERO).lex()
def check(adopt: Shape => Unit, expression: String, part: YPart = YNode.Empty)(implicit
ctx: ShapeParserContext
): Option[Shape] = {
val tokens = new RamlExpressionLexer(expression, part.location.from).lex()
val builder =
new RamlExpressionASTBuilder(tokens, ContextDeclarationFinder(ctx), unresolvedRegister = EmptyRegister())(ctx.eh)
new RamlExpressionASTBuilder(
tokens,
ContextDeclarationFinder(ctx),
annotations(getValue(part)),
unresolvedRegister = EmptyRegister()
)(ctx.eh)
val result = builder.build()
result.foreach(adopt(_))
result
}

private def annotations(node: YValue) = {
val annotations = Annotations(node).reject(a => a.isInstanceOf[LexicalInformation])
annotations
}

private def addAnnotations(shape: Shape, part: YPart, expression: String): Shape = {
shape.annotations.reject(a =>
a.isInstanceOf[LexicalInformation] || a.isInstanceOf[SourceNode] || a.isInstanceOf[SourceAST] || a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,11 @@ object ShapeParserSideValidations extends Validations {
"The definition key present in the ref is not the correct for the JSON Schema"
)

val EmptyTypeExpressionArray = validation(
"empty-type-expression-array",
"Empty type expression array"
)

override val levels: Map[String, Map[ProfileName, String]] = Map(
InvalidShapeFormat.id -> all(WARNING),
JsonSchemaInheritanceWarning.id -> all(WARNING),
Expand Down Expand Up @@ -442,6 +447,7 @@ object ShapeParserSideValidations extends Validations {
JsonSchemaDefinitionNotFound,
InvalidJsonSchemaReference,
MultipleDefinitionKey,
IncorrectDefinitionKey
IncorrectDefinitionKey,
EmptyTypeExpressionArray
)
}

0 comments on commit 98aedf3

Please sign in to comment.