Skip to content
Closed
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.5.0</VersionPrefix>
<VersionPrefix>8.6.0</VersionPrefix>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,11 @@ public interface IMethodCallDefinition
{
string Name { get; }
IReadOnlyList<IMethodArgumentDefinition> Arguments { get; }

/// <summary>
/// The 1-based number of this call among the calls of a non-idempotent method with the same name and arguments,
/// or <c>null</c> if the method is idempotent and all its identical calls share a single definition.
/// </summary>
int? CallOrdinal { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public interface IModelMethod

IReadOnlyList<object> Arguments { get; }

/// <summary>
/// The <see cref="IMethodCallDefinition.CallOrdinal"/> of the call this value belongs to,
/// or <c>null</c> for a value shared by all the calls of an idempotent method.
/// </summary>
int? CallOrdinal { get; }

IModelValue Value { get; }
}
}
20 changes: 15 additions & 5 deletions Engine/Quokka.Core/DefaultTemplateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// // See the License for the specific language governing permissions and
// // limitations under the License.

using System;
using System.Collections.Generic;
using System.Linq;

Expand All @@ -23,36 +24,45 @@ public class DefaultTemplateFactory : ITemplateFactory
{
private readonly FunctionRegistry functionRegistry;

public DefaultTemplateFactory(IEnumerable<TemplateFunction> additionalFunctions = null)
private readonly IReadOnlyCollection<string> nonIdempotentMethodNames;

public DefaultTemplateFactory(
IEnumerable<TemplateFunction> additionalFunctions = null,
IEnumerable<string> nonIdempotentMethodNames = null)
{
var functions = new List<TemplateFunction>(Template.GetStandardFunctions());
if (additionalFunctions != null)
functions.AddRange(additionalFunctions);

functionRegistry = new FunctionRegistry(functions);
this.nonIdempotentMethodNames = nonIdempotentMethodNames?.ToArray() ?? Array.Empty<string>();
}

public ITemplate CreateTemplate(string templateText)
{
return new Template(templateText, functionRegistry, true);
return new Template(templateText, functionRegistry, true, nonIdempotentMethodNames: nonIdempotentMethodNames);
}

public ITemplate TryCreateTemplate(string templateText, out IList<ITemplateError> errors)
{
var template = new Template(templateText, functionRegistry, false);
var template = new Template(
templateText,
functionRegistry,
false,
nonIdempotentMethodNames: nonIdempotentMethodNames);
errors = template.Errors;

return errors.Any() ? null : template;
}

public IHtmlTemplate CreateHtmlTemplate(string templateText)
{
return new HtmlTemplate(templateText, functionRegistry, true);
return new HtmlTemplate(templateText, functionRegistry, true, nonIdempotentMethodNames);
}

public IHtmlTemplate TryCreateHtmlTemplate(string templateText, out IList<ITemplateError> errors)
{
var template = new HtmlTemplate(templateText, functionRegistry, false);
var template = new HtmlTemplate(templateText, functionRegistry, false, nonIdempotentMethodNames);
errors = template.Errors;

return errors.Any() ? null : template;
Expand Down
6 changes: 4 additions & 2 deletions Engine/Quokka.Core/Html/HtmlTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ internal class HtmlTemplate : Template, IHtmlTemplate
internal HtmlTemplate(
string templateText,
FunctionRegistry functionRegistry,
bool throwIfErrorsEncountered = true)
bool throwIfErrorsEncountered = true,
IEnumerable<string> nonIdempotentMethodNames = null)
: base(
templateText,
functionRegistry,
throwIfErrorsEncountered,
context => new HtmlStaticBlockVisitor(context),
new[] { new HtmlSemanticErrorSubListener() })
new[] { new HtmlSemanticErrorSubListener() },
nonIdempotentMethodNames)
{
}

Expand Down
14 changes: 12 additions & 2 deletions Engine/Quokka.Core/Model/Definitions/MethodCallDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@ public class MethodCallDefinition : IMethodCallDefinition, IEquatable<IMethodCal

public IReadOnlyList<IMethodArgumentDefinition> Arguments { get; }

internal MethodCallDefinition(string name, IReadOnlyList<IMethodArgumentDefinition> arguments)
public int? CallOrdinal { get; }

internal MethodCallDefinition(
string name,
IReadOnlyList<IMethodArgumentDefinition> arguments,
int? callOrdinal = null)
{
Name = name;
Arguments = arguments;
CallOrdinal = callOrdinal;
}

public bool Equals(IMethodCallDefinition other)
Expand All @@ -38,6 +44,9 @@ public bool Equals(IMethodCallDefinition other)
if (!StringComparer.OrdinalIgnoreCase.Equals(Name, other.Name))
return false;

if (CallOrdinal != other.CallOrdinal)
return false;

if (Arguments.Count != other.Arguments.Count)
return false;

Expand Down Expand Up @@ -75,7 +84,8 @@ public override int GetHashCode()
}

public override string ToString() =>
$"{Name}({string.Join(", ", Arguments.Select(arg => $"{arg.Type.Name}: {arg.Value}"))})";
$"{Name}({string.Join(", ", Arguments.Select(arg => $"{arg.Type.Name}: {arg.Value}"))})"
+ (CallOrdinal == null ? string.Empty : $"#{CallOrdinal}");
}


Expand Down
8 changes: 5 additions & 3 deletions Engine/Quokka.Core/Model/InputValues/ModelMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class ModelMethod : IModelMethod
{
public string Name { get; }
public IReadOnlyList<object> Arguments { get; }
public int? CallOrdinal { get; }
public IModelValue Value { get; }

public ModelMethod(string name, IModelValue value)
Expand All @@ -36,15 +37,16 @@ public ModelMethod(string name, object primitiveValue)
{
}

public ModelMethod(string name, IEnumerable<object> arguments, IModelValue value)
public ModelMethod(string name, IEnumerable<object> arguments, IModelValue value, int? callOrdinal = null)
{
Name = name;
Arguments = arguments.ToArray();
CallOrdinal = callOrdinal;
Value = value;
}

public ModelMethod(string name, IEnumerable<object> arguments, object primitiveValue)
: this(name, arguments, new PrimitiveModelValue(primitiveValue))
public ModelMethod(string name, IEnumerable<object> arguments, object primitiveValue, int? callOrdinal = null)
: this(name, arguments, new PrimitiveModelValue(primitiveValue), callOrdinal)
{
}
}
Expand Down
5 changes: 3 additions & 2 deletions Engine/Quokka.Core/Model/ModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ private void ValidationCompositeModelMethods(
var requiredMethods = requiredModelDefinition.Methods.ToList();
var actualMethods = model
.Methods
.ToDictionary(method => new MethodCall(method.Name, method.Arguments));
.ToDictionary(method => new MethodCall(method.Name, method.Arguments, method.CallOrdinal));

foreach (var requiredMethod in requiredMethods)
{
Expand All @@ -111,7 +111,8 @@ private void ValidationCompositeModelMethods(

var requiredMethodCall = new MethodCall(
requiredMethod.Key.Name,
requiredMethod.Key.Arguments.Select(arg => arg.Value).ToArray());
requiredMethod.Key.Arguments.Select(arg => arg.Value).ToArray(),
requiredMethod.Key.CallOrdinal);

if (!actualMethods.TryGetValue(requiredMethodCall, out IModelMethod actualMethod))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public CompositeVariableValueStorage(ICompositeModelValue modelValue)
methods = modelValue
.Methods
.ToDictionary(
method => new MethodCall(method.Name, method.Arguments),
method => new MethodCall(method.Name, method.Arguments, method.CallOrdinal),
method => method.Value != null ? CreateStorageForValue(method.Value) : null);
}

Expand Down
15 changes: 12 additions & 3 deletions Engine/Quokka.Core/Semantics/Variables/MethodCall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ internal sealed class MethodCall : IEquatable<MethodCall>
{
public string Name { get; }

public int? CallOrdinal { get; }

private readonly IReadOnlyList<object> argumentValues;

public MethodCall(string name, IReadOnlyList<object> argumentValues)
public MethodCall(string name, IReadOnlyList<object> argumentValues, int? callOrdinal = null)
{
Name = name;
this.argumentValues = argumentValues;
CallOrdinal = callOrdinal;
}

public IMethodCallDefinition ToMethodCallDefinition()
Expand All @@ -39,7 +42,8 @@ public IMethodCallDefinition ToMethodCallDefinition()
new MethodArgumentDefinition(
TypeDefinition.GetTypeDefinitionByRuntimeType(argumentValue.GetType()),
argumentValue))
.ToArray());
.ToArray(),
CallOrdinal);
}

public bool Equals(MethodCall other)
Expand All @@ -52,6 +56,9 @@ public bool Equals(MethodCall other)
if (!StringComparer.OrdinalIgnoreCase.Equals(Name, other.Name))
return false;

if (CallOrdinal != other.CallOrdinal)
return false;

if (argumentValues.Count != other.argumentValues.Count)
return false;

Expand Down Expand Up @@ -91,7 +98,9 @@ public override int GetHashCode()

public override string ToString()
{
return $"{Name}({string.Join(", ", argumentValues)})";
var callOrdinalSuffix = CallOrdinal == null ? string.Empty : $"#{CallOrdinal}";

return $"{Name}({string.Join(", ", argumentValues)}){callOrdinalSuffix}";
}
}
}
6 changes: 4 additions & 2 deletions Engine/Quokka.Core/Template.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ internal Template(
FunctionRegistry functionRegistry,
bool throwIfErrorsEncountered = true,
Func<VisitingContext, IQuokkaVisitor<StaticBlock>> staticBlockVisitorCreator = null,
IEnumerable<SemanticErrorSubListenerBase> semanticErrorSubListeners = null)
IEnumerable<SemanticErrorSubListenerBase> semanticErrorSubListeners = null,
IEnumerable<string> nonIdempotentMethodNames = null)
{
ArgumentNullException.ThrowIfNull(templateText);
ArgumentNullException.ThrowIfNull(functionRegistry);
Expand All @@ -69,7 +70,8 @@ internal Template(
{
VisitingContext visitingContext = new VisitingContext(
syntaxErrorListener,
staticBlockVisitorCreator ?? (context => new StaticBlockVisitor(context)));
staticBlockVisitorCreator ?? (context => new StaticBlockVisitor(context)),
nonIdempotentMethodNames);

compiledTemplateTree = new RootTemplateVisitor(visitingContext).Visit(templateParseTree);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@ internal class MethodMember : Member
{
private readonly string name;
private readonly IReadOnlyList<ArgumentValue> arguments;
private readonly int? callOrdinal;

private readonly MethodCall methodCall;

public MethodMember(string name, IEnumerable<ArgumentValue> arguments, Location location)
public MethodMember(
string name,
IEnumerable<ArgumentValue> arguments,
Location location,
int? callOrdinal = null)
: base(location)
{
this.name = name;
this.arguments = arguments.ToList().AsReadOnly();
this.callOrdinal = callOrdinal;

methodCall = BuildMethodCall();
}
Expand Down Expand Up @@ -72,7 +78,7 @@ private MethodCall BuildMethodCall()
.Where(argumentValue => argumentValue != null)
.ToList();

return new MethodCall(name, argumentValues);
return new MethodCall(name, argumentValues, callOrdinal);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,14 @@ public override Member VisitField(QuokkaParser.FieldContext context)

public override Member VisitMethodCall(QuokkaParser.MethodCallContext context)
{
var name = context.Identifier().GetText();
var argumentList = context.argumentList();

return new MethodMember(
context.Identifier().GetText(),
context.argumentList().Accept(new ArgumentListVisitor(VisitingContext)),
GetLocationFromToken(context.Identifier().Symbol));
name,
argumentList.Accept(new ArgumentListVisitor(VisitingContext)),
GetLocationFromToken(context.Identifier().Symbol),
VisitingContext.GetNextCallOrdinal(name, argumentList.GetText()));
}
}
}
26 changes: 25 additions & 1 deletion Engine/Quokka.Core/Templating/Visitors/VisitingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// // limitations under the License.

using System;
using System.Collections.Generic;

using Mindbox.Quokka.Generated;

Expand All @@ -22,19 +23,42 @@ internal class VisitingContext
{
private readonly Func<VisitingContext, IQuokkaVisitor<StaticBlock>> staticBlockFactoryMethod;

private readonly HashSet<string> nonIdempotentMethodNames;

private readonly Dictionary<(string Name, string Arguments), int> nonIdempotentMethodCallCounts = new();

public SyntaxErrorListener ErrorListener { get; }

public VisitingContext(
SyntaxErrorListener errorListener,
Func<VisitingContext, IQuokkaVisitor<StaticBlock>> staticBlockFactoryMethod)
Func<VisitingContext, IQuokkaVisitor<StaticBlock>> staticBlockFactoryMethod,
IEnumerable<string> nonIdempotentMethodNames = null)
{
this.staticBlockFactoryMethod = staticBlockFactoryMethod;
this.nonIdempotentMethodNames = new HashSet<string>(
nonIdempotentMethodNames ?? Array.Empty<string>(),
StringComparer.OrdinalIgnoreCase);
ErrorListener = errorListener;
}

public IQuokkaVisitor<StaticBlock> CreateStaticBlockVisitor()
{
return staticBlockFactoryMethod(this);
}

public int? GetNextCallOrdinal(string methodName, string argumentsSource)
{
if (!nonIdempotentMethodNames.Contains(methodName))
return null;

var callKey = (methodName.ToLowerInvariant(), (argumentsSource ?? string.Empty).ToLowerInvariant());
var callOrdinal = nonIdempotentMethodCallCounts.TryGetValue(callKey, out var previousCallOrdinal)
? previousCallOrdinal + 1
: 1;

nonIdempotentMethodCallCounts[callKey] = callOrdinal;

return callOrdinal;
}
}
}
Loading
Loading