Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate that type expressions that will be compiled to ARM schema nodes can be expressed in ARM's type system prior to compilation #15901

Merged
merged 4 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
24 changes: 24 additions & 0 deletions src/Bicep.Core.IntegrationTests/UserDefinedTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1823,4 +1823,28 @@ param siteProperties resourceInput<'Microsoft.Web/sites@2022-09-01'>.properties
"""The property "availabilityState" is read-only. Expressions cannot be assigned to read-only properties."""
);
}

// https://github.com/Azure/bicep/issues/15277
[DataTestMethod]
[DataRow("type resourceDerived = resourceInput<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties.settings")]
[DataRow("param resourceDerived resourceInput<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties.settings")]
[DataRow("output resourceDerived resourceInput<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties.settings = 'foo'")]
[DataRow("type t = { property: resourceInput<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties.settings }")]
[DataRow("type t = { *: resourceInput<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties.settings }")]
[DataRow("type t = [ resourceInput<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties.settings ]")]
[DataRow("type t = resourceInput<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties.settings[]")]
[DataRow("func f() resourceInput<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties.settings => 'foo'")]
[DataRow("func f(p resourceInput<'Microsoft.Compute/virtualMachines/extensions@2019-12-01'>.properties.settings) string => 'foo'")]
public void Type_expressions_that_will_become_ARM_schema_nodes_are_checked_for_ARM_type_system_compatibility_prior_to_compilation(string template)
{
var result = CompilationHelper.Compile(
new ServiceBuilder().WithFeatureOverrides(new(TestContext, ResourceDerivedTypesEnabled: true)),
template);

result.Template.Should().BeNull();
result.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[]
{
("BCP410", DiagnosticLevel.Error, """The type "any" cannot be used in a type assignment because it does not fit within one of ARM's primitive type categories (string, int, bool, array, object). If this is a resource type definition inaccuracy, report it using https://aka.ms/bicep-type-issues."""),
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type nullLiteral = null
//@[19:023) [BCP289 (Error)] The type definition is not valid. (bicep https://aka.ms/bicep/core-diagnostics#BCP289) |null|

type unionOfNulls = null|null
//@[20:029) [BCP410 (Error)] The type "null" cannot be used in a type assignment because it does not fit within one of ARM's primitive type categories (string, int, bool, array, object). If this is a resource type definition inaccuracy, report it using https://aka.ms/bicep-type-issues. (bicep https://aka.ms/bicep/core-diagnostics#BCP410) |null|null|
//@[20:029) [BCP294 (Error)] Type unions must be reducible to a single ARM type (such as 'string', 'int', or 'bool'). (bicep https://aka.ms/bicep/core-diagnostics#BCP294) |null|null|

@minLength(3)
Expand Down
4 changes: 4 additions & 0 deletions src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,10 @@ public Diagnostic ResourceParameterizedTypeIsDeprecated(ParameterizedTypeInstant
with
{ Fixes = [fixToResourceInput, fixToResourceOutput] };
}

public Diagnostic TypeExpressionResolvesToUnassignableType(TypeSymbol type) => CoreError(
"BCP410",
$"The type \"{type}\" cannot be used in a type assignment because it does not fit within one of ARM's primitive type categories (string, int, bool, array, object).{TypeInaccuracyClause}");
}

public static DiagnosticBuilderInternal ForPosition(TextSpan span)
Expand Down
54 changes: 50 additions & 4 deletions src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Concurrent;
using System.Collections.Immutable;
using Azure.Deployments.Core.Diagnostics;
using Azure.Deployments.Templates.Export;
using Bicep.Core.Diagnostics;
using Bicep.Core.Emit;
Expand Down Expand Up @@ -216,13 +217,15 @@ VariableBlockSyntax block when this.binder.GetParent(block) is LambdaSyntax lamb
});

public override void VisitTypedLocalVariableSyntax(TypedLocalVariableSyntax syntax)
=> AssignType(syntax, () =>
=> AssignTypeWithDiagnostics(syntax, diagnostics =>
{
if (typeManager.GetDeclaredType(syntax) is not { } declaredType)
{
return ErrorType.Empty();
}

diagnostics.WriteMultiple(ValidateTypeAssignability(syntax.Type, declaredType));

base.VisitTypedLocalVariableSyntax(syntax);

return declaredType;
Expand Down Expand Up @@ -521,9 +524,10 @@ public override void VisitTypeDeclarationSyntax(TypeDeclarationSyntax syntax)

if (declaredType is not null)
{
ValidateDecorators(syntax.Decorators,
declaredType is TypeType wrapped ? wrapped.Unwrapped : declaredType,
diagnostics);
var unwrapped = declaredType is TypeType wrapped ? wrapped.Unwrapped : declaredType;
ValidateDecorators(syntax.Decorators, unwrapped, diagnostics);

diagnostics.WriteMultiple(ValidateTypeAssignability(syntax.Value, unwrapped));
}

return declaredType ?? ErrorType.Empty();
Expand Down Expand Up @@ -600,6 +604,8 @@ public override void VisitArrayTypeMemberSyntax(ArrayTypeMemberSyntax syntax)

base.VisitArrayTypeMemberSyntax(syntax);

diagnostics.WriteMultiple(ValidateTypeAssignability(syntax.Value, declaredType));

return declaredType;
});

Expand Down Expand Up @@ -878,6 +884,7 @@ private TypeSymbol GetDeclaredTypeAndValidateDecorators(DecorableSyntax targetSy
}

this.ValidateDecorators(targetSyntax.Decorators, declaredType, diagnostics);
diagnostics.WriteMultiple(ValidateTypeAssignability(typeSyntax, declaredType));

return declaredType;
}
Expand Down Expand Up @@ -1958,6 +1965,8 @@ public override void VisitTypedLambdaSyntax(TypedLambdaSyntax syntax)
CollectErrors(errors, argumentType.Type);
}

diagnostics.WriteMultiple(ValidateTypeAssignability(syntax.ReturnType, declaredLambdaType.ReturnType.Type));

var returnType = TypeValidator.NarrowTypeAndCollectDiagnostics(typeManager, binder, this.parsingErrorLookup, diagnostics, syntax.Body, declaredLambdaType.ReturnType.Type);
CollectErrors(errors, returnType);

Expand Down Expand Up @@ -2396,6 +2405,43 @@ private IEnumerable<IDiagnostic> ValidateDefaultValue(ParameterDefaultValueSynta
return diagnosticWriter.GetDiagnostics();
}

private IEnumerable<IDiagnostic> ValidateTypeAssignability(SyntaxBase typeSyntax, TypeSymbol assignedType)
{
if (typeSyntax is not SkippedTriviaSyntax &&
assignedType is not ErrorType &&
TryGetArmPrimitiveType(assignedType, typeSyntax) is null)
{
yield return DiagnosticBuilder.ForPosition(typeSyntax)
.TypeExpressionResolvesToUnassignableType(assignedType);
}
}

private TypeSymbol? TryGetArmPrimitiveType(TypeSymbol type, SyntaxBase syntax) => type switch
{
BooleanLiteralType or BooleanType => LanguageConstants.Bool,
IntegerLiteralType or IntegerType => LanguageConstants.Int,
StringLiteralType or StringType => LanguageConstants.String,
ResourceType when features.ResourceTypedParamsAndOutputsEnabled => LanguageConstants.String,
ObjectType or DiscriminatedObjectType => LanguageConstants.Object,
TupleType or ArrayType => LanguageConstants.Array,
UnionType when TypeHelper.TryRemoveNullability(type) is { } nonNull => TryGetArmPrimitiveType(nonNull, syntax),
UnionType when IsExplicitUnion(syntax) => LanguageConstants.Any,
UnionType union when union.Members.Select(m => TryGetArmPrimitiveType(m.Type, syntax)).ToArray() is { } mTypes &&
!mTypes.Any(t => t is null) &&
mTypes.ToHashSet() is { } mUniqueTypes &&
mUniqueTypes.Count == 1 => mUniqueTypes.Single(),
_ => null,
};

private static bool IsExplicitUnion(SyntaxBase syntax) => syntax switch
{
UnionTypeSyntax => true,
ParenthesizedTypeSyntax parenthesized => IsExplicitUnion(parenthesized.Expression),
NonNullableTypeSyntax nonNullable => IsExplicitUnion(nonNullable.Base),
NullableTypeSyntax nullable => IsExplicitUnion(nullable.Base),
_ => false,
};

private IEnumerable<IDiagnostic> ValidateIdentifierAccess(SyntaxBase syntax)
{
return SyntaxAggregator.Aggregate(syntax, new List<IDiagnostic>(), (accumulated, current) =>
Expand Down
Loading