Добавьте файлы проекта.
This commit is contained in:
4
SQLLint.slnx
Normal file
4
SQLLint.slnx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="SQLLinter.CLI/SQLLinter.CLI.csproj" />
|
||||||
|
<Project Path="SQLLinter/SQLLinter.csproj" />
|
||||||
|
</Solution>
|
||||||
58
SQLLinter.CLI/Program.cs
Normal file
58
SQLLinter.CLI/Program.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using SQLLinter.Infrastructure.Configuration;
|
||||||
|
using SQLLinter.Infrastructure.Reporters;
|
||||||
|
|
||||||
|
namespace SQLLinter.CLI
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var rep = new MarkdownFileReporter();
|
||||||
|
var con = new Config()
|
||||||
|
{
|
||||||
|
CompatibilityLevel = 170,
|
||||||
|
Plugins = [],
|
||||||
|
Rules = new()
|
||||||
|
{
|
||||||
|
["CaseSensitiveVariables"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["ConditionalBeginEnd"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["CountStar"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["CrossDatabaseTransaction"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["DataCompression"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["DataTypeLength"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["DeleteWhere"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["DisallowCursors"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["DuplicateEmptyLine"] = Common.RuleViolationSeverity.Off,
|
||||||
|
["DuplicateGo"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["FullText"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["InformationSchema"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["KeywordCapitalization"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["LinkedServer"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["MultiTableAlias"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["NamedConstraint"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["NonSargable"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["ObjectProperty"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["PrintStatement"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["SchemaQualify"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["SelectStar"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["SemicolonTermination"] = Common.RuleViolationSeverity.Off,
|
||||||
|
["UnicodeString"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["UpdateWhere"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["UpperLower"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
["SetVariable"] = Common.RuleViolationSeverity.Critical,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var linter = new Linter(con, rep);
|
||||||
|
|
||||||
|
using (StreamReader reader = new StreamReader(@"C:\Users\frost\Desktop\DISTR-2599\test.sql"))
|
||||||
|
{
|
||||||
|
linter.Run("test.sql", reader.BaseStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
//linter.Run(@"C:\Users\frost\Desktop\DISTR-2599\test.sql");
|
||||||
|
|
||||||
|
rep.SaveReport(@"C:\Users\frost\Desktop\DISTR-2599\test.md");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
SQLLinter.CLI/SQLLinter.CLI.csproj
Normal file
29
SQLLinter.CLI/SQLLinter.CLI.csproj
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<Authors>FrigaT</Authors>
|
||||||
|
<Company>FrigaT</Company>
|
||||||
|
<Product>SQLLinter.CLI</Product>
|
||||||
|
<Description>cli клиент для проверки MS SQL кода</Description>
|
||||||
|
<Copyright>Copyright © 2025 FrigaT</Copyright>
|
||||||
|
<RepositoryUrl>https://git.frigat.duckdns.org/FrigaT/SQLLint</RepositoryUrl>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<PackageProjectUrl>https://git.frigat.duckdns.org/FrigaT/SQLLint</PackageProjectUrl>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ArgumentsToolkit" Version="0.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SQLLinter\SQLLinter.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
72
SQLLinter/Common/BaseRuleVisitor.cs
Normal file
72
SQLLinter/Common/BaseRuleVisitor.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||||
|
|
||||||
|
namespace SQLLinter.Common;
|
||||||
|
|
||||||
|
public abstract class BaseRuleVisitor : TSqlFragmentVisitor, IRule
|
||||||
|
{
|
||||||
|
protected readonly List<Violation> _violations = new();
|
||||||
|
|
||||||
|
public int DynamicSqlStartColumn { get; set; }
|
||||||
|
public int DynamicSqlStartLine { get; set; }
|
||||||
|
public virtual string Name { get => GetDefaultRuleName(GetType().Name); }
|
||||||
|
public abstract string Text { get; }
|
||||||
|
public virtual RuleViolationSeverity Severity { get; set; } = RuleViolationSeverity.Info;
|
||||||
|
|
||||||
|
protected string GetDefaultRuleName(string className) =>
|
||||||
|
className.Substring(className.Length - 4).ToLower() == "rule" ? className.Substring(0, className.Length - 4) : className;
|
||||||
|
|
||||||
|
|
||||||
|
protected virtual int GetLineNumber(TSqlFragment node) => node.StartLine + GetDynamicSqlLineOffset();
|
||||||
|
|
||||||
|
protected virtual int GetLineNumber(TSqlParserToken node) => node.Line + GetDynamicSqlLineOffset();
|
||||||
|
|
||||||
|
private int GetDynamicSqlLineOffset() =>
|
||||||
|
DynamicSqlStartLine > 0
|
||||||
|
? DynamicSqlStartLine - 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
protected virtual int GetColumnNumber(TSqlFragment node) => node.StartColumn + GetDynamicSqlColumnOffset(node);
|
||||||
|
|
||||||
|
protected virtual int GetColumnNumber(TSqlParserToken node) => node.Column + GetDynamicSqlColumnOffset(node);
|
||||||
|
|
||||||
|
protected virtual int GetDynamicSqlColumnOffset(TSqlFragment node) => GetDynamicSqlColumnOffset(node.StartLine);
|
||||||
|
|
||||||
|
protected virtual int GetDynamicSqlColumnOffset(TSqlParserToken node) => GetDynamicSqlColumnOffset(node.Line);
|
||||||
|
|
||||||
|
private int GetDynamicSqlColumnOffset(int line) =>
|
||||||
|
DynamicSqlStartLine > 0 && line == 1
|
||||||
|
? DynamicSqlStartColumn
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
public virtual void FixViolation(
|
||||||
|
List<string> fileLines, IRuleViolation ruleViolation, FileLineActions actions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IEnumerable<Violation> Analyze(TSqlFragment fragment)
|
||||||
|
{
|
||||||
|
_violations.Clear();
|
||||||
|
fragment.Accept(this);
|
||||||
|
return _violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddViolation(Violation violation)
|
||||||
|
{
|
||||||
|
_violations.Add(violation);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddViolation(string RuleName, string Message, int Line, int Column)
|
||||||
|
{
|
||||||
|
_violations.Add(new(RuleName, Message, Line, Column));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddViolation(TSqlFragment node, params string[] param)
|
||||||
|
{
|
||||||
|
AddViolation(Name, this.GetText(param), GetLineNumber(node), GetColumnNumber(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string GetText(params string[] param)
|
||||||
|
{
|
||||||
|
return string.Format(this.Text, param);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
SQLLinter/Common/FileHelpers.cs
Normal file
53
SQLLinter/Common/FileHelpers.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
namespace SQLLinter.Common.Helpers;
|
||||||
|
|
||||||
|
public static class FileHelpers
|
||||||
|
{
|
||||||
|
public static List<string> FindFilesWithMask(List<string> paths)
|
||||||
|
{
|
||||||
|
return paths.SelectMany(path =>
|
||||||
|
{
|
||||||
|
var fullPath = Path.GetFullPath(path);
|
||||||
|
var directory = Path.GetDirectoryName(fullPath);
|
||||||
|
if (directory == null) return Enumerable.Empty<string>();
|
||||||
|
|
||||||
|
string pattern = Path.GetFileName(fullPath);
|
||||||
|
|
||||||
|
// Если маска есть в директории
|
||||||
|
if (directory.Contains("*") || directory.Contains("?"))
|
||||||
|
{
|
||||||
|
var root = Path.GetPathRoot(directory);
|
||||||
|
if (root == null) return Enumerable.Empty<string>();
|
||||||
|
|
||||||
|
string relative = directory.Substring(root.Length);
|
||||||
|
|
||||||
|
return ExpandDirectories(root, relative)
|
||||||
|
.SelectMany(dir => Directory.EnumerateFiles(dir, pattern));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Directory.Exists(directory)
|
||||||
|
? Directory.EnumerateFiles(directory, pattern)
|
||||||
|
: Enumerable.Empty<string>();
|
||||||
|
}
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<string> ExpandDirectories(string root, string relative)
|
||||||
|
{
|
||||||
|
string[] parts = relative.Split(Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
IEnumerable<string> dirs = new[] { root };
|
||||||
|
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
dirs = part.Contains("*") || part.Contains("?")
|
||||||
|
? dirs.SelectMany(d => Directory.Exists(d)
|
||||||
|
? Directory.EnumerateDirectories(d, part)
|
||||||
|
: Enumerable.Empty<string>())
|
||||||
|
: dirs.Select(d => Path.Combine(d, part));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirs.Where(Directory.Exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
98
SQLLinter/Common/FileLineActions.cs
Normal file
98
SQLLinter/Common/FileLineActions.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
namespace SQLLinter.Common;
|
||||||
|
|
||||||
|
public class FileLineActions
|
||||||
|
{
|
||||||
|
private readonly List<string> FileLines;
|
||||||
|
|
||||||
|
private readonly List<IRuleViolation> RuleViolations;
|
||||||
|
|
||||||
|
public FileLineActions(List<IRuleViolation> ruleViolations, List<string> fileLines)
|
||||||
|
{
|
||||||
|
RuleViolations = ruleViolations;
|
||||||
|
FileLines = fileLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Insert(int index, string line)
|
||||||
|
{
|
||||||
|
InsertRange(index, new string[1] { line });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InsertInLine(int lineIndex, int charIndex, string content)
|
||||||
|
{
|
||||||
|
string text = FileLines[lineIndex];
|
||||||
|
text = text.Insert(charIndex, content);
|
||||||
|
FileLines[lineIndex] = text;
|
||||||
|
foreach (IRuleViolation item in RuleViolations.Where((IRuleViolation x) => x.Line == lineIndex + 1 && x.Column > charIndex))
|
||||||
|
{
|
||||||
|
item.Column += content.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InsertRange(int index, IList<string> lines)
|
||||||
|
{
|
||||||
|
FileLines.InsertRange(index, lines);
|
||||||
|
foreach (IRuleViolation item in RuleViolations.Where((IRuleViolation x) => x.Line > index))
|
||||||
|
{
|
||||||
|
item.Line += lines.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAll(Func<string, bool> where)
|
||||||
|
{
|
||||||
|
for (int num = FileLines.Count - 1; num >= 0; num--)
|
||||||
|
{
|
||||||
|
if (where(FileLines[num]))
|
||||||
|
{
|
||||||
|
RemoveAt(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAt(int index)
|
||||||
|
{
|
||||||
|
RemoveRange(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveInLine(int lineIndex, int charIndex, int length)
|
||||||
|
{
|
||||||
|
string text = FileLines[lineIndex];
|
||||||
|
text = text.Remove(charIndex, length);
|
||||||
|
FileLines[lineIndex] = text;
|
||||||
|
foreach (IRuleViolation item in RuleViolations.Where((IRuleViolation x) => x.Column == lineIndex + 1 && x.Column > charIndex))
|
||||||
|
{
|
||||||
|
item.Column -= length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveRange(int index, int count)
|
||||||
|
{
|
||||||
|
FileLines.RemoveRange(index, count);
|
||||||
|
foreach (IRuleViolation item in RuleViolations.Where((IRuleViolation x) => x.Line > index))
|
||||||
|
{
|
||||||
|
item.Line -= count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RepaceInlineAt(int lineIndex, int charIndex, string content, int? replaceLength = null)
|
||||||
|
{
|
||||||
|
string text = FileLines[lineIndex];
|
||||||
|
text = text.Remove(charIndex, replaceLength ?? content.Length);
|
||||||
|
text = text.Insert(charIndex, content);
|
||||||
|
FileLines[lineIndex] = text;
|
||||||
|
if (!replaceLength.HasValue || replaceLength == content.Length)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? num = content.Length - replaceLength;
|
||||||
|
foreach (IRuleViolation item in RuleViolations.Where((IRuleViolation x) => x.Line == lineIndex + 1 && x.Column > charIndex + content.Length))
|
||||||
|
{
|
||||||
|
item.Column += num.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateLine(int lineIndex, string content)
|
||||||
|
{
|
||||||
|
FileLines[lineIndex] = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
94
SQLLinter/Common/FixHelpers.cs
Normal file
94
SQLLinter/Common/FixHelpers.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace SQLLinter.Common.Helpers;
|
||||||
|
|
||||||
|
public static class FixHelpers
|
||||||
|
{
|
||||||
|
public class FindViolatingNodeVisitor<T> : TSqlFragmentVisitor where T : TSqlFragment
|
||||||
|
{
|
||||||
|
private readonly Func<T, bool> Where;
|
||||||
|
|
||||||
|
public List<T> Nodes = new List<T>();
|
||||||
|
|
||||||
|
public FindViolatingNodeVisitor(Func<T, bool> where = null)
|
||||||
|
{
|
||||||
|
Where = where;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(TSqlFragment node)
|
||||||
|
{
|
||||||
|
if (node is T val && (Where == null || Where(val)))
|
||||||
|
{
|
||||||
|
Nodes.Add(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (TReturn, TFind) FindViolatingNode<TFind, TReturn>(List<string> fileLines, IRuleViolation ruleViolation, Func<TFind, TReturn> getFragment) where TFind : TSqlFragment where TReturn : TSqlFragment
|
||||||
|
{
|
||||||
|
TFind val = FindNodes<TFind>(fileLines).FirstOrDefault(delegate (TFind x)
|
||||||
|
{
|
||||||
|
TReturn val2 = getFragment(x);
|
||||||
|
return val2?.StartLine == ruleViolation.Line && val2?.StartColumn == ruleViolation.Column;
|
||||||
|
});
|
||||||
|
return (getFragment(val), val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<T> FindNodes<T>(List<string> fileLines, Func<T, bool> where = null) where T : TSqlFragment
|
||||||
|
{
|
||||||
|
using StringReader input = new StringReader(string.Join("\n", fileLines));
|
||||||
|
IList<ParseError> errors;
|
||||||
|
TSqlFragment tSqlFragment = new TSql150Parser(initialQuotedIdentifiers: true, SqlEngineType.All).Parse(input, out errors);
|
||||||
|
if (errors != null && errors.Any())
|
||||||
|
{
|
||||||
|
throw new Exception("Parsing failed. " + string.Join(". ", errors.Select((ParseError x) => x.Message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
FindViolatingNodeVisitor<T> findViolatingNodeVisitor = new FindViolatingNodeVisitor<T>(where);
|
||||||
|
tSqlFragment.Accept(findViolatingNodeVisitor);
|
||||||
|
return findViolatingNodeVisitor.Nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<T> FindNodes<T>(TSqlFragment statement, Func<T, bool> where = null) where T : TSqlFragment
|
||||||
|
{
|
||||||
|
FindViolatingNodeVisitor<T> findViolatingNodeVisitor = new FindViolatingNodeVisitor<T>(where);
|
||||||
|
statement.Accept(findViolatingNodeVisitor);
|
||||||
|
return findViolatingNodeVisitor.Nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T FindViolatingNode<T>(List<string> fileLines, IRuleViolation ruleViolation) where T : TSqlFragment
|
||||||
|
{
|
||||||
|
return FindViolatingNode(fileLines, ruleViolation, (T x) => x).Item1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetIndent(List<string> fileLines, IRuleViolation ruleViolation)
|
||||||
|
{
|
||||||
|
return GetIndent(fileLines[ruleViolation.Line - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetIndent(List<string> fileLines, TSqlStatement statement)
|
||||||
|
{
|
||||||
|
return GetIndent(fileLines[statement.StartLine - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetString(TSqlFragment fragment)
|
||||||
|
{
|
||||||
|
return string.Join(string.Empty, from x in fragment.ScriptTokenStream.Where((TSqlParserToken x, int i) => i >= fragment.FirstTokenIndex && i <= fragment.LastTokenIndex)
|
||||||
|
select x.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetIndent(string ifLine)
|
||||||
|
{
|
||||||
|
Match match = new Regex("^\\s+").Match(ifLine);
|
||||||
|
string result = string.Empty;
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
result = match.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
SQLLinter/Common/IBaseReporter.cs
Normal file
6
SQLLinter/Common/IBaseReporter.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace SQLLinter.Common;
|
||||||
|
|
||||||
|
public interface IBaseReporter
|
||||||
|
{
|
||||||
|
void Report(string message);
|
||||||
|
}
|
||||||
8
SQLLinter/Common/IReporter.cs
Normal file
8
SQLLinter/Common/IReporter.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace SQLLinter.Common;
|
||||||
|
|
||||||
|
public interface IReporter : IBaseReporter
|
||||||
|
{
|
||||||
|
void ReportViolation(IRuleViolation violation);
|
||||||
|
|
||||||
|
void ReportViolation(string fileName, int line, int column, RuleViolationSeverity severity, string ruleName, string violationText);
|
||||||
|
}
|
||||||
17
SQLLinter/Common/IRule.cs
Normal file
17
SQLLinter/Common/IRule.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||||
|
|
||||||
|
namespace SQLLinter.Common;
|
||||||
|
|
||||||
|
public interface IRule
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
string Text { get; }
|
||||||
|
|
||||||
|
RuleViolationSeverity Severity { get; set; }
|
||||||
|
int DynamicSqlStartColumn { get; set; }
|
||||||
|
|
||||||
|
int DynamicSqlStartLine { get; set; }
|
||||||
|
|
||||||
|
IEnumerable<Violation> Analyze(TSqlFragment fragment);
|
||||||
|
|
||||||
|
}
|
||||||
10
SQLLinter/Common/IRuleException.cs
Normal file
10
SQLLinter/Common/IRuleException.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace SQLLinter.Common;
|
||||||
|
|
||||||
|
public interface IRuleException
|
||||||
|
{
|
||||||
|
int StartLine { get; }
|
||||||
|
|
||||||
|
int EndLine { get; }
|
||||||
|
|
||||||
|
string RuleName { get; }
|
||||||
|
}
|
||||||
16
SQLLinter/Common/IRuleViolation.cs
Normal file
16
SQLLinter/Common/IRuleViolation.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace SQLLinter.Common;
|
||||||
|
|
||||||
|
public interface IRuleViolation
|
||||||
|
{
|
||||||
|
string FileName { get; }
|
||||||
|
|
||||||
|
int Column { get; set; }
|
||||||
|
|
||||||
|
int Line { get; set; }
|
||||||
|
|
||||||
|
string RuleName { get; }
|
||||||
|
|
||||||
|
RuleViolationSeverity Severity { get; }
|
||||||
|
|
||||||
|
string Text { get; }
|
||||||
|
}
|
||||||
9
SQLLinter/Common/RuleViolationSeverity.cs
Normal file
9
SQLLinter/Common/RuleViolationSeverity.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace SQLLinter.Common;
|
||||||
|
|
||||||
|
public enum RuleViolationSeverity
|
||||||
|
{
|
||||||
|
Off,
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Critical,
|
||||||
|
}
|
||||||
59
SQLLinter/Common/SQLHelpers.cs
Normal file
59
SQLLinter/Common/SQLHelpers.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SQLLinter.Common.Helpers;
|
||||||
|
|
||||||
|
public static class SQLHelpers
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, что строка может быть закодирована в указанной SQL кодировке (collation).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">Строка для проверки</param>
|
||||||
|
/// <param name="sqlEncodingName">Имя кодировки, например "windows-1251" или "iso-8859-1"</param>
|
||||||
|
/// <returns>true, если строка полностью совместима</returns>
|
||||||
|
public static bool IsValidForEncoding(string input, string sqlEncodingName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
Encoding enc;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Включаем поддержку старых кодировок (ANSI, OEM)
|
||||||
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
|
||||||
|
enc = Encoding.GetEncoding(sqlEncodingName,
|
||||||
|
new EncoderReplacementFallback("?"),
|
||||||
|
new DecoderReplacementFallback("?"));
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Неизвестная кодировка: {sqlEncodingName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsValidForEncoding(input, enc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidForEncoding(string input, Encoding enc)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Пробуем закодировать и декодировать обратно
|
||||||
|
byte[] bytes = enc.GetBytes(input);
|
||||||
|
string roundtrip = enc.GetString(bytes);
|
||||||
|
|
||||||
|
// Если после кодирования/декодирования строка совпала - значит все символы поддерживаются
|
||||||
|
return roundtrip == input;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ObjectGetFullName(SchemaObjectName name) => ObjectGetFullName(name.Identifiers);
|
||||||
|
|
||||||
|
|
||||||
|
public static string ObjectGetFullName(IList<Identifier> identifiers)
|
||||||
|
{
|
||||||
|
return string.Join(".", identifiers.Select(i => "[" + i.Value + "]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
4
SQLLinter/Common/Violation.cs
Normal file
4
SQLLinter/Common/Violation.cs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
namespace SQLLinter.Common
|
||||||
|
{
|
||||||
|
public record Violation(string RuleName, string Message, int Line, int Column);
|
||||||
|
}
|
||||||
310
SQLLinter/Core/Constants.cs
Normal file
310
SQLLinter/Core/Constants.cs
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
namespace SQLLinter.Core;
|
||||||
|
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public static readonly string[] _TSqlKeywords =
|
||||||
|
{
|
||||||
|
"ADD",
|
||||||
|
"ALL",
|
||||||
|
"ALTER",
|
||||||
|
"AND",
|
||||||
|
"ANY",
|
||||||
|
"AS",
|
||||||
|
"ASC",
|
||||||
|
"AUTHORIZATION",
|
||||||
|
"BACKUP",
|
||||||
|
"BEGIN",
|
||||||
|
"BETWEEN",
|
||||||
|
"BREAK",
|
||||||
|
"BROWSE",
|
||||||
|
"BULK",
|
||||||
|
"BY",
|
||||||
|
"CASCADE",
|
||||||
|
"CASE",
|
||||||
|
"CHECK",
|
||||||
|
"CHECKPOINT",
|
||||||
|
"CLOSE",
|
||||||
|
"CLUSTERED",
|
||||||
|
"COALESCE",
|
||||||
|
"COLLATE",
|
||||||
|
"COLUMN",
|
||||||
|
"COMMIT",
|
||||||
|
"COMPUTE",
|
||||||
|
"CONSTRAINT",
|
||||||
|
"CONTAINS",
|
||||||
|
"CONTAINSTABLE",
|
||||||
|
"CONTINUE",
|
||||||
|
"CONVERT",
|
||||||
|
"CREATE",
|
||||||
|
"CROSS",
|
||||||
|
"CURRENT",
|
||||||
|
"CURRENT_DATE",
|
||||||
|
"CURRENT_TIME",
|
||||||
|
"CURRENT_TIMESTAMP",
|
||||||
|
"CURRENT_USER",
|
||||||
|
"CURSOR",
|
||||||
|
"DATABASE",
|
||||||
|
"DBCC",
|
||||||
|
"DEALLOCATE",
|
||||||
|
"DECLARE",
|
||||||
|
"DEFAULT",
|
||||||
|
"DELETE",
|
||||||
|
"DENY",
|
||||||
|
"DESC",
|
||||||
|
"DISK",
|
||||||
|
"DISTINCT",
|
||||||
|
"DISTRIBUTED",
|
||||||
|
"DOUBLE",
|
||||||
|
"DROP",
|
||||||
|
"DUMP",
|
||||||
|
"ELSE",
|
||||||
|
"END",
|
||||||
|
"ERRLVL",
|
||||||
|
"ESCAPE",
|
||||||
|
"EXCEPT",
|
||||||
|
"EXEC",
|
||||||
|
"EXECUTE",
|
||||||
|
"EXISTS",
|
||||||
|
"EXIT",
|
||||||
|
"EXTERNAL",
|
||||||
|
"FETCH",
|
||||||
|
"FILE",
|
||||||
|
"FILLFACTOR",
|
||||||
|
"FOR",
|
||||||
|
"FOREIGN",
|
||||||
|
"FREETEXT",
|
||||||
|
"FREETEXTTABLE",
|
||||||
|
"FROM",
|
||||||
|
"FULL",
|
||||||
|
"FUNCTION",
|
||||||
|
"GOTO",
|
||||||
|
"GRANT",
|
||||||
|
"GROUP",
|
||||||
|
"HAVING",
|
||||||
|
"HOLDLOCK",
|
||||||
|
"IDENTITY",
|
||||||
|
"IDENTITYCOL",
|
||||||
|
"IDENTITY_INSERT",
|
||||||
|
"IF",
|
||||||
|
"IN",
|
||||||
|
"INDEX",
|
||||||
|
"INNER",
|
||||||
|
"INSERT",
|
||||||
|
"INTERSECT",
|
||||||
|
"INTO",
|
||||||
|
"IS",
|
||||||
|
"JOIN",
|
||||||
|
"KEY",
|
||||||
|
"KILL",
|
||||||
|
"LEFT",
|
||||||
|
"LIKE",
|
||||||
|
"LINENO",
|
||||||
|
"LOAD",
|
||||||
|
"MERGE",
|
||||||
|
"NATIONAL",
|
||||||
|
"NOCHECK",
|
||||||
|
"NONCLUSTERED",
|
||||||
|
"NOT",
|
||||||
|
"NULL",
|
||||||
|
"NULLIF",
|
||||||
|
"OF",
|
||||||
|
"OFF",
|
||||||
|
"OFFSETS",
|
||||||
|
"ON",
|
||||||
|
"OPEN",
|
||||||
|
"OPENDATASOURCE",
|
||||||
|
"OPENQUERY",
|
||||||
|
"OPENROWSET",
|
||||||
|
"OPENXML",
|
||||||
|
"OPTION",
|
||||||
|
"OR",
|
||||||
|
"ORDER",
|
||||||
|
"OUTER",
|
||||||
|
"OVER",
|
||||||
|
"PERCENT",
|
||||||
|
"PIVOT",
|
||||||
|
"PLAN",
|
||||||
|
"PRECISION",
|
||||||
|
"PRIMARY",
|
||||||
|
"PRINT",
|
||||||
|
"PROC",
|
||||||
|
"PROCEDURE",
|
||||||
|
"PUBLIC",
|
||||||
|
"RAISERROR",
|
||||||
|
"READ",
|
||||||
|
"READTEXT",
|
||||||
|
"RECONFIGURE",
|
||||||
|
"REFERENCES",
|
||||||
|
"REPLICATION",
|
||||||
|
"RESTORE",
|
||||||
|
"RESTRICT",
|
||||||
|
"RETURN",
|
||||||
|
"REVERT",
|
||||||
|
"REVOKE",
|
||||||
|
"RIGHT",
|
||||||
|
"ROLLBACK",
|
||||||
|
"ROWCOUNT",
|
||||||
|
"ROWGUIDCOL",
|
||||||
|
"RULE",
|
||||||
|
"SAVE",
|
||||||
|
"SCHEMA",
|
||||||
|
"SECURITYAUDIT",
|
||||||
|
"SELECT",
|
||||||
|
"SEMANTICKEYPHRASETABLE",
|
||||||
|
"SEMANTICSIMILARITYDETAILSTABLE",
|
||||||
|
"SEMANTICSIMILARITYTABLE",
|
||||||
|
"SESSION_USER",
|
||||||
|
"SET",
|
||||||
|
"SETUSER",
|
||||||
|
"SHUTDOWN",
|
||||||
|
"SOME",
|
||||||
|
"STATISTICS",
|
||||||
|
"SYSTEM_USER",
|
||||||
|
"TABLE",
|
||||||
|
"TABLESAMPLE",
|
||||||
|
"TEXTSIZE",
|
||||||
|
"THEN",
|
||||||
|
"TO",
|
||||||
|
"TOP",
|
||||||
|
"TRAN",
|
||||||
|
"TRANSACTION",
|
||||||
|
"TRIGGER",
|
||||||
|
"TRUNCATE",
|
||||||
|
"TRY_CONVERT",
|
||||||
|
"TSEQUAL",
|
||||||
|
"UNION",
|
||||||
|
"UNIQUE",
|
||||||
|
"UNPIVOT",
|
||||||
|
"UPDATE",
|
||||||
|
"UPDATETEXT",
|
||||||
|
"USE",
|
||||||
|
"USER",
|
||||||
|
"VALUES",
|
||||||
|
"VARYING",
|
||||||
|
"VIEW",
|
||||||
|
"WAITFOR",
|
||||||
|
"WHEN",
|
||||||
|
"WHERE",
|
||||||
|
"WHILE",
|
||||||
|
"WITH",
|
||||||
|
"WITHIN GROUP",
|
||||||
|
"WRITETEXT"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly string[] _TSqlDataTypes =
|
||||||
|
{
|
||||||
|
"BIGINT",
|
||||||
|
"BIT",
|
||||||
|
"DECIMAL",
|
||||||
|
"INT",
|
||||||
|
"MONEY",
|
||||||
|
"NUMERIC",
|
||||||
|
"SMALLINT",
|
||||||
|
"SMALLMONEY",
|
||||||
|
"TINYINT",
|
||||||
|
"FLOAT",
|
||||||
|
"REAL",
|
||||||
|
"DATE",
|
||||||
|
"DATETIME2",
|
||||||
|
"DATETIME",
|
||||||
|
"DATETIMEOFFSET",
|
||||||
|
"SMALLDATETIME",
|
||||||
|
"TIME",
|
||||||
|
"CHAR",
|
||||||
|
"TEXT",
|
||||||
|
"VARCHAR",
|
||||||
|
"NCHAR",
|
||||||
|
"NTEXT",
|
||||||
|
"NVARCHAR",
|
||||||
|
"BINARY",
|
||||||
|
"IMAGE",
|
||||||
|
"VARBINARY",
|
||||||
|
"CURSOR",
|
||||||
|
"ROWVERSION",
|
||||||
|
"UNIQUEIDENTIFIER",
|
||||||
|
"XML"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly HashSet<string> TSqlKeywords = new HashSet<string>(_TSqlKeywords);
|
||||||
|
|
||||||
|
public static readonly HashSet<string> TSqlDataTypes = new HashSet<string>(_TSqlDataTypes);
|
||||||
|
|
||||||
|
public static int TabWidth => 4;
|
||||||
|
|
||||||
|
public static int DefaultCompatabilityLevel => 120;
|
||||||
|
|
||||||
|
public static int MaxLineWidthForRegexEval => 300;
|
||||||
|
|
||||||
|
public static HashSet<string> SystemFunctions = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
// Метаданные
|
||||||
|
"APP_NAME", "HOST_NAME", "HOST_ID", "CONNECTIONPROPERTY", "SESSION_CONTEXT",
|
||||||
|
"CURRENT_USER", "SYSTEM_USER", "SUSER_NAME", "SUSER_SID", "USER_NAME", "USER_ID",
|
||||||
|
|
||||||
|
// Ошибки
|
||||||
|
"@@ERROR", "ERROR_MESSAGE", "ERROR_LINE", "ERROR_NUMBER", "ERROR_SEVERITY", "ERROR_STATE", "FORMATMESSAGE",
|
||||||
|
|
||||||
|
// Идентификаторы
|
||||||
|
"@@IDENTITY", "SCOPE_IDENTITY", "IDENT_CURRENT", "@@ROWCOUNT", "ROWCOUNT_BIG",
|
||||||
|
"@@TRANCOUNT", "XACT_STATE", "CURRENT_TRANSACTION_ID",
|
||||||
|
|
||||||
|
// Дата/время
|
||||||
|
"GETDATE", "SYSDATETIME", "SYSUTCDATETIME", "SYSDATETIMEOFFSET", "CURRENT_TIMESTAMP", "GETUTCDATE",
|
||||||
|
"DATEADD", "DATEDIFF", "DATENAME", "DATEPART", "EOMONTH",
|
||||||
|
|
||||||
|
// Строковые/бинарные
|
||||||
|
"ISNULL", "NULLIF", "COALESCE", "DATALENGTH", "COMPRESS", "DECOMPRESS",
|
||||||
|
"BINARY_CHECKSUM", "CHECKSUM", "PARSENAME",
|
||||||
|
|
||||||
|
// Уникальные идентификаторы
|
||||||
|
"NEWID", "NEWSEQUENTIALID",
|
||||||
|
|
||||||
|
// Системные переменные
|
||||||
|
"@@VERSION", "@@SERVERNAME", "@@SPID", "@@LANGUAGE", "@@MAX_CONNECTIONS",
|
||||||
|
|
||||||
|
// Database Functions
|
||||||
|
"DB_ID", "DB_NAME", "OBJECT_ID", "OBJECT_NAME", "OBJECT_SCHEMA_NAME",
|
||||||
|
"COL_LENGTH", "COL_NAME", "FILE_ID", "FILE_NAME", "SCHEMA_ID", "SCHEMA_NAME", "TYPE_ID", "TYPE_NAME",
|
||||||
|
|
||||||
|
// Математические
|
||||||
|
"ABS", "ACOS", "ASIN", "ATAN", "ATN2", "CEILING", "COS", "COT", "DEGREES",
|
||||||
|
"EXP", "FLOOR", "LOG", "LOG10", "PI", "POWER", "RADIANS", "RAND", "ROUND", "SIGN", "SIN", "SQRT", "SQUARE", "TAN",
|
||||||
|
|
||||||
|
// Агрегатные
|
||||||
|
"AVG", "COUNT", "COUNT_BIG", "MIN", "MAX", "SUM",
|
||||||
|
|
||||||
|
// Курсоры
|
||||||
|
"CURSOR_STATUS",
|
||||||
|
|
||||||
|
// Конфигурационные и серверные
|
||||||
|
"@@OPTIONS", "SESSIONPROPERTY", "INDEXPROPERTY", "INDEX_COL", "COLLATIONPROPERTY",
|
||||||
|
"SERVERPROPERTY", "DATABASEPROPERTYEX", "OBJECTPROPERTY", "OBJECTPROPERTYEX",
|
||||||
|
|
||||||
|
// Безопасность
|
||||||
|
"HAS_DBACCESS", "IS_MEMBER", "IS_ROLEMEMBER", "IS_SRVROLEMEMBER", "PERMISSIONS", "PWDCOMPARE", "PWDENCRYPT",
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
"JSON_VALUE", "JSON_QUERY", "JSON_MODIFY",
|
||||||
|
|
||||||
|
// Аналитические (оконные)
|
||||||
|
"CUME_DIST", "RANK", "DENSE_RANK", "NTILE", "ROW_NUMBER",
|
||||||
|
"LEAD", "LAG", "FIRST_VALUE", "LAST_VALUE",
|
||||||
|
"PERCENT_RANK", "PERCENTILE_CONT", "PERCENTILE_DISC",
|
||||||
|
|
||||||
|
// XML
|
||||||
|
"nodes", "value", "query", "exist", "modify",
|
||||||
|
|
||||||
|
// CLR
|
||||||
|
"FORMAT", "TRY_CONVERT", "TRY_CAST", "TRY_PARSE", "PARSE",
|
||||||
|
|
||||||
|
// Spatial (geometry/geography)
|
||||||
|
"STArea", "STLength", "STDistance", "STIntersects", "STBuffer", "STUnion", "STDifference", "STIntersection",
|
||||||
|
"STGeomFromText", "STGeomFromWKB", "STPointFromText", "STPointFromWKB",
|
||||||
|
"STAsText", "STAsBinary", "STEnvelope", "STCentroid", "STIsEmpty", "STIsValid",
|
||||||
|
|
||||||
|
// Statistical
|
||||||
|
"STDEV", "STDEVP", "VAR", "VARP", "CHECKSUM_AGG", "COLUMNS_UPDATED"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
20
SQLLinter/Core/DTO/HandlerResponseMessage.cs
Normal file
20
SQLLinter/Core/DTO/HandlerResponseMessage.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace SQLLinter.Core.DTO;
|
||||||
|
|
||||||
|
public class HandlerResponseMessage
|
||||||
|
{
|
||||||
|
public HandlerResponseMessage(bool success, bool shouldLint)
|
||||||
|
: this(success, shouldLint, false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public HandlerResponseMessage(bool success, bool shouldLint, bool shouldFix)
|
||||||
|
{
|
||||||
|
Success = success;
|
||||||
|
ShouldLint = shouldLint;
|
||||||
|
ShouldFix = shouldFix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Success { get; }
|
||||||
|
public bool ShouldLint { get; }
|
||||||
|
public bool ShouldFix { get; }
|
||||||
|
}
|
||||||
10
SQLLinter/Core/Interfaces/IAssemblyWrapper.cs
Normal file
10
SQLLinter/Core/Interfaces/IAssemblyWrapper.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface IAssemblyWrapper
|
||||||
|
{
|
||||||
|
Assembly LoadFrom(string path);
|
||||||
|
|
||||||
|
Type[] GetExportedTypes(Assembly assembly);
|
||||||
|
}
|
||||||
24
SQLLinter/Core/Interfaces/IConfig.cs
Normal file
24
SQLLinter/Core/Interfaces/IConfig.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
|
||||||
|
namespace SQLLinter.Core.Interfaces
|
||||||
|
{
|
||||||
|
public interface IConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Уровень совместимости SQL Server
|
||||||
|
/// </summary>
|
||||||
|
int CompatibilityLevel { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Включенные правила.
|
||||||
|
/// Key - Название правила.
|
||||||
|
/// Value - Уровень предупреждения.
|
||||||
|
/// </summary>
|
||||||
|
Dictionary<string, RuleViolationSeverity> Rules { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Список сторонних плагинов.
|
||||||
|
/// </summary>
|
||||||
|
List<string> Plugins { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
8
SQLLinter/Core/Interfaces/IExtendedRuleException.cs
Normal file
8
SQLLinter/Core/Interfaces/IExtendedRuleException.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
|
||||||
|
namespace SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface IExtendedRuleException : IRuleException
|
||||||
|
{
|
||||||
|
void SetEndLine(int endLine);
|
||||||
|
}
|
||||||
10
SQLLinter/Core/Interfaces/IFileSystemWrapper.cs
Normal file
10
SQLLinter/Core/Interfaces/IFileSystemWrapper.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface IFileSystemWrapper
|
||||||
|
{
|
||||||
|
bool FileExists(string path);
|
||||||
|
|
||||||
|
bool PathIsValidForLint(string path);
|
||||||
|
|
||||||
|
string CombinePath(params string[] paths);
|
||||||
|
}
|
||||||
8
SQLLinter/Core/Interfaces/IFileversionWrapper.cs
Normal file
8
SQLLinter/Core/Interfaces/IFileversionWrapper.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface IFileversionWrapper
|
||||||
|
{
|
||||||
|
string GetVersion(Assembly assembly);
|
||||||
|
}
|
||||||
6
SQLLinter/Core/Interfaces/IGlobPatternMatcher.cs
Normal file
6
SQLLinter/Core/Interfaces/IGlobPatternMatcher.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface IGlobPatternMatcher
|
||||||
|
{
|
||||||
|
IEnumerable<string> GetResultsInFullPath(string path);
|
||||||
|
}
|
||||||
4
SQLLinter/Core/Interfaces/IOverride.cs
Normal file
4
SQLLinter/Core/Interfaces/IOverride.cs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
namespace SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface IOverride
|
||||||
|
{ }
|
||||||
9
SQLLinter/Core/Interfaces/IPluginHandler.cs
Normal file
9
SQLLinter/Core/Interfaces/IPluginHandler.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
|
||||||
|
namespace SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface IPluginHandler
|
||||||
|
{
|
||||||
|
IList<IRule> Rules { get; }
|
||||||
|
IDictionary<string, IRule> RuleWithNames { get; }
|
||||||
|
}
|
||||||
19
SQLLinter/Core/Interfaces/IRequest.cs
Normal file
19
SQLLinter/Core/Interfaces/IRequest.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace SQLLinter.Core.Interfaces.Config.Contracts;
|
||||||
|
|
||||||
|
public interface IRequestHandler<in TRequest, out TResponse> where TRequest : IRequest<TResponse>
|
||||||
|
{
|
||||||
|
TResponse Handle(TRequest request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IRequestHandler<in TRequest> where TRequest : IRequest
|
||||||
|
{
|
||||||
|
void Handle(TRequest message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IRequest
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IRequest<out TResponse>
|
||||||
|
{
|
||||||
|
}
|
||||||
6
SQLLinter/Core/Interfaces/IRuleExceptionFinder.cs
Normal file
6
SQLLinter/Core/Interfaces/IRuleExceptionFinder.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface IRuleExceptionFinder
|
||||||
|
{
|
||||||
|
IEnumerable<IExtendedRuleException> GetIgnoredRuleList(Stream fileStream);
|
||||||
|
}
|
||||||
8
SQLLinter/Core/Interfaces/IRuleVisitor.cs
Normal file
8
SQLLinter/Core/Interfaces/IRuleVisitor.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
|
||||||
|
namespace SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface IRuleVisitor
|
||||||
|
{
|
||||||
|
void VisitRules(string path, IEnumerable<IRuleException> igoredRules, Stream sqlFileStream);
|
||||||
|
}
|
||||||
11
SQLLinter/Core/Interfaces/ISqlFileProcessor.cs
Normal file
11
SQLLinter/Core/Interfaces/ISqlFileProcessor.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
namespace SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface ISqlFileProcessor
|
||||||
|
{
|
||||||
|
int FileCount { get; }
|
||||||
|
|
||||||
|
void ProcessList(List<string> filePaths);
|
||||||
|
void ProcessList(Dictionary<string, Stream> files);
|
||||||
|
void ProcessPath(string path);
|
||||||
|
}
|
||||||
12
SQLLinter/Infrastructure/Configuration/Config.cs
Normal file
12
SQLLinter/Infrastructure/Configuration/Config.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Configuration
|
||||||
|
{
|
||||||
|
public class Config : IConfig
|
||||||
|
{
|
||||||
|
public int CompatibilityLevel { get; set; }
|
||||||
|
public Dictionary<string, RuleViolationSeverity> Rules { get; set; } = new();
|
||||||
|
public List<string> Plugins { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
SQLLinter/Infrastructure/Configuration/EnvironmentWrapper.cs
Normal file
10
SQLLinter/Infrastructure/Configuration/EnvironmentWrapper.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace SQLLinter.Infrastructure.Configuration
|
||||||
|
{
|
||||||
|
public class EnvironmentWrapper
|
||||||
|
{
|
||||||
|
public string GetEnvironmentVariable(string name)
|
||||||
|
{
|
||||||
|
return Environment.GetEnvironmentVariable(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using SQLLinter.Core;
|
||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Configuration.Overrides
|
||||||
|
{
|
||||||
|
public class OverrideCompatabilityLevel : IOverride
|
||||||
|
{
|
||||||
|
public OverrideCompatabilityLevel(string value)
|
||||||
|
{
|
||||||
|
if (int.TryParse(value, out var parsedCompatabilityLevel))
|
||||||
|
{
|
||||||
|
CompatabilityLevel = parsedCompatabilityLevel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CompatabilityLevel = Constants.DefaultCompatabilityLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompatabilityLevel { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using SQLLinter.Core;
|
||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Configuration.Overrides;
|
||||||
|
|
||||||
|
public class OverrideFinder
|
||||||
|
{
|
||||||
|
private static Regex _OverrideRegex = new Regex(@".*?sqllinter-override ?(.* += +.*)+.*", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public IEnumerable<IOverride> GetOverrideList(Stream fileStream)
|
||||||
|
{
|
||||||
|
var overrideList = new List<IOverride>();
|
||||||
|
TextReader reader = new StreamReader(fileStream);
|
||||||
|
|
||||||
|
string line;
|
||||||
|
while ((line = reader.ReadLine()) != null)
|
||||||
|
{
|
||||||
|
if (line.Length > Constants.MaxLineWidthForRegexEval || !line.Contains("sqllinter-override"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = _OverrideRegex.Match(line);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var overrideDetails = match.Groups[1].Value.Split(',').Select(p => p.Trim()).ToList();
|
||||||
|
foreach (var overrideDetail in overrideDetails)
|
||||||
|
{
|
||||||
|
var details = overrideDetail.Split(' ').Select(p => p.Trim()).ToList();
|
||||||
|
if (OverrideTypeMap.List.ContainsKey(details[0]))
|
||||||
|
{
|
||||||
|
var overrideType = OverrideTypeMap.List.GetValueOrDefault(details[0]);
|
||||||
|
overrideList.Add((IOverride)Activator.CreateInstance(overrideType, details[2]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
return overrideList;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace SQLLinter.Infrastructure.Configuration.Overrides
|
||||||
|
{
|
||||||
|
public class OverrideTypeMap
|
||||||
|
{
|
||||||
|
public static readonly Dictionary<string, Type> List = new Dictionary<string, Type>
|
||||||
|
{
|
||||||
|
{ "compatibility-level", typeof(OverrideCompatabilityLevel) },
|
||||||
|
// Deprecate usage of misspelled "compatability-level".
|
||||||
|
{ "compatability-level", typeof(OverrideCompatabilityLevel) }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
10
SQLLinter/Infrastructure/Interfaces/IFragmentBuilder.cs
Normal file
10
SQLLinter/Infrastructure/Interfaces/IFragmentBuilder.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Interfaces
|
||||||
|
{
|
||||||
|
public interface IFragmentBuilder
|
||||||
|
{
|
||||||
|
TSqlFragment? GetFragment(string sqlPath, TextReader txtRdr, out IList<ParseError> errors, IEnumerable<IOverride> overrides = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace SQLLinter.Infrastructure.Interfaces
|
||||||
|
{
|
||||||
|
public interface ISqlStreamReaderBuilder
|
||||||
|
{
|
||||||
|
StreamReader CreateReader(Stream sqlFileStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
SQLLinter/Infrastructure/Interfaces/IViolationFixer.cs
Normal file
7
SQLLinter/Infrastructure/Interfaces/IViolationFixer.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace SQLLinter.Infrastructure.Interfaces
|
||||||
|
{
|
||||||
|
public interface IViolationFixer
|
||||||
|
{
|
||||||
|
void Fix();
|
||||||
|
}
|
||||||
|
}
|
||||||
152
SQLLinter/Infrastructure/Parser/DynamicSQLParser.cs
Normal file
152
SQLLinter/Infrastructure/Parser/DynamicSQLParser.cs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Parser
|
||||||
|
{
|
||||||
|
public class DynamicSQLParser : TSqlFragmentVisitor
|
||||||
|
{
|
||||||
|
private readonly Action<string, int, int> callback;
|
||||||
|
private string executableSql = string.Empty;
|
||||||
|
private Dictionary<string, VariableVisitor.VariableRef> VariableValues = new();
|
||||||
|
|
||||||
|
private int DynamicSQLStartingLine { get; set; }
|
||||||
|
|
||||||
|
private int DynamicSQLStartingColumn { get; set; }
|
||||||
|
|
||||||
|
public DynamicSQLParser(Action<string, int, int> callback)
|
||||||
|
{
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(TSqlBatch node)
|
||||||
|
{
|
||||||
|
var variableVisitor = new VariableVisitor();
|
||||||
|
node.Accept(variableVisitor);
|
||||||
|
VariableValues = variableVisitor.VariableValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(ExecuteStatement node)
|
||||||
|
{
|
||||||
|
DynamicSQLStartingColumn = node.ExecuteSpecification.ExecutableEntity.StartColumn;
|
||||||
|
DynamicSQLStartingLine = node.ExecuteSpecification.ExecutableEntity.StartLine;
|
||||||
|
|
||||||
|
var visitor = new VariableVisitor();
|
||||||
|
node.Accept(visitor);
|
||||||
|
|
||||||
|
var executableStrings = node.ExecuteSpecification.ExecutableEntity as ExecutableStringList;
|
||||||
|
if (executableStrings?.Strings == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var counter = 0;
|
||||||
|
foreach (var executableString in executableStrings.Strings)
|
||||||
|
{
|
||||||
|
counter++;
|
||||||
|
if (executableString is StringLiteral literal)
|
||||||
|
{
|
||||||
|
HandleLiteral(counter, executableStrings.Strings.Count, literal);
|
||||||
|
}
|
||||||
|
else if (executableString is VariableReference variableReference)
|
||||||
|
{
|
||||||
|
HandleVariable(counter, executableStrings.Strings.Count, variableReference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleVariable(int counter, int executableCount, VariableReference variableReference)
|
||||||
|
{
|
||||||
|
if (!VariableValues.ContainsKey(variableReference.Name) || !VariableValues.TryGetValue(variableReference.Name, out var value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
executableSql += value.Value;
|
||||||
|
|
||||||
|
if (counter == executableCount)
|
||||||
|
{
|
||||||
|
callback(executableSql, value.StartLine, value.StartColumn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleLiteral(int counter, int executableCount, StringLiteral literal)
|
||||||
|
{
|
||||||
|
executableSql += literal.Value;
|
||||||
|
if (counter == executableCount)
|
||||||
|
{
|
||||||
|
callback(executableSql, DynamicSQLStartingLine, DynamicSQLStartingColumn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VariableVisitor : TSqlFragmentVisitor
|
||||||
|
{
|
||||||
|
public Dictionary<string, VariableRef> VariableValues { get; } = new();
|
||||||
|
|
||||||
|
public override void Visit(SelectSetVariable node)
|
||||||
|
{
|
||||||
|
HandleExpression(node.Variable.Name, node.Expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(SetVariableStatement node)
|
||||||
|
{
|
||||||
|
HandleExpression(node.Variable.Name, node.Expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleExpression(string name, ScalarExpression expression)
|
||||||
|
{
|
||||||
|
switch (expression)
|
||||||
|
{
|
||||||
|
case StringLiteral strLiteral:
|
||||||
|
VariableValues[name] = new VariableRef(strLiteral);
|
||||||
|
break;
|
||||||
|
case IntegerLiteral intLiteral:
|
||||||
|
VariableValues[name] = new VariableRef(intLiteral);
|
||||||
|
break;
|
||||||
|
case BinaryExpression binaryExpression:
|
||||||
|
HandleBinaryExpression(name, binaryExpression);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleBinaryExpression(string name, BinaryExpression expression)
|
||||||
|
{
|
||||||
|
if (expression.BinaryExpressionType != BinaryExpressionType.Add)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expression.FirstExpression is StringLiteral first
|
||||||
|
&& expression.SecondExpression is StringLiteral second)
|
||||||
|
{
|
||||||
|
VariableValues[name] = new VariableRef(first)
|
||||||
|
{
|
||||||
|
Value = first.Value + second.Value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct VariableRef
|
||||||
|
{
|
||||||
|
public VariableRef(StringLiteral stringLiteral)
|
||||||
|
: this((Literal)stringLiteral)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public VariableRef(IntegerLiteral integerLiteral)
|
||||||
|
: this((Literal)integerLiteral)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private VariableRef(Literal literal)
|
||||||
|
{
|
||||||
|
StartColumn = literal.StartColumn;
|
||||||
|
StartLine = literal.StartLine;
|
||||||
|
Value = literal.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int StartColumn { get; set; }
|
||||||
|
public int StartLine { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
SQLLinter/Infrastructure/Parser/FileSystemWrapper.cs
Normal file
39
SQLLinter/Infrastructure/Parser/FileSystemWrapper.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Parser
|
||||||
|
{
|
||||||
|
public class FileSystemWrapper : IFileSystemWrapper
|
||||||
|
{
|
||||||
|
|
||||||
|
public bool FileExists(string path)
|
||||||
|
{
|
||||||
|
path = RemoveQuotes(path);
|
||||||
|
return File.Exists(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PathIsValidForLint(string path)
|
||||||
|
{
|
||||||
|
path = RemoveQuotes(path);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return Directory.Exists(path) || PathContainsWildCard(path);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool PathContainsWildCard(string filePath)
|
||||||
|
{
|
||||||
|
return filePath.Contains("*") || filePath.Contains("?");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string RemoveQuotes(string path)
|
||||||
|
{
|
||||||
|
return path.Replace("\"", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CombinePath(params string[] paths)
|
||||||
|
{
|
||||||
|
return Path.Combine(paths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
SQLLinter/Infrastructure/Parser/FragmentBuilder.cs
Normal file
84
SQLLinter/Infrastructure/Parser/FragmentBuilder.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||||
|
using SQLLinter.Common;
|
||||||
|
using SQLLinter.Core;
|
||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
using SQLLinter.Infrastructure.Configuration.Overrides;
|
||||||
|
using SQLLinter.Infrastructure.Interfaces;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Parser;
|
||||||
|
|
||||||
|
public class FragmentBuilder : IFragmentBuilder
|
||||||
|
{
|
||||||
|
private readonly TSqlParser parser;
|
||||||
|
private readonly IReporter _reporter;
|
||||||
|
|
||||||
|
public FragmentBuilder(IReporter reporter) : this(reporter, Constants.DefaultCompatabilityLevel)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public FragmentBuilder(IReporter reporter, int compatabilityLevel)
|
||||||
|
{
|
||||||
|
parser = GetSqlParser(compatabilityLevel);
|
||||||
|
_reporter = reporter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TSqlFragment? GetFragment(string path, TextReader txtRdr, out IList<ParseError> errors, IEnumerable<IOverride> overrides = null)
|
||||||
|
{
|
||||||
|
TSqlFragment fragment;
|
||||||
|
|
||||||
|
OverrideCompatabilityLevel? compatibilityLevel = null;
|
||||||
|
if (overrides != null)
|
||||||
|
{
|
||||||
|
foreach (var lintingOverride in overrides)
|
||||||
|
{
|
||||||
|
if (lintingOverride is OverrideCompatabilityLevel overrideCompatability)
|
||||||
|
{
|
||||||
|
compatibilityLevel = overrideCompatability;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TSqlParser curParser;
|
||||||
|
|
||||||
|
if (compatibilityLevel != null)
|
||||||
|
{
|
||||||
|
curParser = GetSqlParser(compatibilityLevel.CompatabilityLevel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
curParser = parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment = curParser.Parse(txtRdr, out errors); //TODO: Возвращать эти ошибки
|
||||||
|
|
||||||
|
if (fragment == null)
|
||||||
|
{
|
||||||
|
foreach (var err in errors)
|
||||||
|
{
|
||||||
|
_reporter.ReportViolation(path, err.Line, err.Column, RuleViolationSeverity.Critical, "parse", err.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fragment?.FirstTokenIndex != -1 ? fragment : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TSqlParser GetSqlParser(int compatabilityLevel)
|
||||||
|
{
|
||||||
|
TSqlParser parser = compatabilityLevel switch
|
||||||
|
{
|
||||||
|
80 => new TSql80Parser(true),
|
||||||
|
90 => new TSql90Parser(true),
|
||||||
|
100 => new TSql100Parser(true),
|
||||||
|
110 => new TSql110Parser(true),
|
||||||
|
120 => new TSql120Parser(true),
|
||||||
|
130 => new TSql130Parser(true),
|
||||||
|
140 => new TSql140Parser(true),
|
||||||
|
150 => new TSql150Parser(true),
|
||||||
|
160 => new TSql160Parser(true),
|
||||||
|
170 => new TSql170Parser(true),
|
||||||
|
_ => new TSql120Parser(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
return parser;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
SQLLinter/Infrastructure/Parser/ParsingUtility.cs
Normal file
23
SQLLinter/Infrastructure/Parser/ParsingUtility.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Parser
|
||||||
|
{
|
||||||
|
public static class ParsingUtility
|
||||||
|
{
|
||||||
|
public static TextReader CreateTextReaderFromString(string str)
|
||||||
|
{
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(str);
|
||||||
|
return new StreamReader(new MemoryStream(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stream GenerateStreamFromString(string s)
|
||||||
|
{
|
||||||
|
var stream = new MemoryStream();
|
||||||
|
var writer = new StreamWriter(stream);
|
||||||
|
writer.Write(s);
|
||||||
|
writer.Flush();
|
||||||
|
stream.Position = 0;
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Parser
|
||||||
|
{
|
||||||
|
public static class RuleVisitorFriendlyNameTypeMap
|
||||||
|
{
|
||||||
|
public static List<Type> DefaultRuleTypes
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
|
|
||||||
|
return GetRuleTypes(assembly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Type> GetRuleTypes(Assembly assembly)
|
||||||
|
{
|
||||||
|
List<Type> sqlRuleTypes = assembly
|
||||||
|
.GetTypes()
|
||||||
|
.Where(t => typeof(IRule).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract) // исключаем абстрактные
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return sqlRuleTypes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
SQLLinter/Infrastructure/Parser/SqlFileProcessor.cs
Normal file
109
SQLLinter/Infrastructure/Parser/SqlFileProcessor.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
using SQLLinter.Infrastructure.Rules.RuleExceptions;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Parser;
|
||||||
|
|
||||||
|
public class SqlFileProcessor : ISqlFileProcessor
|
||||||
|
{
|
||||||
|
private readonly IRuleVisitor ruleVisitor;
|
||||||
|
|
||||||
|
private readonly IReporter reporter;
|
||||||
|
|
||||||
|
private readonly IPluginHandler pluginHandler;
|
||||||
|
|
||||||
|
private readonly IRuleExceptionFinder ruleExceptionFinder;
|
||||||
|
|
||||||
|
public SqlFileProcessor(
|
||||||
|
IRuleVisitor ruleVisitor,
|
||||||
|
IPluginHandler pluginHandler,
|
||||||
|
IReporter reporter)
|
||||||
|
{
|
||||||
|
this.ruleVisitor = ruleVisitor;
|
||||||
|
this.pluginHandler = pluginHandler;
|
||||||
|
this.reporter = reporter;
|
||||||
|
ruleExceptionFinder = new RuleExceptionFinder(pluginHandler.RuleWithNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _fileCount;
|
||||||
|
public int FileCount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _fileCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessList(List<string> filePaths)
|
||||||
|
{
|
||||||
|
foreach (var path in filePaths)
|
||||||
|
{
|
||||||
|
ProcessFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessPath(string path)
|
||||||
|
{
|
||||||
|
ProcessFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessFile(string filePath)
|
||||||
|
{
|
||||||
|
var fileStream = GetFileContents(filePath);
|
||||||
|
HandleProcessing(filePath, fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessList(Dictionary<string, Stream> files)
|
||||||
|
{
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
HandleProcessing(file.Key, file.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsWholeFileIgnored(string filePath, IEnumerable<IExtendedRuleException> ignoredRules)
|
||||||
|
{
|
||||||
|
var ignoredRulesEnum = ignoredRules.ToArray();
|
||||||
|
if (!ignoredRulesEnum.Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineOneRuleIgnores = ignoredRulesEnum.OfType<GlobalRuleException>().Where(x => 1 == x.StartLine).ToArray();
|
||||||
|
if (!lineOneRuleIgnores.Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineCount = 0;
|
||||||
|
using (var reader = new StreamReader(GetFileContents(filePath)))
|
||||||
|
{
|
||||||
|
while (reader.ReadLine() != null)
|
||||||
|
{
|
||||||
|
lineCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lineOneRuleIgnores.Any(x => x.EndLine == lineCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleProcessing(string filePath, Stream fileStream)
|
||||||
|
{
|
||||||
|
var ignoredRules = ruleExceptionFinder.GetIgnoredRuleList(fileStream).ToList();
|
||||||
|
if (IsWholeFileIgnored(filePath, ignoredRules))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProcessRules(fileStream, ignoredRules, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessRules(Stream fileStream, IEnumerable<IRuleException> ignoredRules, string filePath)
|
||||||
|
{
|
||||||
|
ruleVisitor.VisitRules(filePath, ignoredRules, fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream GetFileContents(string filePath)
|
||||||
|
{
|
||||||
|
return File.OpenRead(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
SQLLinter/Infrastructure/Parser/SqlRuleVisitor.cs
Normal file
123
SQLLinter/Infrastructure/Parser/SqlRuleVisitor.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||||
|
using SQLLinter.Common;
|
||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
using SQLLinter.Infrastructure.Configuration.Overrides;
|
||||||
|
using SQLLinter.Infrastructure.Interfaces;
|
||||||
|
using SQLLinter.Infrastructure.Rules.RuleExceptions;
|
||||||
|
using SQLLinter.Infrastructure.Rules.RuleViolations;
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Parser;
|
||||||
|
|
||||||
|
public class SqlRuleVisitor : IRuleVisitor
|
||||||
|
{
|
||||||
|
private readonly IFragmentBuilder _fragmentBuilder;
|
||||||
|
private readonly IReporter _reporter;
|
||||||
|
private readonly ISqlStreamReaderBuilder _sqlStreamReaderBuilder;
|
||||||
|
private readonly IPluginHandler _pluginHandler;
|
||||||
|
|
||||||
|
private readonly OverrideFinder _overrideFinder = new OverrideFinder();
|
||||||
|
|
||||||
|
public SqlRuleVisitor(IPluginHandler pluginHandler, IFragmentBuilder fragmentBuilder, IReporter reporter)
|
||||||
|
: this(pluginHandler, fragmentBuilder, reporter, new SqlStreamReaderBuilder()) { }
|
||||||
|
|
||||||
|
public SqlRuleVisitor(IPluginHandler pluginHandler, IFragmentBuilder fragmentBuilder, IReporter reporter, ISqlStreamReaderBuilder sqlStreamReaderBuilder)
|
||||||
|
{
|
||||||
|
this._fragmentBuilder = fragmentBuilder;
|
||||||
|
this._reporter = reporter;
|
||||||
|
this._pluginHandler = pluginHandler;
|
||||||
|
this._sqlStreamReaderBuilder = sqlStreamReaderBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void VisitRules(string sqlPath, IEnumerable<IRuleException> ignoredRules, Stream sqlFileStream)
|
||||||
|
{
|
||||||
|
var overrides = _overrideFinder.GetOverrideList(sqlFileStream);
|
||||||
|
var overrideArray = overrides as IOverride[] ?? overrides.ToArray();
|
||||||
|
|
||||||
|
var sqlFragment = _fragmentBuilder.GetFragment(sqlPath, GetSqlTextReader(sqlFileStream), out var errors, overrideArray);
|
||||||
|
|
||||||
|
if (sqlFragment == null) return;
|
||||||
|
|
||||||
|
var ruleExceptions = ignoredRules as IRuleException[] ?? ignoredRules.ToArray();
|
||||||
|
if (errors.Any())
|
||||||
|
{
|
||||||
|
HandleParserErrors(sqlPath, errors, ruleExceptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules = _pluginHandler.Rules;
|
||||||
|
foreach (var rule in rules)
|
||||||
|
{
|
||||||
|
VisitFragment(sqlFragment, rule, overrideArray, sqlPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VisitFragment(TSqlFragment sqlFragment, IRule rule, IEnumerable<IOverride> overrides, string filePath)
|
||||||
|
{
|
||||||
|
var violations = rule.Analyze(sqlFragment).ToList();
|
||||||
|
|
||||||
|
|
||||||
|
if (!VisitorIsBlackListedForDynamicSql(rule))
|
||||||
|
{
|
||||||
|
var dynamicSqlVisitor = new DynamicSQLParser(DynamicSqlCallback);
|
||||||
|
sqlFragment?.Accept(dynamicSqlVisitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DynamicSqlCallback(string dynamicSQL, int DynamicSqlStartLine, int DynamicSqlStartColumn)
|
||||||
|
{
|
||||||
|
rule.DynamicSqlStartLine = DynamicSqlStartLine;
|
||||||
|
rule.DynamicSqlStartColumn = DynamicSqlStartColumn;
|
||||||
|
|
||||||
|
var dynamicSqlStream = ParsingUtility.GenerateStreamFromString(dynamicSQL);
|
||||||
|
var dynamicFragment = _fragmentBuilder.GetFragment(filePath, GetSqlTextReader(dynamicSqlStream), out var errors, overrides);
|
||||||
|
|
||||||
|
if (dynamicFragment != null)
|
||||||
|
{
|
||||||
|
violations.AddRange(rule.Analyze(dynamicFragment));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
violations.ForEach(t => _reporter.ReportViolation(filePath, t.Line, t.Column, rule.Severity, t.RuleName, t.Message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool VisitorIsBlackListedForDynamicSql(IRule visitor)
|
||||||
|
{
|
||||||
|
return new List<string>
|
||||||
|
{
|
||||||
|
"SetAnsiNullsRule",
|
||||||
|
"SetNoCountRule",
|
||||||
|
"SetQuotedIdentifierRule",
|
||||||
|
"SetTransactionIsolationLevelRule",
|
||||||
|
"UnicodeStringRule"
|
||||||
|
}.Any(x => visitor.GetType().ToString().Contains(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
private StreamReader GetSqlTextReader(Stream sqlFileStream)
|
||||||
|
{
|
||||||
|
return _sqlStreamReaderBuilder.CreateReader(sqlFileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleParserErrors(string sqlPath, IEnumerable<ParseError> errors, IEnumerable<IRuleException> ignoredRules)
|
||||||
|
{
|
||||||
|
var updatedExitCode = false;
|
||||||
|
var ruleExceptions = ignoredRules as IRuleException[] ?? ignoredRules.ToArray();
|
||||||
|
|
||||||
|
foreach (var error in errors)
|
||||||
|
{
|
||||||
|
var globalRulesOnLine = ruleExceptions.OfType<GlobalRuleException>().Where(
|
||||||
|
x => error.Line >= x.StartLine
|
||||||
|
&& error.Line <= x.EndLine);
|
||||||
|
|
||||||
|
if (!globalRulesOnLine.Any())
|
||||||
|
{
|
||||||
|
_reporter.ReportViolation(new RuleViolation(sqlPath, "invalid-syntax", error.Message, error.Line, error.Column, RuleViolationSeverity.Critical));
|
||||||
|
if (updatedExitCode)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedExitCode = true;
|
||||||
|
Environment.ExitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
SQLLinter/Infrastructure/Parser/SqlStreamReaderBuilder.cs
Normal file
44
SQLLinter/Infrastructure/Parser/SqlStreamReaderBuilder.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using SQLLinter.Infrastructure.Interfaces;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Parser;
|
||||||
|
|
||||||
|
public class SqlStreamReaderBuilder : ISqlStreamReaderBuilder
|
||||||
|
{
|
||||||
|
private static readonly Regex _placeholderRegex = new Regex(@"\$\((?<placeholder>[^)]+)\)", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public StreamReader CreateReader(Stream sqlFileStream)
|
||||||
|
{
|
||||||
|
var sqlText = new StreamReader(sqlFileStream);
|
||||||
|
sqlFileStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
var sql = ReplaceSqlPlaceholders(sqlText.ReadToEnd());
|
||||||
|
return new StreamReader(new MemoryStream(sqlText.CurrentEncoding.GetBytes(sql)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReplaceSqlPlaceholders(string sql)
|
||||||
|
{
|
||||||
|
var matches = _placeholderRegex.Matches(sql);
|
||||||
|
|
||||||
|
if (matches.Count == 0)
|
||||||
|
{
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newSql = new StringBuilder();
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
foreach (Match match in matches)
|
||||||
|
{
|
||||||
|
var placeholder = match.Groups["placeholder"].Value;
|
||||||
|
var replacement = match.Value;
|
||||||
|
newSql.Append(sql.Substring(i, match.Index - i));
|
||||||
|
newSql.Append(replacement);
|
||||||
|
i = match.Index + match.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
newSql.Append(sql.Substring(i));
|
||||||
|
|
||||||
|
return newSql.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
SQLLinter/Infrastructure/Parser/ViolationFixer.cs
Normal file
50
SQLLinter/Infrastructure/Parser/ViolationFixer.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
using SQLLinter.Infrastructure.Interfaces;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Parser;
|
||||||
|
|
||||||
|
public class ViolationFixer : IViolationFixer
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, IRule> Rules;
|
||||||
|
private readonly IList<IRuleViolation> Violations;
|
||||||
|
|
||||||
|
public ViolationFixer(
|
||||||
|
Dictionary<string, IRule> rules,
|
||||||
|
IList<IRuleViolation> violations)
|
||||||
|
{
|
||||||
|
Rules = rules;
|
||||||
|
Violations = violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fix()
|
||||||
|
{
|
||||||
|
var files = Violations.GroupBy(x => x.FileName);
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
var fileViolations = file
|
||||||
|
.OrderByDescending(x => x.Line)
|
||||||
|
.ThenByDescending(x => x.Column)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var fileLines = File.ReadAllLines(file.Key).ToList();
|
||||||
|
var fileLineActions = new Common.FileLineActions(fileViolations, fileLines);
|
||||||
|
|
||||||
|
foreach (var violation in fileViolations)
|
||||||
|
{
|
||||||
|
if (Rules.ContainsKey(violation.RuleName))
|
||||||
|
{
|
||||||
|
if (violation.Line == 1 && violation.Column > fileLines[violation.Line - 1].Length + 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = new List<string>(fileLines);
|
||||||
|
//Rules[violation.RuleName].FixViolation(lines, violation, fileLineActions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllLines(file.Key, fileLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
SQLLinter/Infrastructure/Plugins/AssemblyWrapper.cs
Normal file
18
SQLLinter/Infrastructure/Plugins/AssemblyWrapper.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Plugins
|
||||||
|
{
|
||||||
|
public class AssemblyWrapper : IAssemblyWrapper
|
||||||
|
{
|
||||||
|
public Assembly LoadFrom(string path)
|
||||||
|
{
|
||||||
|
return Assembly.LoadFrom(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type[] GetExportedTypes(Assembly assembly)
|
||||||
|
{
|
||||||
|
return assembly.GetExportedTypes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
SQLLinter/Infrastructure/Plugins/PluginHandler.cs
Normal file
130
SQLLinter/Infrastructure/Plugins/PluginHandler.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
using SQLLinter.Common.Helpers;
|
||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
using SQLLinter.Infrastructure.Parser;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Plugins
|
||||||
|
{
|
||||||
|
public class PluginHandler : IPluginHandler
|
||||||
|
{
|
||||||
|
private readonly IConfig _config;
|
||||||
|
private readonly IAssemblyWrapper _assemblyWrapper;
|
||||||
|
private readonly IReporter _reporter;
|
||||||
|
private readonly IFileversionWrapper _versionWrapper;
|
||||||
|
|
||||||
|
private List<Type> _ruleTypes = new();
|
||||||
|
private Dictionary<string, IRule> _rules = new();
|
||||||
|
|
||||||
|
public IList<IRule> Rules => _rules.Values.ToList();
|
||||||
|
|
||||||
|
public IDictionary<string, IRule> RuleWithNames => _rules;
|
||||||
|
|
||||||
|
public PluginHandler(IReporter reporter, IConfig config)
|
||||||
|
: this(reporter, new AssemblyWrapper(), new VersionInfoWrapper(), config) { }
|
||||||
|
|
||||||
|
public PluginHandler(
|
||||||
|
IReporter reporter,
|
||||||
|
IAssemblyWrapper assemblyWrapper,
|
||||||
|
IFileversionWrapper versionWrapper,
|
||||||
|
IConfig config
|
||||||
|
)
|
||||||
|
{
|
||||||
|
this._reporter = reporter;
|
||||||
|
this._assemblyWrapper = assemblyWrapper;
|
||||||
|
this._versionWrapper = versionWrapper;
|
||||||
|
this._config = config;
|
||||||
|
|
||||||
|
LoadDefaultRules();
|
||||||
|
LoadPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ActivatePlugin(Type type)
|
||||||
|
{
|
||||||
|
var instance = (IRule?)Activator.CreateInstance(type);
|
||||||
|
|
||||||
|
if (instance == null)
|
||||||
|
{
|
||||||
|
_reporter.Report($"Ошибка создания экземпляра правила '{type.FullName}'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ruleName = instance.Name;
|
||||||
|
|
||||||
|
if (_config.Rules.TryGetValue(ruleName, out var severity))
|
||||||
|
{
|
||||||
|
instance.Severity = severity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance.Severity == RuleViolationSeverity.Off)
|
||||||
|
{
|
||||||
|
_reporter.Report($"Правило '{ruleName}' выключено");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_rules.ContainsKey(ruleName))
|
||||||
|
{
|
||||||
|
_reporter.Report($"Ошибка добавления правила '{type.FullName}': правило '{ruleName}' уже создано");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rules.Add(ruleName, instance);
|
||||||
|
|
||||||
|
_reporter.Report($"Правило '{ruleName}' ({instance.Severity}) добавлено с типом '{type.FullName}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadDefaultRules()
|
||||||
|
{
|
||||||
|
//Загрузка дефолтных правил
|
||||||
|
var defaultRuleTypes = RuleVisitorFriendlyNameTypeMap.DefaultRuleTypes;
|
||||||
|
defaultRuleTypes.ForEach(AddRuleType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddRuleType(Type ruleType)
|
||||||
|
{
|
||||||
|
if (_ruleTypes.Contains(ruleType))
|
||||||
|
{
|
||||||
|
_reporter.Report($"Уже загружено правило '{ruleType.FullName}'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ruleTypes.Add(ruleType);
|
||||||
|
_reporter.Report($"Добавлено правило '{ruleType.FullName}'");
|
||||||
|
|
||||||
|
ActivatePlugin(ruleType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadPlugins()
|
||||||
|
{
|
||||||
|
var paths = FileHelpers.FindFilesWithMask(_config.Plugins);
|
||||||
|
|
||||||
|
//Загрузка плагинов
|
||||||
|
foreach (var path in paths)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
_reporter.Report($"Ошибка загрузки плагина '{path}': Файл не найден.");
|
||||||
|
}
|
||||||
|
else if (Path.GetExtension(path)?.ToLower() != "dll")
|
||||||
|
{
|
||||||
|
_reporter.Report($"Ошибка загрузки плагина '{path}': Неподдерживаемый формат файла. Ожидается файл с расширением .dll.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LoadPlugin(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadPlugin(string path)
|
||||||
|
{
|
||||||
|
var dll = _assemblyWrapper.LoadFrom(path);
|
||||||
|
var version = _versionWrapper.GetVersion(dll);
|
||||||
|
|
||||||
|
foreach (var type in _assemblyWrapper.GetExportedTypes(dll))
|
||||||
|
{
|
||||||
|
var rules = RuleVisitorFriendlyNameTypeMap.GetRuleTypes(dll);
|
||||||
|
rules.ForEach(AddRuleType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
SQLLinter/Infrastructure/Plugins/VersionInfoWrapper.cs
Normal file
14
SQLLinter/Infrastructure/Plugins/VersionInfoWrapper.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Plugins
|
||||||
|
{
|
||||||
|
public class VersionInfoWrapper : IFileversionWrapper
|
||||||
|
{
|
||||||
|
public string GetVersion(Assembly assembly)
|
||||||
|
{
|
||||||
|
return FileVersionInfo.GetVersionInfo(assembly.Location).FileVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
SQLLinter/Infrastructure/Reporters/FileReporter.cs
Normal file
14
SQLLinter/Infrastructure/Reporters/FileReporter.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace SQLLinter.Infrastructure.Reporters;
|
||||||
|
|
||||||
|
public class FileReporter : Reporter
|
||||||
|
{
|
||||||
|
public virtual void SaveReport(string path)
|
||||||
|
{
|
||||||
|
File.WriteAllText(path, GetContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual string GetContent()
|
||||||
|
{
|
||||||
|
return string.Join(Environment.NewLine, this.Violations);
|
||||||
|
}
|
||||||
|
}
|
||||||
145
SQLLinter/Infrastructure/Reporters/HTMLReporter.cs
Normal file
145
SQLLinter/Infrastructure/Reporters/HTMLReporter.cs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Reporters;
|
||||||
|
|
||||||
|
public class HTMLReporter : FileReporter
|
||||||
|
{
|
||||||
|
public string GetContent()
|
||||||
|
{
|
||||||
|
var violations = Violations;
|
||||||
|
if (violations.Count == 0)
|
||||||
|
{
|
||||||
|
return "<p><em>Нет нарушений</em></p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupedByFile = violations
|
||||||
|
.GroupBy(v => v.FileName)
|
||||||
|
.OrderBy(g => g.Key);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("<!DOCTYPE html>");
|
||||||
|
sb.AppendLine("<html lang=\"ru\">");
|
||||||
|
sb.AppendLine("<head>");
|
||||||
|
sb.AppendLine("<meta charset=\"UTF-8\">");
|
||||||
|
sb.AppendLine("<title>Отчёт по SQL‑проверкам</title>");
|
||||||
|
sb.AppendLine("<style>");
|
||||||
|
sb.AppendLine("body { font-family: 'Segoe UI', Arial, sans-serif; background-color: #f5f5f5; margin: 0; padding: 0; color: #000; }");
|
||||||
|
sb.AppendLine("h1 { padding: 20px; margin: 0; background-color: #0078d4; color: white; }");
|
||||||
|
sb.AppendLine("h2 { margin-top: 20px; color: #333; }");
|
||||||
|
sb.AppendLine("h3 { margin-top: 15px; color: #555; }");
|
||||||
|
sb.AppendLine("table { border-collapse: collapse; width: 100%; margin: 20px 0; box-shadow: 0 2px 6px rgba(0,0,0,0.1); background-color: white; border-radius: 4px; overflow: hidden; }");
|
||||||
|
sb.AppendLine("th, td { border: 1px solid #e0e0e0; padding: 4px 8px; text-align: left; line-height: 1.2; }");
|
||||||
|
sb.AppendLine("th { background-color: #fafafa; font-weight: 600; }");
|
||||||
|
sb.AppendLine("td.line, td.column { width: 60px; text-align: center; }");
|
||||||
|
sb.AppendLine("td.rule { width: 300px; text-align: left; }");
|
||||||
|
sb.AppendLine("td.index { width: 40px; text-align: center; }");
|
||||||
|
sb.AppendLine(".critical { border-left: 4px solid #d13438; padding: 10px; margin-bottom: 20px; background-color: #fde7e9; }");
|
||||||
|
sb.AppendLine(".warning { border-left: 4px solid #ffaa44; padding: 10px; margin-bottom: 20px; background-color: #fff4ce; }");
|
||||||
|
sb.AppendLine(".info { border-left: 4px solid #0078d4; padding: 10px; margin-bottom: 20px; background-color: #deecf9; }");
|
||||||
|
sb.AppendLine(".tabs { position: fixed; bottom: 0; left: 0; right: 0; background-color: #ffffff; border-top: 1px solid #ccc; padding: 10px; display: flex; overflow-x: auto; scrollbar-width: thin; justify-content: flex-start; box-shadow: 0 -2px 6px rgba(0,0,0,0.1); }");
|
||||||
|
sb.AppendLine(".tab { margin-right: 10px; padding: 8px 16px; border-radius: 4px; background-color: #f3f2f1; cursor: pointer; transition: background-color 0.2s; }");
|
||||||
|
sb.AppendLine(".tab:hover { background-color: #e1dfdd; }");
|
||||||
|
sb.AppendLine(".tab.active { background-color: #0078d4; color: white; }");
|
||||||
|
sb.AppendLine(".file-report { display: none; padding: 20px 0; }");
|
||||||
|
sb.AppendLine(".file-report.active { display: block; }");
|
||||||
|
|
||||||
|
// Тёмная тема
|
||||||
|
sb.AppendLine("@media (prefers-color-scheme: dark) {");
|
||||||
|
sb.AppendLine(" body { background-color: #1e1e1e; color: #ddd; }");
|
||||||
|
sb.AppendLine(" h1 { background-color: #005a9e; }");
|
||||||
|
sb.AppendLine(" h2, h3 { color: #ddd; }");
|
||||||
|
sb.AppendLine(" table { background-color: #2d2d2d; box-shadow: none; }");
|
||||||
|
sb.AppendLine(" th { background-color: #3c3c3c; color: #fff; }");
|
||||||
|
sb.AppendLine(" td { border: 1px solid #444; }");
|
||||||
|
sb.AppendLine(" .tabs { background-color: #2d2d2d; border-top: 1px solid #444; }");
|
||||||
|
sb.AppendLine(" .tab { background-color: #3c3c3c; color: #ddd; }");
|
||||||
|
sb.AppendLine(" .tab:hover { background-color: #555; }");
|
||||||
|
sb.AppendLine(" .tab.active { background-color: #0078d4; color: white; }");
|
||||||
|
sb.AppendLine(" .critical { background-color: #4d1f1f; border-left-color: #d13438; }");
|
||||||
|
sb.AppendLine(" .warning { background-color: #4d3b1f; border-left-color: #ffaa44; }");
|
||||||
|
sb.AppendLine(" .info { background-color: #1f3b4d; border-left-color: #0078d4; }");
|
||||||
|
sb.AppendLine("}");
|
||||||
|
sb.AppendLine("</style>");
|
||||||
|
sb.AppendLine("</head>");
|
||||||
|
sb.AppendLine("<body>");
|
||||||
|
//sb.AppendLine("<h1>Отчёт по SQL‑проверкам</h1>");
|
||||||
|
|
||||||
|
int fileIndex = 0;
|
||||||
|
foreach (var fileGroup in groupedByFile)
|
||||||
|
{
|
||||||
|
string divId = $"file_{fileIndex}";
|
||||||
|
sb.AppendLine($"<div id=\"{divId}\" class=\"file-report{(fileIndex == 0 ? " active" : "")}\">");
|
||||||
|
sb.AppendLine($"<h1>Файл: {fileGroup.Key}</h1>");
|
||||||
|
|
||||||
|
var groupedBySeverity = fileGroup
|
||||||
|
.GroupBy(v => v.Severity)
|
||||||
|
.OrderByDescending(g => g.Key);
|
||||||
|
|
||||||
|
foreach (var severityGroup in groupedBySeverity)
|
||||||
|
{
|
||||||
|
string severityClass = severityGroup.Key switch
|
||||||
|
{
|
||||||
|
RuleViolationSeverity.Critical => "critical",
|
||||||
|
RuleViolationSeverity.Warning => "warning",
|
||||||
|
RuleViolationSeverity.Info => "info",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
sb.AppendLine($"<div class=\"{severityClass}\">");
|
||||||
|
sb.AppendLine($"<h3>{severityGroup.Key}</h3>");
|
||||||
|
sb.AppendLine("<table>");
|
||||||
|
sb.AppendLine("<thead>");
|
||||||
|
sb.AppendLine("<tr><th>#</th><th>Строка</th><th>Колонка</th><th>Правило</th><th>Описание</th></tr>");
|
||||||
|
sb.AppendLine("</thead>");
|
||||||
|
sb.AppendLine("<tbody>");
|
||||||
|
|
||||||
|
int rowIndex = 1;
|
||||||
|
foreach (var v in severityGroup
|
||||||
|
.OrderBy(x => x.Line)
|
||||||
|
.ThenBy(x => x.Column))
|
||||||
|
{
|
||||||
|
sb.AppendLine($"<tr><td class=\"index\">{rowIndex}</td><td class=\"line\">{v.Line}</td><td class=\"column\">{v.Column}</td><td class=\"rule\">{v.RuleName}</td><td>{v.Text}</td></tr>");
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("</tbody>");
|
||||||
|
sb.AppendLine("</table>");
|
||||||
|
sb.AppendLine("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("</div>");
|
||||||
|
fileIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Табы снизу
|
||||||
|
sb.AppendLine("<div class=\"tabs\">");
|
||||||
|
fileIndex = 0;
|
||||||
|
foreach (var fileGroup in groupedByFile)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"<div class=\"tab{(fileIndex == 0 ? " active" : "")}\" onclick=\"showReport('file_{fileIndex}', this)\">{fileGroup.Key}</div>");
|
||||||
|
fileIndex++;
|
||||||
|
}
|
||||||
|
sb.AppendLine("</div>");
|
||||||
|
|
||||||
|
// JS для переключения
|
||||||
|
sb.AppendLine("<script>");
|
||||||
|
sb.AppendLine("function showReport(id, tab) {");
|
||||||
|
sb.AppendLine(" document.querySelectorAll('.file-report').forEach(el => el.classList.remove('active'));");
|
||||||
|
sb.AppendLine(" document.getElementById(id).classList.add('active');");
|
||||||
|
sb.AppendLine(" document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));");
|
||||||
|
sb.AppendLine(" tab.classList.add('active');");
|
||||||
|
sb.AppendLine(" window.scrollTo({ top: 0, behavior: 'smooth' });");
|
||||||
|
sb.AppendLine("}");
|
||||||
|
sb.AppendLine("document.querySelector('.tabs').addEventListener('wheel', function(e) {");
|
||||||
|
sb.AppendLine(" e.preventDefault();");
|
||||||
|
sb.AppendLine(" this.scrollLeft += e.deltaY;");
|
||||||
|
sb.AppendLine("});");
|
||||||
|
sb.AppendLine("</script>");
|
||||||
|
|
||||||
|
sb.AppendLine("</body>");
|
||||||
|
sb.AppendLine("</html>");
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
SQLLinter/Infrastructure/Reporters/MarkdownFileReporter.cs
Normal file
53
SQLLinter/Infrastructure/Reporters/MarkdownFileReporter.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Reporters;
|
||||||
|
|
||||||
|
public class MarkdownFileReporter : FileReporter
|
||||||
|
{
|
||||||
|
public override string GetContent()
|
||||||
|
{
|
||||||
|
var violations = Violations;
|
||||||
|
if (violations.Count == 0)
|
||||||
|
{
|
||||||
|
return "_Нет нарушений_";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Группировка по файлу
|
||||||
|
var groupedByFile = violations
|
||||||
|
.GroupBy(v => v.FileName)
|
||||||
|
.OrderBy(g => g.Key); // сортировка файлов по имени
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var fileGroup in groupedByFile)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"## Файл: {fileGroup.Key}");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// Группировка по severity внутри файла
|
||||||
|
var groupedBySeverity = fileGroup
|
||||||
|
.GroupBy(v => v.Severity)
|
||||||
|
.OrderByDescending(g => g.Key); // сначала Error, потом Warning, потом Info
|
||||||
|
|
||||||
|
foreach (var severityGroup in groupedBySeverity)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"### {severityGroup.Key}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Строка | Колонка | Правило | Описание |");
|
||||||
|
sb.AppendLine("|--------|---------|---------|----------|");
|
||||||
|
|
||||||
|
foreach (var v in severityGroup
|
||||||
|
.OrderBy(x => x.Line)
|
||||||
|
.ThenBy(x => x.Column))
|
||||||
|
{
|
||||||
|
sb.AppendLine($"| {v.Line} | {v.Column} | {v.RuleName} | {v.Text} |");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
41
SQLLinter/Infrastructure/Reporters/Reporter.cs
Normal file
41
SQLLinter/Infrastructure/Reporters/Reporter.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
using SQLLinter.Infrastructure.Rules.RuleViolations;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Reporters;
|
||||||
|
|
||||||
|
public class Reporter : IReporter
|
||||||
|
{
|
||||||
|
private readonly List<string> _log = new();
|
||||||
|
|
||||||
|
public int? FixedCount { get; set; }
|
||||||
|
private readonly ConcurrentBag<IRuleViolation> ruleViolations = new();
|
||||||
|
|
||||||
|
public List<IRuleViolation> Violations => ruleViolations.ToList();
|
||||||
|
|
||||||
|
public virtual void Report(string message)
|
||||||
|
{
|
||||||
|
_log.Add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetLog() => _log;
|
||||||
|
|
||||||
|
public void ClearViolations()
|
||||||
|
{
|
||||||
|
Report("Очистка ошибок");
|
||||||
|
|
||||||
|
ruleViolations.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReportViolation(IRuleViolation violation)
|
||||||
|
{
|
||||||
|
ruleViolations.Add(violation);
|
||||||
|
|
||||||
|
Report(violation.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReportViolation(string fileName, int line, int column, RuleViolationSeverity severity, string ruleName, string violationText)
|
||||||
|
{
|
||||||
|
ReportViolation(new RuleViolation(fileName, ruleName, violationText, line, column, severity));
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user