Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Engine/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
</PropertyGroup>

<PropertyGroup>
<VersionPrefix>8.3.0</VersionPrefix>
<VersionPrefix>8.4.0</VersionPrefix>
</PropertyGroup>
</Project>
2 changes: 1 addition & 1 deletion Engine/Mindbox.Quokka.Abstractions/Errors/Location.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public sealed class Location
public int Line { get; }

/// <summary>
/// Column index (1-based)
/// Column index (0-based)
/// </summary>
public int Column { get; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// // Copyright 2022 Mindbox Ltd
// //
// // Licensed under the Apache License, Version 2.0 (the "License");
// // you may not use this file except in compliance with the License.
// // You may obtain a copy of the License at
// //
// // http://www.apache.org/licenses/LICENSE-2.0
// //
// // Unless required by applicable law or agreed to in writing, software
// // distributed under the License is distributed on an "AS IS" BASIS,
// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// // See the License for the specific language governing permissions and
// // limitations under the License.

namespace Mindbox.Quokka
{
/// <summary>
/// The reason why an arithmetic operation result could not be evaluated.
/// Meant to be used by the calling code to build its own message, e.g. a localized one.
/// </summary>
public enum ArithmeticErrorReason
{
/// <summary>
/// The result is infinite. Within a template this practically always means a division by zero;
/// an overflow of intermediate values to infinity is also reported this way.
/// </summary>
DivisionByZero,

/// <summary>
/// The result is not a number, e.g. when zero is divided by zero.
/// </summary>
NotANumber,

/// <summary>
/// The result is a finite number, but it is too large to be represented as a template value.
/// </summary>
ResultOutOfRange
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// // Copyright 2022 Mindbox Ltd
// //
// // Licensed under the Apache License, Version 2.0 (the "License");
// // you may not use this file except in compliance with the License.
// // You may obtain a copy of the License at
// //
// // http://www.apache.org/licenses/LICENSE-2.0
// //
// // Unless required by applicable law or agreed to in writing, software
// // distributed under the License is distributed on an "AS IS" BASIS,
// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// // See the License for the specific language governing permissions and
// // limitations under the License.

using System;

namespace Mindbox.Quokka
{
/// <summary>
/// This exception occurs when an arithmetic operation within a template produces a result
/// which can't be used as a template value: an infinite one, a not-a-number one or an out of range one.
/// </summary>
[Serializable]
public class ArithmeticOperationException : UnrenderableTemplateModelException
{
/// <summary>
/// The error text without the reason, the expression and the location appended to it.
/// </summary>
public const string ErrorText = "Arithmetic operation result could not be evaluated";

/// <summary>
/// The reason the result could not be evaluated.
/// </summary>
public ArithmeticErrorReason Reason { get; }

public ArithmeticOperationException(
ArithmeticErrorReason reason,
string expression,
Location location,
Exception inner)
: base(ErrorText, GetReasonText(reason), expression, location, inner)
{
Reason = reason;

Data[QuokkaExceptionData.Reason] = reason.ToString();
}

/// <summary>
/// The reason in the same wording the exception message uses.
/// </summary>
public static string GetReasonText(ArithmeticErrorReason reason)
{
switch (reason)
{
case ArithmeticErrorReason.DivisionByZero:
return "division by zero";

case ArithmeticErrorReason.NotANumber:
return "the result is not a number";

case ArithmeticErrorReason.ResultOutOfRange:
return "the result is out of the supported number range";

default:
throw new ArgumentOutOfRangeException(nameof(reason), reason, null);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// // Copyright 2022 Mindbox Ltd
// //
// // Licensed under the Apache License, Version 2.0 (the "License");
// // you may not use this file except in compliance with the License.
// // You may obtain a copy of the License at
// //
// // http://www.apache.org/licenses/LICENSE-2.0
// //
// // Unless required by applicable law or agreed to in writing, software
// // distributed under the License is distributed on an "AS IS" BASIS,
// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// // See the License for the specific language governing permissions and
// // limitations under the License.

namespace Mindbox.Quokka
{
/// <summary>
/// Keys of the <see cref="System.Exception.Data"/> entries filled in by runtime render errors
/// (<see cref="UnrenderableTemplateModelException"/> and its descendants).
/// The entries hold the parts the exception message is built from, so that the calling code
/// can render its own message (e.g. a localized one) without parsing the message text.
/// </summary>
public static class QuokkaExceptionData
{
/// <summary>
/// The error text without any details appended to it, e.g.
/// "Arithmetic operation result could not be evaluated". Value type is <see cref="string"/>.
/// </summary>
public const string ErrorText = "Quokka.ErrorText";

/// <summary>
/// The name of the reason enumeration member, e.g. "DivisionByZero". Value type is <see cref="string"/>.
/// </summary>
public const string Reason = "Quokka.Reason";

/// <summary>
/// The source text of the template expression which failed, e.g. "cart.total / cart.itemCount".
/// Value type is <see cref="string"/>.
/// </summary>
public const string Expression = "Quokka.Expression";

/// <summary>
/// The location of the failure within the template in the "line:column" form. Value type is <see cref="string"/>.
/// </summary>
public const string Location = "Quokka.Location";

/// <summary>
/// The line of the failure within the template. Value type is <see cref="int"/>.
/// </summary>
public const string Line = "Quokka.Line";

/// <summary>
/// The column of the failure within the template. Value type is <see cref="int"/>.
/// </summary>
public const string Column = "Quokka.Column";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,89 @@ namespace Mindbox.Quokka
[Serializable]
public class UnrenderableTemplateModelException : TemplateException
{
/// <summary>
/// The error text of a null value usage, without the expression and the location appended to it.
/// </summary>
public const string NullValueErrorText = "An attempt to use a null value";

/// <summary>
/// The error text of a variable whose value is not present in the model,
/// without the expression and the location appended to it.
/// </summary>
public const string ValueNotFoundErrorText = "Value for variable not found";

/// <summary>
/// The error text of a failed template function call, without the failure details,
/// the expression and the location appended to it.
/// </summary>
public const string FunctionFailedErrorText = "Function invocation resulted in error";

public Location Location { get; }

/// <summary>
/// The source text of the failed expression as it is written in the template,
/// e.g. "cart.total / cart.itemCount", truncated if it is too long.
/// <c>Null</c> if the text can't be restored.
/// </summary>
public string Expression { get; }

public UnrenderableTemplateModelException(string message, Location location)
: base(message)
{
Location = location;
FillLocationData(location);
}

public UnrenderableTemplateModelException(string message, Exception inner, Location location)
: base(message, inner)
{
Location = location;
FillLocationData(location);
}

public UnrenderableTemplateModelException(
string errorText,
string details,
string expression,
Location location,
Exception inner)
: base(BuildMessage(errorText, details, expression, location), inner)
{
Location = location;
Expression = string.IsNullOrWhiteSpace(expression) ? null : expression;

Data[QuokkaExceptionData.ErrorText] = errorText;

if (Expression != null)
Data[QuokkaExceptionData.Expression] = Expression;

FillLocationData(location);
}

private static string BuildMessage(string errorText, string details, string expression, Location location)
{
var message = errorText;

if (!string.IsNullOrWhiteSpace(details))
message += $": {details}";

if (!string.IsNullOrWhiteSpace(expression))
message += $" in \"{expression}\"";

if (location != null)
message += $" at {location}";

return message;
}

private void FillLocationData(Location location)
{
if (location == null)
return;

Data[QuokkaExceptionData.Location] = location.ToString();
Data[QuokkaExceptionData.Line] = location.Line;
Data[QuokkaExceptionData.Column] = location.Column;
}
}
}
16 changes: 16 additions & 0 deletions Engine/Quokka.Core/Generated.Partials/QuokkaBaseVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System;

using Antlr4.Runtime;
using Antlr4.Runtime.Misc;
using Antlr4.Runtime.Tree;

namespace Mindbox.Quokka.Generated
Expand All @@ -28,6 +29,21 @@ protected Location GetLocationFromToken(IToken token)
return new Location(token.Line, token.Column);
}

private const int MaxExpressionSourceLength = 100;

protected ExpressionSource GetExpressionSource(ParserRuleContext context)
{
var lastToken = context.Stop;
var text = lastToken != null && lastToken.StopIndex >= context.Start.StartIndex
? context.Start.InputStream.GetText(new Interval(context.Start.StartIndex, lastToken.StopIndex))
: null;

if (text != null && text.Length > MaxExpressionSourceLength)
text = text.Substring(0, MaxExpressionSourceLength) + "…";

return new ExpressionSource(GetLocationFromToken(context.Start), text);
}

protected QuokkaBaseVisitor(VisitingContext visitingContext)
{
VisitingContext = visitingContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public override void Accept(ITemplateVisitor treeVisitor)
treeVisitor.EndVisit();
}

public AdditionExpression(IEnumerable<AdditionOperand> operands)
public AdditionExpression(ExpressionSource source, IEnumerable<AdditionOperand> operands)
: base(source)
{
this.operands = operands.ToList().AsReadOnly();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ namespace Mindbox.Quokka
{
internal abstract class ArithmeticExpression : Expression
{
private readonly ExpressionSource source;

protected ArithmeticExpression(ExpressionSource source)
{
this.source = source;
}

public abstract double GetValue(RenderContext renderContext);

public abstract void PerformSemanticAnalysis(AnalysisContext context);
Expand Down Expand Up @@ -67,7 +74,7 @@ public sealed override void RegisterAssignmentToVariable(
// do nothing
}

private static object NormalizeValue(double value)
private object NormalizeValue(double value)
{
try
{
Expand All @@ -82,8 +89,18 @@ private static object NormalizeValue(double value)
}
catch (OverflowException ex)
{
throw new UnrenderableTemplateModelException("Arithmetic operation result could not be evaluated", ex, null);
throw new ArithmeticOperationException(GetErrorReason(value), source?.Text, source?.Location, ex);
}
}

private static ArithmeticErrorReason GetErrorReason(double value)
{
if (Double.IsNaN(value))
return ArithmeticErrorReason.NotANumber;

return Double.IsInfinity(value)
? ArithmeticErrorReason.DivisionByZero
: ArithmeticErrorReason.ResultOutOfRange;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public override TypeDefinition GetResultType(AnalysisContext context)
: TypeDefinition.Decimal;
}

public MultiplicationExpression(IEnumerable<MultiplicationOperand> operands)
public MultiplicationExpression(ExpressionSource source, IEnumerable<MultiplicationOperand> operands)
: base(source)
{
this.operands = operands.ToList().AsReadOnly();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public override TypeDefinition GetResultType(AnalysisContext context)
return innerExpression.GetResultType(context);
}

public NegationExpression(ArithmeticExpression innerExpression)
public NegationExpression(ExpressionSource source, ArithmeticExpression innerExpression)
: base(source)
{
this.innerExpression = innerExpression;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ internal class NumberExpression : ArithmeticExpression
{
private readonly double number;

public NumberExpression(double number)
public NumberExpression(ExpressionSource source, double number)
: base(source)
{
this.number = number;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ internal class VariantValueArithmeticExpression : ArithmeticExpression
{
private readonly VariantValueExpression variantValueExpression;

public VariantValueArithmeticExpression(VariantValueExpression variantValueExpression)
public VariantValueArithmeticExpression(ExpressionSource source, VariantValueExpression variantValueExpression)
: base(source)
{
this.variantValueExpression = variantValueExpression;
}
Expand Down
Loading
Loading