forked from Azure/bicep
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a linter to warn on nondeterministic resource names (Azure#7491)
* Add a linter to warn on nondeterministic resource names * Correct typo in schema
- Loading branch information
Showing
6 changed files
with
347 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
...Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseStableResourceIdentifiersRuleTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
124 changes: 124 additions & 0 deletions
124
src/Bicep.Core/Analyzers/Linter/Rules/UseStableResourceIdentifiersRule.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.