Skip to content

Commit

Permalink
Add a linter to warn on nondeterministic resource names (Azure#7491)
Browse files Browse the repository at this point in the history
* Add a linter to warn on nondeterministic resource names

* Correct typo in schema
  • Loading branch information
jeskew authored Jul 14, 2022
1 parent 476668e commit 6cbbb63
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ module moduleWithCalculatedName './child/optionalParams.bicep'= {
resource resWithCalculatedNameDependencies 'Mock.Rp/mockResource@2020-01-01' = {
//@[43:076) [BCP081 (Warning)] Resource type "Mock.Rp/mockResource@2020-01-01" does not have types available. (CodeDescription: none) |'Mock.Rp/mockResource@2020-01-01'|
name: '${optionalWithAllParamsAndManualDependency.name}${deployTimeSuffix}'
//@[08:077) [use-stable-resource-identifiers (Warning)] Resource identifiers should be reproducible outside of their initial deployment context. Resource resWithCalculatedNameDependencies's 'name' identifier is potentially nondeterministic due to its use of the 'newGuid' function (resWithCalculatedNameDependencies.name -> deployTimeSuffix (default value) -> newGuid()). (CodeDescription: bicep core(https://aka.ms/bicep/linter/use-stable-resource-identifiers)) |'${optionalWithAllParamsAndManualDependency.name}${deployTimeSuffix}'|
properties: {
modADep: moduleWithCalculatedName.outputs.outputObj
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Linq;
using Bicep.Core.Analyzers.Linter.Rules;
using Bicep.Core.UnitTests.Assertions;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Bicep.Core.UnitTests.Diagnostics.LinterRuleTests
{
[TestClass]
public class UseStableResourceIdentifiersRuleTests : LinterRuleTestsBase
{
private void CompileAndTest(string text, params string[] expectedMessages)
{
AssertLinterRuleDiagnostics(UseStableResourceIdentifiersRule.Code, text, diags =>
{
if (expectedMessages.Any())
{
diags.Where(e => e.Code == UseStableResourceIdentifiersRule.Code).Select(e => e.Message).Should().Contain(expectedMessages);
}
else
{
diags.Where(e => e.Code == UseStableResourceIdentifiersRule.Code).Count().Should().Be(0);
}
});
}

[DataRow(@"
param location string = resourceGroup().location
resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' = {
name: 'literalName'
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}"
)]
[DataRow(@"
param location string = resourceGroup().location
param snap string
var crackle = 'crackle'
var pop = '${snap}${crackle}'
resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' = {
name: pop
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}"
)]
[DataRow(@"
param location string = resourceGroup().location
param snap string = newGuid()
var crackle = snap
var pop = '${snap}${crackle}'
resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' = {
name: pop
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}",
"Resource identifiers should be reproducible outside of their initial deployment context. Resource storage's 'name' identifier is potentially nondeterministic due to its use of the 'newGuid' function (storage.name -> pop -> snap (default value) -> newGuid()).",
"Resource identifiers should be reproducible outside of their initial deployment context. Resource storage's 'name' identifier is potentially nondeterministic due to its use of the 'newGuid' function (storage.name -> pop -> crackle -> snap (default value) -> newGuid())."
)]
[DataRow(@"
param location string = resourceGroup().location
param snap string = utcNow('F')
var crackle = snap
var pop = '${snap}${crackle}'
resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' = {
name: pop
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}",
"Resource identifiers should be reproducible outside of their initial deployment context. Resource storage's 'name' identifier is potentially nondeterministic due to its use of the 'utcNow' function (storage.name -> pop -> snap (default value) -> utcNow('F')).",
"Resource identifiers should be reproducible outside of their initial deployment context. Resource storage's 'name' identifier is potentially nondeterministic due to its use of the 'utcNow' function (storage.name -> pop -> crackle -> snap (default value) -> utcNow('F'))."
)]
[DataTestMethod]
public void TestRule(string text, params string[] expectedMessages)
{
CompileAndTest(text, expectedMessages);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Bicep.Core.Diagnostics;
using Bicep.Core.Navigation;
using Bicep.Core.Semantics;
using Bicep.Core.Syntax;
using System;
using System.Collections.Generic;
using System.Text;

namespace Bicep.Core.Analyzers.Linter.Rules
{
public sealed class UseStableResourceIdentifiersRule : LinterRuleBase
{
public new const string Code = "use-stable-resource-identifiers";

public UseStableResourceIdentifiersRule() : base(
code: Code,
description: CoreResources.UseStableResourceIdentifiersMessage,
docUri: new Uri($"https://aka.ms/bicep/linter/{Code}"))
{ }

public override IEnumerable<IDiagnostic> AnalyzeInternal(SemanticModel model)
{
foreach (var resource in model.DeclaredResources)
{
foreach (var identifier in resource.Type.UniqueIdentifierProperties)
{
if (resource.Symbol.TryGetBodyPropertyValue(identifier) is { } identifierSyntax)
{
var visitor = new Visitor(model);
identifierSyntax.Accept(visitor);
foreach (var (path, functionName) in visitor.PathsToNonDeterministicFunctionsUsed)
{
yield return CreateDiagnosticForSpan(identifierSyntax.Span, resource.Symbol.Name, identifier, functionName, $"{resource.Symbol.Name}.{identifier} -> {path}");
}
}
}
}
}

public override string FormatMessage(params object[] values)
=> string.Format(CoreResources.UseStableResourceIdentifiersMessageFormat, values);

private class Visitor : SyntaxVisitor
{
private static IReadOnlySet<string> NonDeterministicFunctionNames = new HashSet<string>
{
"newGuid",
"utcNow",
};
private readonly SemanticModel model;
private readonly Dictionary<string, string> pathsToNonDeterministicFunctionsUsed = new();
private readonly LinkedList<Symbol> pathSegments = new();

internal Visitor(SemanticModel model)
{
this.model = model;
}

internal IEnumerable<KeyValuePair<string, string>> PathsToNonDeterministicFunctionsUsed => pathsToNonDeterministicFunctionsUsed;

public override void VisitFunctionCallSyntax(FunctionCallSyntax syntax)
{
if (NonDeterministicFunctionNames.Contains(syntax.Name.IdentifierName))
{
pathsToNonDeterministicFunctionsUsed.Add(FormatPath(syntax.ToText()), syntax.Name.IdentifierName);
}
base.VisitFunctionCallSyntax(syntax);
}

public override void VisitVariableAccessSyntax(VariableAccessSyntax syntax)
{
switch (model.GetSymbolInfo(syntax))
{
case ParameterSymbol @parameter:
if (@parameter.DeclaringParameter.Modifier is ParameterDefaultValueSyntax defaultValueSyntax)
{
pathSegments.AddLast(@parameter);
defaultValueSyntax.Accept(this);
pathSegments.RemoveLast();
}
break;
case VariableSymbol @variable:
// Variable cycles are reported on elsewhere. As far as this visitor is concerned, a cycle does not introduce nondeterminism.
if (pathSegments.Contains(@variable))
{
return;
}

pathSegments.AddLast(@variable);
@variable.DeclaringVariable.Value.Accept(this);
pathSegments.RemoveLast();
break;
}

base.VisitVariableAccessSyntax(syntax);
}

private string FormatPath(string functionCall)
{
var path = new StringBuilder();
foreach (var segment in pathSegments)
{
if (segment is ParameterSymbol @parameter)
{
path.Append(@parameter.Name);
path.Append(" (default value) -> ");
}
else if (segment is VariableSymbol @variable)
{
path.Append(@variable.Name);
path.Append(" -> ");
}
}

path.Append(functionCall);

return path.ToString();
}
}
}
}
Loading

0 comments on commit 6cbbb63

Please sign in to comment.