Skip to content

Commit

Permalink
Add SQL code analysis from PackageReference or ProjectReference (#479)
Browse files Browse the repository at this point in the history
* Add test for project reference

* Fix test

* Add PackageReference test

* Add missing TestData

* Merge from main

* Update DacFx version

* Remove extra line
  • Loading branch information
zijchen authored Sep 17, 2024
1 parent bd6a69c commit ac9e6e2
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 4 deletions.
5 changes: 3 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
<RepositoryUrl>https://github.com/microsoft/DacFx</RepositoryUrl>
<RepositoryType>git</RepositoryType>

<!-- Microsoft.Build.Sql SDK will use DLLs from this version of DacFx Nuget. -->
<DacFxPackageVersion Condition="'$(DacFxPackageVersion)' == ''">162.3.566</DacFxPackageVersion>
<DacFxPackageVersion Condition="'$(DacFxPackageVersion)' == ''">162.4.87-preview</DacFxPackageVersion>
<ScriptDomPackageVersion Condition="'$(ScriptDomPackageVersion)' == ''">161.9142.1</ScriptDomPackageVersion>
<SqlClientPackageVersion Condition="'$(SqlClientPackageVersion)' == ''">5.1.6</SqlClientPackageVersion>
</PropertyGroup>
<ItemGroup>
<None Include="$(EnlistmentRoot)\images\nuspecicon.png" Pack="true" PackagePath="" />
Expand Down
4 changes: 2 additions & 2 deletions src/Microsoft.Build.Sql/Microsoft.Build.Sql.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@

<ItemGroup>
<PackageReference Include="Microsoft.SqlServer.DacFx" Version="$(DacFxPackageVersion)" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="$(SqlClientPackageVersion)" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.SqlServer.Server" Version="1.0.0" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.SqlServer.TransactSql.ScriptDom" Version="161.9118.2" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.SqlServer.TransactSql.ScriptDom" Version="$(ScriptDomPackageVersion)" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.SqlServer.Types" Version="160.1000.6" GeneratePathProperty="true" />
<PackageReference Include="System.ComponentModel.Composition" Version="6.0.0" GeneratePathProperty="true" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" GeneratePathProperty="true" />
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.Build.Sql/sdk/Sdk.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildSdksPath)\Microsoft.NET.Sdk\targets\Microsoft.NET.SupportedTargetFrameworks.props" Condition="Exists('$(MSBuildSdksPath)\Microsoft.NET.Sdk\targets\Microsoft.NET.SupportedTargetFrameworks.props')" />

<PropertyGroup>
<NetCoreBuild Condition="'$(NetCoreBuild)' == '' And '$(MSBuildRuntimeType)' == 'Core'">true</NetCoreBuild>
<NetCoreBuild Condition="'$(NetCoreBuild)' == '' And '$(MSBuildRuntimeType)' == 'Full'">false</NetCoreBuild>
<NETCoreTargetsPath Condition="$(NETCoreTargetsPath) == ''">$(MSBuildThisFileDirectory)..\tools\netstandard2.1</NETCoreTargetsPath>
<TargetFramework Condition="'$(TargetFramework)' == '' AND '$(NetCoreBuild)' == 'true'">netstandard2.1</TargetFramework>
<!-- Allow packages of all target frameworks to be referenced by the sqlproj -->
<PackageTargetFallback Condition="'$(PackageTargetFallback)' == ''">@(SupportedTargetFramework->'%(Alias)')</PackageTargetFallback>
</PropertyGroup>

<!-- building in Visual Studio requires some sort of TargetFrameworkVersion. So we condition to NetCoreBuild as false to avoid failures -->
Expand Down
78 changes: 78 additions & 0 deletions test/Microsoft.Build.Sql.Tests/CodeAnalysisTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, string>()
{
{ "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<string, string>()
{
{ "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");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks>netstandard2.1</TargetFrameworks>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SqlServer.DacFx" Version="162.3.566" PrivateAssets="all" />
</ItemGroup>
<Target Name="GetTargetPath" />
<Target Name="_AddAnalyzersToOutput">
<ItemGroup>
<TfmSpecificPackageFile Include="$(OutputPath)\CodeAnalyzerSample.dll" PackagePath="analyzers/dotnet/cs" />
</ItemGroup>
</Target>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//------------------------------------------------------------------------------
// <copyright>
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//----------------------------------------------------------------------------
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
{

/// <summary>
/// Simple test class - note it doesn't use resources since these aren't handled by the test harness
/// that builds dll files
/// </summary>
[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<SqlRuleProblem> Analyze(SqlRuleExecutionContext ruleExecutionContext)
{
List<SqlRuleProblem> problems = new List<SqlRuleProblem>();

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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE TABLE NotAView (
Col VARCHAR(255)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE TABLE NotAView (
Col VARCHAR(255)
)

0 comments on commit ac9e6e2

Please sign in to comment.