src/Bicep.Core.IntegrationTests/Semantics/SemanticModelTests.cs (286 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Diagnostics.CodeAnalysis;
using Bicep.Core.Diagnostics;
using Bicep.Core.Intermediate;
using Bicep.Core.Navigation;
using Bicep.Core.Samples;
using Bicep.Core.Semantics;
using Bicep.Core.Syntax;
using Bicep.Core.Syntax.Visitors;
using Bicep.Core.Text;
using Bicep.Core.UnitTests;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Syntax;
using Bicep.Core.UnitTests.Utils;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OmniSharp.Extensions.LanguageServer.Protocol;
namespace Bicep.Core.IntegrationTests.Semantics
{
[TestClass]
public class SemanticModelTests
{
[NotNull]
public TestContext? TestContext { get; set; }
private static ServiceBuilder Services => new ServiceBuilder()
.WithEnvironmentVariables(
("stringEnvVariableName", "test"),
("intEnvVariableName", "100"),
("boolEnvironmentVariable", "true")
);
// NOTE: Uses the linter analyzers specified in BicepTestConstants.BuiltInConfigurationWithProblematicAnalyzersDisabled
// Problematic ones that should be disabled in this and most other tests by default can be added to BicepTestConstants.AnalyzerRulesToDisableInTests
[DataTestMethod]
[DynamicData(nameof(GetData), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))]
[TestCategory(BaselineHelper.BaselineTestCategory)]
public async Task ProgramsShouldProduceExpectedDiagnostics(DataSet dataSet)
{
var (compilation, outputDirectory, _) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext);
var model = compilation.GetEntrypointSemanticModel();
// use a deterministic order
var diagnostics = model.GetAllDiagnostics()
.OrderBy(x => x.Span.Position)
.ThenBy(x => x.Span.Length)
.ThenBy(x => x.Message, StringComparer.Ordinal);
var sourceTextWithDiags = DataSet.AddDiagsToSourceText(dataSet, diagnostics, diag => OutputHelper.GetDiagLoggingString(dataSet.Bicep, outputDirectory, diag));
var resultsFile = Path.Combine(outputDirectory, DataSet.TestFileMainDiagnostics);
File.WriteAllText(resultsFile, sourceTextWithDiags);
sourceTextWithDiags.Should().EqualWithLineByLineDiffOutput(
TestContext,
dataSet.Diagnostics,
expectedPath: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainDiagnostics),
actualPath: resultsFile);
}
[TestMethod]
public void EndOfFileFollowingSpaceAfterParameterKeyWordShouldNotThrow()
{
var compilation = Services.BuildCompilation("parameter ");
FluentActions.Invoking(() => compilation.GetEntrypointSemanticModel().GetAllDiagnostics()).Should().NotThrow();
}
[DataTestMethod]
[DynamicData(nameof(GetData), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))]
[TestCategory(BaselineHelper.BaselineTestCategory)]
public async Task ProgramsShouldProduceExpectedUserDeclaredSymbols(DataSet dataSet)
{
var (compilation, outputDirectory, _) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext);
var model = compilation.GetEntrypointSemanticModel();
var symbols = SymbolCollector
.CollectSymbols(model)
.OfType<DeclaredSymbol>();
var lineStarts = compilation.SourceFileGrouping.EntryPoint.LineStarts;
string getLoggingString(DeclaredSymbol symbol)
{
(_, var startChar) = TextCoordinateConverter.GetPosition(lineStarts, symbol.DeclaringSyntax.Span.Position);
return $"{symbol.Kind} {symbol.Name}. Type: {symbol.Type}. Declaration start char: {startChar}, length: {symbol.DeclaringSyntax.Span.Length}";
}
var sourceTextWithDiags = DataSet.AddDiagsToSourceText(dataSet, symbols, symb => symb.NameSource.Span, getLoggingString);
var resultsFile = Path.Combine(outputDirectory, DataSet.TestFileMainDiagnostics);
File.WriteAllText(resultsFile, sourceTextWithDiags);
sourceTextWithDiags.Should().EqualWithLineByLineDiffOutput(
TestContext,
dataSet.Symbols,
expectedPath: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainSymbols),
actualPath: resultsFile);
}
[DataTestMethod]
[DynamicData(nameof(GetData), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))]
public async Task NameBindingsShouldBeConsistent(DataSet dataSet)
{
var (compilation, _, _) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext);
var model = compilation.GetEntrypointSemanticModel();
var symbolReferences = GetAllBoundSymbolReferences(compilation.SourceFileGrouping.EntryPoint.ProgramSyntax);
// just a sanity check
symbolReferences.Should().AllBeAssignableTo<ISymbolReference>();
foreach (SyntaxBase symbolReference in symbolReferences)
{
var symbol = model.GetSymbolInfo(symbolReference);
symbol.Should().NotBeNull();
if (dataSet.IsValid)
{
// valid cases should not return error symbols for any symbol reference node
symbol.Should().NotBeOfType<ErrorSymbol>();
symbol.Should().Match(s =>
s is MetadataSymbol ||
s is ParameterSymbol ||
s is TypeAliasSymbol ||
s is AmbientTypeSymbol ||
s is VariableSymbol ||
s is ResourceSymbol ||
s is ModuleSymbol ||
s is OutputSymbol ||
s is FunctionSymbol ||
s is DeclaredFunctionSymbol ||
s is ExtensionNamespaceSymbol ||
s is BuiltInNamespaceSymbol ||
s is LocalVariableSymbol ||
s is TestSymbol ||
s is ImportedTypeSymbol ||
s is ImportedVariableSymbol ||
s is ImportedFunctionSymbol ||
s is WildcardImportSymbol);
}
else
{
// invalid files may return errors
symbol.Should().Match(s =>
s is ErrorSymbol ||
s is MetadataSymbol ||
s is ParameterSymbol ||
s is TypeAliasSymbol ||
s is AmbientTypeSymbol ||
s is VariableSymbol ||
s is ResourceSymbol ||
s is ModuleSymbol ||
s is OutputSymbol ||
s is FunctionSymbol ||
s is DeclaredFunctionSymbol ||
s is ExtensionNamespaceSymbol ||
s is BuiltInNamespaceSymbol ||
s is LocalVariableSymbol ||
s is TestSymbol ||
s is ImportedTypeSymbol ||
s is ImportedVariableSymbol ||
s is ImportedFunctionSymbol ||
s is ErroredImportSymbol ||
s is WildcardImportSymbol);
}
var foundRefs = model.FindReferences(symbol!);
// the returned references should contain the original ref that we used to find the symbol
foundRefs.Should().Contain(symbolReference);
// each ref should map to the same exact symbol
foreach (SyntaxBase foundRef in foundRefs)
{
model.GetSymbolInfo(foundRef).Should().BeSameAs(symbol);
}
}
}
[DataTestMethod]
[DynamicData(nameof(GetData), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))]
public async Task FindReferencesResultsShouldIncludeAllSymbolReferenceSyntaxNodes(DataSet dataSet)
{
var (compilation, _, _) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext);
var semanticModel = compilation.GetEntrypointSemanticModel();
var symbolReferences = GetAllBoundSymbolReferences(compilation.SourceFileGrouping.EntryPoint.ProgramSyntax);
var symbols = symbolReferences
.Select(semanticModel.GetSymbolInfo)
.Distinct();
symbols.Should().NotContainNulls();
var foundReferences = symbols
.SelectMany(s => semanticModel.FindReferences(s!))
.Where(refSyntax => !(refSyntax is INamedDeclarationSyntax));
symbolReferences.Should().BeSubsetOf(foundReferences);
}
[TestMethod]
public void GetAllDiagnostics_VerifyDisableNextLineDiagnosticsDirectiveDoesNotSupportCoreCompilerErrorSuppression()
{
var bicepFileContents = @"#disable-next-line BCP029 BCP068
resource test";
var bicepFilePath = FileHelper.SaveResultFile(TestContext, "main.bicep", bicepFileContents);
var documentUri = DocumentUri.FromFileSystemPath(bicepFilePath);
var uri = documentUri.ToUriEncoded();
var files = new Dictionary<Uri, string>
{
[uri] = bicepFileContents,
};
var compilation = Services.BuildCompilation(files, uri);
var diagnostics = compilation.GetEntrypointSemanticModel().GetAllDiagnostics();
diagnostics.Count().Should().Be(2);
diagnostics.Should().SatisfyRespectively(
x =>
{
x.Level.Should().Be(DiagnosticLevel.Error);
x.Code.Should().Be("BCP068");
},
x =>
{
x.Level.Should().Be(DiagnosticLevel.Error);
x.Code.Should().Be("BCP029");
});
}
[TestMethod]
public void GetAllDiagnostics_VerifyDisableNextLineDiagnosticsDirectiveSupportsCoreCompilerWarningSuppression()
{
var bicepFileContents = @"var vmProperties = {
diagnosticsProfile: {
bootDiagnostics: {
enabled: 123
storageUri: true
unknownProp: 'asdf'
}
}
evictionPolicy: 'Deallocate'
}
resource vm 'Microsoft.Compute/virtualMachines@2020-12-01' = {
name: 'vm'
#disable-next-line no-hardcoded-location
location: 'West US'
#disable-next-line BCP036 BCP037
properties: vmProperties
}";
var bicepFilePath = FileHelper.SaveResultFile(TestContext, "main.bicep", bicepFileContents);
var documentUri = DocumentUri.FromFileSystemPath(bicepFilePath);
var uri = documentUri.ToUriEncoded();
var files = new Dictionary<Uri, string>
{
[uri] = bicepFileContents,
};
var compilation = Services.BuildCompilation(files, uri);
compilation.GetEntrypointSemanticModel().GetAllDiagnostics().Should().BeEmpty();
}
[TestMethod]
public void GetAllDiagnostics_VerifyDisableNextLineDiagnosticsDirectiveSupportsLinterWarningSuppression()
{
var bicepFileContents = @"#disable-next-line no-unused-params
param storageAccount string = 'testStorageAccount'";
var bicepFilePath = FileHelper.SaveResultFile(TestContext, "main.bicep", bicepFileContents);
var documentUri = DocumentUri.FromFileSystemPath(bicepFilePath);
var uri = documentUri.ToUriEncoded();
var files = new Dictionary<Uri, string>
{
[uri] = bicepFileContents,
};
var compilation = Services.BuildCompilation(files, uri);
compilation.GetEntrypointSemanticModel().GetAllDiagnostics().Should().BeEmpty();
}
[TestMethod]
public void GetAllDiagnostics_WithNoDisableNextLineDiagnosticsDirectiveInPreviousLine_ShouldReturnDiagnostics()
{
var bicepFileContents = @"#disable-next-line no-unused-params
param storageAccount string = 'testStorageAccount'";
var bicepFilePath = FileHelper.SaveResultFile(TestContext, "main.bicep", bicepFileContents);
var documentUri = DocumentUri.FromFileSystemPath(bicepFilePath);
var uri = documentUri.ToUriEncoded();
var files = new Dictionary<Uri, string>
{
[uri] = bicepFileContents,
};
var compilation = Services.BuildCompilation(files, uri);
compilation.GetEntrypointSemanticModel().GetAllDiagnostics().Count().Should().Be(1);
}
[DataTestMethod]
[DynamicData(nameof(GetData), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))]
public async Task All_nodes_should_be_parented(DataSet dataSet)
{
var (compilation, outputDirectory, _) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext);
var model = compilation.GetEntrypointSemanticModel();
var allNodes = SyntaxCollectorVisitor.Build(model.Root.Syntax);
foreach (var node in allNodes)
{
if (node.Syntax == model.Root.Syntax)
{
model.Binder.GetParent(node.Syntax).Should().BeNull();
}
else
{
model.Binder.GetParent(node.Syntax).Should().NotBeNull();
}
}
}
[DataTestMethod]
[DynamicData(nameof(GetValidDataSets), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))]
[TestCategory(BaselineHelper.BaselineTestCategory)]
public async Task ProgramsShouldProduceExpectedIrTree(DataSet dataSet)
{
var (compilation, outputDirectory, _) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext);
var model = compilation.GetEntrypointSemanticModel();
var builder = new ExpressionBuilder(new(model));
var converted = builder.Convert(model.Root.Syntax);
var expressionList = ExpressionCollectorVisitor.Build(converted);
var expressionByParent = expressionList.ToLookup(x => x.Parent);
TextSpan getSpan(ExpressionCollectorVisitor.ExpressionItem data) => data.Expression.SourceSyntax?.Span ?? TextSpan.TextDocumentStart;
var sourceTextWithDiags = DataSet.AddDiagsToSourceText(dataSet, expressionList, getSpan, expression => ExpressionCollectorVisitor.GetExpressionLoggingString(expressionByParent, expression));
var resultsFile = FileHelper.SaveResultFile(this.TestContext, Path.Combine(dataSet.Name, DataSet.TestFileMainIr), sourceTextWithDiags);
sourceTextWithDiags.Should().EqualWithLineByLineDiffOutput(
TestContext,
dataSet.Ir ?? "",
expectedPath: DataSet.GetBaselineUpdatePath(dataSet, DataSet.TestFileMainIr),
actualPath: resultsFile);
}
private static List<SyntaxBase> GetAllBoundSymbolReferences(ProgramSyntax program)
{
return SyntaxAggregator.Aggregate(
program,
new List<SyntaxBase>(),
(accumulated, current) =>
{
if (current is ISymbolReference symbolReference && TestSyntaxHelper.NodeShouldBeBound(symbolReference))
{
accumulated.Add(current);
}
return accumulated;
},
accumulated => accumulated);
}
private static IEnumerable<object[]> GetData() => DataSets.AllDataSets.ToDynamicTestData();
private static IEnumerable<object[]> GetValidDataSets() => DataSets
.AllDataSets
.Where(ds => ds.IsValid)
.ToDynamicTestData();
}
}