Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds code analysis rule sample template #515

Merged
merged 2 commits into from
Nov 15, 2024

Conversation

dzsquared
Copy link
Contributor

No description provided.

@dzsquared dzsquared requested a review from zijchen October 25, 2024 21:51
@dzsquared
Copy link
Contributor Author

can't tag you as a reviewer @ErikEJ

@ErikEJ
Copy link
Contributor

ErikEJ commented Oct 26, 2024

LGTM!

I made some changes to the template code, to make play better with various analyzers and the latest IDE versions:

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;

/// <summary>
/// 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.
/// </summary>
[ExportCodeAnalysisRule(
    id: RuleId,
    displayName: RuleName,
    Description = ProblemDescription,
    Category = RuleCategory,
    RuleScope = SqlRuleScope.Element)]
public sealed class AvoidWaitForDelayRule : SqlCodeAnalysisRule
{
    /// <summary>
    /// 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."
    /// </summary>
    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,
        };
    }

    /// <summary>
    /// For element-scoped rules the Analyze method is executed once for every matching
    /// object in the model.
    /// </summary>
    /// <param name="ruleExecutionContext">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.
    /// </param>
    /// <returns>A list of problems should be returned. These will be displayed in the Visual
    /// Studio error list</returns>
    public override IList<SqlRuleProblem> Analyze(
        SqlRuleExecutionContext ruleExecutionContext)
    {
        var problems = new List<SqlRuleProblem>();

        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<FunctionType>(TableValuedFunction.FunctionType)
            == FunctionType.InlineTableValuedFunction;
    }
}

internal class WaitForDelayVisitor : TSqlConcreteFragmentVisitor
{
    public IList<WaitForStatement> WaitForDelayStatements { get; private set; }

    // Define the class constructor
    public WaitForDelayVisitor()
    {
        WaitForDelayStatements = new List<WaitForStatement>();
    }

    public override void ExplicitVisit(WaitForStatement node)
    {
        // We are only interested in WAITFOR DELAY occurrences
        if (node.WaitForOption == WaitForOption.Delay)
        {
            WaitForDelayStatements.Add(node);
        }
    }
}

</ItemGroup>

<ItemGroup>
<None Include="bin\$(Configuration)\$(TargetFramework)\SqlCodeAnalysis1.dll"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this could be simplified to $(TargetPath) ?

@dzsquared dzsquared requested a review from zijchen November 12, 2024 18:19
Copy link
Contributor

@ErikEJ ErikEJ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@dzsquared dzsquared merged commit aacacda into main Nov 15, 2024
13 checks passed
@dzsquared dzsquared deleted the drskwier/codeanalysis-template branch November 15, 2024 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants