diff --git a/.vscode/launch.json b/.vscode/launch.json
index cfa319c6..4b431fcc 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,7 +6,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/src/Analyzer.Cli/bin/Debug/net6.0/TemplateAnalyzer.dll",
+ "program": "${workspaceFolder}/src/Analyzer.Cli/bin/Debug/net8.0/TemplateAnalyzer.dll",
// Runs the analyzer CLI, analyzing the file currently active/being viewed in VS Code.
// Alternatively, change ${file} to the path of a template to analyze that file.
@@ -17,7 +17,7 @@
"${file}"
//"-p", "path-to-parameters"
],
- "cwd": "${workspaceFolder}/src/Analyzer.Cli/bin/Debug/net6.0/",
+ "cwd": "${workspaceFolder}/src/Analyzer.Cli/bin/Debug/net8.0/",
"console": "internalConsole",
"stopAtEntry": false
},
@@ -26,7 +26,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/src/Analyzer.Cli/bin/Debug/net6.0/TemplateAnalyzer.dll",
+ "program": "${workspaceFolder}/src/Analyzer.Cli/bin/Debug/net8.0/TemplateAnalyzer.dll",
// Runs the analyzer CLI, analyzing the directory of the file currently active/being
// viewed in VS Code. Alternatively, change ${fileDirname} to the path of a directory to
@@ -35,7 +35,7 @@
"analyze-directory",
"${fileDirname}",
],
- "cwd": "${workspaceFolder}/src/Analyzer.Cli/bin/Debug/net6.0/",
+ "cwd": "${workspaceFolder}/src/Analyzer.Cli/bin/Debug/net8.0/",
"console": "internalConsole",
"stopAtEntry": false
},
diff --git a/src/Analyzer.BicepProcessor/BicepTemplateProcessor.cs b/src/Analyzer.BicepProcessor/BicepTemplateProcessor.cs
index 5ba5e290..98be61bf 100644
--- a/src/Analyzer.BicepProcessor/BicepTemplateProcessor.cs
+++ b/src/Analyzer.BicepProcessor/BicepTemplateProcessor.cs
@@ -6,6 +6,7 @@
using System.IO;
using System.IO.Abstractions;
using System.Linq;
+using System.Net.Http;
using System.Text.RegularExpressions;
using Bicep.Core;
using Bicep.Core.Analyzers.Interfaces;
@@ -19,13 +20,14 @@
using Bicep.Core.Navigation;
using Bicep.Core.Registry;
using Bicep.Core.Registry.Auth;
-using Bicep.Core.Registry.PublicRegistry;
+using Bicep.Core.Registry.Catalog;
+using Bicep.Core.Registry.Catalog.Implementation.PublicRegistries;
using Bicep.Core.Semantics.Namespaces;
+using Bicep.Core.SourceGraph;
using Bicep.Core.Syntax;
using Bicep.Core.Text;
using Bicep.Core.TypeSystem.Providers;
using Bicep.Core.Utils;
-using Bicep.Core.Workspaces;
using Bicep.IO.Abstraction;
using Bicep.IO.FileSystem;
using Microsoft.Extensions.DependencyInjection;
@@ -46,18 +48,22 @@ public static class BicepTemplateProcessor
///
///
public static IServiceCollection AddBicepCore(this IServiceCollection services) => services
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
@@ -74,6 +80,7 @@ public static IServiceCollection AddBicepCore(this IServiceCollection services)
///
/// The Bicep template file path.
/// The compiled template as a JSON string and its source map.
+#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
public static (string, BicepMetadata) ConvertBicepToJson(string bicepPath)
{
if (BicepCompiler == null)
@@ -113,7 +120,7 @@ moduleDeclaration.Path is StringSyntax moduleDeclarationPath &&
// Group by the source file path to allow for easy construction of SourceFileModuleInfo.
var moduleInfo = compilation.SourceFileGrouping.ArtifactLookup
.Where(IsResolvedLocalModuleReference)
- .GroupBy(artifact => artifact.Value.Origin)
+ .GroupBy(artifact => artifact.Value.ReferencingFile)
.Select(grouping =>
{
var bicepSourceFile = grouping.Key;
@@ -127,7 +134,7 @@ moduleDeclaration.Path is StringSyntax moduleDeclarationPath &&
{
var module = artifactRefAndUriResult.Key as ModuleDeclarationSyntax;
var moduleLine = TextCoordinateConverter.GetPosition(bicepSourceFile.LineStarts, module.Span.Position).line;
- var modulePath = new FileInfo(artifactRefAndUriResult.Value.Result.Unwrap().AbsolutePath).FullName; // converts path to current platform
+ var modulePath = new FileInfo(artifactRefAndUriResult.Value.Result.Unwrap().Uri).FullName; // converts path to current platform
// Use relative paths for bicep to match file paths used in bicep modules and source map
if (modulePath.EndsWith(".bicep"))
@@ -150,5 +157,6 @@ moduleDeclaration.Path is StringSyntax moduleDeclarationPath &&
return (stringWriter.ToString(), bicepMetadata);
}
+#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}
}
diff --git a/src/Analyzer.BicepProcessor/SourceMapFeatureProvider.cs b/src/Analyzer.BicepProcessor/SourceMapFeatureProvider.cs
index 74bd8a73..abbecece 100644
--- a/src/Analyzer.BicepProcessor/SourceMapFeatureProvider.cs
+++ b/src/Analyzer.BicepProcessor/SourceMapFeatureProvider.cs
@@ -45,9 +45,6 @@ public SourceMapFeatureProvider(IFeatureProvider features)
///
public bool SymbolicNameCodegenEnabled => features.SymbolicNameCodegenEnabled;
- ///
- public bool ExtensibilityEnabled => features.ExtensibilityEnabled;
-
///
public bool ResourceTypedParamsAndOutputsEnabled => features.ResourceTypedParamsAndOutputsEnabled;
@@ -61,27 +58,30 @@ public SourceMapFeatureProvider(IFeatureProvider features)
public bool AssertsEnabled => features.AssertsEnabled;
///
- public bool OptionalModuleNamesEnabled => features.OptionalModuleNamesEnabled;
+ public bool LegacyFormatterEnabled => features.LegacyFormatterEnabled;
///
- public bool ResourceDerivedTypesEnabled => features.ResourceDerivedTypesEnabled;
+ public bool LocalDeployEnabled => features.LocalDeployEnabled;
///
- public bool LegacyFormatterEnabled => features.LegacyFormatterEnabled;
+ public bool ExtendableParamFilesEnabled => features.ExtendableParamFilesEnabled;
///
- public bool LocalDeployEnabled => features.LocalDeployEnabled;
+ public bool ResourceInfoCodegenEnabled => features.ResourceInfoCodegenEnabled;
///
- public bool ExtendableParamFilesEnabled => features.ExtendableParamFilesEnabled;
+ public bool WaitAndRetryEnabled => features.WaitAndRetryEnabled;
///
- public bool SecureOutputsEnabled => features.SecureOutputsEnabled;
+ public bool OnlyIfNotExistsEnabled => features.OnlyIfNotExistsEnabled;
///
- public bool ResourceInfoCodegenEnabled => features.ResourceInfoCodegenEnabled;
+ public bool ModuleExtensionConfigsEnabled => features.ModuleExtensionConfigsEnabled;
+
+ ///
+ public bool DesiredStateConfigurationEnabled => features.DesiredStateConfigurationEnabled;
///
- public bool ExtensibilityV2EmittingEnabled => features.ExtensibilityV2EmittingEnabled;
+ public bool ModuleIdentityEnabled => features.ModuleIdentityEnabled;
}
}
diff --git a/src/Analyzer.Core.NuGet/Analyzer.Core.nuspec b/src/Analyzer.Core.NuGet/Analyzer.Core.nuspec
index 52d9fc1d..031c7d37 100644
--- a/src/Analyzer.Core.NuGet/Analyzer.Core.nuspec
+++ b/src/Analyzer.Core.NuGet/Analyzer.Core.nuspec
@@ -12,17 +12,17 @@
-
-
-
-
+
+
+
+
-
+
-
+
diff --git a/src/Analyzer.Core/TemplateAnalyzer.cs b/src/Analyzer.Core/TemplateAnalyzer.cs
index c9c147e3..d2cd7213 100644
--- a/src/Analyzer.Core/TemplateAnalyzer.cs
+++ b/src/Analyzer.Core/TemplateAnalyzer.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
@@ -136,20 +136,20 @@ public IEnumerable AnalyzeTemplate(string template, string template
///
/// Analyzes ARM templates, recursively going through the nested templates
///
- /// The ARM Template JSON with inherited parameters, variables, and functions, if applicable
+ /// The ARM Template JSON with inherited parameters, variables, and functions, if applicable
/// The parameters for the ARM Template JSON
/// The ARM Template file path
/// Template context for the immediate parent template
/// Prefix for resources' path used for line number mapping in nested templates
/// An enumerable of TemplateAnalyzer evaluations.
- private IEnumerable AnalyzeAllIncludedTemplates(string populatedTemplate, string parameters, string templateFilePath, TemplateContext parentContext, string pathPrefix)
+ private IEnumerable AnalyzeAllIncludedTemplates(string template, string parameters, string templateFilePath, TemplateContext parentContext, string pathPrefix)
{
- JToken templatejObject;
- var armTemplateProcessor = new ArmTemplateProcessor(populatedTemplate, logger: this.logger);
+ JToken expandedTemplate;
+ var armTemplateProcessor = new ArmTemplateProcessor(template, logger: this.logger);
try
{
- templatejObject = armTemplateProcessor.ProcessTemplate(parameters);
+ expandedTemplate = armTemplateProcessor.ProcessTemplate(parameters);
}
catch (Exception e)
{
@@ -158,15 +158,16 @@ private IEnumerable AnalyzeAllIncludedTemplates(string populatedTem
var templateContext = new TemplateContext
{
- OriginalTemplate = JObject.Parse(populatedTemplate),
- ExpandedTemplate = templatejObject,
+ OriginalTemplate = JObject.Parse(template),
+ ExpandedTemplate = expandedTemplate,
IsMainTemplate = parentContext.OriginalTemplate == null, // Even the top level context will have a parent defined, but it won't represent a processed template
ResourceMappings = armTemplateProcessor.ResourceMappings,
TemplateIdentifier = templateFilePath,
IsBicep = parentContext.IsBicep,
BicepMetadata = parentContext.BicepMetadata,
PathPrefix = pathPrefix,
- ParentContext = parentContext
+ ParentContext = parentContext,
+ LanguageVersion = armTemplateProcessor.LanguageVersion,
};
try
@@ -178,62 +179,58 @@ private IEnumerable AnalyzeAllIncludedTemplates(string populatedTem
evaluations = evaluations.Concat(this.powerShellRuleEngine.AnalyzeTemplate(templateContext));
}
- // Recursively handle nested templates
- var jsonTemplate = JObject.Parse(populatedTemplate);
- var processedTemplateResources = templatejObject.InsensitiveToken("resources");
+ var pathResolver = new JsonPathResolver(expandedTemplate, expandedTemplate.Path);
+ var deploymentResources = pathResolver.ResolveResourceType("Microsoft.Resources/deployments");
- for (int i = 0; i < processedTemplateResources.Count(); i++)
+ // Recursively handle nested templates
+ foreach (var deploymentResource in deploymentResources)
{
- var currentProcessedResource = processedTemplateResources[i];
-
- if (currentProcessedResource.InsensitiveToken("type")?.ToString().Equals("Microsoft.Resources/deployments", StringComparison.OrdinalIgnoreCase) ?? false)
+ var currentProcessedResource = deploymentResource.JToken;
+ var nestedTemplate = currentProcessedResource.InsensitiveToken("properties.template");
+ if (nestedTemplate == null)
{
- var nestedTemplate = currentProcessedResource.InsensitiveToken("properties.template");
- if (nestedTemplate == null)
- {
- this.logger?.LogWarning($"A linked template was found on: {templateFilePath}, linked templates are currently not supported");
-
- continue;
- }
- var populatedNestedTemplate = nestedTemplate.DeepClone();
+ this.logger?.LogWarning($"A linked template was found on: {templateFilePath}, linked templates are currently not supported");
- // Check whether scope is set to inner or outer
- var scope = currentProcessedResource.InsensitiveToken("properties.expressionEvaluationOptions.scope")?.ToString();
+ continue;
+ }
+ var populatedNestedTemplate = nestedTemplate.DeepClone();
- if (scope == null || scope.Equals("outer", StringComparison.OrdinalIgnoreCase))
- {
- // Variables, parameters and functions inherited from parent template
- string functionsKey = populatedNestedTemplate.InsensitiveToken("functions")?.Parent.Path ?? "functions";
- string variablesKey = populatedNestedTemplate.InsensitiveToken("variables")?.Parent.Path ?? "variables";
- string parametersKey = populatedNestedTemplate.InsensitiveToken("parameters")?.Parent.Path ?? "parameters";
+ // Check whether scope is set to inner or outer
+ var scope = currentProcessedResource.InsensitiveToken("properties.expressionEvaluationOptions.scope")?.ToString();
- populatedNestedTemplate[functionsKey] = jsonTemplate.InsensitiveToken("functions");
- populatedNestedTemplate[variablesKey] = jsonTemplate.InsensitiveToken("variables");
- populatedNestedTemplate[parametersKey] = jsonTemplate.InsensitiveToken("parameters");
- }
- else // scope is inner
- {
- // Pass variables and functions to child template
- (populatedNestedTemplate.InsensitiveToken("variables") as JObject)?.Merge(currentProcessedResource.InsensitiveToken("properties.variables"));
- (populatedNestedTemplate.InsensitiveToken("functions") as JObject)?.Merge(currentProcessedResource.InsensitiveToken("properties.functions)"));
+ if (scope == null || scope.Equals("outer", StringComparison.OrdinalIgnoreCase))
+ {
+ // Variables, parameters and functions inherited from parent template
+ string functionsKey = populatedNestedTemplate.InsensitiveToken("functions")?.Parent.Path ?? "functions";
+ string variablesKey = populatedNestedTemplate.InsensitiveToken("variables")?.Parent.Path ?? "variables";
+ string parametersKey = populatedNestedTemplate.InsensitiveToken("parameters")?.Parent.Path ?? "parameters";
+
+ populatedNestedTemplate[functionsKey] = templateContext.OriginalTemplate.InsensitiveToken("functions");
+ populatedNestedTemplate[variablesKey] = templateContext.OriginalTemplate.InsensitiveToken("variables");
+ populatedNestedTemplate[parametersKey] = templateContext.OriginalTemplate.InsensitiveToken("parameters");
+ }
+ else // scope is inner
+ {
+ // Pass variables and functions to child template
+ (populatedNestedTemplate.InsensitiveToken("variables") as JObject)?.Merge(currentProcessedResource.InsensitiveToken("properties.variables"));
+ (populatedNestedTemplate.InsensitiveToken("functions") as JObject)?.Merge(currentProcessedResource.InsensitiveToken("properties.functions)"));
- // Pass parameters to child template as the 'parameters' argument
- var parametersToPass = currentProcessedResource.InsensitiveToken("properties.parameters");
+ // Pass parameters to child template as the 'parameters' argument
+ var parametersToPass = currentProcessedResource.InsensitiveToken("properties.parameters");
- if (parametersToPass != null)
- {
- parametersToPass["parameters"] = parametersToPass;
- parametersToPass["$schema"] = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#";
- parametersToPass["contentVersion"] = "1.0.0.0";
- parameters = JsonConvert.SerializeObject(parametersToPass);
- }
+ if (parametersToPass != null)
+ {
+ parametersToPass["parameters"] = parametersToPass;
+ parametersToPass["$schema"] = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#";
+ parametersToPass["contentVersion"] = "1.0.0.0";
+ parameters = JsonConvert.SerializeObject(parametersToPass);
}
+ }
- string jsonPopulatedNestedTemplate = JsonConvert.SerializeObject(populatedNestedTemplate);
+ string jsonPopulatedNestedTemplate = JsonConvert.SerializeObject(populatedNestedTemplate);
- IEnumerable result = AnalyzeAllIncludedTemplates(jsonPopulatedNestedTemplate, parameters, templateFilePath, templateContext, nestedTemplate.Path);
- evaluations = evaluations.Concat(result);
- }
+ IEnumerable result = AnalyzeAllIncludedTemplates(jsonPopulatedNestedTemplate, parameters, templateFilePath, templateContext, nestedTemplate.Path);
+ evaluations = evaluations.Concat(result);
}
return evaluations;
}
diff --git a/src/Analyzer.PowerShellRuleEngine/PowerShellRuleEngine.cs b/src/Analyzer.PowerShellRuleEngine/PowerShellRuleEngine.cs
index cc62887f..7b39f4ed 100644
--- a/src/Analyzer.PowerShellRuleEngine/PowerShellRuleEngine.cs
+++ b/src/Analyzer.PowerShellRuleEngine/PowerShellRuleEngine.cs
@@ -128,7 +128,10 @@ public IEnumerable AnalyzeTemplate(TemplateContext templateContext)
// placeholder value for location is westus2
optionsForFileAnalysis.Configuration["AZURE_RESOURCE_ALLOWED_LOCATIONS"] = new[] { "westus2" };
- var resources = templateContext.ExpandedTemplate.InsensitiveToken("resources").Values();
+ string resourcesPath = templateContext.LanguageVersion == 2
+ ? "resources.*"
+ : "resources[*]";
+ var resources = templateContext.ExpandedTemplate.InsensitiveTokens(resourcesPath);
var builder = CommandLineBuilder.Invoke(modules, optionsForFileAnalysis, hostContext);
builder.InputPath(new string[] { tempTemplateFile });
diff --git a/src/Analyzer.Reports.NuGet/Analyzer.Reports.nuspec b/src/Analyzer.Reports.NuGet/Analyzer.Reports.nuspec
index d346959f..cf1ff2d3 100644
--- a/src/Analyzer.Reports.NuGet/Analyzer.Reports.nuspec
+++ b/src/Analyzer.Reports.NuGet/Analyzer.Reports.nuspec
@@ -14,8 +14,8 @@
-
-
+
+
diff --git a/src/Analyzer.TemplateProcessing.NuGet/Analyzer.TemplateProcessing.nuspec b/src/Analyzer.TemplateProcessing.NuGet/Analyzer.TemplateProcessing.nuspec
index 98d48616..d6c6ed26 100644
--- a/src/Analyzer.TemplateProcessing.NuGet/Analyzer.TemplateProcessing.nuspec
+++ b/src/Analyzer.TemplateProcessing.NuGet/Analyzer.TemplateProcessing.nuspec
@@ -13,12 +13,12 @@
-
-
-
-
+
+
+
+
-
+
diff --git a/src/Analyzer.TemplateProcessor/ArmTemplateProcessor.cs b/src/Analyzer.TemplateProcessor/ArmTemplateProcessor.cs
index b3a36247..c7ce11d7 100644
--- a/src/Analyzer.TemplateProcessor/ArmTemplateProcessor.cs
+++ b/src/Analyzer.TemplateProcessor/ArmTemplateProcessor.cs
@@ -29,9 +29,9 @@ public class ArmTemplateProcessor
private readonly string armTemplate;
private readonly string apiVersion;
private readonly ILogger logger;
- private Dictionary> originalToExpandedMapping = new Dictionary>();
- private Dictionary expandedToOriginalMapping = new Dictionary();
- private Dictionary flattenedResources = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary> originalToExpandedMapping = [];
+ private readonly Dictionary expandedToOriginalMapping = [];
+ private readonly Dictionary flattenedResources = new(StringComparer.OrdinalIgnoreCase);
///
/// Mapping between resources in the expanded template to their original resource in
@@ -39,7 +39,16 @@ public class ArmTemplateProcessor
/// The key is the path in the expanded template with value being the path
/// in the original template.
///
- public Dictionary ResourceMappings = new Dictionary();
+ public Dictionary ResourceMappings { get; } = [];
+
+ ///
+ /// The language version of the ARM template processed.
+ /// This is used to determine the language features available in the template.
+ ///
+ ///
+ /// 2 if "languageVersion": "2.0" is specified in the template processed; otherwise, 1.
+ ///
+ public int LanguageVersion { get; private set; } = 1;
///
/// Constructor for the ARM Template Processing library
@@ -105,6 +114,11 @@ internal Template ParseAndValidateTemplate(InsensitiveDictionary paramet
Template template = TemplateEngine.ParseTemplate(armTemplate);
+ if (template.LanguageVersion?.Value == "2.0")
+ {
+ LanguageVersion = 2;
+ }
+
TemplateEngine.ValidateTemplate(template, apiVersion, TemplateDeploymentScope.NotSpecified);
SetOriginalResourceNames(template);
@@ -127,7 +141,7 @@ internal Template ParseAndValidateTemplate(InsensitiveDictionary paramet
try
{
- TemplateEngine.ProcessTemplateLanguageExpressions(managementGroupName, subscriptionId, resourceGroupName, template, apiVersion, parameters, metadata, null);
+ TemplateEngine.ProcessTemplateLanguageExpressions(managementGroupName, subscriptionId, resourceGroupName, template, apiVersion, parameters, metadata, null, null);
}
catch (Exception ex)
{
@@ -250,7 +264,10 @@ internal void CopyResourceDependants(TemplateResource templateResource)
else
{
parentResourceName = parentResourceIds.Value;
- var matchingResources = this.flattenedResources.Where(k => k.Key.StartsWith($"{parentResourceName} ", StringComparison.OrdinalIgnoreCase)).ToList();
+ var matchingResources = this.flattenedResources.Where(k =>
+ k.Key.StartsWith($"{parentResourceName} ", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(k.Value.resource.SymbolicName, parentResourceName, StringComparison.OrdinalIgnoreCase))
+ .ToList();
if (matchingResources.Count == 1)
{
parentResourceInfo = matchingResources.First().Value;
@@ -437,6 +454,8 @@ private TemplateExpressionEvaluationHelper GetTemplateFunctionEvaluationHelper(T
functionsLookup: functionsLookup,
parametersLookup: parametersLookup,
variablesLookup: variablesLookup,
+ templateExtensionAliases: template.Extensions?.Keys,
+ extensionConfigLookup: null,
validationContext: null,
metricsRecorder: null);
diff --git a/src/Analyzer.Types/TemplateContext.cs b/src/Analyzer.Types/TemplateContext.cs
index 12e5b0d4..518005b0 100644
--- a/src/Analyzer.Types/TemplateContext.cs
+++ b/src/Analyzer.Types/TemplateContext.cs
@@ -58,5 +58,10 @@ public class TemplateContext
/// Template context for the immediate parent template
///
public TemplateContext ParentContext { get; set; }
+
+ ///
+ /// The language version of the template
+ ///
+ public int LanguageVersion { get; set; } = 1;
}
}
\ No newline at end of file
diff --git a/src/Analyzer.Utilities.UnitTests/JsonPathResolverTests.cs b/src/Analyzer.Utilities.UnitTests/JsonPathResolverTests.cs
index 639d075b..06d81c51 100644
--- a/src/Analyzer.Utilities.UnitTests/JsonPathResolverTests.cs
+++ b/src/Analyzer.Utilities.UnitTests/JsonPathResolverTests.cs
@@ -9,6 +9,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.Azure.Templates.Analyzer.Types;
namespace Microsoft.Azure.Templates.Analyzer.Utilities.UnitTests
{
@@ -68,18 +69,14 @@ public class JsonPathResolverTests
[DataRow("", DisplayName = "Empty path")]
public void Resolve_NullOrEmptyPath_ReturnsResolverWithOriginalJtoken(string path)
{
- var jtoken = JObject.Parse("{ \"Property\": \"Value\" }");
+ var jobject = JObject.Parse("{ \"Property\": \"Value\" }");
- var resolver = new JsonPathResolver(jtoken, jtoken.Path);
+ var resolver = new JsonPathResolver(jobject, jobject.Path);
- // Do twice to verify internal cache correctness
- for (int i = 0; i < 2; i++)
- {
- var results = resolver.Resolve(path).ToList();
+ var results = resolver.Resolve(path).ToList();
- Assert.AreEqual(1, results.Count);
- Assert.AreEqual(jtoken, results[0].JToken);
- }
+ Assert.AreEqual(1, results.Count);
+ Assert.AreEqual(jobject, results[0].JToken);
}
[DataTestMethod]
@@ -89,7 +86,7 @@ public void Resolve_NullOrEmptyPath_ReturnsResolverWithOriginalJtoken(string pat
[DataRow("twochildlevels.child2.lastprop", DisplayName = "Resolve three properties deep")]
public void Resolve_JsonContainsPath_ReturnsResolverWithCorrectJtokenAndPath(string path)
{
- JToken jtoken = JObject.Parse(
+ JObject jobject = JObject.Parse(
@"{
""NoChildren"": true,
""OneChildLevel"": {
@@ -104,19 +101,15 @@ public void Resolve_JsonContainsPath_ReturnsResolverWithCorrectJtokenAndPath(str
},
}");
- var resolver = new JsonPathResolver(jtoken, jtoken.Path);
+ var resolver = new JsonPathResolver(jobject, jobject.Path);
- // Do twice to verify internal cache correctness
- for (int i = 0; i < 2; i++)
- {
- var results = resolver.Resolve(path).ToList();
+ var results = resolver.Resolve(path).ToList();
- Assert.AreEqual(1, results.Count);
+ Assert.AreEqual(1, results.Count);
- // Verify correct property was resolved and resolver returns correct path
- Assert.AreEqual(path, results[0].JToken.Path, ignoreCase: true);
- Assert.AreEqual(path, results[0].Path, ignoreCase: true);
- }
+ // Verify correct property was resolved and resolver returns correct path
+ Assert.AreEqual(path, results[0].JToken.Path, ignoreCase: true);
+ Assert.AreEqual(path, results[0].Path, ignoreCase: true);
}
// Combinations of wildcards are tested more extensively in JTokenExtensionsTests.cs
@@ -128,7 +121,7 @@ public void Resolve_JsonContainsPath_ReturnsResolverWithCorrectJtokenAndPath(str
[DataRow("NoChildren.*", 0, DisplayName = "Wildcard matching nothing")]
public void Resolve_JsonContainsWildcardPath_ReturnsResolverWithCorrectJtokensAndPath(string path, int expectedCount)
{
- JToken jtoken = JObject.Parse(
+ JObject jobject = JObject.Parse(
@"{
""NoChildren"": true,
""OneChildLevel"": {
@@ -143,42 +136,38 @@ public void Resolve_JsonContainsWildcardPath_ReturnsResolverWithCorrectJtokensAn
},
}");
- var resolver = new JsonPathResolver(jtoken, jtoken.Path);
+ var resolver = new JsonPathResolver(jobject, jobject.Path);
var arrayRegex = new Regex(@"(?\w+)\[(\d|\*)\]");
- // Do twice to verify internal cache correctness
- for (int i = 0; i < 2; i++)
- {
- var results = resolver.Resolve(path).ToList();
+ var results = resolver.Resolve(path).ToList();
- Assert.AreEqual(expectedCount, results.Count);
+ Assert.AreEqual(expectedCount, results.Count);
- foreach (var resolved in results)
+ foreach (var resolved in results)
+ {
+ // Verify path on each segment
+ var expectedPath = path.Split('.');
+ var actualPath = resolved.JToken.Path.Split('.');
+ Assert.AreEqual(expectedPath.Length, actualPath.Length);
+ for (int j = 0; j < expectedPath.Length; j++)
{
- // Verify path on each segment
- var expectedPath = path.Split('.');
- var actualPath = resolved.JToken.Path.Split('.');
- Assert.AreEqual(expectedPath.Length, actualPath.Length);
- for (int j = 0; j < expectedPath.Length; j++)
- {
- var expectedSegment = expectedPath[j];
- var actualSegment = actualPath[j];
- var arrayMatch = arrayRegex.Match(expectedSegment);
+ var expectedSegment = expectedPath[j];
+ var actualSegment = actualPath[j];
+ var arrayMatch = arrayRegex.Match(expectedSegment);
- if (arrayMatch.Success)
- {
- Assert.AreEqual(arrayMatch.Groups["property"].Value, arrayRegex.Match(actualSegment).Groups["property"].Value, ignoreCase: true);
- }
- else
- {
- Assert.IsTrue(expectedSegment.Equals("*") || expectedSegment.Equals(actualPath[j], StringComparison.OrdinalIgnoreCase));
- }
+ if (arrayMatch.Success)
+ {
+ Assert.AreEqual(arrayMatch.Groups["property"].Value, arrayRegex.Match(actualSegment).Groups["property"].Value, ignoreCase: true);
+ }
+ else
+ {
+ Assert.IsTrue(expectedSegment.Equals("*") || expectedSegment.Equals(actualPath[j], StringComparison.OrdinalIgnoreCase));
}
-
- // Verify returned path matches JToken path
- Assert.AreEqual(resolved.JToken.Path, resolved.Path, ignoreCase: true);
}
+
+ // Verify returned path matches JObject path
+ Assert.AreEqual(resolved.JToken.Path, resolved.Path, ignoreCase: true);
}
}
@@ -190,20 +179,15 @@ public void Resolve_JsonContainsWildcardPath_ReturnsResolverWithCorrectJtokensAn
[DataRow("Not.Existing.Property", DisplayName = "Non-existant path (multi-level, top level doesn't exist)")]
public void Resolve_InvalidPath_ReturnsResolverWithNullJtokenAndCorrectResolvedPath(string path)
{
- var jtoken = JObject.Parse("{ \"Property\": \"Value\" }");
+ var jobject = JObject.Parse("{ \"Property\": \"Value\" }");
- var resolver = new JsonPathResolver(jtoken, jtoken.Path);
- var expectedPath = $"{jtoken.Path}.{path}";
+ var resolver = new JsonPathResolver(jobject, jobject.Path);
- // Do twice to verify internal cache correctness
- for (int i = 0; i < 2; i++)
- {
- var results = resolver.Resolve(path).ToList();
+ var results = resolver.Resolve(path).ToList();
- Assert.AreEqual(1, results.Count);
- Assert.AreEqual(null, results[0].JToken);
- Assert.AreEqual(expectedPath, results[0].Path);
- }
+ Assert.AreEqual(1, results.Count);
+ Assert.AreEqual(null, results[0].JToken);
+ Assert.AreEqual(path, results[0].Path);
}
[DataTestMethod]
@@ -213,30 +197,26 @@ public void ResolveResourceType_JObjectWithExpectedResourcesArray_ReturnsResourc
var jToken = JObject.Parse(template);
var resolver = new JsonPathResolver(jToken, jToken.Path);
- // Do twice to verify internal cache correctness
- for (int i = 0; i < 2; i++)
+ var resources = resolver.ResolveResourceType(resourceType).ToList();
+ Assert.AreEqual(matchingResourceIndexes.Length, resources.Count);
+
+ // Verify resources of correct type were returned
+ for (int numOfResourceMatched = 0; numOfResourceMatched < matchingResourceIndexes.Length; numOfResourceMatched++)
{
- var resources = resolver.ResolveResourceType(resourceType).ToList();
- Assert.AreEqual(matchingResourceIndexes.Length, resources.Count);
+ var resource = resources[numOfResourceMatched];
+ var resourceIndexes = matchingResourceIndexes[numOfResourceMatched];
+ var expectedPath = "";
- // Verify resources of correct type were returned
- for (int numOfResourceMatched = 0; numOfResourceMatched < matchingResourceIndexes.Length; numOfResourceMatched++)
+ for (int numOfResourceIndex = 0; numOfResourceIndex < resourceIndexes.Length; numOfResourceIndex++)
{
- var resource = resources[numOfResourceMatched];
- var resourceIndexes = matchingResourceIndexes[numOfResourceMatched];
- var expectedPath = "";
-
- for (int numOfResourceIndex = 0; numOfResourceIndex < resourceIndexes.Length; numOfResourceIndex++)
+ if (numOfResourceIndex != 0)
{
- if (numOfResourceIndex != 0)
- {
- expectedPath += ".";
- }
- expectedPath += $"resources[{resourceIndexes[numOfResourceIndex]}]";
+ expectedPath += ".";
}
-
- Assert.AreEqual(expectedPath, resource.JToken.Path);
+ expectedPath += $"resources[{resourceIndexes[numOfResourceIndex]}]";
}
+
+ Assert.AreEqual(expectedPath, resource.JToken.Path);
}
}
@@ -255,6 +235,174 @@ public void ResolveResourceType_JObjectWithResourcesNotArrayOfObjects_ReturnsEmp
Assert.AreEqual(0, new JsonPathResolver(jToken, jToken.Path).ResolveResourceType("anything").Count());
}
+ [TestMethod]
+ public void ResolveResourceType_LanguageVersion2SymbolicNaming_ReturnsResourcesOfCorrectType()
+ {
+ // Test template with language version 2.0 symbolic naming (resources as object)
+ var template = @"{
+ ""languageVersion"": ""2.0"",
+ ""resources"": {
+ ""storageAccount"": {
+ ""type"": ""Microsoft.Storage/storageAccounts"",
+ ""apiVersion"": ""2021-04-01"",
+ ""name"": ""mystorageaccount""
+ },
+ ""virtualNetwork"": {
+ ""type"": ""Microsoft.Network/virtualNetworks"",
+ ""apiVersion"": ""2021-02-01"",
+ ""name"": ""myvnet""
+ },
+ ""anotherStorage"": {
+ ""type"": ""Microsoft.Storage/storageAccounts"",
+ ""apiVersion"": ""2021-04-01"",
+ ""name"": ""anotherstorageaccount""
+ }
+ }
+ }";
+
+ var jToken = JObject.Parse(template);
+ var resolver = new JsonPathResolver(jToken, jToken.Path);
+
+ // Test 1: Find all storage accounts (should return 2)
+ var storageAccounts = resolver.ResolveResourceType("Microsoft.Storage/storageAccounts").ToList();
+ Assert.AreEqual(2, storageAccounts.Count);
+
+ // Verify the correct resources were found
+ foreach (var account in storageAccounts)
+ {
+ Assert.AreEqual("Microsoft.Storage/storageAccounts",
+ account.JToken.InsensitiveToken("type")?.Value());
+ }
+
+ // Test 2: Find virtual networks (should return 1)
+ var virtualNetworks = resolver.ResolveResourceType("Microsoft.Network/virtualNetworks").ToList();
+ Assert.AreEqual(1, virtualNetworks.Count);
+ Assert.AreEqual("Microsoft.Network/virtualNetworks",
+ virtualNetworks[0].JToken.InsensitiveToken("type")?.Value());
+
+ // Test 3: Find non-existent resource type (should return 0)
+ var nonExistent = resolver.ResolveResourceType("Microsoft.Compute/virtualMachines").ToList();
+ Assert.AreEqual(0, nonExistent.Count);
+
+ // Test 4: Verify caching works correctly (run twice)
+ var storageAccountsAgain = resolver.ResolveResourceType("Microsoft.Storage/storageAccounts").ToList();
+ Assert.AreEqual(2, storageAccountsAgain.Count);
+ }
+
+ [TestMethod]
+ public void ResolveResourceType_SymbolicNamingWithNestedResources_ReturnsCorrectResources()
+ {
+ // Test template with nested resources in language version 2.0 format.
+ // Child resources are specified using both object and array formats.
+ var template = @"{
+ ""languageVersion"": ""2.0"",
+ ""resources"": {
+ ""parentSite"": {
+ ""type"": ""Microsoft.Web/sites"",
+ ""apiVersion"": ""2021-02-01"",
+ ""name"": ""mywebsite"",
+ ""resources"": {
+ ""siteExtension"": {
+ ""type"": ""Microsoft.Web/sites/siteextensions"",
+ ""apiVersion"": ""2021-02-01"",
+ ""name"": ""myextension""
+ }
+ }
+ },
+ ""parentSite2"": {
+ ""type"": ""Microsoft.Web/sites"",
+ ""apiVersion"": ""2021-02-01"",
+ ""name"": ""mywebsite2"",
+ ""resources"": [
+ {
+ ""type"": ""Microsoft.Web/sites/siteextensions"",
+ ""apiVersion"": ""2021-02-01"",
+ ""name"": ""myextension2""
+ }
+ ]
+ }
+ }
+ }";
+
+ var jToken = JObject.Parse(template);
+ var resolver = new JsonPathResolver(jToken, jToken.Path);
+
+ // Test finding parent resource
+ var sites = resolver.ResolveResourceType("Microsoft.Web/sites").ToList();
+ Assert.AreEqual(2, sites.Count);
+
+ // Test finding child resource
+ var extensions = resolver.ResolveResourceType("Microsoft.Web/sites/siteextensions").ToList();
+ Assert.AreEqual(2, extensions.Count);
+ }
+
+ [TestMethod]
+ public void Resolve_RepeatLookupForPath_UsesInternalCache()
+ {
+ JObject jobject = JObject.Parse(
+ @"{
+ ""NoChildren"": true,
+ ""OneChildLevel"": {
+ ""Child"": ""aValue"",
+ ""Child2"": 2
+ },
+ ""TwoChildLevels"": {
+ ""Child"": [ 0, 1, 2 ],
+ ""Child2"": {
+ ""LastProp"": true
+ }
+ },
+ }");
+
+ var resolver = new JsonPathResolver(jobject, jobject.Path);
+
+ // Resolve root and compare to confirm that resolver is using the passed JObject directly (not copied).
+ var rootResult = resolver.Resolve("").ToList();
+ Assert.AreEqual(1, rootResult.Count);
+ Assert.AreEqual