Добавьте файлы проекта.
This commit is contained in:
35
SQLLinter/Infrastructure/Rules/AliasStyleConsistencyRule.cs
Normal file
35
SQLLinter/Infrastructure/Rules/AliasStyleConsistencyRule.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class AliasStyleConsistencyRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Алиасы должны использовать единый стиль (AS): {0}";
|
||||
|
||||
public override void Visit(NamedTableReference node)
|
||||
{
|
||||
if (node.Alias != null && node.Alias.QuoteType == QuoteType.NotQuoted)
|
||||
{
|
||||
// ScriptDom не хранит явно "AS", но можно проверять TokenStream
|
||||
var aliasToken = node.Alias;
|
||||
var tokens = node.ScriptTokenStream;
|
||||
if (tokens != null)
|
||||
{
|
||||
int idx = node.FirstTokenIndex;
|
||||
bool hasAs = false;
|
||||
for (int i = idx; i <= node.LastTokenIndex; i++)
|
||||
{
|
||||
if (tokens[i].Text.Equals("AS", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hasAs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasAs)
|
||||
AddViolation(node, $"[{node.Alias.Value}]");
|
||||
}
|
||||
}
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class AlterInsteadOfCreateOrAlterRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещено использовать ALTER, допускается только CREATE OR ALTER: {0}.";
|
||||
|
||||
public override void Visit(AlterProcedureStatement node)
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
}
|
||||
|
||||
public override void Visit(AlterFunctionStatement node)
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.Name));
|
||||
}
|
||||
|
||||
public override void Visit(AlterTriggerStatement node)
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.Name));
|
||||
}
|
||||
}
|
||||
18
SQLLinter/Infrastructure/Rules/AlterProcedureInDboRule.cs
Normal file
18
SQLLinter/Infrastructure/Rules/AlterProcedureInDboRule.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class AlterProcedureInDboRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещено изменение процедур в схеме dbo: {0}";
|
||||
|
||||
public override void Visit(AlterProcedureStatement node)
|
||||
{
|
||||
if (node.ProcedureReference.Name.SchemaIdentifier?.Value.Equals("dbo", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
26
SQLLinter/Infrastructure/Rules/BetweenRule.cs
Normal file
26
SQLLinter/Infrastructure/Rules/BetweenRule.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class BetweenRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Избегать BETWEEN, использовать >= и <";
|
||||
|
||||
public override void Visit(TSqlScript node)
|
||||
{
|
||||
base.Visit(node);
|
||||
|
||||
var tokens = node.ScriptTokenStream;
|
||||
if (tokens == null) return;
|
||||
|
||||
foreach (var t in tokens)
|
||||
{
|
||||
if (t.TokenType == TSqlTokenType.Between)
|
||||
{
|
||||
AddViolation(Name, Text, t.Line, t.Column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
46
SQLLinter/Infrastructure/Rules/CaseSensitiveVariablesRule.cs
Normal file
46
SQLLinter/Infrastructure/Rules/CaseSensitiveVariablesRule.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// Имена переменных будут использовать общий регистр
|
||||
/// </summary>
|
||||
public class CaseSensitiveVariablesRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private readonly List<string> variableNames;
|
||||
|
||||
public CaseSensitiveVariablesRule()
|
||||
{
|
||||
this.variableNames = new List<string>();
|
||||
}
|
||||
|
||||
public override string Text => "Ожидается, что имена переменных будут использовать один регистр: {0} (DECLARE {1})";
|
||||
|
||||
public override void Visit(TSqlBatch node)
|
||||
{
|
||||
variableNames.Clear();
|
||||
}
|
||||
|
||||
public override void Visit(DeclareVariableStatement node)
|
||||
{
|
||||
foreach (var declaration in node.Declarations)
|
||||
{
|
||||
variableNames.Add(declaration.VariableName.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Visit(VariableReference node)
|
||||
{
|
||||
var variableName = node.Name;
|
||||
var caseInsensitiveMatch = variableNames.Where(v => v.ToUpper() == variableName.ToUpper());
|
||||
|
||||
if (!caseInsensitiveMatch.Any()) return;
|
||||
|
||||
var declareName = caseInsensitiveMatch.First();
|
||||
|
||||
if (declareName == variableName) return;
|
||||
|
||||
AddViolation(node, variableName, declareName);
|
||||
}
|
||||
}
|
||||
80
SQLLinter/Infrastructure/Rules/ColumnNullabilityRule.cs
Normal file
80
SQLLinter/Infrastructure/Rules/ColumnNullabilityRule.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class ColumnNullabilityRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "При объявлении таблицы необходимо указывать для столбцов NULL/NOT NULL: таблица {0} - столбец [{1}]";
|
||||
|
||||
private string? _currentTable;
|
||||
|
||||
public override void Visit(CreateTableStatement node)
|
||||
{
|
||||
_currentTable = SQLHelpers.ObjectGetFullName(node.SchemaObjectName);
|
||||
|
||||
|
||||
foreach (var element in node.Definition.ColumnDefinitions)
|
||||
{
|
||||
CheckColumn(element);
|
||||
}
|
||||
|
||||
_currentTable = null;
|
||||
}
|
||||
|
||||
public override void Visit(AlterTableAddTableElementStatement node)
|
||||
{
|
||||
_currentTable = SQLHelpers.ObjectGetFullName(node.SchemaObjectName);
|
||||
|
||||
foreach (var element in node.Definition.ColumnDefinitions)
|
||||
{
|
||||
CheckColumn(element);
|
||||
}
|
||||
|
||||
_currentTable = null;
|
||||
}
|
||||
|
||||
public override void Visit(AlterTableAlterColumnStatement node)
|
||||
{
|
||||
_currentTable = SQLHelpers.ObjectGetFullName(node.SchemaObjectName);
|
||||
|
||||
bool hasNullabilityConstraint = node.Options
|
||||
.OfType<NullableConstraintDefinition>()
|
||||
.Any();
|
||||
|
||||
if (!hasNullabilityConstraint)
|
||||
{
|
||||
AddViolation(node,
|
||||
_currentTable ?? "<unknown>",
|
||||
node.ColumnIdentifier?.Value ?? "<unknown>");
|
||||
}
|
||||
|
||||
_currentTable = null;
|
||||
|
||||
}
|
||||
|
||||
public override void Visit(DeclareTableVariableStatement node)
|
||||
{
|
||||
_currentTable = node.Body.VariableName.Value;
|
||||
|
||||
foreach (var column in node.Body.Definition.ColumnDefinitions)
|
||||
{
|
||||
CheckColumn(column);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckColumn(ColumnDefinition node)
|
||||
{
|
||||
bool hasNullabilityConstraint = node.Constraints
|
||||
.OfType<NullableConstraintDefinition>()
|
||||
.Any();
|
||||
|
||||
if (!hasNullabilityConstraint)
|
||||
{
|
||||
AddViolation(node,
|
||||
_currentTable ?? "<unknown>",
|
||||
node.ColumnIdentifier?.Value ?? "<unknown>");
|
||||
}
|
||||
}
|
||||
}
|
||||
107
SQLLinter/Infrastructure/Rules/Common/ColumnNumberCalculator.cs
Normal file
107
SQLLinter/Infrastructure/Rules/Common/ColumnNumberCalculator.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Core;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules.Common;
|
||||
|
||||
public static class ColumnNumberCalculator
|
||||
{
|
||||
public static int GetNodeColumnPosition(TSqlFragment node)
|
||||
{
|
||||
var line = string.Empty;
|
||||
var nodeStartLine = node.StartLine;
|
||||
var nodeLastLine = node.ScriptTokenStream[node.LastTokenIndex].Line;
|
||||
|
||||
for (var tokenIndex = 0; tokenIndex <= node.LastTokenIndex; tokenIndex++)
|
||||
{
|
||||
var token = node.ScriptTokenStream[tokenIndex];
|
||||
if (token.Line >= nodeStartLine && token.Line <= nodeLastLine)
|
||||
{
|
||||
line += token.Text;
|
||||
}
|
||||
}
|
||||
|
||||
var positionOfNodeOnLine = line.LastIndexOf(node.ScriptTokenStream[node.FirstTokenIndex].Text, StringComparison.Ordinal);
|
||||
var charactersBeforeNode = line.Substring(0, positionOfNodeOnLine);
|
||||
|
||||
var offSet = 0;
|
||||
if (charactersBeforeNode.IndexOf(" ", StringComparison.Ordinal) != -1)
|
||||
{
|
||||
offSet = 1;
|
||||
}
|
||||
|
||||
var tabCount = CountTabs(charactersBeforeNode);
|
||||
var totalTabLength = tabCount * Constants.TabWidth;
|
||||
|
||||
var nodePosition = totalTabLength + (charactersBeforeNode.Length - tabCount) + offSet;
|
||||
return nodePosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns -1 if can't be found
|
||||
/// </summary>
|
||||
/// <param name="line"></param>
|
||||
/// <param name="column"></param>
|
||||
/// <returns></returns>
|
||||
public static int GetIndex(string line, int column)
|
||||
{
|
||||
var index = 0;
|
||||
while (index != column)
|
||||
{
|
||||
if (line.Length <= index)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (line[index] == '\t')
|
||||
{
|
||||
column -= (Constants.TabWidth - 1);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return column - 1;
|
||||
}
|
||||
|
||||
private static int CountTabs(string charactersBeforeNode)
|
||||
{
|
||||
int tabCount = 0;
|
||||
foreach (var c in charactersBeforeNode)
|
||||
{
|
||||
if (c == '\t')
|
||||
{
|
||||
tabCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return tabCount;
|
||||
}
|
||||
|
||||
// count all tabs on a line up to the last token index
|
||||
public static int CountTabsBeforeToken(int lastTokenLine, int lastTokenIndex, IEnumerable<TSqlParserToken> tokens)
|
||||
{
|
||||
var tabCount = 0;
|
||||
var sqlParserTokens = tokens as TSqlParserToken[] ?? tokens.ToArray();
|
||||
|
||||
for (var tokenIndex = 0; tokenIndex < lastTokenIndex; tokenIndex++)
|
||||
{
|
||||
var token = sqlParserTokens[tokenIndex];
|
||||
if (token.Line != lastTokenLine || string.IsNullOrEmpty(token.Text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
tabCount += CountTabs(token.Text);
|
||||
}
|
||||
|
||||
return tabCount;
|
||||
}
|
||||
|
||||
public static int GetColumnNumberBeforeToken(int tabsOnLine, TSqlParserToken token)
|
||||
{
|
||||
return token.Column + ((tabsOnLine * Constants.TabWidth) - tabsOnLine);
|
||||
}
|
||||
|
||||
public static int GetColumnNumberAfterToken(int tabsOnLine, TSqlParserToken token)
|
||||
{
|
||||
return token.Column + token.Text.Length + ((tabsOnLine * Constants.TabWidth) - tabsOnLine);
|
||||
}
|
||||
}
|
||||
69
SQLLinter/Infrastructure/Rules/ConditionalBeginEndRule.cs
Normal file
69
SQLLinter/Infrastructure/Rules/ConditionalBeginEndRule.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// Ожидается наличие BEGIN - END в блоке IF
|
||||
/// </summary>
|
||||
public class ConditionalBeginEndRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private readonly Regex IsWhiteSpaceOrSemiColon = new Regex(@"\s|;", RegexOptions.Compiled);
|
||||
|
||||
|
||||
public override string Text => "Ожидается наличие BEGIN - END в блоке IF";
|
||||
|
||||
public override void Visit(IfStatement node)
|
||||
{
|
||||
if (node.ThenStatement is not BeginEndBlockStatement)
|
||||
{
|
||||
AddViolation(Name, Text, GetLineNumber(node), GetColumnNumber(node));
|
||||
}
|
||||
|
||||
if (node.ElseStatement != null && node.ElseStatement is not BeginEndBlockStatement && node.ElseStatement is not IfStatement)
|
||||
{
|
||||
AddViolation(Name, Text, GetLineNumber(node.ElseStatement), GetColumnNumber(node.ElseStatement));
|
||||
}
|
||||
}
|
||||
|
||||
public override void FixViolation(List<string> fileLines, IRuleViolation ruleViolation, FileLineActions actions)
|
||||
{
|
||||
var ifNode = FixHelpers.FindViolatingNode<IfStatement>(fileLines, ruleViolation);
|
||||
TSqlStatement statement;
|
||||
|
||||
if (ifNode == null)
|
||||
{
|
||||
(statement, ifNode) = FindElse(fileLines, ruleViolation);
|
||||
}
|
||||
else
|
||||
{
|
||||
statement = ifNode.ThenStatement;
|
||||
}
|
||||
|
||||
var stream = statement.ScriptTokenStream;
|
||||
var indent = FixHelpers.GetIndent(fileLines, ifNode);
|
||||
var beingLine = stream[statement.FirstTokenIndex].Line - 1;
|
||||
var ifNodeLastToken = stream[statement.LastTokenIndex];
|
||||
var endLine = stream[statement.LastTokenIndex].Line;
|
||||
|
||||
if (statement.StartLine == ifNodeLastToken.Line)
|
||||
{
|
||||
var index = statement.LastTokenIndex;
|
||||
actions.InsertInLine(statement.StartLine - 1, stream[index].Column, " END");
|
||||
actions.InsertInLine(statement.StartLine - 1, statement.StartColumn - 1, "BEGIN ");
|
||||
}
|
||||
else
|
||||
{
|
||||
actions.Insert(endLine, $"{indent}END");
|
||||
actions.Insert(beingLine, $"{indent}BEGIN");
|
||||
}
|
||||
|
||||
static (TSqlStatement, IfStatement) FindElse(List<string> fileLines, IRuleViolation ruleViolation)
|
||||
{
|
||||
return FixHelpers.FindViolatingNode<IfStatement, TSqlStatement>(
|
||||
fileLines, ruleViolation, x => x.ElseStatement);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
SQLLinter/Infrastructure/Rules/CountStarRule.cs
Normal file
61
SQLLinter/Infrastructure/Rules/CountStarRule.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class CountStarRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
public override string Text => "COUNT(*) запрещен. Используйте COUNT(1) или COUNT(<PK>)";
|
||||
|
||||
public override void Visit(FunctionCall node)
|
||||
{
|
||||
var functionName = node.FunctionName?.Value;
|
||||
if (functionName == null || !functionName.ToUpper().Equals("COUNT"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (ScalarExpression param in node.Parameters)
|
||||
{
|
||||
var paramVisitor = new ParameterVisitor();
|
||||
param.Accept(paramVisitor);
|
||||
if (paramVisitor.IsWildcard)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void FixViolation(List<string> fileLines, IRuleViolation ruleViolation, FileLineActions actions)
|
||||
{
|
||||
var node = FixHelpers.FindViolatingNode<FunctionCall>(fileLines, ruleViolation);
|
||||
|
||||
foreach (ScalarExpression param in node.Parameters)
|
||||
{
|
||||
var paramVisitor = new ParameterVisitor();
|
||||
param.Accept(paramVisitor);
|
||||
if (paramVisitor.IsWildcard)
|
||||
{
|
||||
var whileCard = paramVisitor.Expression;
|
||||
actions.RepaceInlineAt(whileCard.StartLine - 1, whileCard.StartColumn - 1, "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ParameterVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool IsWildcard { get; private set; }
|
||||
public ColumnReferenceExpression Expression { get; private set; }
|
||||
|
||||
public ParameterVisitor()
|
||||
{
|
||||
IsWildcard = false;
|
||||
}
|
||||
|
||||
public override void Visit(ColumnReferenceExpression node)
|
||||
{
|
||||
IsWildcard = node.ColumnType.Equals(ColumnType.Wildcard);
|
||||
Expression = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
SQLLinter/Infrastructure/Rules/CreateProcedureInDboRule.cs
Normal file
25
SQLLinter/Infrastructure/Rules/CreateProcedureInDboRule.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class CreateProcedureInDboRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещено создание процедур в схеме dbo: {0}";
|
||||
|
||||
public override void Visit(CreateProcedureStatement node)
|
||||
{
|
||||
if (node.ProcedureReference.Name.SchemaIdentifier?.Value.Equals("dbo", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
}
|
||||
}
|
||||
public override void Visit(CreateOrAlterProcedureStatement node)
|
||||
{
|
||||
if (node.ProcedureReference.Name.SchemaIdentifier?.Value.Equals("dbo", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
19
SQLLinter/Infrastructure/Rules/CrossDatabaseReferenceRule.cs
Normal file
19
SQLLinter/Infrastructure/Rules/CrossDatabaseReferenceRule.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class CrossDatabaseReferenceRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Запрещено указывать имя базы данных в объекте. Используйте синонимы: {0}";
|
||||
|
||||
public override void Visit(NamedTableReference node)
|
||||
{
|
||||
if (node.SchemaObject.DatabaseIdentifier != null)
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.SchemaObject));
|
||||
}
|
||||
}
|
||||
}
|
||||
118
SQLLinter/Infrastructure/Rules/CrossDatabaseTransactionRule.cs
Normal file
118
SQLLinter/Infrastructure/Rules/CrossDatabaseTransactionRule.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class CrossDatabaseTransactionRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Межбазовые вставки или обновления, включенные в транзакцию, могут привести к повреждению данных.";
|
||||
|
||||
public override void Visit(TSqlBatch node)
|
||||
{
|
||||
var childTransactionVisitor = new ChildTransactionVisitor();
|
||||
node.Accept(childTransactionVisitor);
|
||||
foreach (var transaction in childTransactionVisitor.TransactionLists)
|
||||
{
|
||||
var childInsertUpdateQueryVisitor = new ChildInsertUpdateQueryVisitor(transaction);
|
||||
node.Accept(childInsertUpdateQueryVisitor);
|
||||
if (childInsertUpdateQueryVisitor.DatabasesUpdated.Count > 1)
|
||||
{
|
||||
AddViolation(
|
||||
Name,
|
||||
Text,
|
||||
GetLineNumber(transaction.Begin),
|
||||
GetColumnNumber(transaction.Begin));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TrackedTransaction
|
||||
{
|
||||
public BeginTransactionStatement Begin { get; set; }
|
||||
|
||||
public CommitTransactionStatement Commit { get; set; }
|
||||
}
|
||||
|
||||
public class ChildTransactionVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public List<TrackedTransaction> TransactionLists { get; } = new List<TrackedTransaction>();
|
||||
|
||||
public override void Visit(BeginTransactionStatement node)
|
||||
{
|
||||
TransactionLists.Add(new TrackedTransaction { Begin = node });
|
||||
}
|
||||
|
||||
public override void Visit(CommitTransactionStatement node)
|
||||
{
|
||||
var firstUncomitted = TransactionLists.LastOrDefault(x => x.Commit == null);
|
||||
if (firstUncomitted != null)
|
||||
{
|
||||
firstUncomitted.Commit = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ChildInsertUpdateQueryVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
private readonly TrackedTransaction transaction;
|
||||
|
||||
private readonly ChildDatabaseNameVisitor childDatabaseNameVisitor = new ChildDatabaseNameVisitor();
|
||||
|
||||
public ChildInsertUpdateQueryVisitor(TrackedTransaction transaction)
|
||||
{
|
||||
this.transaction = transaction;
|
||||
}
|
||||
|
||||
public HashSet<string> DatabasesUpdated { get; } = new HashSet<string>();
|
||||
|
||||
public override void Visit(InsertStatement node)
|
||||
{
|
||||
GetDatabasesUpdated(node);
|
||||
}
|
||||
|
||||
public override void Visit(UpdateStatement node)
|
||||
{
|
||||
GetDatabasesUpdated(node);
|
||||
}
|
||||
|
||||
private void GetDatabasesUpdated(TSqlFragment node)
|
||||
{
|
||||
if (IsWithinTransaction(node))
|
||||
{
|
||||
node.Accept(childDatabaseNameVisitor);
|
||||
DatabasesUpdated.UnionWith(childDatabaseNameVisitor.DatabasesUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsWithinTransaction(TSqlFragment node)
|
||||
{
|
||||
if (node.StartLine == transaction.Begin?.StartLine &&
|
||||
node.StartColumn < transaction.Begin?.StartColumn)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.StartLine == transaction.Commit?.StartLine &&
|
||||
node.StartColumn > transaction.Commit?.StartColumn)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return node.StartLine >= transaction.Begin?.StartLine && node.StartLine <= transaction.Commit?.StartLine;
|
||||
}
|
||||
}
|
||||
|
||||
public class ChildDatabaseNameVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public HashSet<string> DatabasesUpdated { get; } = new HashSet<string>();
|
||||
|
||||
public override void Visit(NamedTableReference node)
|
||||
{
|
||||
if (node.SchemaObject.DatabaseIdentifier != null)
|
||||
{
|
||||
DatabasesUpdated.Add(node.SchemaObject.DatabaseIdentifier.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
SQLLinter/Infrastructure/Rules/DataCompressionOptionRule.cs
Normal file
38
SQLLinter/Infrastructure/Rules/DataCompressionOptionRule.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class DataCompressionOptionRule : BaseRuleVisitor
|
||||
{
|
||||
|
||||
public override string Text => "Объявление таблицы без использования сжатия данных: {0}";
|
||||
|
||||
public override void Visit(CreateTableStatement node)
|
||||
{
|
||||
if (node.SchemaObjectName.BaseIdentifier.Value.StartsWith("#")) return;
|
||||
|
||||
var childCompressionVisitor = new ChildCompressionVisitor();
|
||||
node.AcceptChildren(childCompressionVisitor);
|
||||
|
||||
if (!childCompressionVisitor.CompressionOptionExists)
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.SchemaObjectName));
|
||||
}
|
||||
}
|
||||
|
||||
private class ChildCompressionVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool CompressionOptionExists
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public override void Visit(DataCompressionOption node)
|
||||
{
|
||||
CompressionOptionExists = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
SQLLinter/Infrastructure/Rules/DataTypeLengthRule.cs
Normal file
35
SQLLinter/Infrastructure/Rules/DataTypeLengthRule.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class DataTypeLengthRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private readonly SqlDataTypeOption[] typesThatRequireLength =
|
||||
{
|
||||
SqlDataTypeOption.Char,
|
||||
SqlDataTypeOption.VarChar,
|
||||
SqlDataTypeOption.NVarChar,
|
||||
SqlDataTypeOption.NChar,
|
||||
SqlDataTypeOption.Binary,
|
||||
SqlDataTypeOption.VarBinary,
|
||||
SqlDataTypeOption.Decimal,
|
||||
SqlDataTypeOption.Numeric,
|
||||
SqlDataTypeOption.Float
|
||||
};
|
||||
|
||||
public override string Text => "Длина типа данных не указана: {0}";
|
||||
|
||||
public override void Visit(SqlDataTypeReference node)
|
||||
{
|
||||
if (typesThatRequireLength.Any(option => Equals(option, node.SqlDataTypeOption) && node.Parameters.Count < 1))
|
||||
{
|
||||
AddViolation(node, node.Name.BaseIdentifier.Value);
|
||||
}
|
||||
}
|
||||
|
||||
protected override int GetColumnNumber(TSqlFragment node)
|
||||
{
|
||||
return node.FragmentLength + base.GetColumnNumber(node);
|
||||
}
|
||||
}
|
||||
25
SQLLinter/Infrastructure/Rules/DeleteWhereRule.cs
Normal file
25
SQLLinter/Infrastructure/Rules/DeleteWhereRule.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class DeleteWhereRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
public override string Text => "Ожидается предложение WHERE для оператора DELETE: {0}";
|
||||
|
||||
public override void Visit(DeleteSpecification node)
|
||||
{
|
||||
if (node.WhereClause != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string name = string.Join("", node.Target.ScriptTokenStream
|
||||
.Skip(node.Target.FirstTokenIndex)
|
||||
.Take(node.Target.LastTokenIndex - node.Target.FirstTokenIndex + 1)
|
||||
.Select(t => t.Text)
|
||||
);
|
||||
|
||||
AddViolation(node, name);
|
||||
}
|
||||
}
|
||||
15
SQLLinter/Infrastructure/Rules/DisallowCursorRule.cs
Normal file
15
SQLLinter/Infrastructure/Rules/DisallowCursorRule.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class DisallowCursorRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Обнаружено использование оператора CURSOR";
|
||||
|
||||
public override void Visit(CursorStatement node)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
18
SQLLinter/Infrastructure/Rules/DistinctRule.cs
Normal file
18
SQLLinter/Infrastructure/Rules/DistinctRule.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class DistinctRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Избегать DISTINCT, отдавать предпочтение GROUP BY.";
|
||||
|
||||
public override void Visit(SelectStatement node)
|
||||
{
|
||||
var query = node.QueryExpression as QuerySpecification;
|
||||
if (query != null && query.SelectElements.Any() && query.UniqueRowFilter == UniqueRowFilter.Distinct)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
209
SQLLinter/Infrastructure/Rules/DuplicateAliasRule.cs
Normal file
209
SQLLinter/Infrastructure/Rules/DuplicateAliasRule.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class DuplicateAliasRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Алиасы таблиц должны быть уникальными: {0}";
|
||||
|
||||
private readonly Dictionary<int, HashSet<string>> _activeAliases = new();
|
||||
private int _dmlDepth = 0;
|
||||
private bool _insideApply = false;
|
||||
|
||||
private void EnterDmlScope(bool inherit)
|
||||
{
|
||||
_dmlDepth++;
|
||||
if (_dmlDepth == 1 || !inherit)
|
||||
{
|
||||
_activeAliases[_dmlDepth] = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
// дочерний скоуп видит алиасы родителя (для глобальной уникальности в рамках одного DML)
|
||||
var parent = _activeAliases[_dmlDepth - 1];
|
||||
_activeAliases[_dmlDepth] = new HashSet<string>(parent, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExitDmlScope()
|
||||
{
|
||||
if (_activeAliases.ContainsKey(_dmlDepth))
|
||||
_activeAliases.Remove(_dmlDepth);
|
||||
_dmlDepth--;
|
||||
}
|
||||
|
||||
private void RegisterAlias(string alias, TSqlFragment node)
|
||||
{
|
||||
if (_dmlDepth == 0) return; // вне DML не проверяем
|
||||
var set = _activeAliases[_dmlDepth];
|
||||
|
||||
if (set.Contains(alias))
|
||||
{
|
||||
AddViolation(node, "[" + alias + "]");
|
||||
}
|
||||
else
|
||||
{
|
||||
set.Add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
// Корневые DML-выражения
|
||||
public override void ExplicitVisit(SelectStatement node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
base.ExplicitVisit(node);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(InsertStatement node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
base.ExplicitVisit(node);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(UpdateStatement node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
base.ExplicitVisit(node);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(DeleteStatement node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
base.ExplicitVisit(node);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(MergeStatement node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
base.ExplicitVisit(node);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
// IF: каждая ветка - отдельный скоуп
|
||||
public override void ExplicitVisit(IfStatement node)
|
||||
{
|
||||
EnterDmlScope(false);
|
||||
node.ThenStatement.Accept(this);
|
||||
ExitDmlScope();
|
||||
|
||||
if (node.ElseStatement != null)
|
||||
{
|
||||
EnterDmlScope(false);
|
||||
node.ElseStatement.Accept(this);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
// UNION: каждая часть - отдельный скоуп
|
||||
public override void ExplicitVisit(BinaryQueryExpression node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
node.FirstQueryExpression.Accept(this);
|
||||
ExitDmlScope();
|
||||
|
||||
EnterDmlScope(true);
|
||||
node.SecondQueryExpression.Accept(this);
|
||||
ExitDmlScope();
|
||||
|
||||
//base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
// CTE: регистрируем имя, тело - в дочернем скоупе
|
||||
public override void ExplicitVisit(CommonTableExpression node)
|
||||
{
|
||||
if (node.ExpressionName?.Value is { Length: > 0 } cteAlias)
|
||||
RegisterAlias(cteAlias, node);
|
||||
|
||||
EnterDmlScope(false);
|
||||
node.QueryExpression.Accept(this);
|
||||
ExitDmlScope();
|
||||
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
// Производная таблица: регистрируем её алиас; внутренняя QueryExpression обойдётся базой
|
||||
public override void ExplicitVisit(QueryDerivedTable node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
|
||||
|
||||
// тело подзапроса в FROM - свой локальный скоуп
|
||||
EnterDmlScope(_insideApply);
|
||||
node.QueryExpression.Accept(this);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
// CROSS APPLY
|
||||
public override void ExplicitVisit(UnqualifiedJoin node)
|
||||
{
|
||||
if (node.UnqualifiedJoinType == UnqualifiedJoinType.CrossApply ||
|
||||
node.UnqualifiedJoinType == UnqualifiedJoinType.OuterApply)
|
||||
{
|
||||
var prev = _insideApply;
|
||||
_insideApply = true;
|
||||
|
||||
// обходим без нового скоупа, т.к. алиасы учитываются глобально
|
||||
node.FirstTableReference.Accept(this);
|
||||
node.SecondTableReference.Accept(this);
|
||||
|
||||
_insideApply = prev;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Табличные источники с алиасами
|
||||
public override void ExplicitVisit(NamedTableReference node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(VariableTableReference node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(SchemaObjectFunctionTableReference node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(PivotedTableReference node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(UnpivotedTableReference node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
// Сброс состояния перед анализом скрипта
|
||||
public override void ExplicitVisit(TSqlScript node)
|
||||
{
|
||||
_dmlDepth = 0;
|
||||
_activeAliases.Clear();
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
}
|
||||
51
SQLLinter/Infrastructure/Rules/DuplicateEmptyLineRule.cs
Normal file
51
SQLLinter/Infrastructure/Rules/DuplicateEmptyLineRule.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class DuplicateEmptyLineRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private static readonly Regex EmptyLineRegex = new(@"^\s*$", RegexOptions.Compiled);
|
||||
|
||||
|
||||
public override string Text => "Обнаружен дубликат новой строки";
|
||||
|
||||
public override void Visit(TSqlScript node)
|
||||
{
|
||||
var isEmptyLine = false;
|
||||
var fileLines = FixHelpers.GetString(node)
|
||||
.Split('\n')
|
||||
.ToList();
|
||||
|
||||
for (var i = 0; i < fileLines.Count; i++)
|
||||
{
|
||||
if (EmptyLineRegex.IsMatch(fileLines[i]))
|
||||
{
|
||||
if (isEmptyLine)
|
||||
{
|
||||
AddViolation(Name, Text, i + 1, 1);
|
||||
}
|
||||
|
||||
isEmptyLine = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isEmptyLine = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void FixViolation(List<string> fileLines, IRuleViolation ruleViolation, FileLineActions actions)
|
||||
{
|
||||
if (ruleViolation.Line - 1 == fileLines.Count)
|
||||
{
|
||||
actions.RemoveAt(ruleViolation.Line - 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
actions.RemoveAt(ruleViolation.Line - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
SQLLinter/Infrastructure/Rules/DuplicateGoRule.cs
Normal file
39
SQLLinter/Infrastructure/Rules/DuplicateGoRule.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class DuplicateGoRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Обнаружен дублирующийся оператор GO";
|
||||
|
||||
public override void Visit(TSqlScript node)
|
||||
{
|
||||
TSqlParserToken lastToken = null;
|
||||
TSqlParserToken currentToken;
|
||||
for (var index = 0; index <= node.LastTokenIndex; index++)
|
||||
{
|
||||
var tokenType = node.ScriptTokenStream[index].TokenType;
|
||||
|
||||
// Skip these
|
||||
switch (tokenType)
|
||||
{
|
||||
case TSqlTokenType.MultilineComment:
|
||||
case TSqlTokenType.WhiteSpace:
|
||||
case TSqlTokenType.Semicolon:
|
||||
continue;
|
||||
}
|
||||
|
||||
currentToken = node.ScriptTokenStream[index];
|
||||
|
||||
if (tokenType is TSqlTokenType.Go &&
|
||||
lastToken?.TokenType is TSqlTokenType.Go)
|
||||
{
|
||||
AddViolation(Name, Text, currentToken.Line, currentToken.Column);
|
||||
}
|
||||
|
||||
lastToken = currentToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
SQLLinter/Infrastructure/Rules/ExcessiveJoinsRule.cs
Normal file
20
SQLLinter/Infrastructure/Rules/ExcessiveJoinsRule.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class ExcessiveJoinsRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Слишком много таблиц в запросе (>" + _maxJoins + "): {0}";
|
||||
|
||||
private const int _maxJoins = 10;
|
||||
|
||||
public override void Visit(QuerySpecification node)
|
||||
{
|
||||
if (node.FromClause != null && node.FromClause.TableReferences.Count > _maxJoins)
|
||||
{
|
||||
AddViolation(node.FromClause, $"Количество таблиц: {node.FromClause.TableReferences.Count}");
|
||||
}
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
30
SQLLinter/Infrastructure/Rules/ExecuteAsOwnerRule.cs
Normal file
30
SQLLinter/Infrastructure/Rules/ExecuteAsOwnerRule.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class ExecuteAsOwnerRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Процедура должна содержать EXECUTE AS OWNER: {0}";
|
||||
|
||||
public override void Visit(CreateProcedureStatement node) => check(node);
|
||||
public override void Visit(CreateOrAlterProcedureStatement node) => check(node);
|
||||
public override void Visit(AlterProcedureStatement node) => check(node);
|
||||
|
||||
private void check(ProcedureStatementBody node)
|
||||
{
|
||||
foreach (var option in node.Options)
|
||||
{
|
||||
if (option.OptionKind == ProcedureOptionKind.ExecuteAs
|
||||
&& option is ExecuteAsProcedureOption execOpt
|
||||
&& execOpt.ExecuteAs.ExecuteAsOption == ExecuteAsOption.Owner)
|
||||
{
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
}
|
||||
}
|
||||
15
SQLLinter/Infrastructure/Rules/FullTextRule.cs
Normal file
15
SQLLinter/Infrastructure/Rules/FullTextRule.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class FullTextRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Обнаружен полнотекстовый предикат, это может вызвать проблемы с производительностью.";
|
||||
|
||||
public override void Visit(FullTextPredicate node)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
42
SQLLinter/Infrastructure/Rules/HavingWithoutAggregateRule.cs
Normal file
42
SQLLinter/Infrastructure/Rules/HavingWithoutAggregateRule.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class HavingWithoutAggregateRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "HAVING должен содержать агрегатные функции: {0}";
|
||||
|
||||
public override void Visit(HavingClause node)
|
||||
{
|
||||
bool hasAggregate = ContainsAggregate(node.SearchCondition);
|
||||
|
||||
if (!hasAggregate)
|
||||
{
|
||||
AddViolation(node, node.SearchCondition?.ToString() ?? "HAVING без агрегатов");
|
||||
}
|
||||
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
private bool ContainsAggregate(TSqlFragment fragment)
|
||||
{
|
||||
var finder = new AggregateFinder();
|
||||
fragment.Accept(finder);
|
||||
return finder.HasAggregate;
|
||||
}
|
||||
|
||||
private class AggregateFinder : TSqlFragmentVisitor
|
||||
{
|
||||
public bool HasAggregate { get; private set; }
|
||||
|
||||
public override void Visit(FunctionCall node)
|
||||
{
|
||||
var name = node.FunctionName.Value.ToUpperInvariant();
|
||||
if (name is "COUNT" or "SUM" or "AVG" or "MIN" or "MAX")
|
||||
HasAggregate = true;
|
||||
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
SQLLinter/Infrastructure/Rules/HavingWithoutGroupByRule.cs
Normal file
18
SQLLinter/Infrastructure/Rules/HavingWithoutGroupByRule.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class HavingWithoutGroupByRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "HAVING без GROUP BY недопустим: {0}";
|
||||
|
||||
public override void Visit(QuerySpecification node)
|
||||
{
|
||||
if (node.HavingClause != null && node.GroupByClause == null)
|
||||
{
|
||||
AddViolation(node.HavingClause, node.HavingClause.ToString());
|
||||
}
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
46
SQLLinter/Infrastructure/Rules/HeaderCommentRule.cs
Normal file
46
SQLLinter/Infrastructure/Rules/HeaderCommentRule.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class HeaderCommentRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "У процедур/функций/триггеров должна быть шапка-комментарий с автором, департаментом, назначением: {0}";
|
||||
|
||||
public override void Visit(CreateProcedureStatement node) => private_visit(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
|
||||
public override void Visit(CreateOrAlterProcedureStatement node) => private_visit(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
|
||||
public override void Visit(CreateFunctionStatement node) => private_visit(node, SQLHelpers.ObjectGetFullName(node.Name));
|
||||
|
||||
public override void Visit(CreateOrAlterFunctionStatement node) => private_visit(node, SQLHelpers.ObjectGetFullName(node.Name));
|
||||
|
||||
public override void Visit(CreateTriggerStatement node) => private_visit(node, SQLHelpers.ObjectGetFullName(node.Name));
|
||||
|
||||
public override void Visit(CreateOrAlterTriggerStatement node) => private_visit(node, SQLHelpers.ObjectGetFullName(node.Name));
|
||||
|
||||
public override void Visit(CreateViewStatement node) => private_visit(node, "");
|
||||
|
||||
public override void Visit(CreateOrAlterViewStatement node) => private_visit(node, "");
|
||||
|
||||
private void private_visit(TSqlFragment node, string name)
|
||||
{
|
||||
var prevTokenIndex = node.FirstTokenIndex;
|
||||
TSqlParserToken? prevToken = null;
|
||||
|
||||
while (prevTokenIndex > 0)
|
||||
{
|
||||
prevTokenIndex -= 1;
|
||||
prevToken = node.ScriptTokenStream[prevTokenIndex];
|
||||
if (prevToken.TokenType != TSqlTokenType.WhiteSpace) break;
|
||||
}
|
||||
|
||||
if (prevToken == null ||
|
||||
prevToken.TokenType != TSqlTokenType.SingleLineComment && prevToken.TokenType != TSqlTokenType.MultilineComment
|
||||
)
|
||||
{
|
||||
AddViolation(node, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
SQLLinter/Infrastructure/Rules/IndexHintRule.cs
Normal file
17
SQLLinter/Infrastructure/Rules/IndexHintRule.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class IndexHintRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещено использование хинтов WITH (INDEX).";
|
||||
|
||||
public override void Visit(TableHint node)
|
||||
{
|
||||
if (node.HintKind == TableHintKind.Index)
|
||||
{
|
||||
AddViolation(Name, Text, GetLineNumber(node), GetColumnNumber(node));
|
||||
}
|
||||
}
|
||||
}
|
||||
190
SQLLinter/Infrastructure/Rules/InformationSchemaRule.cs
Normal file
190
SQLLinter/Infrastructure/Rules/InformationSchemaRule.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class InformationSchemaRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Ожидается использование SYS.Partitions вместо представлений INFORMATION SCHEMA.";
|
||||
|
||||
public override void Visit(SchemaObjectName node)
|
||||
{
|
||||
var schemaIdentifier = node.SchemaIdentifier?.Value != null;
|
||||
|
||||
if (schemaIdentifier && node.SchemaIdentifier.Value.Equals("INFORMATION_SCHEMA", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
|
||||
public override void FixViolation(List<string> fileLines, IRuleViolation ruleViolation, FileLineActions actions)
|
||||
{
|
||||
var node = FixHelpers.FindNodes<TSqlStatement>(fileLines, x => x.StartLine == ruleViolation.Line);
|
||||
|
||||
if (node.Count == 1)
|
||||
{
|
||||
switch (node[0])
|
||||
{
|
||||
case IfStatement ifStatement:
|
||||
HandleFragment(actions, ifStatement.Predicate);
|
||||
break;
|
||||
|
||||
case SelectStatement selectStatement:
|
||||
HandleFragment(actions, selectStatement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleFragment(FileLineActions actions, TSqlFragment statement)
|
||||
{
|
||||
var fromClauses = FixHelpers.FindNodes<FromClause>(statement);
|
||||
var whereClauses = FixHelpers.FindNodes<WhereClause>(statement);
|
||||
|
||||
if (fromClauses.Count == 1 && whereClauses.Count <= 1)
|
||||
{
|
||||
var fromClause = fromClauses[0];
|
||||
var whereClause = whereClauses.FirstOrDefault();
|
||||
var tableName = FixHelpers.FindNodes<SchemaObjectName>(fromClause)[0].BaseIdentifier.Value;
|
||||
string newFrom = null;
|
||||
|
||||
switch (tableName)
|
||||
{
|
||||
case "TABLES":
|
||||
newFrom = "FROM sys.tables";
|
||||
UpdateWhereTable(actions, fromClause, whereClause);
|
||||
|
||||
break;
|
||||
|
||||
case "ROUTINES":
|
||||
newFrom = "FROM sys.procedures";
|
||||
UpdateWhereRoutine(actions, fromClause, whereClause);
|
||||
break;
|
||||
|
||||
case "COLUMNS":
|
||||
newFrom = "FROM sys.columns";
|
||||
UpdateWhereColumns(actions, fromClause, whereClause);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
string oldFrom = FixHelpers.GetString(fromClause);
|
||||
|
||||
if (newFrom != null)
|
||||
{
|
||||
actions.RepaceInlineAt(fromClause.StartLine - 1, fromClause.StartColumn - 1,
|
||||
newFrom, oldFrom.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetWhereColumnValueName(WhereClause whereClause, string columnName)
|
||||
{
|
||||
var node = FixHelpers.FindNodes<BooleanComparisonExpression>(whereClause,
|
||||
x => FixHelpers.FindNodes<Identifier>(x.FirstExpression)[0].Value == columnName);
|
||||
|
||||
if (node.Count == 1 && node[0].SecondExpression is StringLiteral stringLiteral)
|
||||
{
|
||||
return stringLiteral.Value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void UpdateWhere(FileLineActions actions, FromClause fromClause, WhereClause whereClause, string newWhere)
|
||||
{
|
||||
var firstWhereLine = whereClause.ScriptTokenStream[whereClause.FirstTokenIndex].Line;
|
||||
var lastWhereLine = whereClause.ScriptTokenStream[whereClause.LastTokenIndex].Line;
|
||||
|
||||
// Delete mutliline where
|
||||
var anyDeleted = false;
|
||||
for (int line = lastWhereLine; line > firstWhereLine; line--)
|
||||
{
|
||||
actions.RemoveAt(line - 1);
|
||||
anyDeleted = true;
|
||||
}
|
||||
if (anyDeleted)
|
||||
{
|
||||
newWhere += ")";
|
||||
}
|
||||
|
||||
var oldWhere = string.Join(string.Empty, whereClause.ScriptTokenStream
|
||||
.Where((x, i) => x.Line == firstWhereLine
|
||||
&& x.Column >= whereClause.StartColumn
|
||||
&& i <= whereClause.LastTokenIndex
|
||||
&& x.Text != "\n")
|
||||
.Select(x => x.Text));
|
||||
|
||||
actions.RepaceInlineAt(whereClause.StartLine - 1, whereClause.StartColumn - 1,
|
||||
newWhere, oldWhere.Length);
|
||||
}
|
||||
|
||||
private static void UpdateWhereColumns(
|
||||
FileLineActions actions,
|
||||
FromClause fromClause,
|
||||
WhereClause whereClause)
|
||||
{
|
||||
if (whereClause != null)
|
||||
{
|
||||
var schema = GetWhereColumnValueName(whereClause, "TABLE_SCHEMA");
|
||||
var columnName = GetWhereColumnValueName(whereClause, "COLUMN_NAME");
|
||||
var tableName = GetWhereColumnValueName(whereClause, "TABLE_NAME");
|
||||
var dataType = GetWhereColumnValueName(whereClause, "DATA_TYPE");
|
||||
|
||||
if (schema != null && tableName != null && columnName != null)
|
||||
{
|
||||
var newWhere = $"WHERE [object_id] = OBJECT_ID(N'{schema}.{tableName}') AND [name] = '{columnName}'";
|
||||
if (dataType != null)
|
||||
{
|
||||
newWhere += $" AND [system_type_id] = TYPE_ID(N'{dataType}')";
|
||||
}
|
||||
|
||||
UpdateWhere(actions, fromClause, whereClause, newWhere);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateWhereRoutine(
|
||||
FileLineActions actions,
|
||||
FromClause fromClause,
|
||||
WhereClause whereClause)
|
||||
{
|
||||
if (whereClause != null)
|
||||
{
|
||||
var schema = GetWhereColumnValueName(whereClause, "ROUTINE_SCHEMA");
|
||||
var name = GetWhereColumnValueName(whereClause, "ROUTINE_NAME");
|
||||
var type = GetWhereColumnValueName(whereClause, "ROUTINE_TYPE");
|
||||
|
||||
if (type == "PROCEDURE" && schema != null && name != null)
|
||||
{
|
||||
var newWhere = $"WHERE [object_id] = OBJECT_ID(N'{schema}.{name}')";
|
||||
|
||||
UpdateWhere(actions, fromClause, whereClause, newWhere);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateWhereTable(
|
||||
FileLineActions actions,
|
||||
FromClause fromClause,
|
||||
WhereClause whereClause)
|
||||
{
|
||||
if (whereClause != null)
|
||||
{
|
||||
var schema = GetWhereColumnValueName(whereClause, "TABLE_SCHEMA");
|
||||
var name = GetWhereColumnValueName(whereClause, "TABLE_NAME");
|
||||
var type = GetWhereColumnValueName(whereClause, "TABLE_TYPE");
|
||||
|
||||
if (type == "BASE TABLE" && schema != null && name != null)
|
||||
{
|
||||
var newWhere = $"WHERE [object_id] = OBJECT_ID(N'{schema}.{name}')";
|
||||
|
||||
UpdateWhere(actions, fromClause, whereClause, newWhere);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
SQLLinter/Infrastructure/Rules/InnerJoinRule.cs
Normal file
45
SQLLinter/Infrastructure/Rules/InnerJoinRule.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class InnerJoinRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Используйте полную запись INNER JOIN.";
|
||||
|
||||
public override void Visit(QualifiedJoin node)
|
||||
{
|
||||
if (node.QualifiedJoinType != QualifiedJoinType.Inner)
|
||||
return;
|
||||
|
||||
var tokens = node.ScriptTokenStream;
|
||||
if (tokens == null) return;
|
||||
|
||||
var secondIndex = node.SecondTableReference.FirstTokenIndex;
|
||||
|
||||
int start = node.FirstTableReference.LastTokenIndex;
|
||||
int end = node.SecondTableReference.FirstTokenIndex;
|
||||
|
||||
bool hasInner = tokens
|
||||
.Skip(start)
|
||||
.Take(end - start + 1)
|
||||
.Any(t => t.TokenType == TSqlTokenType.Inner);
|
||||
|
||||
if (!hasInner)
|
||||
{
|
||||
var joinToken = tokens
|
||||
.Skip(start)
|
||||
.Take(end - start + 1)
|
||||
.FirstOrDefault(t => t.TokenType == TSqlTokenType.Join);
|
||||
|
||||
if (joinToken != null)
|
||||
{
|
||||
AddViolation(Name, Text, joinToken.Line, joinToken.Column);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
SQLLinter/Infrastructure/Rules/InsertStarRule.cs
Normal file
17
SQLLinter/Infrastructure/Rules/InsertStarRule.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class InsertStarRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещено INSERT без столбцов.";
|
||||
|
||||
public override void Visit(InsertStatement node)
|
||||
{
|
||||
if (node.InsertSpecification.Columns.Count == 0) // INSERT без перечисления колонок
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class InsertValuesInsteadOfSelectRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Для вставки константных значений используйте VALUES(...)";
|
||||
|
||||
public override void Visit(InsertStatement node)
|
||||
{
|
||||
// Проверяем, что источник данных - SELECT
|
||||
if (node.InsertSpecification.InsertSource is SelectInsertSource selectSource)
|
||||
{
|
||||
var query = selectSource.Select as QuerySpecification;
|
||||
if (query != null)
|
||||
{
|
||||
// Если в SELECT нет таблиц (т.е. просто SELECT 1,2,3)
|
||||
if (query.FromClause == null || query.FromClause.TableReferences.Count == 0)
|
||||
{
|
||||
AddViolation(Name, Text, GetLineNumber(node), GetColumnNumber(node));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
SQLLinter/Infrastructure/Rules/JoinKeywordRule.cs
Normal file
38
SQLLinter/Infrastructure/Rules/JoinKeywordRule.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class JoinKeywordRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Вместо неявного синтаксиса (соединения через запятую) следует использовать ключевое слово join. Замените соединения через запятую синтаксисом «INNER JOIN».";
|
||||
|
||||
public override void Visit(FromClause node)
|
||||
{
|
||||
// Проверьте, используются ли в соединении запятые (синтаксис неявного соединения).
|
||||
if (node.TableReferences.Count > 1)
|
||||
{
|
||||
for (int i = 0; i < node.TableReferences.Count; i++)
|
||||
{
|
||||
if (node.TableReferences[i] is QualifiedJoin)
|
||||
{
|
||||
// Пропустить, если это правильное ПРИСОЕДИНЕНИЕ
|
||||
continue;
|
||||
}
|
||||
if (i < node.TableReferences.Count - 1)
|
||||
{
|
||||
// Если следующая ссылка на таблицу не является соединением, это соединение через запятую.
|
||||
if (!(node.TableReferences[i + 1] is QualifiedJoin))
|
||||
{
|
||||
AddViolation(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
}
|
||||
59
SQLLinter/Infrastructure/Rules/KeywordCapitalizationRule.cs
Normal file
59
SQLLinter/Infrastructure/Rules/KeywordCapitalizationRule.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Core;
|
||||
using SQLLinter.Infrastructure.Rules.Common;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class KeywordCapitalizationRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Ожидается, что ключевое слово TSQL будет написано в верхнем регистре: {0}";
|
||||
|
||||
public override void Visit(TSqlScript node)
|
||||
{
|
||||
var typesToUpcase = Constants.TSqlKeywords.Concat(Constants.TSqlDataTypes).ToArray();
|
||||
for (var index = 0; index < node.ScriptTokenStream?.Count; index++)
|
||||
{
|
||||
var token = node.ScriptTokenStream[index];
|
||||
if (!typesToUpcase.Contains(token.Text, StringComparer.CurrentCultureIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsUpperCase(token.Text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var dynamicSQLAdjustment = GetDynamicSqlColumnOffset(token);
|
||||
|
||||
// получить количество всех вкладок в строке, которые встречаются до последнего токена в этом узле
|
||||
var tabsOnLine = ColumnNumberCalculator.CountTabsBeforeToken(token.Line, index, node.ScriptTokenStream);
|
||||
var column = ColumnNumberCalculator.GetColumnNumberBeforeToken(tabsOnLine, token);
|
||||
|
||||
AddViolation(Name, GetText(token.Text), GetLineNumber(token), column + dynamicSQLAdjustment);
|
||||
}
|
||||
}
|
||||
|
||||
public override void FixViolation(List<string> fileLines, IRuleViolation ruleViolation, FileLineActions actions)
|
||||
{
|
||||
var lineIndex = ruleViolation.Line - 1;
|
||||
var line = fileLines[lineIndex];
|
||||
|
||||
var startCharIndex = ColumnNumberCalculator.GetIndex(line, ruleViolation.Column);
|
||||
|
||||
if (startCharIndex != -1)
|
||||
{
|
||||
var errorWord = new Regex(@"\w+").Matches(line[startCharIndex..]).First().Value;
|
||||
|
||||
actions.RepaceInlineAt(lineIndex, startCharIndex, errorWord.ToUpper());
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsUpperCase(string input)
|
||||
{
|
||||
return input.All(t => !char.IsLetter(t) || char.IsUpper(t));
|
||||
}
|
||||
}
|
||||
19
SQLLinter/Infrastructure/Rules/LinkedServerReferenceRule.cs
Normal file
19
SQLLinter/Infrastructure/Rules/LinkedServerReferenceRule.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class LinkedServerReferenceRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Запрещены межсерверные запросы: {0}";
|
||||
|
||||
public override void Visit(NamedTableReference node)
|
||||
{
|
||||
if (node.SchemaObject.ServerIdentifier != null)
|
||||
{
|
||||
AddViolation(node, node.SchemaObject.ServerIdentifier.Value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
97
SQLLinter/Infrastructure/Rules/MultiTableAliasRule.cs
Normal file
97
SQLLinter/Infrastructure/Rules/MultiTableAliasRule.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
using SQLLinter.Infrastructure.Rules.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class MultiTableAliasRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private HashSet<string> cteNames = new HashSet<string>();
|
||||
|
||||
|
||||
public override string Text => "Найдена таблица без псевдонимов при объединении нескольких таблиц: {0}";
|
||||
|
||||
public override void Visit(TSqlStatement node)
|
||||
{
|
||||
var childCommonTableExpressionVisitor = new ChildCommonTableExpressionVisitor();
|
||||
node.AcceptChildren(childCommonTableExpressionVisitor);
|
||||
cteNames = childCommonTableExpressionVisitor.CommonTableExpressionIdentifiers;
|
||||
}
|
||||
|
||||
public override void Visit(TableReference node)
|
||||
{
|
||||
void ChildCallback(TSqlFragment childNode)
|
||||
{
|
||||
var dynamicSqlAdjustment = GetDynamicSqlColumnOffset(childNode);
|
||||
var tabsOnLine = ColumnNumberCalculator.CountTabsBeforeToken(childNode.StartLine, childNode.LastTokenIndex, childNode.ScriptTokenStream);
|
||||
var column = ColumnNumberCalculator.GetColumnNumberBeforeToken(tabsOnLine, childNode.ScriptTokenStream[childNode.FirstTokenIndex]);
|
||||
|
||||
string tableName = "";
|
||||
|
||||
if (childNode is NamedTableReference namedTable)
|
||||
{
|
||||
tableName = SQLHelpers.ObjectGetFullName(namedTable.SchemaObject);
|
||||
}
|
||||
|
||||
AddViolation(Name, GetText(tableName), GetLineNumber(childNode), column + dynamicSqlAdjustment);
|
||||
}
|
||||
|
||||
var childTableJoinVisitor = new ChildTableJoinVisitor();
|
||||
node.AcceptChildren(childTableJoinVisitor);
|
||||
|
||||
if (!childTableJoinVisitor.TableJoined)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var childTableAliasVisitor = new ChildTableAliasVisitor(ChildCallback, cteNames);
|
||||
node.AcceptChildren(childTableAliasVisitor);
|
||||
}
|
||||
|
||||
public class ChildCommonTableExpressionVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public HashSet<string> CommonTableExpressionIdentifiers { get; } = new HashSet<string>();
|
||||
|
||||
public override void Visit(CommonTableExpression node)
|
||||
{
|
||||
CommonTableExpressionIdentifiers.Add(node.ExpressionName.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChildTableJoinVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool TableJoined { get; private set; }
|
||||
|
||||
public override void Visit(JoinTableReference node)
|
||||
{
|
||||
TableJoined = true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ChildTableAliasVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
private readonly Action<TSqlFragment> childCallback;
|
||||
|
||||
public ChildTableAliasVisitor(Action<TSqlFragment> errorCallback, HashSet<string> cteNames)
|
||||
{
|
||||
CteNames = cteNames;
|
||||
childCallback = errorCallback;
|
||||
}
|
||||
|
||||
public HashSet<string> CteNames { get; }
|
||||
|
||||
public override void Visit(NamedTableReference node)
|
||||
{
|
||||
if (CteNames.Contains(node.SchemaObject.BaseIdentifier.Value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.Alias == null)
|
||||
{
|
||||
childCallback(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
SQLLinter/Infrastructure/Rules/NamedConstraintRule.cs
Normal file
47
SQLLinter/Infrastructure/Rules/NamedConstraintRule.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class NamedConstraintRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Именованные ограничения во временных таблицах могут вызывать коллизии при параллельном запуске: {0}";
|
||||
|
||||
public override void Visit(CreateTableStatement node)
|
||||
{
|
||||
// применять правило только к временным таблицам
|
||||
if (!node.SchemaObjectName.BaseIdentifier.Value.Contains("#"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var constraintVisitor = new ConstraintVisitor();
|
||||
node.AcceptChildren(constraintVisitor);
|
||||
|
||||
if (constraintVisitor.NamedConstraintExists)
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.SchemaObjectName));
|
||||
}
|
||||
}
|
||||
|
||||
private class ConstraintVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool NamedConstraintExists
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public override void Visit(ConstraintDefinition node)
|
||||
{
|
||||
if (NamedConstraintExists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NamedConstraintExists = node.ConstraintIdentifier != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
SQLLinter/Infrastructure/Rules/NestedSubqueryDepthRule.cs
Normal file
36
SQLLinter/Infrastructure/Rules/NestedSubqueryDepthRule.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class NestedSubqueryDepthRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Слишком глубокие подзапросы (>" + _maxDepth + "} уровней): {0}";
|
||||
|
||||
private int _depth = 0;
|
||||
private const int _maxDepth = 3;
|
||||
|
||||
public override void Visit(QueryDerivedTable node)
|
||||
{
|
||||
_depth++;
|
||||
if (_depth > _maxDepth)
|
||||
{
|
||||
AddViolation(node, $"Глубина подзапроса {_depth}");
|
||||
}
|
||||
node.QueryExpression.Accept(this);
|
||||
_depth--;
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
public override void Visit(ScalarSubquery node)
|
||||
{
|
||||
_depth++;
|
||||
if (_depth > _maxDepth)
|
||||
{
|
||||
AddViolation(node, $"Глубина подзапроса {_depth}");
|
||||
}
|
||||
node.QueryExpression.Accept(this);
|
||||
_depth--;
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
17
SQLLinter/Infrastructure/Rules/NoLockRule.cs
Normal file
17
SQLLinter/Infrastructure/Rules/NoLockRule.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class NoLockRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещено использование NOLOCK.";
|
||||
|
||||
public override void Visit(TableHint node)
|
||||
{
|
||||
if (node.HintKind == TableHintKind.NoLock)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
151
SQLLinter/Infrastructure/Rules/NonSargableRule.cs
Normal file
151
SQLLinter/Infrastructure/Rules/NonSargableRule.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Infrastructure.Rules.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class NonSargableRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private readonly List<TSqlFragment> errorsReported = new();
|
||||
|
||||
public override string Text => "Выполнение функций с предложениями фильтра или предикатами соединения может вызвать проблемы с производительностью.";
|
||||
|
||||
public override void Visit(JoinTableReference node)
|
||||
{
|
||||
var predicateExpressionVisitor = new PredicateVisitor();
|
||||
node.AcceptChildren(predicateExpressionVisitor);
|
||||
var multiClauseQuery = predicateExpressionVisitor.PredicatesFound;
|
||||
|
||||
var joinVisitor = new JoinQueryVisitor(VisitorCallback, multiClauseQuery);
|
||||
node.AcceptChildren(joinVisitor);
|
||||
}
|
||||
|
||||
public override void Visit(WhereClause node)
|
||||
{
|
||||
var predicateExpressionVisitor = new PredicateVisitor();
|
||||
node.Accept(predicateExpressionVisitor);
|
||||
var multiClauseQuery = predicateExpressionVisitor.PredicatesFound;
|
||||
|
||||
var childVisitor = new FunctionVisitor(VisitorCallback, multiClauseQuery);
|
||||
node.Accept(childVisitor);
|
||||
}
|
||||
|
||||
private void VisitorCallback(TSqlFragment childNode)
|
||||
{
|
||||
if (errorsReported.Contains(childNode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dynamicSqlColumnAdjustment = GetDynamicSqlColumnOffset(childNode);
|
||||
|
||||
errorsReported.Add(childNode);
|
||||
AddViolation(Name, Text, GetLineNumber(childNode), ColumnNumberCalculator.GetNodeColumnPosition(childNode) + dynamicSqlColumnAdjustment);
|
||||
}
|
||||
|
||||
private class JoinQueryVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
private readonly Action<TSqlFragment> childCallback;
|
||||
private readonly bool isMultiClauseQuery;
|
||||
|
||||
public JoinQueryVisitor(Action<TSqlFragment> childCallback, bool multiClauseQuery)
|
||||
{
|
||||
this.childCallback = childCallback;
|
||||
isMultiClauseQuery = multiClauseQuery;
|
||||
}
|
||||
|
||||
public override void Visit(BooleanComparisonExpression node)
|
||||
{
|
||||
var childVisitor = new FunctionVisitor(childCallback, isMultiClauseQuery);
|
||||
node.Accept(childVisitor);
|
||||
}
|
||||
}
|
||||
|
||||
private class PredicateVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool PredicatesFound { get; private set; }
|
||||
|
||||
public override void Visit(BooleanBinaryExpression node)
|
||||
{
|
||||
PredicatesFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
private class FunctionVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
private readonly bool isMultiClause;
|
||||
private readonly Action<TSqlFragment> childCallback;
|
||||
private bool hasColumnReferenceParameter;
|
||||
|
||||
public FunctionVisitor(Action<TSqlFragment> errorCallback, bool isMultiClause)
|
||||
{
|
||||
childCallback = errorCallback;
|
||||
this.isMultiClause = isMultiClause;
|
||||
}
|
||||
|
||||
public override void Visit(FunctionCall node)
|
||||
{
|
||||
switch (node.FunctionName.Value.ToUpper())
|
||||
{
|
||||
// разрешить предикаты isnull при наличии других фильтров
|
||||
case "ISNULL" when isMultiClause:
|
||||
return;
|
||||
case "DATEADD":
|
||||
case "DATEDIFF":
|
||||
case "DATEDIFF_BIG":
|
||||
case "DATENAME":
|
||||
case "DATEPART":
|
||||
case "DATETRUNC":
|
||||
case "DATE_BUCKET":
|
||||
hasColumnReferenceParameter = true;
|
||||
break;
|
||||
}
|
||||
|
||||
FindColumnReferences(node);
|
||||
}
|
||||
|
||||
public override void Visit(LeftFunctionCall node)
|
||||
{
|
||||
FindColumnReferences(node);
|
||||
}
|
||||
|
||||
public override void Visit(RightFunctionCall node)
|
||||
{
|
||||
FindColumnReferences(node);
|
||||
}
|
||||
|
||||
public override void Visit(ConvertCall node)
|
||||
{
|
||||
FindColumnReferences(node);
|
||||
}
|
||||
|
||||
public override void Visit(CastCall node)
|
||||
{
|
||||
FindColumnReferences(node);
|
||||
}
|
||||
|
||||
private void FindColumnReferences(TSqlFragment node)
|
||||
{
|
||||
var columnReferenceVisitor = new ColumnReferenceVisitor();
|
||||
node.AcceptChildren(columnReferenceVisitor);
|
||||
|
||||
if (columnReferenceVisitor.ColumnReferenceFound && (!hasColumnReferenceParameter || columnReferenceVisitor.ColumnReferenceCount > 1))
|
||||
{
|
||||
childCallback(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ColumnReferenceVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool ColumnReferenceFound { get; private set; }
|
||||
|
||||
public int ColumnReferenceCount { get; private set; }
|
||||
|
||||
public override void Visit(ColumnReferenceExpression node)
|
||||
{
|
||||
ColumnReferenceCount++;
|
||||
ColumnReferenceFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
SQLLinter/Infrastructure/Rules/NullComparisonRule.cs
Normal file
20
SQLLinter/Infrastructure/Rules/NullComparisonRule.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class NullComparisonRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещено сравнение с NULL через '='. Используйте 'IS'': {0}";
|
||||
public override void Visit(BooleanComparisonExpression node)
|
||||
{
|
||||
// Если один из операндов - NULL
|
||||
if (node.FirstExpression is NullLiteral || node.SecondExpression is NullLiteral)
|
||||
{
|
||||
// Это значит, что используется = и т.п. с NULL
|
||||
AddViolation(node, node.ToString());
|
||||
}
|
||||
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
18
SQLLinter/Infrastructure/Rules/ObjectPropertyRule.cs
Normal file
18
SQLLinter/Infrastructure/Rules/ObjectPropertyRule.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class ObjectPropertyRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Ожидается использование SYS.COLUMNS вместо функции свойства объекта.";
|
||||
|
||||
public override void Visit(FunctionCall node)
|
||||
{
|
||||
if (node.FunctionName.Value.Equals("OBJECTPROPERTY", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using System.Linq;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class OrderByWithoutTopOffsetRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Обнаружен ORDER BY без TOP или OFFSET: {0}";
|
||||
|
||||
public override void Visit(QuerySpecification node)
|
||||
{
|
||||
if (node.OrderByClause != null && node.TopRowFilter == null && node.OffsetClause == null)
|
||||
{
|
||||
var tokens = node.OrderByClause.ScriptTokenStream
|
||||
.Skip(node.OrderByClause.FirstTokenIndex)
|
||||
.Take(node.OrderByClause.LastTokenIndex - node.OrderByClause.FirstTokenIndex + 1)
|
||||
.Select(t => t.TokenType == TSqlTokenType.WhiteSpace ? " " : t.Text);
|
||||
|
||||
var orderByString = string.Join("", tokens);
|
||||
if (orderByString.Length > 53)
|
||||
{
|
||||
orderByString = orderByString[..50] + "...";
|
||||
}
|
||||
|
||||
AddViolation(node.OrderByClause, orderByString);
|
||||
}
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
15
SQLLinter/Infrastructure/Rules/PrintStatementRule.cs
Normal file
15
SQLLinter/Infrastructure/Rules/PrintStatementRule.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class PrintStatementRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Оператор PRINT найден.";
|
||||
|
||||
public override void Visit(PrintStatement node)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
87
SQLLinter/Infrastructure/Rules/ProcedureLoggingRule.cs
Normal file
87
SQLLinter/Infrastructure/Rules/ProcedureLoggingRule.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class ProcedureLoggingRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "В процедурах обязательно логирование (@DebugLog, LABEL_FINISH): {0}";
|
||||
|
||||
public override void Visit(CreateProcedureStatement node) => check(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
public override void Visit(CreateOrAlterProcedureStatement node) => check(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
public override void Visit(AlterProcedureStatement node) => check(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
|
||||
private void check(TSqlStatement node, string name)
|
||||
{
|
||||
var tokens = node.ScriptTokenStream;
|
||||
|
||||
bool hasDebugLog = false;
|
||||
bool hasLabelFinish = false;
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (token.Text is null) continue;
|
||||
|
||||
if (token.Text.Equals("@DebugLog", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hasDebugLog = true;
|
||||
}
|
||||
else if (token.Text.Equals("LABEL_FINISH:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hasLabelFinish = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDebugLog || !hasLabelFinish)
|
||||
{
|
||||
AddViolation(node, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ProcedureLoggingReturnRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "В процедурах с логированием RETURN запрещён: {0}";
|
||||
|
||||
public override void Visit(CreateProcedureStatement node) => check(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
public override void Visit(CreateOrAlterProcedureStatement node) => check(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
public override void Visit(AlterProcedureStatement node) => check(node, SQLHelpers.ObjectGetFullName(node.ProcedureReference.Name));
|
||||
|
||||
private void check(TSqlStatement node, string name)
|
||||
{
|
||||
var tokens = node.ScriptTokenStream;
|
||||
|
||||
bool hasDebugLog = false;
|
||||
bool hasLabelFinish = false;
|
||||
bool hasReturn = false;
|
||||
|
||||
List<ReturnPosition> returnPositions = new();
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (token.Text is null) continue;
|
||||
|
||||
if (token.Text.Equals("@DebugLog", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hasDebugLog = true;
|
||||
}
|
||||
else if (token.Text.Equals("LABEL_FINISH:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hasLabelFinish = true;
|
||||
}
|
||||
else if (token.Text.Equals("RETURN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hasReturn = true;
|
||||
returnPositions.Add(new(Line: token.Line, Column: token.Column));
|
||||
}
|
||||
}
|
||||
|
||||
if ((hasDebugLog || hasLabelFinish) || hasReturn)
|
||||
{
|
||||
returnPositions.ForEach(t => AddViolation(Name, GetText(name), t.Line, t.Column));
|
||||
}
|
||||
}
|
||||
|
||||
private record ReturnPosition(int Line, int Column);
|
||||
}
|
||||
17
SQLLinter/Infrastructure/Rules/RecompileRule.cs
Normal file
17
SQLLinter/Infrastructure/Rules/RecompileRule.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class RecompileRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Нежелательно использовать RECOMPILE.";
|
||||
|
||||
public override void Visit(OptimizeForOptimizerHint node)
|
||||
{
|
||||
if (node.HintKind == OptimizerHintKind.Recompile)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using SQLLinter.Core.Interfaces;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules.RuleExceptions
|
||||
{
|
||||
public class GlobalRuleException : IExtendedRuleException
|
||||
{
|
||||
public GlobalRuleException(int startLine, int endLine)
|
||||
{
|
||||
EndLine = endLine;
|
||||
StartLine = startLine;
|
||||
}
|
||||
|
||||
public int EndLine { get; private set; }
|
||||
|
||||
public string RuleName => "Global";
|
||||
|
||||
public int StartLine { get; }
|
||||
|
||||
public void SetEndLine(int endLine)
|
||||
{
|
||||
EndLine = endLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using SQLLinter.Core.Interfaces;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules.RuleExceptions
|
||||
{
|
||||
public class RuleException : IExtendedRuleException
|
||||
{
|
||||
public RuleException(Type ruleType, string ruleName, int startLine, int endLine)
|
||||
{
|
||||
RuleType = ruleType;
|
||||
RuleName = ruleName;
|
||||
StartLine = startLine;
|
||||
EndLine = endLine;
|
||||
}
|
||||
|
||||
public Type RuleType { get; }
|
||||
|
||||
public int StartLine { get; }
|
||||
|
||||
public int EndLine { get; private set; }
|
||||
|
||||
public string RuleName { get; }
|
||||
|
||||
public void SetEndLine(int endLine)
|
||||
{
|
||||
EndLine = endLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Core;
|
||||
using SQLLinter.Core.Interfaces;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules.RuleExceptions
|
||||
{
|
||||
public class RuleExceptionFinder : IRuleExceptionFinder
|
||||
{
|
||||
public static Regex RuleExceptionRegex = new Regex(@"(sqllinter-(?:dis|en)able)\s*(.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private readonly IDictionary<string, IRule> Rules;
|
||||
|
||||
public RuleExceptionFinder(IDictionary<string, IRule> rules)
|
||||
{
|
||||
Rules = rules;
|
||||
}
|
||||
|
||||
public IEnumerable<IExtendedRuleException> GetIgnoredRuleList(Stream fileStream)
|
||||
{
|
||||
var ruleExceptionList = new List<IExtendedRuleException>();
|
||||
TextReader reader = new StreamReader(fileStream);
|
||||
|
||||
var lineNumber = 0;
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
lineNumber++;
|
||||
if (line.Length > Constants.MaxLineWidthForRegexEval || !line.Contains("sqllinter-"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var match = RuleExceptionRegex.Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FindIgnoredRules(ruleExceptionList, lineNumber, match);
|
||||
}
|
||||
|
||||
fileStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
foreach (var ruleException in ruleExceptionList)
|
||||
{
|
||||
if (ruleException.EndLine == 0)
|
||||
{
|
||||
ruleException.SetEndLine(lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return ruleExceptionList;
|
||||
}
|
||||
|
||||
private void FindIgnoredRules(ICollection<IExtendedRuleException> ruleExceptionList, int lineNumber, Match match)
|
||||
{
|
||||
var action = match.Groups[1].Value;
|
||||
|
||||
var disableCommand = action.Equals("sqllinter-disable", StringComparison.OrdinalIgnoreCase);
|
||||
var enableCommand = action.Equals("sqllinter-enable", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var ruleExceptionDetails = match.Groups[2].Value.Split(' ').Select(p => p.Trim()).ToList();
|
||||
var matchedFriendlyNames = ruleExceptionDetails.Intersect(Rules.Keys).ToList();
|
||||
|
||||
if (!matchedFriendlyNames.Any())
|
||||
{
|
||||
if (disableCommand)
|
||||
{
|
||||
var ruleException = new GlobalRuleException(lineNumber, 0);
|
||||
ruleExceptionList.Add(ruleException);
|
||||
}
|
||||
|
||||
if (enableCommand)
|
||||
{
|
||||
var ruleException = ruleExceptionList.OfType<GlobalRuleException>().FirstOrDefault(r => r.EndLine == 0);
|
||||
ruleException?.SetEndLine(lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var matchedFriendlyName in matchedFriendlyNames)
|
||||
{
|
||||
Rules.TryGetValue(matchedFriendlyName, out var matched);
|
||||
|
||||
if (disableCommand)
|
||||
{
|
||||
var ruleException = new RuleException(matched.GetType(), matchedFriendlyName, lineNumber, 0);
|
||||
ruleExceptionList.Add(ruleException);
|
||||
}
|
||||
|
||||
if (enableCommand)
|
||||
{
|
||||
var ruleException = ruleExceptionList.OfType<RuleException>().FirstOrDefault(r => r.RuleName == matchedFriendlyName && r.EndLine == 0);
|
||||
ruleException?.SetEndLine(lineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules.RuleViolations
|
||||
{
|
||||
public class RuleViolation : IRuleViolation
|
||||
{
|
||||
public RuleViolation(string fileName, string ruleName, string text, int startLine, int startColumn, RuleViolationSeverity severity)
|
||||
{
|
||||
FileName = fileName;
|
||||
RuleName = ruleName;
|
||||
Text = text;
|
||||
Line = startLine;
|
||||
Column = startColumn;
|
||||
Severity = severity;
|
||||
}
|
||||
|
||||
public RuleViolation(string fileName, string ruleName, int startLine, int startColumn)
|
||||
{
|
||||
FileName = fileName;
|
||||
RuleName = ruleName;
|
||||
Line = startLine;
|
||||
Column = startColumn;
|
||||
}
|
||||
|
||||
public RuleViolation(string ruleName, int startLine, int startColumn)
|
||||
{
|
||||
RuleName = ruleName;
|
||||
Line = startLine;
|
||||
Column = startColumn;
|
||||
}
|
||||
|
||||
public int Column { get; set; }
|
||||
|
||||
public string FileName { get; set; }
|
||||
|
||||
public int Line { get; set; }
|
||||
|
||||
public string RuleName { get; set; }
|
||||
|
||||
public RuleViolationSeverity Severity { get; set; }
|
||||
|
||||
public string Text { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $@"{Severity.ToString().ToUpper()}: L{Line} C{Column} {FileName} ""{Text}""";
|
||||
}
|
||||
}
|
||||
}
|
||||
80
SQLLinter/Infrastructure/Rules/SchemaQualifyRule.cs
Normal file
80
SQLLinter/Infrastructure/Rules/SchemaQualifyRule.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class SchemaQualifyRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private readonly List<string> tableAliases = new()
|
||||
{
|
||||
"INSERTED",
|
||||
"UPDATED",
|
||||
"DELETED"
|
||||
};
|
||||
|
||||
public override string Text => "Имя объекта без схемы: {0}";
|
||||
|
||||
public override void Visit(TSqlStatement node)
|
||||
{
|
||||
var childAliasVisitor = new ChildAliasVisitor();
|
||||
node.AcceptChildren(childAliasVisitor);
|
||||
tableAliases.AddRange(childAliasVisitor.TableAliases);
|
||||
}
|
||||
|
||||
public override void Visit(NamedTableReference node) => VisitTableName(node.SchemaObject, true);
|
||||
|
||||
public override void Visit(CreateTableStatement node) => VisitTableName(node.SchemaObjectName, false);
|
||||
|
||||
public override void Visit(AlterTableStatement node) => VisitTableName(node.SchemaObjectName, false);
|
||||
|
||||
public override void Visit(TruncateTableStatement node) => VisitTableName(node.TableName, false);
|
||||
|
||||
public override void Visit(DropTableStatement node)
|
||||
{
|
||||
foreach (var schemaObjectName in node.Objects)
|
||||
{
|
||||
VisitTableName(schemaObjectName, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitTableName(SchemaObjectName node, bool canHaveTableAliases)
|
||||
{
|
||||
if (node.SchemaIdentifier != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// не проверять схему во временных таблицах
|
||||
if (node.BaseIdentifier.Value.Contains('#'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// не проверять схему псевдонимов таблиц
|
||||
if (canHaveTableAliases && tableAliases.Exists(x => x.Equals(node.BaseIdentifier.Value, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node));
|
||||
}
|
||||
|
||||
public class ChildAliasVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public List<string> TableAliases { get; } = new();
|
||||
|
||||
public override void Visit(TableReferenceWithAlias node)
|
||||
{
|
||||
if (node.Alias != null)
|
||||
{
|
||||
TableAliases.Add(node.Alias.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Visit(CommonTableExpression node)
|
||||
{
|
||||
TableAliases.Add(node.ExpressionName.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
SQLLinter/Infrastructure/Rules/SelectStarRule.cs
Normal file
39
SQLLinter/Infrastructure/Rules/SelectStarRule.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class SelectStarRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private int expressionCounter;
|
||||
|
||||
public override string Text => "Ожидаются имена столбцов в SELECT";
|
||||
|
||||
public override void Visit(ExistsPredicate node)
|
||||
{
|
||||
var childVisitor = new ChildVisitor();
|
||||
node.AcceptChildren(childVisitor);
|
||||
expressionCounter += childVisitor.SelectStarExpressionCount;
|
||||
}
|
||||
|
||||
public override void Visit(SelectStarExpression node)
|
||||
{
|
||||
if (expressionCounter > 0)
|
||||
{
|
||||
expressionCounter--;
|
||||
return;
|
||||
}
|
||||
|
||||
AddViolation(node);
|
||||
}
|
||||
|
||||
public class ChildVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public int SelectStarExpressionCount { get; set; }
|
||||
|
||||
public override void Visit(SelectStarExpression node)
|
||||
{
|
||||
SelectStarExpressionCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
SQLLinter/Infrastructure/Rules/SemicolonTerminationRule.cs
Normal file
73
SQLLinter/Infrastructure/Rules/SemicolonTerminationRule.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Infrastructure.Rules.Common;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class SemicolonTerminationRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private readonly IList<TSqlFragment> waitForStatements = new List<TSqlFragment>();
|
||||
private readonly IList<TSqlFragment> functionReturnTypeSelectStatements = new List<TSqlFragment>();
|
||||
private static Regex WhiteSpaceRegex = new Regex(@"\s", RegexOptions.Compiled);
|
||||
private static Regex AllWhiteSpaceRegex = new Regex(@"^\s$", RegexOptions.Compiled);
|
||||
|
||||
|
||||
// не принудительно завершать эти операторы точкой с запятой
|
||||
private readonly Type[] typesToSkip =
|
||||
{
|
||||
typeof(BeginEndBlockStatement),
|
||||
typeof(GoToStatement),
|
||||
typeof(IndexDefinition),
|
||||
typeof(LabelStatement),
|
||||
typeof(WhileStatement),
|
||||
typeof(IfStatement),
|
||||
typeof(CreateViewStatement)
|
||||
};
|
||||
|
||||
public override string Text => "Оператор не заканчивается точкой с запятой";
|
||||
|
||||
public override void Visit(WaitForStatement node)
|
||||
{
|
||||
waitForStatements.Add(node.Statement);
|
||||
}
|
||||
|
||||
public override void Visit(CreateFunctionStatement node)
|
||||
{
|
||||
if (node.ReturnType is SelectFunctionReturnType returnType)
|
||||
{
|
||||
functionReturnTypeSelectStatements.Add(returnType.SelectStatement);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Visit(TSqlStatement node)
|
||||
{
|
||||
if (Array.IndexOf(typesToSkip, node.GetType()) > -1 ||
|
||||
EndsWithSemicolon(node) ||
|
||||
waitForStatements.Contains(node) ||
|
||||
functionReturnTypeSelectStatements.Contains(node))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dynamicSqlColumnOffset = GetDynamicSqlColumnOffset(node);
|
||||
|
||||
var (lastToken, column) = GetLastTokenAndColumn(node);
|
||||
AddViolation(Name, Text, GetLineNumber(lastToken), column + dynamicSqlColumnOffset);
|
||||
}
|
||||
|
||||
private static (TSqlParserToken, int) GetLastTokenAndColumn(TSqlStatement node)
|
||||
{
|
||||
var lastToken = node.ScriptTokenStream[node.LastTokenIndex];
|
||||
var tabsOnLine = ColumnNumberCalculator.CountTabsBeforeToken(lastToken.Line, node.LastTokenIndex, node.ScriptTokenStream);
|
||||
var column = ColumnNumberCalculator.GetColumnNumberAfterToken(tabsOnLine, lastToken);
|
||||
|
||||
return (lastToken, column);
|
||||
}
|
||||
|
||||
private static bool EndsWithSemicolon(TSqlFragment node)
|
||||
{
|
||||
return node.ScriptTokenStream[node.LastTokenIndex].TokenType == TSqlTokenType.Semicolon
|
||||
|| node.ScriptTokenStream[node.LastTokenIndex + 1].TokenType == TSqlTokenType.Semicolon;
|
||||
}
|
||||
}
|
||||
18
SQLLinter/Infrastructure/Rules/TempTableDropRule.cs
Normal file
18
SQLLinter/Infrastructure/Rules/TempTableDropRule.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class TempTableDropRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Нежелательно использовать DROP временных таблиц: {0}";
|
||||
|
||||
public override void Visit(DropTableStatement node)
|
||||
{
|
||||
var check = node.Objects.Where(o => o.BaseIdentifier.Value.StartsWith("#"));
|
||||
if (check.Any())
|
||||
{
|
||||
AddViolation(node, string.Join(", ", check));
|
||||
}
|
||||
}
|
||||
}
|
||||
34
SQLLinter/Infrastructure/Rules/TempTableModificationRule.cs
Normal file
34
SQLLinter/Infrastructure/Rules/TempTableModificationRule.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class TempTableModificationRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Избегать ALTER/UPDATE/DELETE для временных таблиц: {0}";
|
||||
|
||||
public override void Visit(UpdateStatement node)
|
||||
{
|
||||
if (node.UpdateSpecification.Target is NamedTableReference tbl && tbl.SchemaObject.BaseIdentifier.Value.StartsWith("#"))
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(tbl.SchemaObject));
|
||||
}
|
||||
}
|
||||
|
||||
public override void Visit(DeleteStatement node)
|
||||
{
|
||||
if (node.DeleteSpecification.Target is NamedTableReference tbl && tbl.SchemaObject.BaseIdentifier.Value.StartsWith("#"))
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(tbl.SchemaObject));
|
||||
}
|
||||
}
|
||||
|
||||
public override void Visit(AlterTableStatement node)
|
||||
{
|
||||
if (node.SchemaObjectName.BaseIdentifier.Value.StartsWith("#"))
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.SchemaObjectName));
|
||||
}
|
||||
}
|
||||
}
|
||||
18
SQLLinter/Infrastructure/Rules/TopWithoutOrderByRule.cs
Normal file
18
SQLLinter/Infrastructure/Rules/TopWithoutOrderByRule.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class TopWithoutOrderByRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "TOP без ORDER BY может дать непредсказуемый результат";
|
||||
|
||||
public override void Visit(QuerySpecification node)
|
||||
{
|
||||
if (node.TopRowFilter != null && node.OrderByClause == null)
|
||||
{
|
||||
AddViolation(node.TopRowFilter);
|
||||
}
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
28
SQLLinter/Infrastructure/Rules/UnicodeStringRule.cs
Normal file
28
SQLLinter/Infrastructure/Rules/UnicodeStringRule.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class UnicodeStringRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
public override string Text => "Использование символов Юникода в строке, отличной от Юникода";
|
||||
|
||||
public override void Visit(StringLiteral node)
|
||||
{
|
||||
if (node.IsNational)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsAscii(node.Value))
|
||||
{
|
||||
AddViolation(Name, Text, node.StartLine, node.StartColumn);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAscii(string part)
|
||||
{
|
||||
return SQLHelpers.IsValidForEncoding(part, "windows-1251");
|
||||
}
|
||||
}
|
||||
17
SQLLinter/Infrastructure/Rules/UnionRule.cs
Normal file
17
SQLLinter/Infrastructure/Rules/UnionRule.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class UnionRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Избегать UNION, использовать UNION ALL.";
|
||||
|
||||
public override void Visit(BinaryQueryExpression node)
|
||||
{
|
||||
if (node.BinaryQueryExpressionType == BinaryQueryExpressionType.Union && !node.All)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
SQLLinter/Infrastructure/Rules/UpdateWhereRule.cs
Normal file
28
SQLLinter/Infrastructure/Rules/UpdateWhereRule.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class UpdateWhereRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Ожидается выражение WHERE для оператора UPDATE: {0}";
|
||||
|
||||
|
||||
public override void Visit(UpdateSpecification node)
|
||||
{
|
||||
if (node.WhereClause != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
string name = string.Join("", node.Target.ScriptTokenStream
|
||||
.Skip(node.Target.FirstTokenIndex)
|
||||
.Take(node.Target.LastTokenIndex - node.Target.FirstTokenIndex + 1)
|
||||
.Select(t => t.Text)
|
||||
);
|
||||
|
||||
AddViolation(node, name);
|
||||
}
|
||||
}
|
||||
64
SQLLinter/Infrastructure/Rules/UpperLowerRule.cs
Normal file
64
SQLLinter/Infrastructure/Rules/UpperLowerRule.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class UpperLowerRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Использование функций UPPER или LOWER при выполнении сравнений в операторах SELECT не требуется при запуске базы данных в режиме без учета регистра.";
|
||||
|
||||
public override void Visit(SelectStatement node)
|
||||
{
|
||||
var visitor = new ChildQueryComparisonVisitor();
|
||||
node.Accept(visitor);
|
||||
if (visitor.QueryExpressionUpperLowerFunctionFound)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChildQueryComparisonVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool QueryExpressionUpperLowerFunctionFound { get; private set; }
|
||||
|
||||
public override void Visit(QueryExpression node)
|
||||
{
|
||||
var visitor = new ChildBooleanComparisonVisitor();
|
||||
node.Accept(visitor);
|
||||
if (visitor.UpperLowerFunctionCallInComparison)
|
||||
{
|
||||
QueryExpressionUpperLowerFunctionFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ChildBooleanComparisonVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool UpperLowerFunctionCallInComparison { get; private set; }
|
||||
|
||||
public override void Visit(BooleanComparisonExpression node)
|
||||
{
|
||||
var visitor = new ChildFunctionCallVisitor();
|
||||
node.Accept(visitor);
|
||||
if (visitor.UpperLowerFound)
|
||||
{
|
||||
UpperLowerFunctionCallInComparison = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ChildFunctionCallVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool UpperLowerFound { get; private set; }
|
||||
|
||||
public override void Visit(FunctionCall node)
|
||||
{
|
||||
if (node.FunctionName.Value.Equals("UPPER", StringComparison.OrdinalIgnoreCase) ||
|
||||
node.FunctionName.Value.Equals("LOWER", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
UpperLowerFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
SQLLinter/Infrastructure/Rules/UserFunctionJoinRule.cs
Normal file
158
SQLLinter/Infrastructure/Rules/UserFunctionJoinRule.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
using SQLLinter.Core;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class UserFunctionJoinRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещено использование пользовательских функций внутри запросов: {0}";
|
||||
|
||||
private bool _inQueryContext;
|
||||
|
||||
// Входим в контекст запроса
|
||||
public override void Visit(QuerySpecification node)
|
||||
{
|
||||
_inQueryContext = true;
|
||||
|
||||
// Проверяем FROM
|
||||
if (node.FromClause != null)
|
||||
{
|
||||
var tables = node.FromClause.TableReferences;
|
||||
|
||||
// Разрешаем ровно одну табличную функцию
|
||||
if (!(tables.Count == 1 && tables[0] is SchemaObjectFunctionTableReference ft && IsUserFunction(ft.SchemaObject?.BaseIdentifier?.Value)))
|
||||
{
|
||||
foreach (var tr in tables)
|
||||
CheckTableReference(tr);
|
||||
}
|
||||
}
|
||||
|
||||
base.Visit(node);
|
||||
_inQueryContext = false;
|
||||
}
|
||||
|
||||
// Скалярные функции
|
||||
public override void Visit(FunctionCall node)
|
||||
{
|
||||
if (_inQueryContext && IsUserFunction(node.FunctionName.Value))
|
||||
{
|
||||
AddViolationFunction(node);
|
||||
}
|
||||
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
// Табличные функции
|
||||
public override void Visit(SchemaObjectFunctionTableReference node)
|
||||
{
|
||||
// Проверка делается в QuerySpecification - здесь ничего не делаем
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
// Подзапросы
|
||||
public override void Visit(ScalarSubquery node)
|
||||
{
|
||||
node.QueryExpression.Accept(this);
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
public override void Visit(QueryDerivedTable node)
|
||||
{
|
||||
node.QueryExpression.Accept(this);
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
public override void Visit(QualifiedJoin node)
|
||||
{
|
||||
if (node.SearchCondition != null)
|
||||
node.SearchCondition.Accept(this);
|
||||
|
||||
if (node.FirstTableReference != null)
|
||||
CheckTableReference(node.FirstTableReference);
|
||||
if (node.SecondTableReference != null)
|
||||
CheckTableReference(node.SecondTableReference);
|
||||
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
public override void Visit(UnqualifiedJoin node)
|
||||
{
|
||||
// Проверяем APPLY
|
||||
if (node.UnqualifiedJoinType == UnqualifiedJoinType.CrossApply ||
|
||||
node.UnqualifiedJoinType == UnqualifiedJoinType.OuterApply)
|
||||
{
|
||||
if (node.SecondTableReference is SchemaObjectFunctionTableReference ft &&
|
||||
IsUserFunction(ft.SchemaObject?.BaseIdentifier?.Value))
|
||||
{
|
||||
AddViolation(ft, SQLHelpers.ObjectGetFullName(ft.SchemaObject));
|
||||
}
|
||||
}
|
||||
|
||||
// Обходим обе стороны
|
||||
if (node.FirstTableReference != null)
|
||||
CheckTableReference(node.FirstTableReference);
|
||||
if (node.SecondTableReference != null)
|
||||
CheckTableReference(node.SecondTableReference);
|
||||
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ------------------------
|
||||
// Вспомогательные методы
|
||||
// ------------------------
|
||||
|
||||
private void AddViolationFunction(FunctionCall func)
|
||||
{
|
||||
string ident = "";
|
||||
if (func.CallTarget is MultiPartIdentifierCallTarget callTarget)
|
||||
{
|
||||
ident = SQLHelpers.ObjectGetFullName(callTarget.MultiPartIdentifier.Identifiers);
|
||||
if (!string.IsNullOrWhiteSpace(ident)) ident += ".";
|
||||
}
|
||||
|
||||
ident += "[" + func.FunctionName.Value + "]";
|
||||
AddViolation(func, ident);
|
||||
}
|
||||
|
||||
private void CheckTableReference(TableReference tr)
|
||||
{
|
||||
switch (tr)
|
||||
{
|
||||
case SchemaObjectFunctionTableReference ft:
|
||||
if (IsUserFunction(ft.SchemaObject?.BaseIdentifier?.Value))
|
||||
AddViolation(ft, SQLHelpers.ObjectGetFullName(ft.SchemaObject));
|
||||
break;
|
||||
|
||||
case QueryDerivedTable qdt:
|
||||
qdt.QueryExpression.Accept(this);
|
||||
break;
|
||||
|
||||
case QualifiedJoin qj:
|
||||
if (qj.FirstTableReference != null)
|
||||
CheckTableReference(qj.FirstTableReference);
|
||||
if (qj.SecondTableReference != null)
|
||||
CheckTableReference(qj.SecondTableReference);
|
||||
if (qj.SearchCondition != null)
|
||||
qj.SearchCondition.Accept(this);
|
||||
break;
|
||||
|
||||
case UnqualifiedJoin aj:
|
||||
if (aj.FirstTableReference != null)
|
||||
CheckTableReference(aj.FirstTableReference);
|
||||
if (aj.SecondTableReference != null)
|
||||
CheckTableReference(aj.SecondTableReference);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsUserFunction(string? functionName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(functionName) &&
|
||||
!Constants.SystemFunctions.Contains(functionName);
|
||||
}
|
||||
}
|
||||
17
SQLLinter/Infrastructure/Rules/WhereInSelectRule.cs
Normal file
17
SQLLinter/Infrastructure/Rules/WhereInSelectRule.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class WhereInSelectRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещено IN (SELECT ...), используйте EXISTS/NOT EXISTS.";
|
||||
|
||||
public override void Visit(InPredicate node)
|
||||
{
|
||||
if (node.Subquery != null)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
SQLLinter/Infrastructure/Rules/XmlJsonParsingRule.cs
Normal file
18
SQLLinter/Infrastructure/Rules/XmlJsonParsingRule.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class XmlJsonParsingRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещён парсинг XML/JSON.";
|
||||
|
||||
public override void Visit(FunctionCall node)
|
||||
{
|
||||
var fn = node.FunctionName.Value.ToUpperInvariant();
|
||||
if (fn.Contains("OPENXML") || fn.Contains("OPENJSON") || fn.Contains("NODES"))
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user