diff --git a/src/Bicep.Core.IntegrationTests/ModuleTests.cs b/src/Bicep.Core.IntegrationTests/ModuleTests.cs index ea0d478e4d9..a91feba7d39 100644 --- a/src/Bicep.Core.IntegrationTests/ModuleTests.cs +++ b/src/Bicep.Core.IntegrationTests/ModuleTests.cs @@ -95,6 +95,32 @@ param inputb string compilation.GetTestTemplate().Should().NotBeEmpty(); } + [TestMethod] + public void Module_param_string_assignment_to_string_literal_union_should_warn_and_compile() + { + var result = CompilationHelper.Compile( + ("main.bicep", @" +var foobar string = any('foo') + +module mod './mod.bicep' = { + name: 'mod' + params: { + foobar: foobar + } +} +"), + ("mod.bicep", @" +@allowed(['foo', 'bar']) +param foobar string +")); + + result.Should().NotHaveAnyCompilationBlockingDiagnostics(); + result.Should().HaveDiagnostics(new[] + { + ("BCP036", DiagnosticLevel.Warning, "The property \"foobar\" expected a value of type \"'bar' | 'foo'\" but the provided value is of type \"string\"."), + }); + } + [TestMethod] public void Module_self_cycle_is_detected_correctly() { diff --git a/src/Bicep.Core.UnitTests/TypeSystem/TypeValidatorAssignabilityTests.cs b/src/Bicep.Core.UnitTests/TypeSystem/TypeValidatorAssignabilityTests.cs index 931b2c311b8..76712e141bc 100644 --- a/src/Bicep.Core.UnitTests/TypeSystem/TypeValidatorAssignabilityTests.cs +++ b/src/Bicep.Core.UnitTests/TypeSystem/TypeValidatorAssignabilityTests.cs @@ -251,11 +251,19 @@ private static IEnumerable GetStringDomainNarrowingData() static object[] Row(TypeSymbol sourceType, TypeSymbol targetType, TypeSymbol expectedType, params (string code, DiagnosticLevel level, string message)[] diagnostics) => [sourceType, targetType, expectedType, diagnostics]; + var fooBarUnion = TypeHelper.CreateTypeUnion(TypeFactory.CreateStringLiteralType("foo"), TypeFactory.CreateStringLiteralType("bar")); + return new[] { // A matching source and target type should narrow to the same and produce no warnings Row(LanguageConstants.String, LanguageConstants.String, LanguageConstants.String), + // A generic source string overlaps with a fixed set of string literals, so it should narrow and warn + Row(LanguageConstants.String, + fooBarUnion, + fooBarUnion, + ("BCP033", DiagnosticLevel.Warning, "Expected a value of type \"'bar' | 'foo'\" but the provided value is of type \"string\".")), + // A source type whose domain is a subset of the target type should narrow to the source Row(TypeFactory.CreateStringType(1, 10), TypeFactory.CreateStringType(0, 11), TypeFactory.CreateStringType(1, 10)), diff --git a/src/Bicep.Core/TypeSystem/TypeValidator.cs b/src/Bicep.Core/TypeSystem/TypeValidator.cs index adbe77f5221..fce53f86c73 100644 --- a/src/Bicep.Core/TypeSystem/TypeValidator.cs +++ b/src/Bicep.Core/TypeSystem/TypeValidator.cs @@ -16,7 +16,7 @@ namespace Bicep.Core.TypeSystem { public class TypeValidator { - private delegate void TypeMismatchDiagnosticWriter(TypeSymbol targetType, TypeSymbol expressionType, SyntaxBase expression); + private delegate void TypeMismatchDiagnosticWriter(TypeSymbol targetType, TypeSymbol expressionType, SyntaxBase expression, bool warnInsteadOfError); private readonly ITypeManager typeManager; @@ -287,17 +287,20 @@ private TypeSymbol NarrowType(TypeValidatorConfig config, SyntaxBase expression, return NarrowType(config, expression, nonNullableExpressionType, targetType); } + var narrowedStringLiteralType = TryNarrowStringTypeToFixedStringLiteralType(expressionType, targetType); + var warnInsteadOfError = narrowedStringLiteralType is not null; + // fundamentally different types - cannot assign if (config.OnTypeMismatch is not null) { - config.OnTypeMismatch(targetType, expressionType, expression); + config.OnTypeMismatch(targetType, expressionType, expression, warnInsteadOfError); } else { - diagnosticWriter.Write(config.OriginSyntax ?? expression, x => x.ExpectedValueTypeMismatch(ShouldWarn(targetType), targetType, expressionType)); + diagnosticWriter.Write(config.OriginSyntax ?? expression, x => x.ExpectedValueTypeMismatch(ShouldWarn(targetType) || warnInsteadOfError, targetType, expressionType)); } - return targetType; + return narrowedStringLiteralType ?? targetType; } if (targetType is ResourceParameterType) @@ -396,6 +399,52 @@ private static bool AreLambdaTypesAssignable(LambdaType source, LambdaType targe return true; } + private static TypeSymbol? TryNarrowStringTypeToFixedStringLiteralType(TypeSymbol expressionType, TypeSymbol targetType) + { + if (expressionType is not StringType expressionString) + { + return null; + } + + var targetLiteralTypes = GetFixedStringLiteralTypes(targetType); + if (targetLiteralTypes.IsEmpty) + { + return null; + } + + var overlappingLiteralTypes = targetLiteralTypes + .Where(targetLiteral => IsStringLiteralInDomain(expressionString, targetLiteral)) + .ToImmutableArray(); + + return overlappingLiteralTypes.IsEmpty + ? null + : TypeHelper.CreateTypeUnion(overlappingLiteralTypes); + } + + private static ImmutableArray GetFixedStringLiteralTypes(TypeSymbol targetType) => targetType switch + { + StringLiteralType stringLiteral => [stringLiteral], + UnionType union when union.Members.All(member => member.Type is StringLiteralType or NullType) => + union.Members.Select(member => member.Type).OfType().ToImmutableArray(), + _ => [], + }; + + private static bool IsStringLiteralInDomain(StringType stringType, StringLiteralType stringLiteralType) + { + var stringLength = stringLiteralType.RawStringValue.Length; + if (stringType.MinLength.HasValue && stringLength < stringType.MinLength.Value) + { + return false; + } + + if (stringType.MaxLength.HasValue && stringLength > stringType.MaxLength.Value) + { + return false; + } + + return stringType.Pattern is null || TypeHelper.MatchesPattern(stringType.Pattern, stringLiteralType.RawStringValue); + } + private TypeSymbol NarrowIntegerAssignmentType(TypeValidatorConfig config, SyntaxBase expression, TypeSymbol expressionType, IntegerType targetType) { bool shouldWarn = config.IsResourceDeclaration || ShouldWarn(targetType); @@ -686,7 +735,7 @@ private TypeSymbol NarrowArrayAssignmentType(TypeValidatorConfig config, ArraySy var newConfig = config with { SkipTypeErrors = true, - OnTypeMismatch = (expected, actual, position) => diagnosticWriter.Write(position, x => x.ArrayTypeMismatch(ShouldWarn(targetType), expected, actual)), + OnTypeMismatch = (expected, actual, position, warnInsteadOfError) => diagnosticWriter.Write(position, x => x.ArrayTypeMismatch(ShouldWarn(targetType) || warnInsteadOfError, expected, actual)), }; var narrowedItem = NarrowType(newConfig, item.Value, itemTarget); @@ -700,7 +749,7 @@ private TypeSymbol NarrowArrayAssignmentType(TypeValidatorConfig config, ArraySy var newConfig = config with { SkipTypeErrors = true, - OnTypeMismatch = (expected, actual, position) => diagnosticWriter.Write(position, x => x.ArrayTypeMismatchSpread(ShouldWarn(targetType), expected, actual)), + OnTypeMismatch = (expected, actual, position, warnInsteadOfError) => diagnosticWriter.Write(position, x => x.ArrayTypeMismatchSpread(ShouldWarn(targetType) || warnInsteadOfError, expected, actual)), }; var narrowedSpread = NarrowType(newConfig, spread.Expression, spreadTarget); @@ -1313,26 +1362,27 @@ TypeSymbol GetNarrowedPropertyType() private TypeMismatchDiagnosticWriter GetPropertyMismatchDiagnosticWriter(TypeValidatorConfig config, bool shouldWarn, string propertyName, bool showTypeInaccuracyClause) { - return (expectedType, actualType, errorExpression) => + return (expectedType, actualType, errorExpression, warnInsteadOfError) => { diagnosticWriter.Write( config.OriginSyntax ?? errorExpression, x => { + var shouldWarnForMismatch = shouldWarn || warnInsteadOfError; var sourceDeclaration = TryGetSourceDeclaration(config); if (sourceDeclaration is not null) { // only look up suggestions if we're not sourcing this type from another declaration. - return x.PropertyTypeMismatch(shouldWarn, sourceDeclaration, propertyName, expectedType, actualType, showTypeInaccuracyClause); + return x.PropertyTypeMismatch(shouldWarnForMismatch, sourceDeclaration, propertyName, expectedType, actualType, showTypeInaccuracyClause); } if (actualType is StringLiteralType actualStringLiteral && TryGetStringLiteralSuggestion(actualStringLiteral, expectedType) is { } suggestion) { - return x.PropertyStringLiteralMismatchWithSuggestion(shouldWarn, propertyName, expectedType, actualType.Name, suggestion); + return x.PropertyStringLiteralMismatchWithSuggestion(shouldWarnForMismatch, propertyName, expectedType, actualType.Name, suggestion); } - return x.PropertyTypeMismatch(shouldWarn, sourceDeclaration, propertyName, expectedType, actualType, showTypeInaccuracyClause); + return x.PropertyTypeMismatch(shouldWarnForMismatch, sourceDeclaration, propertyName, expectedType, actualType, showTypeInaccuracyClause); }); }; }