Git Product home page Git Product logo

tsqllint's Introduction

CircleCI Coverage Status npm version npm

TSQLLint

TSQLLint is a tool for describing, identifying, and reporting the presence of anti-patterns in TSQL scripts.

Installation

TSQLLint currently officially supports installation with dotnet-tool, Homebrew, and NPM.

Install with dotnet tool

$ dotnet tool install --global TSQLLint

Install with Homebrew

For OSX and Linux the recommended method of installing TSQLLint is to install the tool using homebrew.

$ brew tap tsqllint/homebrew-tsqllint
$ brew install tsqllint

Upgrade with Homebrew

$ brew upgrade tsqllint 

Install with NPM

TSQLLint may also be installed with NPM

This binary can be installed though the npm registry. First, install Node.js version 4 or higher, and then install using the npm install command:

npm install tsqllint -g

Usage

# lint a single file
tsqllint test.sql

# fix all auto-fixable problems (--fix or -x)
tsqllint --fix test.sql

# lint all files in a directory
tsqllint c:\database_scripts

# lint a list of files and directories, paths with whitespace must be enclosed in quotes
tsqllint file_one.sql file_two.sql "c:\database scripts"

# lint using wild cards
tsqllint c:\database_scripts\sp_*.sql

# print path to .tsqllintrc config file
tsqllint --print-config

# display usage info
tsqllint --help

# list the plugins loaded
tsqllint --list-plugins

Integrating TSQLLint with other Tools

Visual Studio Code Extension

In addition to the CLI tool, TSQLLint is built into a Visual Studio Code Extension. This extension can be installed from within the VS Code Extension Interface or from the VS Code Extension marketplace.

Usage Example

SQL Server Management Studio

TSQLLint uses a common message format that allows for integration into off the shelf tools. SQL Server Management Studio can use TSQLLint using SSMS's External Tools feature.

SSMS Integration Image

Configuration

TSQLLint utilizes a configuration file called .tsqllintrc. This file can be generated and edited by users to create their own configurations. To generate this file use the -i or --init flags. If no .tsqllintrc is found the tool will use a default configuration loaded in memory.

# generate a default .tsqllintrc file using the init flag (optional if just using a default configuration)
tsqllint --init

Creating custom configurations

To configure TSQLLint edit its config file, which is named .tsqllintrc. To find its location run tsqllint with the --print-confg or -p option.

TSQLLint will load its config file in the following order or precedence:

  1. The value passed with the -c command line argument, if one is passed
  2. An Environment Variable named TSQLLINTRC
  3. A file named .tsqllintrc in the same local directory in which TSQLLint is executing
  4. A file named .tsqllintrc in the user's home directory

Rule configuration

TSQLLint rules may be set to "off", "warning", or "error". Rules that are violated and are set to "error" will result in TSQLLint returning a non-zero exit code. Rules that are violated, but configured to "warning" will result in a zero exit code, but a warning message will be displayed in the shell output. Rules that are set to "off" will be completely ignored. Rules that are not present in the .tsqllintrc configuration are set to "off"

{
  "rules": {
    "case-sensitive-variables": "error",
    "conditional-begin-end": "error",
    "count-star": "error",
    "cross-database-transaction": "error",
    "data-compression": "error",
    "data-type-length": "error",
    "delete-where": "error",
    "disallow-cursors": "error",
    "full-text": "error",
    "information-schema": "error",
    "keyword-capitalization": "error",
    "linked-server": "error",
    "multi-table-alias": "error",
    "named-constraint": "error",
    "non-sargable": "error",
    "object-property": "error",
    "print-statement": "error",
    "schema-qualify": "error",
    "select-star": "error",
    "semicolon-termination": "error",
    "set-ansi": "error",
    "set-nocount": "error",
    "set-quoted-identifier": "error",
    "set-transaction-isolation-level": "error",
    "set-variable": "error",
    "update-where": "error",
    "upper-lower": "error",
    "unicode-string": "error"
  }
}

Disabling Rules with Inline Comments

To temporarily disable all rule warnings in a script, use comments in the following format:

/* tsqllint-disable */

SELECT * FROM FOO;

/* tsqllint-enable */

To disable or enable warnings for specific rules:

/* tsqllint-disable select-star */

SELECT * FROM FOO;

/* tsqllint-enable select-star */

To disable warnings for the entire script, place a /_ tsqllint-disable _/ comment at the top of the file:

/* tsqllint-disable */

SELECT * FROM FOO;

To disable specific rule warnings for the entire script place a comment similar to the following at the top of the file:

/* tsqllint-disable select-star */

SELECT * FROM FOO;

SQL Compatibility Level

TSQLLint provides a configurable "compatibility-level" that aligns with SQL Server's Compatibility Level. The value defaults to 120 but may be changed with the following edit to the .tsqllintrc or by using inline comments within the SQL file. TSQLLint supports the following compatibility levels 80, 90, 100, 110, 120, 130, 140, and 150.

Setting a default Compatibility Level using .tsqllintrc

Setting the compatibility level within the .tsqllintrc file configures the default Compatibility Level for all files.

{
  "rules": {
    "upper-lower": "error"
  },
  "compatibility-level": 90
}

Setting Compatibility Level Using Inline Comments

Setting the compatibility level using inline comments configures the Compatibility Level for just that file. Overrides should be placed at the top of files.

/* tsqllint-override compatibility-level = 130 */

SELECT * FROM FOO;

SQL Placeholders

Many tools in the SQL ecosystem support placeholders to templatize SQL files as shown in the example below:

SELECT * FROM FOO WHERE BAR = '$(MyPlaceholderValue)';

Before applying any linting rules, TSQLLint will replace any placeholder in a SQL file with values provided via environment variables.

Plugins

You can extend the base functionality of TSQLLint by creating a custom plugin. TSQLLint plugins are Dotnet assemblies that implement the IPlugin interface from TSQLLint.Common. Ensure the plugin is targeting net6.0.

After developing the plugin, update the .tsqllintrc file to point to its .dll.

{
  "rules": {
    "upper-lower": "error"
  },
  "plugins": {
    "my-first-plugin": "c:/users/someone/my-plugins/my-first-plugin.dll",
    "my-second-plugin": "c:/users/someone/my-plugins/my-second-plugin.dll/"
  }
}

This sample plugin notifies users that spaces should be used rather than tabs.

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.SqlServer.TransactSql.ScriptDom;
using TSQLLint.Common;

namespace TSQLLint_Sample_Plugin
{
    public class SamplePlugin : IPlugin
    {
        // This method is required but can be a no-op if using the GetRules method to parse code through the plugin's rules.
        public void PerformAction(IPluginContext context, IReporter reporter)
        {
            string line;
            var lineNumber = 0;

			var reader = new IO.StreamReader(File.OpenRead(context.FilePath));

            while ((line = reader.ReadLine()) != null)
            {
                lineNumber++;
                var column = line.IndexOf("\t", StringComparison.Ordinal);
                reporter.ReportViolation(new SampleRuleViolation(
                    context.FilePath,
                    "prefer-tabs",
                    "Should use spaces rather than tabs",
                    lineNumber,
                    column,
                    RuleViolationSeverity.Warning));
            }
        }

        // Starting with TSQLLint.Common version 3.3.0, this method can be used to return rules to be used by the TSQLLint parser.
        public IDictionary<string, ISqlLintRule> GetRules() => new Dictionary<string, ISqlLintRule>
        {
            ["sample-plugin-rule"] = new SampleRule((Action<string, string, int, int>)null)
        };
    }

    class SampleRuleViolation : IRuleViolation
    {
        public int Column { get; set; }
        public string FileName { get; private set; }
        public int Line { get; set; }
        public string RuleName { get; private set; }
        public RuleViolationSeverity Severity { get; private set; }
        public string Text { get; private set; }

        public SampleRuleViolation(string fileName, string ruleName, string text, int lineNumber, int column, RuleViolationSeverity ruleViolationSeverity)
        {
            FileName = fileName;
            RuleName = ruleName;
            Text = text;
            Line = lineNumber;
            Column = column;
            Severity = ruleViolationSeverity;
        }
    }

    class SampleRule : TSqlFragmentVisitor, ISqlLintRule
    {
        protected readonly Action<string, string, int, int> ErrorCallback;

        public SampleRule(Action<string, string, int, int> errorCallback)
        {
            ErrorCallback = errorCallback;
        }

        public string RULE_NAME => "sample-plugin-rule";
        public string RULE_TEXT => "Sample plugin rule message text";
        public RuleViolationSeverity RULE_SEVERITY => RuleViolationSeverity.Warning;

        public override void Visit(TSqlScript node)
        {
            var line = 0;
            var column = 0;

            // Logic for testing TSQL code for rule goes here.

            ErrorCallback(RULE_NAME, RULE_TEXT, line, column);
        }

        public void FixViolation(List<string> fileLines, IRuleViolation ruleViolation, FileLineActions actions)
        {
            // Logic for fixing rule violation goes here.
        }
    }
}

tsqllint's People

Contributors

alsorokin avatar bdovaz avatar c-w avatar danielgasperut avatar dependabot[bot] avatar dougwilson avatar esauser avatar gentoo90 avatar geoffbaker avatar grantwforsythe avatar jamisonr avatar jeffnowiski-lr avatar kfishkin avatar kolenichsj avatar lowlydba avatar ltylenda avatar nathan-boyd avatar olivia-zh-xm avatar ostreifel avatar pidibi avatar robboek avatar stephenliberty avatar stephenweaver avatar tommye123 avatar yifgu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tsqllint's Issues

Add Support for Parsing Dynamic SQL

This feature would attempt to parse all strings in sql scripts, and if possible apply rules.

Open to feedback, but starting with the following requirements:

  • Parsing shall descend no more than one level. No support for dynamic SQL within dynamic SQL.
  • Error reporting will occur with line numbers and columns relative to the parent script
    • If an error occurs on line 5 of the dynamic SQL but on line 50 of the script then line 50 will be reported.

The application is limited by the version of .net framework installed on host machine

Since we ported to dotnet core I've been striving to update some of the patterns within the application to meet current best practices. Many of these updates require a newer version of the .net framework than we can count on being installed on our user's host machines. Rather than update our framework version and expect users to update on their end as well, we should evaluate appropriateness of Self-contained deployments (SCD) https://docs.microsoft.com/en-us/dotnet/core/deploying/. This appears to provide us with the ability to deploy the application to any host machine regardless of the version of the .net framework is installed, and perhaps even if no framework is installed.

Implement "use strict" feature

This would allow users to tag their scripts with a "use strict" identifier of some sort that would identify their script as one which should be linted strictly. A strict lint would mean that all rules listed in the config would be enforced as errors, despite their configured error level.

CTE's used with derived table asking for aliasing.

When using a derived table with a common table expression inside, the linter asks for the common table expression to be aliased with default lintrc. This error disappears if you remove the Group clause.
This is the error:
Test.sql(25,20): error multi-table-alias : Unaliased table found in multi table joins.

Test.sql

SET NOCOUNT ON;
SET ANSI_NULLS ON;
SET QUOTED_IDENTIFIER ON;

WITH CommonTableExpression (Foo, Bar)
AS (
	SELECT FooBars.Foo,
			FooCars.Bar
	FROM dbo.FooBars AS FooBars WITH (NOLOCK)
	LEFT JOIN dbo.FooCars AS FooCars
			ON Foobars.Foo = FooCars.Foo
)

SELECT
JoinedTable.Foo,
JoinedTable.Bar,
JoinedTable.Secret
FROM (
		SELECT
			CommonTableExpression.Foo,
			CommonTableExpression.Bar,
			CoolTable.Secret
		FROM dbo.CoolTable AS CoolTable WITH (NOLOCK)
		INNER JOIN CommonTableExpression    --multi-table-alias error here
			ON CommonTableExpression.Foo = CoolTable.Foo
		WHERE CoolTable.Secret < 5
) AS JoinedTable
GROUP BY JoinedTable.Foo,
JoinedTable.Bar,
JoinedTable.Secret;

Illegal Unexpected Token

When installing node package I receive the following:

OS Windows_NT 6.2.9200
Node 0.10.40
NPM 1.4.28

execSync(`call ${process.env.APPDATA}/npm/node_modules/tsqllint/scripts/instal
         ^
SyntaxError: Unexpected token ILLEGAL
    at Module._compile (module.js:439:25)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Function.Module.runMain (module.js:497:10)
    at startup (node.js:119:16)
    at node.js:935:3

The init and force options display help even if they work as expected

Both the --init and the --force options will create/rectreate the config file as expeceted; however, they also display the help option. This can be misleading if you miss the output statement that the config file has been created. If these options work as expected, don't display help

Incorrect Semicolon Termination Within Waitfor

the following code snippet incorrectly expects a semicolon following FROM dbo.SomeServiceQueue

DECLARE @ConversationHandle UNIQUEIDENTIFIER;
DECLARE @MessageTypeName NVARCHAR(256);
DECLARE @MessageBody XML;
DECLARE @ServiceName NVARCHAR(256);
DECLARE @MessageContract NVARCHAR(256);

WAITFOR (
    RECEIVE TOP(1)
        @ConversationHandle = conversation_handle,
        @MessageTypeName = message_type_name,
        @MessageBody = CAST(message_body AS XML),
        @ServiceName = service_name,
        @MessageContract = service_contract_name
        FROM dbo.SomeServiceQueue
), TIMEOUT 3000;

Order error output by row number

Can output be reordered by line number vs whatever is occurring now?

for example:

ImportAssociationAttributes.sql(74,1): error conditional-begin-end : Expected BEGIN and END statement within conditional logic block.
ImportAssociationAttributes.sql(94,1): error data-compression : Expected table to use data compression.
ImportAssociationAttributes.sql(4,26): error information-schema : Expected use of SYS.Partitions rather than INFORMATION_SCHEMA views.
ImportAssociationAttributes.sql(176,12): error schema-qualify : Object name not schema qualified.
ImportAssociationAttributes.sql(1,18): error semicolon-termination : Statement not terminated with semicolon.
ImportAssociationAttributes.sql(2,25): error semicolon-termination : Statement not terminated with semicolon.
ImportAssociationAttributes.sql(10,4): error semicolon-termination : Statement not terminated with semicolon.
ImportAssociationAttributes.sql(9,51): error semicolon-termination : Statement not terminated with semicolon.

Linter expect semicolons after goto 'Label:'

If goto statement is used lint expects ';' after referenced label:

(50)		BEGIN CATCH
			.......
(55)			GOTO ERROR_HANDLER;
(56)		END CATCH;
		.....
(212)		ERROR_HANDLER:
(213)		BEGIN
(214)			    RAISERROR (@ErrorMessage, 11, 1);
(215) 	    		    RETURN; 
(216)		END;

Test.sql(212,19): error semicolon-termination : Statement not terminated with semicolon.

Concat strings rule Object reference not set to an instance of an object.

Object reference not set to an instance of an object.
   at TSQLLint.Lib.Rules.ConcatStringsRule.Visit(DeclareVariableElement node)
   at Microsoft.SqlServer.TransactSql.ScriptDom.TSqlFragmentVisitor.ExplicitVisit(DeclareVariableElement node)
   at Microsoft.SqlServer.TransactSql.ScriptDom.DeclareVariableStatement.AcceptChildren(TSqlFragmentVisitor visitor)
   at Microsoft.SqlServer.TransactSql.ScriptDom.TSqlBatch.AcceptChildren(TSqlFragmentVisitor visitor)
   at Microsoft.SqlServer.TransactSql.ScriptDom.TSqlScript.AcceptChildren(TSqlFragmentVisitor visitor)
   at TSQLLint.Lib.Parser.SqlRuleVisitor.VisitRules(String sqlPath, Stream sqlFileStream)
   at TSQLLint.Lib.Parser.SqlFileProcessor.ProcessRules(Stream fileStream, String filePath)
   at TSQLLint.Lib.Parser.SqlFileProcessor.ProcessFile(Stream fileStream, String filePath)
   at TSQLLint.Lib.Parser.SqlFileProcessor.ProcessIfSqlFile(String fileName)
   at TSQLLint.Lib.Parser.SqlFileProcessor.ProcessDirectory(String path)
   at TSQLLint.Lib.Parser.SqlFileProcessor.ProcessPath(String path)
   at TSQLLint.Lib.Parser.SqlFileProcessor.ProcessList(List`1 paths)
   at TSQLLint.Console.Application.Run()
   at TSQLLint.Console.Program.Main(String[] args)

Removing this rule from default configs until issue is resolved

ScriptDom Not Found

Application fails when Microsoft.SqlServer.TransactSql.ScriptDom not already installed on host system.

Linter shouldn't expect semicolons after "BEGIN"

Given the following input, tsqllint outputs an error.

IF (1 = 1)
BEGIN
  SELECT 0;
END;

The error is:
test.sql(2,7): error semicolon-termination : Statement not terminated with semicolon.

My understanding is that is poor style to put semi-colons after BEGIN, so this error should not appear.

Crash when given a path to a non-existent directory

> tsqllint C:\BogusDir\BogusFile.sql

A Windows application crash dialog box opens, the following is on the console:

Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\BogusDir'.
   at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
   at System.IO.FileSystemEnumerableIterator`1.CommonInit()
   at System.IO.FileSystemEnumerableIterator`1..ctor(String path, String originalUserPath, String searchPattern, SearchOption searchOption, SearchResultHandler`1 resultHandler, Boolean checkHost)
   at System.IO.Directory.EnumerateFiles(String path, String searchPattern, SearchOption searchOption)
   at System.IO.Abstractions.DirectoryWrapper.EnumerateFiles(String path, String searchPattern, SearchOption searchOption)
   at TSQLLINT_LIB.Parser.SqlFileProcessor.ProcessWildCard(String path) in d:\git\tsqllint\TSQLLINT_LIB\Parser\SqlFileProcessor.cs:line 109
   at TSQLLINT_LIB.Parser.SqlFileProcessor.ProcessPath(String path) in d:\git\tsqllint\TSQLLINT_LIB\Parser\SqlFileProcessor.cs:line 58
   at TSQLLINT_LIB.Parser.SqlFileProcessor.ProcessList(List`1 paths) in d:\git\tsqllint\TSQLLINT_LIB\Parser\SqlFileProcessor.cs:line 129
   at TSQLLINT_CONSOLE.LintingHandler.Lint()
   at TSQLLINT_CONSOLE.Application.Run()
   at TSQLLINT_CONSOLE.Program.Main(String[] args)

Schema blacklist

Small feature request (sorry if this has been discussed before, I am new to this channel) Could TSQLLint flag if the string representing schemaname.procedurename ('foo' in my example matches in the following standard store procedure script pattern? IF NOT EXISTS (SELECT * FROM sys.procedures WHERE object_id = OBJECT_ID(N'foo'))
BEGIN
EXEC (N'CREATE PROCEDURE foo AS RETURN 0;');
END;
GO

ALTER PROCEDURE foo

Consider making easier to use in Script Review process

Until such time this gates check ins, it would be nice to provide a way to easily run tsqllint during script review process. Recently comparison was added as a link directly from google spreadsheet. Something similar for this tool would help.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.