diff --git a/src/Microsoft.Build.Sql.Templates/Microsoft.Build.Sql.Templates.csproj b/src/Microsoft.Build.Sql.Templates/Microsoft.Build.Sql.Templates.csproj index 8eb980a..37eafef 100644 --- a/src/Microsoft.Build.Sql.Templates/Microsoft.Build.Sql.Templates.csproj +++ b/src/Microsoft.Build.Sql.Templates/Microsoft.Build.Sql.Templates.csproj @@ -11,7 +11,7 @@ false true true - $(BaseIntermediateOutputPath)\$(Configuration)\$(MSBuildThisFileName)\sqlproject + $(BaseIntermediateOutputPath)\$(Configuration)\$(MSBuildThisFileName) $(NoWarn);NU5128 true @@ -29,20 +29,45 @@ - + + + $(TemplateIntermediateOutputPath)\sqlproject + - + - - + + + + + + + + + + + $(TemplateIntermediateOutputPath)\sqlcodeanalysis + + + + + + + + + diff --git a/src/Microsoft.Build.Sql.Templates/README.md b/src/Microsoft.Build.Sql.Templates/README.md index 43f8f00..4e96896 100644 --- a/src/Microsoft.Build.Sql.Templates/README.md +++ b/src/Microsoft.Build.Sql.Templates/README.md @@ -10,6 +10,7 @@ dotnet new install Microsoft.Build.Sql.Templates ## Using the templates +### SQL project Creating a new project "AdventureWorks" (`-n` or `--name`): @@ -36,6 +37,21 @@ Creating a new project "AdventureWorks" with a `.gitignore` file (-g): dotnet new sqlproj -n "AdventureWorks" -g ``` +### New sample code analysis rule + +Creating a new sample code analysis rule "WaitForDelay" (`-n` or `--name`): + +```bash +dotnet new sqlcodeanalysis -n "WaitForDelay" +``` + +Displaying help information for the SQL code analysis template (`-h`): + +```bash +dotnet new sqlcodeanalysis -h +``` + + ## Building the templates If you want to customize or contribute to the templates, you will need to build and install the templates locally. The following instructions will help you get started. diff --git a/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/.template.config/template.json b/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/.template.config/template.json new file mode 100644 index 0000000..72f2473 --- /dev/null +++ b/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/.template.config/template.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ + "Database", + "SqlServer" + ], + "identity": "Microsoft.Build.Sql.CodeAnalysis", + "name": "SQL Server Database Code Analysis", + "description": "A a .NET library project that contains the scaffolding for SQL Code Analysis", + "shortName": "sqlcodeanalysis", + "tags": { + "language": "SQL", + "type": "project" + }, + "sourceName": "SqlCodeAnalysis1", + "preferNameDirectory": true, + "sources": [ + { + "source": "./", + "target": "./", + "include": ["SqlCodeAnalysis1.csproj", "SqlCodeAnalysis1.cs", "README.md"] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/README.md b/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/README.md new file mode 100644 index 0000000..1a76b24 --- /dev/null +++ b/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/README.md @@ -0,0 +1,45 @@ +# New SQL code analysis rule for SQL projects + +## Build + +To build the code analysis project, run the following command: + +```bash +dotnet build +``` + +To package the code analysis project as a NuGet package for referencing in a SQL project, run the following command: + +```bash +dotnet pack +``` + +🎉 Congrats! You have successfully built the project and now have a NuGet package to reference in your SQL project. + +## Use the code analysis rule in SQL projects + +To reference the code analysis project in a SQL project, we need to complete 2 steps: + 1. Publish the code analysis project as a NuGet package. + 1. Reference the code analysis project in the SQL project. + +### Publish the code analysis project + +We packaged the code analysis project as a NuGet package in the previous step and will publish it to a remote feed or a [local source](https://learn.microsoft.com/dotnet/core/tools/dotnet-nuget-add-source) (folder). Add a folder as a local feed by running the following command: + +```bash +dotnet nuget add source c:\packages +``` + +Copy the NuGet package from `bin/Release` to the local source folder. + +### Reference the code analysis project in the SQL project + +The following example demonstrates how to reference the code analysis project in a SQL project: + +```xml + + + +``` + +Set either the SQL project property `True` or run `dotnet build /p:RunSqlCodeAnalysis=True` to generate code analysis output in the build log. diff --git a/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/SqlCodeAnalysis1.cs b/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/SqlCodeAnalysis1.cs new file mode 100644 index 0000000..a99c380 --- /dev/null +++ b/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/SqlCodeAnalysis1.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.SqlServer.Dac.CodeAnalysis; +using Microsoft.SqlServer.Dac.Model; +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Sample.SqlCodeAnalysis1 { + /// + /// This is a rule that returns a warning message + /// whenever there is a WAITFOR DELAY statement appears inside a subroutine body. + /// This rule only applies to stored procedures, functions and triggers. + /// + [ExportCodeAnalysisRule( + id: RuleId, + displayName: RuleName, + Description = ProblemDescription, + Category = RuleCategory, + RuleScope = SqlRuleScope.Element)] + public sealed class AvoidWaitForDelayRule : SqlCodeAnalysisRule + { + /// + /// The Rule ID should resemble a fully-qualified class name. In the Visual Studio UI + /// rules are grouped by "Namespace + Category", and each rule is shown using "Short ID: DisplayName". + /// For this rule, that means the grouping will be "Public.Dac.Samples.Performance", with the rule + /// shown as "SR1004: Avoid using WaitFor Delay statements in stored procedures, functions and triggers." + /// + public const string RuleId = "Sample.SqlCodeAnalysis1.SSCA1004"; + public const string RuleName = "Avoid using WaitFor Delay statements in stored procedures, functions and triggers."; + public const string ProblemDescription = "Avoid using WAITFOR DELAY in {0}"; + public const string RuleCategory = "Performance"; + + public AvoidWaitForDelayRule() + { + // This rule supports Procedures, Functions and Triggers. Only those objects will be passed to the Analyze method + SupportedElementTypes = new[] + { + // Note: can use the ModelSchema definitions, or access the TypeClass for any of these types + ModelSchema.ExtendedProcedure, + ModelSchema.Procedure, + ModelSchema.TableValuedFunction, + ModelSchema.ScalarFunction, + ModelSchema.DatabaseDdlTrigger, + ModelSchema.DmlTrigger, + ModelSchema.ServerDdlTrigger, + }; + } + + /// + /// For element-scoped rules the Analyze method is executed once for every matching + /// object in the model. + /// + /// The context object contains the TSqlObject being + /// analyzed, a TSqlFragment + /// that's the AST representation of the object, the current rule's descriptor, and a + /// reference to the model being + /// analyzed. + /// + /// A list of problems should be returned. These will be displayed in the Visual + /// Studio error list + public override IList Analyze( + SqlRuleExecutionContext ruleExecutionContext) + { + var problems = new List(); + var modelElement = ruleExecutionContext.ModelElement; + + // this rule does not apply to inline table-valued function + // we simply do not return any problem in that case. + if (IsInlineTableValuedFunction(modelElement)) + { + return problems; + } + + var elementName = GetElementName(ruleExecutionContext, modelElement); + + // The rule execution context has all the objects we'll need, including the + // fragment representing the object, + // and a descriptor that lets us access rule metadata + var fragment = ruleExecutionContext.ScriptFragment; + var ruleDescriptor = ruleExecutionContext.RuleDescriptor; + + // To process the fragment and identify WAITFOR DELAY statements we will use a + // visitor + var visitor = new WaitForDelayVisitor(); + fragment.Accept(visitor); + var waitforDelayStatements = visitor.WaitForDelayStatements; + + // Create problems for each WAITFOR DELAY statement found + // When creating a rule problem, always include the TSqlObject being analyzed. This + // is used to determine + // the name of the source this problem was found in and a best guess as to the + // line/column the problem was found at. + // + // In addition if you have a specific TSqlFragment that is related to the problem + // also include this + // since the most accurate source position information (start line and column) will + // be read from the fragment + foreach (WaitForStatement waitForStatement in waitforDelayStatements) + { + var problem = new SqlRuleProblem( + string.Format(CultureInfo.InvariantCulture, ruleDescriptor.DisplayDescription, elementName), + modelElement, + waitForStatement); + problems.Add(problem); + } + + return problems; + } + + private static string GetElementName( + SqlRuleExecutionContext ruleExecutionContext, + TSqlObject modelElement) + { + // Get the element name using the built in DisplayServices. This provides a number of + // useful formatting options to + // make a name user-readable + var displayServices = ruleExecutionContext.SchemaModel.DisplayServices; + var elementName = displayServices.GetElementName( + modelElement, ElementNameStyle.EscapedFullyQualifiedName); + return elementName; + } + + private static bool IsInlineTableValuedFunction(TSqlObject modelElement) + { + return TableValuedFunction.TypeClass.Equals(modelElement.ObjectType) + && modelElement.GetMetadata(TableValuedFunction.FunctionType) + == FunctionType.InlineTableValuedFunction; + } + } + + internal class WaitForDelayVisitor : TSqlConcreteFragmentVisitor + { + public IList WaitForDelayStatements { get; private set; } + + // Define the class constructor + public WaitForDelayVisitor() + { + WaitForDelayStatements = new List(); + } + + public override void ExplicitVisit(WaitForStatement node) + { + // We are only interested in WAITFOR DELAY occurrences + if (node.WaitForOption == WaitForOption.Delay) + { + WaitForDelayStatements.Add(node); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/SqlCodeAnalysis1.csproj b/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/SqlCodeAnalysis1.csproj new file mode 100644 index 0000000..0162f65 --- /dev/null +++ b/src/Microsoft.Build.Sql.Templates/sqlcodeanalysis/SqlCodeAnalysis1.csproj @@ -0,0 +1,19 @@ + + + + Sample.SqlCodeAnalysis1 + 1.0.0 + netstandard2.1 + + + + + + + + + + diff --git a/src/Microsoft.Build.Sql.Templates/sqlproject/.template.config/template.json b/src/Microsoft.Build.Sql.Templates/sqlproject/.template.config/template.json index d5a7ce5..1f02936 100644 --- a/src/Microsoft.Build.Sql.Templates/sqlproject/.template.config/template.json +++ b/src/Microsoft.Build.Sql.Templates/sqlproject/.template.config/template.json @@ -5,7 +5,7 @@ "Database", "SqlServer" ], - "identity": "Microsoft.Build.Sql", + "identity": "Microsoft.Build.Sql.Project", "name": "SQL Server Database Project", "description": "A project that creates a SQL Server Data-Tier Application package (.dacpac)", "shortName": "sqlproj",