Skip to content

Commit

Permalink
Initial spread implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-c-martin committed Mar 19, 2024
1 parent a5e1cb9 commit 3b87b56
Show file tree
Hide file tree
Showing 41 changed files with 1,009 additions and 242 deletions.
28 changes: 14 additions & 14 deletions src/Bicep.Core.IntegrationTests/EvaluationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1211,45 +1211,45 @@ public void New_functions_are_evaluated_correctly()
{
var evaluated = TemplateEvaluator.Evaluate(template);

evaluated.Should().HaveValueAtPath("$.outputs['sayHello'].value", JToken.Parse("""
evaluated.Should().HaveJsonAtPath("$.outputs['sayHello'].value", """
[
"Hi Evie!",
"Ahoy Casper!",
"Hi Lady Lechuga!"
]
"""));
""");

evaluated.Should().HaveValueAtPath("$.outputs['evenEntries'].value", JToken.Parse("""
evaluated.Should().HaveJsonAtPath("$.outputs['evenEntries'].value", """
[
"a",
"c"
]
"""));
""");

evaluated.Should().HaveValueAtPath("$.outputs['concatIfEven'].value", "abcghi");

evaluated.Should().HaveValueAtPath("$.outputs['mapValuesTest'].value", JToken.Parse("""
evaluated.Should().HaveJsonAtPath("$.outputs['mapValuesTest'].value", """
{
"a": 246,
"b": 912
}
"""));
""");

evaluated.Should().HaveValueAtPath("$.outputs['objectKeysTest'].value", JToken.Parse("""
evaluated.Should().HaveJsonAtPath("$.outputs['objectKeysTest'].value", """
[
"a",
"b"
]
"""));
""");

evaluated.Should().HaveValueAtPath("$.outputs['shallowMergeTest'].value", JToken.Parse("""
evaluated.Should().HaveJsonAtPath("$.outputs['shallowMergeTest'].value", """
{
"a": 123,
"b": 456
}
"""));
""");

evaluated.Should().HaveValueAtPath("$.outputs['groupByTest'].value", JToken.Parse("""
evaluated.Should().HaveJsonAtPath("$.outputs['groupByTest'].value", """
{
"a": [
{
Expand All @@ -1268,9 +1268,9 @@ public void New_functions_are_evaluated_correctly()
}
]
}
"""));
""");

evaluated.Should().HaveValueAtPath("$.outputs['groupByWithValMapTest'].value", JToken.Parse("""
evaluated.Should().HaveJsonAtPath("$.outputs['groupByWithValMapTest'].value", """
{
"a": [
123,
Expand All @@ -1280,7 +1280,7 @@ public void New_functions_are_evaluated_correctly()
456
]
}
"""));
""");
}
}
}
173 changes: 173 additions & 0 deletions src/Bicep.Core.IntegrationTests/SpreadTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Utils;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Bicep.Core.IntegrationTests;

[TestClass]
public class SpreadTests
{
[TestMethod]
public void Spread_operator_results_in_correct_codegen()
{
var result = CompilationHelper.Compile("""
var other = {
bar: [1, ...[2, 3], 4]
}

output test object = {
foo: 'foo'
...other
baz: 'baz'
}
""");

result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();
result.Template.Should().HaveValueAtPath("$.variables['other']['bar']", "[flatten(createArray(createArray(1), createArray(2, 3), createArray(4)))]");
result.Template.Should().HaveValueAtPath("$.outputs['test'].value", "[shallowMerge(createArray(createObject('foo', 'foo'), variables('other'), createObject('baz', 'baz')))]");

var evaluated = TemplateEvaluator.Evaluate(result.Template);
evaluated.Should().HaveJsonAtPath("$.outputs['test'].value", """
{
"foo": "foo",
"bar": [
1,
2,
3,
4
],
"baz": "baz"
}
""");
}

[TestMethod]
public void Spread_operator_works_on_single_line()
{
var result = CompilationHelper.Compile("""
param other object

var test = { foo: 'foo', ...other, baz: 'baz' }
""");

result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();
}

[TestMethod]
public void Array_spread_cannot_be_used_inside_object()
{
var result = CompilationHelper.Compile("""
var other = ['bar']

var test = {
foo: 'foo'
...other
baz: 'baz'
}
""");

result.ExcludingLinterDiagnostics().Should().ContainDiagnostic(
"BCP397", Diagnostics.DiagnosticLevel.Error, """The spread operator "..." can only be used in this context for an expression assignable to type "object".""");
}

[TestMethod]
public void Object_spread_cannot_be_used_inside_array()
{
var result = CompilationHelper.Compile("""
var other = {
bar: 'bar'
}

var test = [
'foo'
...other
'baz'
]
""");

result.ExcludingLinterDiagnostics().Should().ContainDiagnostic(
"BCP397", Diagnostics.DiagnosticLevel.Error, """The spread operator "..." can only be used in this context for an expression assignable to type "array".""");
}

[TestMethod]
public void Spread_works_with_any()
{
var result = CompilationHelper.Compile("""
var badObj = {
...any(['foo'])
}

var badArray = [
...any({ foo: 'foo' })
]
""");

result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();

// this will result in a runtime failure, but at least the codegen is correct.
result.Template.Should().HaveValueAtPath("$.variables['badObj']", "[shallowMerge(createArray(createArray('foo')))]");
result.Template.Should().HaveValueAtPath("$.variables['badArray']", "[flatten(createArray(createObject('foo', 'foo')))]");
}

[TestMethod]
public void Spread_is_blocked_in_resource_body()
{
var result = CompilationHelper.Compile("""
var other = { location: 'westus' }

resource foo 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'foo'
...other
}
""");

result.ExcludingLinterDiagnostics().Should().ContainDiagnostic(
"BCP396", Diagnostics.DiagnosticLevel.Error, """The spread operator "..." is not permitted in this location.""");
}

[TestMethod]
public void Object_spread_edge_cases()
{
var result = CompilationHelper.Compile("""
output test1 object = {
a: 0
...{ a: 1, b: 0 }
c: 0
}

output test2 object = {
'ABC': 0
...{ 'aBC': 1 }
}

output test3 object = {
foo: 'bar'
...{ foo: null }
}
""");

result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();

var evaluated = TemplateEvaluator.Evaluate(result.Template);
evaluated.Should().HaveJsonAtPath("$.outputs['test1'].value", """
{
"a": 1,
"b": 0,
"c": 0
}
""");
evaluated.Should().HaveJsonAtPath("$.outputs['test2'].value", """
{
"ABC": 1
}
""");
evaluated.Should().HaveJsonAtPath("$.outputs['test3'].value", """
{
"foo": null
}
""");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func sayHello(name string) string => 'Hi ${name}!'
//@[05:013) Function sayHello. Type: string => string. Declaration start char: 0, length: 50

output hellos array = map(['Evie', 'Casper'], name => sayHello(name))
//@[46:050) Local name. Type: any. Declaration start char: 46, length: 4
//@[46:050) Local name. Type: 'Casper' | 'Evie'. Declaration start char: 46, length: 4
//@[07:013) Output hellos. Type: array. Declaration start char: 0, length: 69

func objReturnType(name string) object => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var map4 = map(range(0, 10), () => null)
//@[29:39) [BCP070 (Error)] Argument of type "() => null" is not assignable to parameter of type "(any[, int]) => any". (CodeDescription: none) |() => null|
var map5 = map(range(0, 10), (a, b, c) => a)
//@[04:08) [no-unused-vars (Warning)] Variable "map5" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |map5|
//@[29:43) [BCP070 (Error)] Argument of type "(any, int, any) => any" is not assignable to parameter of type "(any[, int]) => any". (CodeDescription: none) |(a, b, c) => a|
//@[29:43) [BCP070 (Error)] Argument of type "(int, int, any) => int" is not assignable to parameter of type "(any[, int]) => any". (CodeDescription: none) |(a, b, c) => a|

var filter1 = filter('abc')
//@[04:11) [no-unused-vars (Warning)] Variable "filter1" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |filter1|
Expand All @@ -37,11 +37,13 @@ var filter4 = filter(range(0, 10), () => null)
//@[35:45) [BCP070 (Error)] Argument of type "() => null" is not assignable to parameter of type "(any[, int]) => bool". (CodeDescription: none) |() => null|
var filter5 = filter(range(0, 10), i => i)
//@[04:11) [no-unused-vars (Warning)] Variable "filter5" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |filter5|
//@[35:41) [BCP070 (Error)] Argument of type "int => int" is not assignable to parameter of type "(any[, int]) => bool". (CodeDescription: none) |i => i|
var filter6 = filter([true, 'hello!'], i => i)
//@[04:11) [no-unused-vars (Warning)] Variable "filter6" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |filter6|
//@[39:45) [BCP070 (Error)] Argument of type "('hello!' | true) => ('hello!' | true)" is not assignable to parameter of type "(any[, int]) => bool". (CodeDescription: none) |i => i|
var filter7 = filter(range(0, 10), (a, b, c) => true)
//@[04:11) [no-unused-vars (Warning)] Variable "filter7" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |filter7|
//@[35:52) [BCP070 (Error)] Argument of type "(any, int, any) => true" is not assignable to parameter of type "(any[, int]) => bool". (CodeDescription: none) |(a, b, c) => true|
//@[35:52) [BCP070 (Error)] Argument of type "(int, int, any) => true" is not assignable to parameter of type "(any[, int]) => bool". (CodeDescription: none) |(a, b, c) => true|

var sort1 = sort('abc')
//@[04:09) [no-unused-vars (Warning)] Variable "sort1" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |sort1|
Expand All @@ -57,9 +59,10 @@ var sort4 = sort(range(0, 10), () => null)
//@[31:41) [BCP070 (Error)] Argument of type "() => null" is not assignable to parameter of type "(any, any) => bool". (CodeDescription: none) |() => null|
var sort5 = sort(range(0, 10), i => i)
//@[04:09) [no-unused-vars (Warning)] Variable "sort5" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |sort5|
//@[31:37) [BCP070 (Error)] Argument of type "any => any" is not assignable to parameter of type "(any, any) => bool". (CodeDescription: none) |i => i|
//@[31:37) [BCP070 (Error)] Argument of type "int => int" is not assignable to parameter of type "(any, any) => bool". (CodeDescription: none) |i => i|
var sort6 = sort(range(0, 10), (i, j) => i)
//@[04:09) [no-unused-vars (Warning)] Variable "sort6" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |sort6|
//@[31:42) [BCP070 (Error)] Argument of type "(int, int) => int" is not assignable to parameter of type "(any, any) => bool". (CodeDescription: none) |(i, j) => i|

var reduce1 = reduce('abc')
//@[04:11) [no-unused-vars (Warning)] Variable "reduce1" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |reduce1|
Expand All @@ -75,10 +78,10 @@ var reduce4 = reduce(range(0, 10), 0, () => null)
//@[38:48) [BCP070 (Error)] Argument of type "() => null" is not assignable to parameter of type "(any, any[, int]) => any". (CodeDescription: none) |() => null|
var reduce5 = reduce(range(0, 10), 0, i => i)
//@[04:11) [no-unused-vars (Warning)] Variable "reduce5" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |reduce5|
//@[38:44) [BCP070 (Error)] Argument of type "any => any" is not assignable to parameter of type "(any, any[, int]) => any". (CodeDescription: none) |i => i|
//@[38:44) [BCP070 (Error)] Argument of type "int => int" is not assignable to parameter of type "(any, any[, int]) => any". (CodeDescription: none) |i => i|
var reduce6 = reduce(range(0, 10), 0, (a, b, c, d) => a)
//@[04:11) [no-unused-vars (Warning)] Variable "reduce6" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |reduce6|
//@[38:55) [BCP070 (Error)] Argument of type "(any, any, int, any) => any" is not assignable to parameter of type "(any, any[, int]) => any". (CodeDescription: none) |(a, b, c, d) => a|
//@[38:55) [BCP070 (Error)] Argument of type "(int, int, int, any) => int" is not assignable to parameter of type "(any, any[, int]) => any". (CodeDescription: none) |(a, b, c, d) => a|

var toObject1 = toObject('abc')
//@[04:13) [no-unused-vars (Warning)] Variable "toObject1" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |toObject1|
Expand All @@ -94,6 +97,7 @@ var toObject4 = toObject(range(0, 10), () => null)
//@[39:49) [BCP070 (Error)] Argument of type "() => null" is not assignable to parameter of type "any => string". (CodeDescription: none) |() => null|
var toObject5 = toObject(range(0, 10), i => i)
//@[04:13) [no-unused-vars (Warning)] Variable "toObject5" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |toObject5|
//@[39:45) [BCP070 (Error)] Argument of type "int => int" is not assignable to parameter of type "any => string". (CodeDescription: none) |i => i|
var toObject6 = toObject(range(0, 10), i => '${i}', 'def')
//@[04:13) [no-unused-vars (Warning)] Variable "toObject6" is declared but never used. (CodeDescription: bicep core(https://aka.ms/bicep/linter/no-unused-vars)) |toObject6|
//@[52:57) [BCP070 (Error)] Argument of type "'def'" is not assignable to parameter of type "any => any". (CodeDescription: none) |'def'|
Expand Down
Loading

0 comments on commit 3b87b56

Please sign in to comment.