src/Analyzer.BicepProcessor/BicepTemplateProcessor.cs (115 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; using System.Text.RegularExpressions; using Bicep.Core; using Bicep.Core.Analyzers.Interfaces; using Bicep.Core.Analyzers.Linter; using Bicep.Core.Configuration; using Bicep.Core.Diagnostics; using Bicep.Core.Emit; using Bicep.Core.Extensions; using Bicep.Core.Features; using Bicep.Core.FileSystem; using Bicep.Core.Navigation; using Bicep.Core.Registry; using Bicep.Core.Registry.Auth; using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Semantics.Namespaces; 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; using BicepEnvironment = Bicep.Core.Utils.Environment; using IOFileSystem = System.IO.Abstractions.FileSystem; using SysEnvironment = System.Environment; namespace Microsoft.Azure.Templates.Analyzer.BicepProcessor { /// <summary> /// Contains functionality to process Bicep templates. /// </summary> public static class BicepTemplateProcessor { /// <summary> /// DI Helper from Bicep.Cli.Helpers.ServiceCollectionExtensions /// </summary> /// <param name="services"></param> /// <returns></returns> public static IServiceCollection AddBicepCore(this IServiceCollection services) => services .AddSingleton<INamespaceProvider, NamespaceProvider>() .AddSingleton<IResourceTypeProviderFactory, ResourceTypeProviderFactory>() .AddSingleton<IContainerRegistryClientFactory, ContainerRegistryClientFactory>() .AddSingleton<IPublicRegistryModuleMetadataProvider, PublicRegistryModuleMetadataProvider>() .AddSingleton<ITemplateSpecRepositoryFactory, TemplateSpecRepositoryFactory>() .AddSingleton<IModuleDispatcher, ModuleDispatcher>() .AddSingleton<IArtifactRegistryProvider, DefaultArtifactRegistryProvider>() .AddSingleton<ITokenCredentialFactory, TokenCredentialFactory>() .AddSingleton<IFileResolver, FileResolver>() .AddSingleton<IFileExplorer, FileSystemFileExplorer>() .AddSingleton<IEnvironment, BicepEnvironment>() .AddSingleton<IFileSystem, IOFileSystem>() .AddSingleton<IConfigurationManager, ConfigurationManager>() .AddSingleton<IBicepAnalyzer, LinterAnalyzer>() .AddSingleton<IFeatureProviderFactory, FeatureProviderFactory>() .AddSingleton<FeatureProviderFactory>() // needed for below .AddSingleton<IFeatureProviderFactory, SourceMapFeatureProviderFactory>() // enable source mapping .AddSingleton<ILinterRulesProvider, LinterRulesProvider>() .AddSingleton<BicepCompiler>(); private static readonly Regex IsModuleRegistryPathRegex = new("^br(:[\\w.]+\\/|\\/public:)", RegexOptions.CultureInvariant | RegexOptions.Compiled); private static BicepCompiler BicepCompiler = null; /// <summary> /// Converts Bicep template into JSON template and returns it as a string and its source map /// </summary> /// <param name="bicepPath">The Bicep template file path.</param> /// <returns>The compiled template as a <c>JSON</c> string and its source map.</returns> public static (string, BicepMetadata) ConvertBicepToJson(string bicepPath) { if (BicepCompiler == null) { var serviceCollection = new ServiceCollection(); serviceCollection.AddBicepCore(); var services = serviceCollection.BuildServiceProvider(); BicepCompiler = services.GetRequiredService<BicepCompiler>(); } using var stringWriter = new StringWriter(); var compilation = BicepCompiler.CreateCompilation(new Uri(bicepPath)).Result; var emitter = new TemplateEmitter(compilation.GetEntrypointSemanticModel()); var emitResult = emitter.Emit(stringWriter); if (emitResult.Status == EmitStatus.Failed) { var bicepIssues = emitResult.Diagnostics .Where(diag => diag.Level == DiagnosticLevel.Error) .Select(diag => diag.Message); throw new Exception($"Bicep issues found:{SysEnvironment.NewLine}{string.Join(SysEnvironment.NewLine, bicepIssues)}"); } string entryPointDirectory = Path.GetDirectoryName(compilation.SourceFileGrouping.EntryPoint.Uri.AbsolutePath); bool IsResolvedLocalModuleReference(KeyValuePair<IArtifactReferenceSyntax, ArtifactResolutionInfo> artifact) => // Only include local module references (not modules imported from public/private registries, i.e. those that match IsModuleRegistryPathRegex), // as it is more useful for user to see line number of the module declaration itself, // rather than the line number in the module (as the user does not control the template in the registry directly). artifact.Key is ModuleDeclarationSyntax moduleDeclaration && moduleDeclaration.Path is StringSyntax moduleDeclarationPath && !moduleDeclarationPath.SegmentValues.Any(IsModuleRegistryPathRegex.IsMatch) && artifact.Value.Result.IsSuccess(); // Create SourceFileModuleInfo collection by gathering all needed module info from SourceFileGrouping metadata. // 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) .Select(grouping => { var bicepSourceFile = grouping.Key; var pathRelativeToEntryPoint = Path.GetRelativePath( Path.GetDirectoryName(compilation.SourceFileGrouping.EntryPoint.Uri.AbsolutePath), bicepSourceFile.Uri.AbsolutePath); // Use the grouping value (KeyValuePair<IArtifactReferenceSyntax,ArtifactResolutionInfo>) to create // a dictionary of module line numbers to file paths. // This represents the modules in the source file, and where (what lines) they are referenced. var modules = grouping.Select(artifactRefAndUriResult => { 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 // Use relative paths for bicep to match file paths used in bicep modules and source map if (modulePath.EndsWith(".bicep")) { modulePath = Path.GetRelativePath(entryPointDirectory, modulePath); } return new { moduleLine, modulePath }; }) .ToDictionary(c => c.moduleLine, c => c.modulePath); return new SourceFileModuleInfo(pathRelativeToEntryPoint, modules); }); var bicepMetadata = new BicepMetadata() { ModuleInfo = moduleInfo, SourceMap = emitResult.SourceMap }; return (stringWriter.ToString(), bicepMetadata); } } }