Skip to content
Open
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
26 changes: 26 additions & 0 deletions src/Bicep.Core.IntegrationTests/ModuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,19 @@ private static IEnumerable<object[]> 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)),

Expand Down
70 changes: 60 additions & 10 deletions src/Bicep.Core/TypeSystem/TypeValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<StringLiteralType> 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<StringLiteralType>().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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
});
};
}
Expand Down
Loading