Skip to content

Commit

Permalink
[#67] Restrict the type equality on alias types
Browse files Browse the repository at this point in the history
Currently, when we use an alias type, we check that the final alias types are the same.
However, types should ensure we compare two same things.
So we restrict the control over alias type to the alias itself.

This commit does the following:

* Type equality does not try to check the final type but use the direct type itself
* Change `EqualityOnSameTypeControl` into `ComparisonOnSameTypeControl` because the control should be on inequalities too
* Add a test for calculator operands and alias types
* Move and add tests for comparison on same type with alias types too
* Add control that function parameters uses the same alias type
* Add control that method parameters uses the same alias type
* Update test from `typeVerificationIsOkKo` to use the alias type instead of its alias
  • Loading branch information
Gaëtan Rizio committed Aug 5, 2018
1 parent ce64e25 commit cef53a4
Show file tree
Hide file tree
Showing 17 changed files with 138 additions and 72 deletions.
2 changes: 1 addition & 1 deletion src/main/scala/definiti/core/validation/Controls.scala
Expand Up @@ -22,7 +22,7 @@ object Controls {
AttributeTypeControl,
CalculatorOperandsAreNumberControl,
EnumerationUniquenessControl,
EqualityOnSameTypeControl,
ComparisonOnSameTypeControl,
FunctionParametersControl,
LogicalOperandsAreBooleanControl,
MethodParametersControl,
Expand Down
Expand Up @@ -5,23 +5,24 @@ import definiti.common.control.{Control, ControlLevel, ControlResult}
import definiti.common.validation.Alert
import definiti.core.validation.helpers.{ExpressionControlHelper, TypeReferenceControlHelper}

private[core] object EqualityOnSameTypeControl extends Control[Root] with ExpressionControlHelper with TypeReferenceControlHelper {
private[core] object ComparisonOnSameTypeControl extends Control[Root] with ExpressionControlHelper with TypeReferenceControlHelper {
override val description: String = "Check if both expressions on equality comparison have the same type"
override val defaultLevel: ControlLevel.Value = ControlLevel.error

private val acceptedOperators = {
import definiti.common.ast.LogicalOperator._
Seq(Equal, NotEqual, Lower, Upper, LowerOrEqual, UpperOrEqual)
}

override def control(root: Root, library: Library): ControlResult = {
testAllExpressions(library) { expression =>
deepControl(expression) {
case logicalExpression: LogicalExpression if shouldBeControlled(logicalExpression) =>
case logicalExpression: LogicalExpression if acceptedOperators.contains(logicalExpression.operator) =>
controlLogicalExpression(logicalExpression, library)
}
}
}

private def shouldBeControlled(logicalExpression: LogicalExpression): Boolean = {
logicalExpression.operator == LogicalOperator.Equal || logicalExpression.operator == LogicalOperator.NotEqual
}

private def controlLogicalExpression(expression: LogicalExpression, library: Library): ControlResult = {
if (expression.left.returnType == expression.right.returnType) {
OK
Expand Down
Expand Up @@ -63,33 +63,7 @@ private[core] trait TypeReferenceControlHelper {
}

def areTypeEqual(expectedType: AbstractTypeReference, gotType: AbstractTypeReference, library: Library): Boolean = {
normalizeType(expectedType, library) == normalizeType(gotType, library)
}

private def normalizeType(abstractTypeReference: AbstractTypeReference, library: Library): AbstractTypeReference = {
abstractTypeReference match {
case typeReference: TypeReference =>
getRealType(typeReference, library)
case lambdaReference: LambdaReference =>
LambdaReference(
inputTypes = lambdaReference.inputTypes.map(getRealType(_, library)),
outputType = getRealType(lambdaReference.outputType, library)
)
case _ => abstractTypeReference
}
}

def getRealType(typeReference: TypeReference, library: Library): TypeReference = {
def process(typeReference: TypeReference): TypeReference = {
library.typesMap.get(typeReference.typeName) match {
case Some(aliasType: AliasType) => process(aliasType.alias)
case _ => TypeReference(
typeName = typeReference.typeName,
genericTypes = typeReference.genericTypes.map(process)
)
}
}
process(typeReference)
expectedType == gotType
}

def controlTypeEquality(expectedType: AbstractTypeReference, gotType: AbstractTypeReference, location: Location, library: Library): ControlResult = {
Expand Down
@@ -0,0 +1,14 @@
type Amount = Number
type Rate = Number

def plus(amount1: Amount, amount2: Amount): Number => {
amount1 + amount2
}

def minus(amount1: Amount, amount2: Number): Number => {
amount1 + amount2
}

def times(amount1: Amount, rate: Rate): Number => {
amount1 * rate
}
@@ -0,0 +1,9 @@
type Amount = Number

def isNotZero(value: Amount): Boolean => {
value == 0
}

def isPositive(value: Amount): Boolean => {
value > 0
}
@@ -0,0 +1,10 @@
type Amount = Number
type Rate = Number

def isEqual(value: Amount, rate: Rate): Boolean => {
value != rate
}

def isLower(value: Amount, rate: Rate): Boolean => {
value < rate
}
Expand Up @@ -27,4 +27,9 @@ verification MyVerification {
(value: String) => {
value.length + 5 != 5
}
}

type Amount = Number
def amountLowerThan(amount: Amount, reference: Amount): Boolean => {
amount < reference
}
@@ -0,0 +1,9 @@
type Title = String

def isEmptyTitle(title: Title): Boolean => {
title.nonEmpty()
}

def isEmpty(string: String): Boolean => {
isEmptyTitle(string)
}
@@ -0,0 +1,5 @@
type Substring = String

def contains(substring: Substring): Boolean => {
"Some text".contains(substring)
}
@@ -1,7 +1,7 @@
type MyString = String {
verify {
// Will be translated into something like "String 'abcdefghijkl' is too long"
message("error.short", String)
message("error.short", MyString)
(value) => {
if (value.length < 10) {
ok
Expand Down
Expand Up @@ -49,6 +49,11 @@ class CalculatorOperandsAreNumberControlSpec extends EndToEndSpec {
CalculatorOperandsAreNumberControl.errorNotNumber(Constants.string, invalidConditionLocation(2, 27, 40))
)
}

it should "validate an expression where both operands are the same type alias of a number" in {
val output = processFile("controls.calculatorOperandsAreNumber.validAliasType", configuration)
output shouldBe ok[Root]
}
}

object CalculatorOperandsAreNumberControlSpec {
Expand Down
@@ -0,0 +1,53 @@
package definiti.core.end2end.controls

import definiti.common.ast.{Root, TypeReference}
import definiti.common.tests.{ConfigurationMock, LocationPath}
import definiti.core.Constants
import definiti.core.ProgramResultMatchers._
import definiti.core.end2end.EndToEndSpec
import definiti.core.validation.controls.ComparisonOnSameTypeControl

class ComparisonOnSameTypeControlSpec extends EndToEndSpec {
import ComparisonOnSameTypeControlSpec._

"Project.generatePublicAST" should "validate an expression when equality operands are both the same typoe" in {
val output = processFile("controls.comparisonOnSameType.nominal", configuration)
output shouldBe ok[Root]
}

it should "invalidate an expression when equality operands are not the same type" in {
val output = processFile("controls.comparisonOnSameType.nominalInvalid", configuration)
output shouldBe ko[Root]
}

it should "invalidate an invalid expression in a condition" in {
val output = processFile("controls.comparisonOnSameType.invalidCondition", configuration)
output should beKo(
ComparisonOnSameTypeControl.errorDifferentTypes(Constants.number, Constants.string, invalidConditionLocation(2, 7, 37))
)
}

it should "invalidate a condition between an alias type and a type" in {
val output = processFile("controls.comparisonOnSameType.invalidAliasType", configuration)
output should beKo(
ComparisonOnSameTypeControl.errorDifferentTypes(TypeReference("Amount"), Constants.number, invalidAliasTypeLocation(4, 3, 13)),
ComparisonOnSameTypeControl.errorDifferentTypes(TypeReference("Amount"), Constants.number, invalidAliasTypeLocation(8, 3, 12))
)
}

it should "invalidate a condition between two alias types" in {
val output = processFile("controls.comparisonOnSameType.invalidDoubleAliasType", configuration)
output should beKo(
ComparisonOnSameTypeControl.errorDifferentTypes(TypeReference("Amount"), TypeReference("Rate"), invalidDoubleAliasTypeLocation(5, 3, 16)),
ComparisonOnSameTypeControl.errorDifferentTypes(TypeReference("Amount"), TypeReference("Rate"), invalidDoubleAliasTypeLocation(9, 3, 15))
)
}
}

object ComparisonOnSameTypeControlSpec {
val configuration = ConfigurationMock().withOnlyControls(ComparisonOnSameTypeControl)

val invalidConditionLocation = LocationPath.control(ComparisonOnSameTypeControl, "invalidCondition")
val invalidAliasTypeLocation = LocationPath.control(ComparisonOnSameTypeControl, "invalidAliasType")
val invalidDoubleAliasTypeLocation = LocationPath.control(ComparisonOnSameTypeControl, "invalidDoubleAliasType")
}

This file was deleted.

@@ -1,6 +1,6 @@
package definiti.core.end2end.controls

import definiti.common.ast.Root
import definiti.common.ast.{Root, TypeReference}
import definiti.common.program.Ko
import definiti.common.tests.{ConfigurationMock, LocationPath}
import definiti.core.Constants
Expand Down Expand Up @@ -29,11 +29,19 @@ class FunctionParametersControlSpec extends EndToEndSpec {
FunctionParametersControl.invalidParameterType(Constants.string, Constants.number, invalidTypeReferenceLocation(6, 11, 17))
))
}

it should "invalidate a function call when the type reference targets the alias type and not the type" in {
val output = processFile("controls.functionParameters.invalidAliasTypeReference", configuration)
output should beResult(Ko[Root](
FunctionParametersControl.invalidParameterType(TypeReference("Title"), Constants.string, invalidAliasTypeReferenceLocation(8, 16, 22))
))
}
}

object FunctionParametersControlSpec {
val configuration = ConfigurationMock().withOnlyControls(FunctionParametersControl)

val invalidNumberOfParametersLocation = LocationPath.control(FunctionParametersControl, "invalidNumberOfParameters")
val invalidTypeReferenceLocation = LocationPath.control(FunctionParametersControl, "invalidTypeReference")
val invalidAliasTypeReferenceLocation = LocationPath.control(FunctionParametersControl, "invalidAliasTypeReference")
}
@@ -1,6 +1,6 @@
package definiti.core.end2end.controls

import definiti.common.ast.{LambdaReference, NamedFunctionReference, Root}
import definiti.common.ast.{LambdaReference, NamedFunctionReference, Root, TypeReference}
import definiti.common.program.Ko
import definiti.common.tests.{ConfigurationMock, LocationPath}
import definiti.core.Constants
Expand Down Expand Up @@ -52,6 +52,13 @@ class MethodParametersControlSpec extends EndToEndSpec {
MethodParametersControl.invalidParameterType(Constants.string, Constants.number, invalidTypeReferenceLocation(2, 21, 24))
))
}

it should "invalidate a function call when the type reference targets the alias type and not the type" in {
val output = processFile("controls.methodParameters.invalidAliasTypeReference", configuration)
output should beResult(Ko[Root](
MethodParametersControl.invalidParameterType(Constants.string, TypeReference("Substring"), invalidAliasTypeReferenceLocation(4, 24, 33))
))
}
}

object MethodParametersControlSpec {
Expand All @@ -61,4 +68,5 @@ object MethodParametersControlSpec {
val invalidNamedFunctionReferenceLocation = LocationPath.control(MethodParametersControl, "invalidNamedFunctionReference")
val invalidNumberOfParametersLocation = LocationPath.control(MethodParametersControl, "invalidNumberOfParameters")
val invalidTypeReferenceLocation = LocationPath.control(MethodParametersControl, "invalidTypeReference")
val invalidAliasTypeReferenceLocation = LocationPath.control(MethodParametersControl, "invalidAliasTypeReference")
}

0 comments on commit cef53a4

Please sign in to comment.