diff --git a/Directory.Build.props b/Directory.Build.props index 2ceb515..29a5bb1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,8 +12,9 @@ https://github.com/microsoft/DacFx git - - 162.3.566 + 162.4.87-preview + 161.9142.1 + 5.1.6 diff --git a/src/Microsoft.Build.Sql/Microsoft.Build.Sql.csproj b/src/Microsoft.Build.Sql/Microsoft.Build.Sql.csproj index 218b087..2b3f573 100644 --- a/src/Microsoft.Build.Sql/Microsoft.Build.Sql.csproj +++ b/src/Microsoft.Build.Sql/Microsoft.Build.Sql.csproj @@ -13,9 +13,9 @@ - + - + diff --git a/src/Microsoft.Build.Sql/sdk/Sdk.props b/src/Microsoft.Build.Sql/sdk/Sdk.props index b9e04f5..2dfff64 100644 --- a/src/Microsoft.Build.Sql/sdk/Sdk.props +++ b/src/Microsoft.Build.Sql/sdk/Sdk.props @@ -6,12 +6,15 @@ + true false $(MSBuildThisFileDirectory)..\tools\netstandard2.1 netstandard2.1 + + @(SupportedTargetFramework->'%(Alias)') diff --git a/test/Microsoft.Build.Sql.Tests/CodeAnalysisTests.cs b/test/Microsoft.Build.Sql.Tests/CodeAnalysisTests.cs new file mode 100644 index 0000000..0d74ae6 --- /dev/null +++ b/test/Microsoft.Build.Sql.Tests/CodeAnalysisTests.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.IO; +using NUnit.Framework; + +namespace Microsoft.Build.Sql.Tests +{ + [TestFixture] + public class CodeAnalysisTests : DotnetTestBase + { + [Test] + public void VerifyCodeAnalyzerFromProjectReference() + { + // Copy the analyzer project to a temp folder + string tempFolder = TestUtils.CreateTempDirectory(); + TestUtils.CopyDirectoryRecursive(Path.Combine(this.CommonTestDataDirectory, "CodeAnalyzerSample"), tempFolder); + + // Add the analyzer csproj as a ProjectReference to the test sqlproj + ProjectUtils.AddItemGroup(this.GetProjectFilePath(), "ProjectReference", + new string[] { Path.Combine(tempFolder, "CodeAnalyzerSample.csproj") }, + item => + { + item.AddMetadata("PrivateAssets", "All"); + item.AddMetadata("ReferenceOutputAssembly", "False"); + item.AddMetadata("OutputItemType", "Analyzer"); + item.AddMetadata("SetTargetFramework", "TargetFramework=netstandard2.1"); + }); + + // Set up code analysis properties + ProjectUtils.AddProperties(this.GetProjectFilePath(), new Dictionary() + { + { "RunSqlCodeAnalysis", "true" }, + { "SqlCodeAnalysisRules", "+!CodeAnalyzerSample.TableNameRule001" } // Should fail build on this rule + }); + + int exitCode = this.RunDotnetCommandOnProject("build", out string stdOutput, out string stdError); + + Assert.AreNotEqual(0, exitCode, "Build should have failed"); + Assert.IsTrue(stdOutput.Contains("Table name [dbo].[NotAView] ends in View. This can cause confusion and should be avoided"), "Unexpected stderr"); + } + + [Test] + public void VerifyCodeAnalyzerFromPackageReference() + { + // Set up and create the analyzer package + string tempFolder = TestUtils.CreateTempDirectory(); + TestUtils.CopyDirectoryRecursive(Path.Combine(CommonTestDataDirectory, "CodeAnalyzerSample"), tempFolder); + RunGenericDotnetCommand($"pack {Path.Combine(tempFolder, "CodeAnalyzerSample.csproj")} -o {tempFolder}", out _, out _); + + // Copy analyzer package to local Nuget source + string analyzerPackagePath = Path.Combine(tempFolder, "CodeAnalyzerSample.1.0.0.nupkg"); + FileAssert.Exists(analyzerPackagePath); + File.Copy(analyzerPackagePath, Path.Combine(WorkingDirectory, "pkg", "CodeAnalyzerSample.1.0.0.nupkg")); + + // Add the analyzer package as a PackageReference to the test sqlproj + ProjectUtils.AddItemGroup(this.GetProjectFilePath(), "PackageReference", + new string[] { "CodeAnalyzerSample" }, + item => + { + item.AddMetadata("Version", "1.0.0"); + }); + + // Set up code analysis properties + ProjectUtils.AddProperties(this.GetProjectFilePath(), new Dictionary() + { + { "RunSqlCodeAnalysis", "true" }, + { "SqlCodeAnalysisRules", "+!CodeAnalyzerSample.TableNameRule001" } // Should fail build on this rule + }); + + int exitCode = this.RunDotnetCommandOnProject("build", out string stdOutput, out string stdError); + + Assert.AreNotEqual(0, exitCode, "Build should have failed"); + Assert.IsTrue(stdOutput.Contains("Table name [dbo].[NotAView] ends in View. This can cause confusion and should be avoided"), "Unexpected stderr"); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Build.Sql.Tests/TestData/CodeAnalyzerSample/CodeAnalyzerSample.csproj b/test/Microsoft.Build.Sql.Tests/TestData/CodeAnalyzerSample/CodeAnalyzerSample.csproj new file mode 100644 index 0000000..f98e8aa --- /dev/null +++ b/test/Microsoft.Build.Sql.Tests/TestData/CodeAnalyzerSample/CodeAnalyzerSample.csproj @@ -0,0 +1,16 @@ + + + Library + netstandard2.1 + $(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.Build.Sql.Tests/TestData/CodeAnalyzerSample/TableNameEndingInViewRule.cs b/test/Microsoft.Build.Sql.Tests/TestData/CodeAnalyzerSample/TableNameEndingInViewRule.cs new file mode 100644 index 0000000..9dffce7 --- /dev/null +++ b/test/Microsoft.Build.Sql.Tests/TestData/CodeAnalyzerSample/TableNameEndingInViewRule.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//---------------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.SqlServer.Dac.Extensibility; +using Microsoft.SqlServer.Dac.CodeAnalysis; +using Microsoft.SqlServer.Dac.Model; + +namespace CodeAnalyzerSample +{ + + /// + /// Simple test class - note it doesn't use resources since these aren't handled by the test harness + /// that builds dll files + /// + [ExportCodeAnalysisRule("CodeAnalyzerSample.TableNameRule001", + "SampleRule", + Description = "Table names should not end in 'View'", + Category = "Naming", + PlatformCompatibility = TSqlPlatformCompatibility.OnPremises)] + class TableNameEndingInViewRule : SqlCodeAnalysisRule + { + private static readonly ModelTypeClass[] _supportedElementTypes = new[] { ModelSchema.Table }; + + public TableNameEndingInViewRule() + { + SupportedElementTypes = new[] { Table.TypeClass }; + } + + public override IList Analyze(SqlRuleExecutionContext ruleExecutionContext) + { + List problems = new List(); + + TSqlObject table = ruleExecutionContext.ModelElement; + if (table != null) + { + if (NameEndsInView(table.Name)) + { + string problemDescription = string.Format("Table name {0} ends in View. This can cause confusion and should be avoided", + GetQualifiedTableName(table.Name)); + SqlRuleProblem problem = new SqlRuleProblem(problemDescription, table); + problems.Add(problem); + } + } + + return problems; + } + + private bool NameEndsInView(ObjectIdentifier id) + { + return id.HasName && id.Parts.Last().EndsWith("View", StringComparison.OrdinalIgnoreCase); + } + + private string GetQualifiedTableName(ObjectIdentifier id) + { + StringBuilder buf = new StringBuilder(); + foreach (string part in id.Parts) + { + if (buf.Length > 0) + { + buf.Append('.'); + } + buf.Append('[').Append(part).Append(']'); + } + return buf.ToString(); + } + } +} diff --git a/test/Microsoft.Build.Sql.Tests/TestData/VerifyCodeAnalyzerFromPackageReference/NotAView.sql b/test/Microsoft.Build.Sql.Tests/TestData/VerifyCodeAnalyzerFromPackageReference/NotAView.sql new file mode 100644 index 0000000..1f67824 --- /dev/null +++ b/test/Microsoft.Build.Sql.Tests/TestData/VerifyCodeAnalyzerFromPackageReference/NotAView.sql @@ -0,0 +1,3 @@ +CREATE TABLE NotAView ( + Col VARCHAR(255) +) \ No newline at end of file diff --git a/test/Microsoft.Build.Sql.Tests/TestData/VerifyCodeAnalyzerFromProjectReference/NotAView.sql b/test/Microsoft.Build.Sql.Tests/TestData/VerifyCodeAnalyzerFromProjectReference/NotAView.sql new file mode 100644 index 0000000..1f67824 --- /dev/null +++ b/test/Microsoft.Build.Sql.Tests/TestData/VerifyCodeAnalyzerFromProjectReference/NotAView.sql @@ -0,0 +1,3 @@ +CREATE TABLE NotAView ( + Col VARCHAR(255) +) \ No newline at end of file