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

Handle more (if not all) corner cases in DTC validation #2769

Merged
merged 10 commits into from
May 26, 2021

Conversation

shenglol
Copy link
Contributor

@shenglol shenglol commented May 21, 2021

The PR fixes #2291 and fixes #2622.

This is basically an attempt to improve the current deploy-time constant checking logic to handle all cases. As the current DeployTimeConstantVisitor and DeployTimeConstaintVariableVisitor maintain several states and do the validation in one pass, it becomes a bit head-scratching when trying to update it to cover more edge cases. To make the code more maintainable I split the two visitors into three:

  • DeployTimeConstantContainerVisitor that collects all syntaxes requiring deploy-time constant values (DTC containers)
  • DeployTimeDirectViolationVisitor that visits all DTC containers and validate property accesses and function calls
  • DeployTimeIndirectViolationVisitor that visits all DTC containers and validate variable dependencies

@@ -292,22 +292,22 @@ module runtimeInvalidModule2 'empty.bicep' = {

module runtimeInvalidModule3 'empty.bicep' = {
name: runtimeValidRes1.sku.name
//@[8:33) [BCP120 (Error)] The property "name" must be evaluable at the start of the deployment, and cannot depend on any values that have not yet been calculated. Accessible properties of runtimeValidRes1 are "apiVersion", "id", "name", "type". |runtimeValidRes1.sku.name|
//@[8:28) [BCP120 (Error)] The property "name" must be evaluable at the start of the deployment, and cannot depend on any values that have not yet been calculated. Accessible properties of runtimeValidRes1 are "apiVersion", "id", "name", "type". |runtimeValidRes1.sku|
Copy link
Contributor Author

@shenglol shenglol May 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old DeployTimeConstantVisitor contains some ad-hoc logic to make sure the error on the full path. Personally I like the new error message better not only because it simplifies the code, but also essentially it is the top level property that triggers the error.

this.diagnosticWriter.Write(DiagnosticBuilder.ForPosition(syntax.ForKeyword).VariableLoopsRuntimeDependencyNotAllowed(variableChain));

break;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the logic to check DTC in ForSyntax from the class because the new DTC validator now convers it, and it can produce more precise error messages.

{
if (syntax.TryGetKeyText() is { } propertyName &&
this.semanticModel.Binder.GetParent(syntax) is { } objectSyntax &&
this.semanticModel.TypeManager.GetDeclaredType(objectSyntax) is ObjectType objectType &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we'll ignore DiscriminiatedObjectType instances? Using the assigned type (rather than declared) should have resolved discriminated object types -> object types, as long as the DTC flag has been preserved properly.

Copy link
Contributor Author

@shenglol shenglol May 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, we should also handle DiscriminiatedObjectType, but using the assigned type won't work for properties whose values contain string interpolations or array accesses, as the DTC flag is not propagated in those cases. I've updated the code block to process DiscriminiatedObjectType instances and turned it into a method of ObjectPropertySyntax.

private void FlagIfPropertyNotReadableAtDeployTime(SyntaxBase syntax, string propertyName, DeclaredSymbol accessedSymbol, ObjectType accessedBodyType)
{
if (accessedBodyType.Properties.TryGetValue(propertyName, out var propertyType) &&
!propertyType.Flags.HasFlag(TypePropertyFlags.ReadableAtDeployTime))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's necessary here unless this is being called a lot, but @rynowak made an interesting observation here about perf of HasFlag vs bitwise operations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's good to know. I was searching for details about this and I happened to find a benchmark testing HasFlag vs bitwise operations, which suggests that we don't need to do the optimization to eliminate boxing/unboxing by ourselves. The .NET team has implemented some JIT optimization to generate bitwise operations for us since .NET core 2.1, which is documented in https://devblogs.microsoft.com/dotnet/ryujit-just-in-time-compiler-optimization-enhancements/.

//@[15:65) [BCP120 (Error)] The property "name" must be evaluable at the start of the deployment, and cannot depend on any values that have not yet been calculated. Accessible properties of moduleLoopForRuntimeCheck are "name". |moduleLoopForRuntimeCheck[1].outputs.stringOutputB|
//@[67:117) [BCP120 (Error)] The property "name" must be evaluable at the start of the deployment, and cannot depend on any values that have not yet been calculated. Accessible properties of moduleLoopForRuntimeCheck are "name". |moduleLoopForRuntimeCheck[1].outputs.stringOutputA|
//@[15:51) [BCP120 (Error)] The property "name" must be evaluable at the start of the deployment, and cannot depend on any values that have not yet been calculated. Accessible properties of moduleLoopForRuntimeCheck are "name". |moduleLoopForRuntimeCheck[1].outputs|
//@[67:103) [BCP120 (Error)] The property "name" must be evaluable at the start of the deployment, and cannot depend on any values that have not yet been calculated. Accessible properties of moduleLoopForRuntimeCheck are "name". |moduleLoopForRuntimeCheck[1].outputs|
Copy link
Member

@anthony-c-martin anthony-c-martin May 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the change to the span that the squiggly is being displayed on. Seeing it here, I wonder if there's something we can do to make this message a bit more obvious - given that the squiggly is being displayed on the value rather than the key, it's not immediately obvious that name in the message relates to the property value assignment, which could be especially hard to follow on a larger expression.

What about something like: 'This expression is being used in an assignment to the "{key}" property of the "{type}" type, which requires a value that can be calculated at the start of the deployment. Properties of "{symbol}" which can be calculated at the start include "{list}".'

I'm interested to hear other people's thoughts here as well - worth spending a bit of time getting right I think!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion! Created #2863 to track it. Will submit another PR to implement it.

Copy link
Member

@anthony-c-martin anthony-c-martin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice work tidying this up!

? variableDeclarationSyntax.Name.IdentifierName
: null;

protected (DeclaredSymbol?, ObjectType?) TryExtractResourceOrModuleSymbolAndBodyType(SyntaxBase syntax, bool isCollection = false)
Copy link
Member

@majastrz majastrz May 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

false

Should we drop the default value here to force every caller to pass it in explicitly? (Could avoid mistakes in the future if the logic gets updated.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Removed the default value.

Copy link
Member

@majastrz majastrz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! The code is so much easier to follow and understand now!

@shenglol shenglol enabled auto-merge (squash) May 26, 2021 09:30
@shenglol shenglol merged commit fe0d29c into main May 26, 2021
@shenglol shenglol deleted the shenglol/deploy-time-constant-refactoring branch May 26, 2021 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants