Skip to content

Commit f62a553

Browse files
authored
Added support for Bicep imports Azure#2537 (Azure#2714)
1 parent af7bc83 commit f62a553

12 files changed

+373
-19
lines changed

bicepconfig.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"experimentalFeaturesEnabled": {
3-
"userDefinedTypes": true,
4-
"optionalModuleNames": true
3+
"optionalModuleNames": true,
4+
"userDefinedFunctions": true
55
}
66
}

docs/CHANGELOG-v1.md

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
3434

3535
What's changed since pre-release v1.34.0-B0022:
3636

37+
- General improvements:
38+
- Added support for type/ variable/ and function imports from Bicep files by @BernieWhite.
39+
[#2537](https://github.com/Azure/PSRule.Rules.Azure/issues/2537)
3740
- Engineering:
3841
- Improved debugging experience by providing symbols for .NET code by @BernieWhite.
3942
[#2712](https://github.com/Azure/PSRule.Rules.Azure/issues/2712)

docs/troubleshooting.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ To resolve this issue:
3737
When expanding Bicep source files you may get an error relating to the Bicep version you have installed.
3838
For example if you attempt to use a Bicep feature that is not supported by the version used by PSRule for Azure.
3939

40+
Check the Bicep version reported by PSRule supports the Bicep features you are using.
41+
4042
PSRule for Azure uses the Bicep CLI installed on your machine or CI worker.
4143
By default, the Bicep CLI binary will be selected by your `PATH` environment variable.
4244

@@ -51,6 +53,17 @@ For more details on installing and configuring the Bicep CLI, see [Setup Bicep][
5153
[10]: setup/setup-bicep.md#using-azure-cli
5254
[11]: setup/setup-bicep.md
5355

56+
## Bicep features
57+
58+
Generally PSRule for Azure plans to support any language features that are supported by the latest version of Bicep.
59+
New language features are often added behind an experimental feature flag for community feedback.
60+
Features flagged by Bicep as experimental may not be supported by PSRule for Azure immediately.
61+
PSRule for Azure will plan to add support as soon as possible after the feature flag is removed.
62+
63+
If you are using a Bicep feature that is not supported by PSRule for Azure, please [join or start a discussion][4].
64+
65+
[4]: https://github.com/Azure/PSRule.Rules.Azure/discussions
66+
5467
## Bicep compilation timeout
5568

5669
When expanding Bicep source files you may get an error similar to the following:
@@ -97,7 +110,6 @@ There is a few common causes of this issue including:
97110

98111
[2]: using-templates.md
99112
[3]: using-bicep.md
100-
[4]: https://github.com/Azure/PSRule.Rules.Azure/discussions
101113

102114
## Custom rules are not running
103115

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using System.Diagnostics;
8+
using System.Resources;
9+
using System.Text;
10+
using Newtonsoft.Json.Linq;
11+
12+
namespace PSRule.Rules.Azure.Data.Template
13+
{
14+
/// <summary>
15+
/// A graph that tracks dependencies between type definitions.
16+
/// </summary>
17+
internal sealed class CustomTypeDependencyGraph
18+
{
19+
private readonly Dictionary<string, Node> _ById = new(StringComparer.OrdinalIgnoreCase);
20+
21+
[DebuggerDisplay("{Resource.Id}")]
22+
private sealed class Node
23+
{
24+
internal readonly string Id;
25+
internal readonly JObject Value;
26+
27+
public Node(string id, JObject value)
28+
{
29+
Id = id;
30+
Value = value;
31+
}
32+
}
33+
34+
/// <summary>
35+
/// Add a custom type to the graph.
36+
/// </summary>
37+
/// <param name="name">The custom type node to add to the graph.</param>
38+
/// <param name="value">The definition of the custom type.</param>
39+
internal void Track(string name, JObject value)
40+
{
41+
if (value == null)
42+
return;
43+
44+
var id = $"#/definitions/{name}";
45+
var item = new Node(id, value);
46+
_ById[id] = item;
47+
}
48+
49+
/// <summary>
50+
/// Get a list of ordered custom types.
51+
/// </summary>
52+
internal IEnumerable<KeyValuePair<string, JObject>> GetOrdered()
53+
{
54+
var stack = new List<KeyValuePair<string, JObject>>(_ById.Values.Count);
55+
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
56+
57+
foreach (var item in _ById.Values)
58+
{
59+
Visit(item, visited, stack);
60+
}
61+
return stack.ToArray();
62+
}
63+
64+
private bool TryGet(string id, out Node item)
65+
{
66+
return _ById.TryGetValue(id, out item);
67+
}
68+
69+
/// <summary>
70+
/// Traverse a node and parent type.
71+
/// </summary>
72+
private void Visit(Node item, HashSet<string> visited, List<KeyValuePair<string, JObject>> stack)
73+
{
74+
if (visited.Contains(item.Id))
75+
return;
76+
77+
visited.Add(item.Id);
78+
if (item.Value.TryStringProperty("$ref", out var id) && TryGet(id, out var parent))
79+
{
80+
Visit(parent, visited, stack);
81+
}
82+
stack.Add(new KeyValuePair<string, JObject>(item.Id, item.Value));
83+
}
84+
}
85+
}

src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs

+52-16
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,7 @@ protected virtual void Template(TemplateContext context, string deploymentName,
940940
ContentVersion(context, contentVersion);
941941

942942
// Handle custom type definitions
943-
if (TryObjectProperty(template, PROPERTY_DEFINITIONS, out var definitions))
943+
if (!isNested && TryObjectProperty(template, PROPERTY_DEFINITIONS, out var definitions))
944944
Definitions(context, definitions);
945945

946946
// Handle compile time function variables
@@ -993,24 +993,33 @@ protected virtual void Definitions(TemplateContext context, JObject definitions)
993993
if (definitions == null || definitions.Count == 0)
994994
return;
995995

996+
var graph = new CustomTypeDependencyGraph();
996997
foreach (var definition in definitions)
997-
Definition(context, definition.Key, definition.Value as JObject);
998+
{
999+
graph.Track(definition.Key, definition.Value as JObject);
1000+
}
1001+
1002+
var ordered = graph.GetOrdered();
1003+
foreach (var definition in ordered)
1004+
{
1005+
Definition(context, definition.Key, definition.Value);
1006+
}
9981007
}
9991008

1000-
protected virtual void Definition(TemplateContext context, string definitionName, JObject definition)
1009+
protected virtual void Definition(TemplateContext context, string definitionId, JObject definition)
10011010
{
1002-
if (TryDefinition(definition, out var value))
1003-
context.AddDefinition($"#/definitions/{definitionName}", value);
1011+
if (TryDefinition(context, definition, out var value))
1012+
context.AddDefinition(definitionId, value);
10041013
}
10051014

1006-
private static bool TryDefinition(JObject definition, out ITypeDefinition value)
1015+
private static bool TryDefinition(TemplateContext context, JObject definition, out ITypeDefinition value)
10071016
{
10081017
value = null;
1009-
var type = GetTypePrimitive(definition);
1018+
var type = GetTypePrimitive(context, definition);
10101019
if (type == TypePrimitive.None)
10111020
return false;
10121021

1013-
var isNullable = definition.TryBoolProperty(PROPERTY_NULLABLE, out var nullable) && nullable.HasValue;
1022+
var isNullable = GetTypeNullable(context, definition);
10141023

10151024
value = new TypeDefinition(type, definition, isNullable);
10161025
return true;
@@ -1121,7 +1130,7 @@ private static bool TryParameterType(ITemplateContext context, JObject parameter
11211130
value = new ParameterType(definition.Type, type);
11221131

11231132
// Try type
1124-
if (parameter.TryGetProperty(PROPERTY_TYPE, out type) &&
1133+
else if (parameter.TryGetProperty(PROPERTY_TYPE, out type) &&
11251134
ParameterType.TrySimpleType(type, out var v))
11261135
value = v;
11271136

@@ -1396,7 +1405,7 @@ protected static bool IsDeploymentResource(string resourceType)
13961405
return string.Equals(resourceType, RESOURCETYPE_DEPLOYMENT, StringComparison.OrdinalIgnoreCase);
13971406
}
13981407

1399-
private static TemplateContext GetDeploymentContext(TemplateContext context, string deploymentName, JObject resource, JObject properties)
1408+
private TemplateContext GetDeploymentContext(TemplateContext context, string deploymentName, JObject resource, JObject properties)
14001409
{
14011410
if (!TryObjectProperty(properties, PROPERTY_EXPRESSIONEVALUATIONOPTIONS, out var options) ||
14021411
!TryStringProperty(options, PROPERTY_SCOPE, out var scope) ||
@@ -1420,12 +1429,17 @@ private static TemplateContext GetDeploymentContext(TemplateContext context, str
14201429
TryObjectProperty(template, PROPERTY_PARAMETERS, out var templateParameters);
14211430

14221431
var deploymentContext = new TemplateContext(context.Pipeline, subscription, resourceGroup, tenant, managementGroup, parameterDefaults);
1432+
1433+
// Handle custom type definitions early to allow type mapping of parameters if required.
1434+
if (TryObjectProperty(template, PROPERTY_DEFINITIONS, out var definitions))
1435+
Definitions(deploymentContext, definitions);
1436+
14231437
if (TryObjectProperty(properties, PROPERTY_PARAMETERS, out var innerParameters))
14241438
{
14251439
foreach (var parameter in innerParameters.Properties())
14261440
{
14271441
var parameterType = templateParameters.TryGetProperty<JObject>(parameter.Name, out var templateParameter) &&
1428-
TryParameterType(context, templateParameter, out var t) ? t.Value.Type : TypePrimitive.None;
1442+
TryParameterType(deploymentContext, templateParameter, out var t) ? t.Value.Type : TypePrimitive.None;
14291443

14301444
if (parameter.Value is JValue parameterValueExpression)
14311445
parameter.Value = ResolveToken(context, ResolveVariable(context, parameterType, parameterValueExpression));
@@ -1569,18 +1583,40 @@ protected void Outputs(TemplateContext context, JObject outputs)
15691583

15701584
protected virtual void Output(TemplateContext context, string name, JObject output)
15711585
{
1572-
ResolveProperty(context, output, PROPERTY_VALUE, GetTypePrimitive(output));
1586+
ResolveProperty(context, output, PROPERTY_VALUE, GetTypePrimitive(context, output));
15731587
context.CheckOutput(name, output);
15741588
context.AddOutput(name, output);
15751589
}
15761590

15771591
#endregion Outputs
15781592

1579-
private static TypePrimitive GetTypePrimitive(JObject value)
1593+
private static TypePrimitive GetTypePrimitive(TemplateContext context, JObject value)
1594+
{
1595+
if (value == null) return TypePrimitive.None;
1596+
1597+
// Find primitive.
1598+
if (value.TryGetProperty(PROPERTY_TYPE, out var t) && Enum.TryParse(t, ignoreCase: true, result: out TypePrimitive type))
1599+
return type;
1600+
1601+
// Find primitive from parent type.
1602+
if (context != null && value.TryGetProperty(PROPERTY_REF, out var id) && context.TryDefinition(id, out var definition))
1603+
return GetTypePrimitive(context, definition.Definition);
1604+
1605+
return TypePrimitive.None;
1606+
}
1607+
1608+
private static bool GetTypeNullable(TemplateContext context, JObject value)
15801609
{
1581-
return value == null ||
1582-
!value.TryGetProperty(PROPERTY_TYPE, out var t) ||
1583-
!Enum.TryParse(t, ignoreCase: true, result: out TypePrimitive type) ? TypePrimitive.None : type;
1610+
if (value == null) return false;
1611+
1612+
// Find nullable from parent type.
1613+
if (value.TryBoolProperty(PROPERTY_NULLABLE, out var nullable) && nullable.Value)
1614+
return nullable.Value;
1615+
1616+
if (context != null && value.TryGetProperty(PROPERTY_REF, out var id) && context.TryDefinition(id, out var definition))
1617+
return definition.Nullable;
1618+
1619+
return false;
15841620
}
15851621

15861622
protected static StringExpression<T> Expression<T>(ITemplateContext context, string s)

src/PSRule.Rules.Azure/Data/Template/TypeDefinition.cs

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ internal interface ITypeDefinition
99
{
1010
TypePrimitive Type { get; }
1111

12+
JObject Definition { get; }
13+
1214
bool Nullable { get; }
1315
}
1416

tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@
203203
<None Update="Tests.Bicep.27.json">
204204
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
205205
</None>
206+
<None Update="Tests.Bicep.28.json">
207+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
208+
</None>
206209
<None Update="Tests.Bicep.29.json">
207210
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
208211
</None>

tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs

+13
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,19 @@ public void NullableParameters()
933933
Assert.Equal("sourceId", result["value"].Value<string>());
934934
}
935935

936+
[Fact]
937+
public void CompileTimeImports()
938+
{
939+
var resources = ProcessTemplate(GetSourcePath("Tests.Bicep.28.json"), null, out var templateContext);
940+
Assert.Equal(2, resources.Length);
941+
942+
Assert.True(templateContext.RootDeployment.TryOutput("outValue", out JObject outValue));
943+
Assert.Equal("t1", outValue["value"].Value<string>());
944+
945+
Assert.True(templateContext.RootDeployment.TryOutput("hello", out JObject helloValue));
946+
Assert.Equal("Hello value!", helloValue["value"].Value<string>());
947+
}
948+
936949
[Fact]
937950
public void MockWellKnownProperties()
938951
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
module child './Tests.Bicep.28.child.bicep' = {
5+
name: 'child'
6+
params: {
7+
value: 't1'
8+
}
9+
}
10+
11+
output outValue string = child.outputs.outValue
12+
13+
output hello string = child.outputs.hello
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { myOtherStringType, myOtherStringTypeNullable2, globals } from './Tests.Bicep.28.export.bicep'
5+
import { sayHello } from './Tests.Bicep.28.export.bicep'
6+
7+
param value myOtherStringType = 't2'
8+
9+
param valueNullable myOtherStringTypeNullable2
10+
11+
output outValue myOtherStringType = value
12+
13+
output outValueNullable myOtherStringTypeNullable2 = valueNullable
14+
15+
output hello string = sayHello('value')
16+
17+
output outGlobal object = globals
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
@export()
5+
type myStringType = 't1' | 't2'
6+
7+
@export()
8+
type myOtherStringType = myStringType
9+
10+
type myOtherStringTypeNullable = myOtherStringType?
11+
12+
@export()
13+
type myOtherStringTypeNullable2 = myOtherStringTypeNullable
14+
15+
@export()
16+
var globals = {
17+
env: 'dev'
18+
instance: 1
19+
}
20+
21+
@export()
22+
func sayHello(name string) string => 'Hello ${name}!'

0 commit comments

Comments
 (0)