Skip to content

Commit

Permalink
Nullable types (#9454)
Browse files Browse the repository at this point in the history
* Segregate type parser

* Add support for nullable types

* Generate baselines

* Disallow default values on nullably-typed params

* Update decorator processing to support nullable types

* Update ArmTemplateSemanticModel to handle "nullable" schema property

* Rebase fixup

* Update Azure.Deployments.* dependencies to latest

* Compact diagnostics

* Post-merge fixup

* Fixup baseline merge

* Incorporate feedback on completion tests and Decorator commentary

* Move invalid recursion detection to EmitLimitationCalculation so that it can build on parsed types
  • Loading branch information
jeskew committed Feb 27, 2023
1 parent 01dd12d commit 3add39c
Show file tree
Hide file tree
Showing 297 changed files with 1,929 additions and 1,354 deletions.
1 change: 1 addition & 0 deletions docs/grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ typeExpression -> singularTypeExpression ("|" singularTypeExpression)*
singularTypeExpression ->
primaryTypeExpression |
singularTypeExpression "[]" |
singularTypeExpression "?" |
parenthesizedTypeExpression
primaryTypeExpression ->
Expand Down
8 changes: 4 additions & 4 deletions src/Bicep.Core.IntegrationTests/ExtensibilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -458,14 +458,14 @@ public void Storage_import_end_to_end_test()
result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();
result.Template.Should().DeepEqual(JToken.Parse(@"{
""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"",
""languageVersion"": ""1.9-experimental"",
""languageVersion"": ""1.10-experimental"",
""contentVersion"": ""1.0.0.0"",
""metadata"": {
""_EXPERIMENTAL_WARNING"": ""Symbolic name support in ARM is experimental, and should be enabled for testing purposes only. Do not enable this setting for any production usage, or you may be unexpectedly broken at any time!"",
""_generator"": {
""name"": ""bicep"",
""version"": ""dev"",
""templateHash"": ""8036895127623403713""
""templateHash"": ""12622870383828628423""
}
},
""parameters"": {
Expand Down Expand Up @@ -500,14 +500,14 @@ public void Storage_import_end_to_end_test()
},
""template"": {
""$schema"": ""https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"",
""languageVersion"": ""1.9-experimental"",
""languageVersion"": ""1.10-experimental"",
""contentVersion"": ""1.0.0.0"",
""metadata"": {
""_EXPERIMENTAL_WARNING"": ""Symbolic name support in ARM is experimental, and should be enabled for testing purposes only. Do not enable this setting for any production usage, or you may be unexpectedly broken at any time!"",
""_generator"": {
""name"": ""bicep"",
""version"": ""dev"",
""templateHash"": ""5609881366445430907""
""templateHash"": ""3998586609553744579""
}
},
""parameters"": {
Expand Down
191 changes: 164 additions & 27 deletions src/Bicep.Core.IntegrationTests/UserDefinedTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ public void Inline_union_types_are_disabled_unless_feature_is_enabled()
result.Should().ContainDiagnostic("BCP284", DiagnosticLevel.Error, "Using a type union declaration requires enabling EXPERIMENTAL feature \"UserDefinedTypes\".");
}

[TestMethod]
public void Nullable_types_are_disabled_unless_feature_is_enabled()
{
var result = CompilationHelper.Compile(@"
param nullableString string?
");
result.Should().ContainDiagnostic("BCP324", DiagnosticLevel.Error, "Using nullable types requires enabling EXPERIMENTAL feature \"UserDefinedTypes\".");
}

[TestMethod]
public void Namespaces_cannot_be_used_as_types()
{
Expand Down Expand Up @@ -217,12 +226,12 @@ public void Unions_that_incorporate_their_parent_object_do_not_blow_the_stack()

blockedBecauseOfCycle.Should().HaveDiagnostics(new[] {
("BCP298", DiagnosticLevel.Error, "This type definition includes itself as required component, which creates a constraint that cannot be fulfilled."),
("BCP062", DiagnosticLevel.Error, "The referenced declaration with name \"anObject\" is not valid."),
("BCP293", DiagnosticLevel.Error, "All members of a union type declaration must be literal values."),
});

var blockedBecauseOfUnionSemantics = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recur?: {foo: 'bar'}|anObject
recur: {foo: 'bar'}|anObject?
}
");

Expand All @@ -241,17 +250,6 @@ public void Unary_operations_that_incorporate_their_parent_object_do_not_blow_th
");

blockedBecauseOfCycle.Should().HaveDiagnostics(new[] {
("BCP298", DiagnosticLevel.Error, "This type definition includes itself as required component, which creates a constraint that cannot be fulfilled."),
("BCP062", DiagnosticLevel.Error, "The referenced declaration with name \"anObject\" is not valid."),
});

var blockedBecauseOfUnionSemantics = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recur?: !anObject
}
");

blockedBecauseOfUnionSemantics.Should().HaveDiagnostics(new[] {
("BCP285", DiagnosticLevel.Error, "The type expression could not be reduced to a literal value."),
});
}
Expand All @@ -267,39 +265,81 @@ public void Arrays_that_incorporate_their_parent_object_do_not_blow_the_stack()

blockedBecauseOfCycle.Should().HaveDiagnostics(new[] {
("BCP298", DiagnosticLevel.Error, "This type definition includes itself as required component, which creates a constraint that cannot be fulfilled."),
("BCP062", DiagnosticLevel.Error, "The referenced declaration with name \"anObject\" is not valid."),
});

var blockedBecauseOfUnionSemantics = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
var permitted = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recur?: anObject[]
recur: (anObject?)[]
}
");

blockedBecauseOfUnionSemantics.Should().NotHaveAnyDiagnostics();
permitted.Should().NotHaveAnyDiagnostics();

permitted = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anArray = (anArray?)[]
");

permitted.Should().NotHaveAnyDiagnostics();

permitted = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anArray = anArray[]?
");

permitted.Should().NotHaveAnyDiagnostics();
}

[TestMethod]
public void Cyclic_nullables_do_not_blow_the_stack()
{
var blockedBecauseOfCycle = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type nullable = nullable?
");

blockedBecauseOfCycle.Should().HaveDiagnostics(new[] {
("BCP298", DiagnosticLevel.Error, "This type definition includes itself as required component, which creates a constraint that cannot be fulfilled."),
});
}

[TestMethod]
public void Tuples_that_incorporate_their_parent_object_do_not_blow_the_stack()
{
var blockedBecauseOfCycle = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recurEventually: [anObject]
recur: [anObject]
}
");

blockedBecauseOfCycle.Should().HaveDiagnostics(new[] {
("BCP298", DiagnosticLevel.Error, "This type definition includes itself as required component, which creates a constraint that cannot be fulfilled."),
("BCP062", DiagnosticLevel.Error, "The referenced declaration with name \"anObject\" is not valid."),
});

var blockedBecauseOfUnionSemantics = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
var permitted = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recur?: [anObject]
recur: [anObject]?
}
");

blockedBecauseOfUnionSemantics.Should().NotHaveAnyDiagnostics();
permitted.Should().NotHaveAnyDiagnostics();

permitted = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recur: [anObject?]
}
");

permitted.Should().NotHaveAnyDiagnostics();

permitted = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type aTuple = [aTuple?]
");

permitted.Should().NotHaveAnyDiagnostics();

permitted = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type aTuple = [aTuple]?
");

permitted.Should().NotHaveAnyDiagnostics();
}

[TestMethod]
Expand All @@ -315,18 +355,75 @@ public void Objects_that_incorporate_their_parent_object_do_not_blow_the_stack()

blockedBecauseOfCycle.Should().HaveDiagnostics(new[] {
("BCP298", DiagnosticLevel.Error, "This type definition includes itself as required component, which creates a constraint that cannot be fulfilled."),
("BCP062", DiagnosticLevel.Error, "The referenced declaration with name \"anObject\" is not valid."),
});

var blockedBecauseOfUnionSemantics = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
var permitted = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recurEventually?: {
recurNow: anObject
recurEventually: {
recurNow: anObject?
}
}
");

blockedBecauseOfUnionSemantics.Should().NotHaveAnyDiagnostics();
permitted.Should().NotHaveAnyDiagnostics();

permitted = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recurEventually: {
recurNow: anObject
}?
}
");

permitted.Should().NotHaveAnyDiagnostics();

permitted = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recurEventually: {
recurNow: anObject
}
}?
");

permitted.Should().NotHaveAnyDiagnostics();
}

[TestMethod]
public void Cyclic_check_understands_nullability_modifiers()
{
var blockedBecauseOfCycle = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recurEventually: {
recurNow: anObject!
}
}?
");

blockedBecauseOfCycle.Should().HaveDiagnostics(new[] {
("BCP298", DiagnosticLevel.Error, "This type definition includes itself as required component, which creates a constraint that cannot be fulfilled."),
});

blockedBecauseOfCycle = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recurEventually: {
recurNow: anObject
}
}?!
");

blockedBecauseOfCycle.Should().HaveDiagnostics(new[] {
("BCP298", DiagnosticLevel.Error, "This type definition includes itself as required component, which creates a constraint that cannot be fulfilled."),
});

var permitted = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
type anObject = {
recurEventually: {
recurNow: anObject!?
}
}?
");

permitted.Should().NotHaveAnyDiagnostics();
}

[TestMethod]
Expand Down Expand Up @@ -400,6 +497,33 @@ public void Additional_properties_may_be_used_alongside_named_properties()
result.Should().NotHaveAnyDiagnostics();
}

[TestMethod]
public void Constraint_decorators_can_be_used_on_nullably_typed_params()
{
var result = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
@minLength(3)
@maxLength(10)
@secure()
#disable-next-line no-unused-params
param constrainedString string?
@minValue(3)
@maxValue(10)
type constrainedInt = int?
@minLength(3)
@maxLength(10)
type constrainedArray = array?
@sealed()
@secure()
#disable-next-line no-unused-params
param sealedObject {}?
");

result.Should().NotHaveAnyDiagnostics();
}

[TestMethod]
public void Nullably_typed_values_can_be_used_as_nonnullable_outputs_with_postfix_assertion()
{
Expand Down Expand Up @@ -445,6 +569,19 @@ public void Nullably_typed_values_can_be_used_as_nonnullable_outputs_with_postfi
result.Should().HaveTemplateWithOutput("quux", "[parameters('foos')[0].bar.baz.quux]");
}

[TestMethod]
public void Error_should_be_emitted_when_setting_a_default_value_on_a_nullable_parameter()
{
var result = CompilationHelper.Compile(ServicesWithUserDefinedTypes, @"
#disable-next-line no-unused-params
param myParam string? = 'foo'
");

result.Should().HaveDiagnostics(new[] {
("BCP326", DiagnosticLevel.Error, "Nullable-typed parameters may not be assigned default values. They have an implicit default of 'null' that cannot be overridden."),
});
}

[TestMethod]
public void Tuples_with_a_literal_index_use_type_at_index()
{
Expand Down
4 changes: 2 additions & 2 deletions src/Bicep.Core.Samples/Files/AKS_LF/main.symbolicnames.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"languageVersion": "1.9-experimental",
"languageVersion": "1.10-experimental",
"contentVersion": "1.0.0.0",
"metadata": {
"_EXPERIMENTAL_WARNING": "Symbolic name support in ARM is experimental, and should be enabled for testing purposes only. Do not enable this setting for any production usage, or you may be unexpectedly broken at any time!",
"_generator": {
"name": "bicep",
"version": "dev",
"templateHash": "16174843579079418342"
"templateHash": "15614060886754515121"
}
},
"parameters": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"languageVersion": "1.9-experimental",
"languageVersion": "1.10-experimental",
"contentVersion": "1.0.0.0",
"metadata": {
"_EXPERIMENTAL_WARNING": "Symbolic name support in ARM is experimental, and should be enabled for testing purposes only. Do not enable this setting for any production usage, or you may be unexpectedly broken at any time!",
"_generator": {
"name": "bicep",
"version": "dev",
"templateHash": "156273920754641258"
"templateHash": "12789338743853513147"
}
},
"parameters": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"languageVersion": "1.9-experimental",
"languageVersion": "1.10-experimental",
"contentVersion": "1.0.0.0",
"metadata": {
"_EXPERIMENTAL_WARNING": "Symbolic name support in ARM is experimental, and should be enabled for testing purposes only. Do not enable this setting for any production usage, or you may be unexpectedly broken at any time!",
"_generator": {
"name": "bicep",
"version": "dev",
"templateHash": "7553805340907595059"
"templateHash": "2686559331697591186"
}
},
"parameters": {
Expand Down
4 changes: 2 additions & 2 deletions src/Bicep.Core.Samples/Files/Empty/main.symbolicnames.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"languageVersion": "1.9-experimental",
"languageVersion": "1.10-experimental",
"contentVersion": "1.0.0.0",
"metadata": {
"_EXPERIMENTAL_WARNING": "Symbolic name support in ARM is experimental, and should be enabled for testing purposes only. Do not enable this setting for any production usage, or you may be unexpectedly broken at any time!",
"_generator": {
"name": "bicep",
"version": "dev",
"templateHash": "2039841905975817114"
"templateHash": "5355286140080065457"
}
},
"resources": {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"languageVersion": "1.9-experimental",
"languageVersion": "1.10-experimental",
"contentVersion": "1.0.0.0",
"metadata": {
"_EXPERIMENTAL_WARNING": "Symbolic name support in ARM is experimental, and should be enabled for testing purposes only. Do not enable this setting for any production usage, or you may be unexpectedly broken at any time!",
"_generator": {
"name": "bicep",
"version": "dev",
"templateHash": "3836420531154098891"
"templateHash": "17782635257394697277"
}
},
"variables": {
Expand Down
Loading

0 comments on commit 3add39c

Please sign in to comment.