Добавьте файлы проекта.
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Core;
|
||||
using SQLLinter.Core.Interfaces;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules.RuleExceptions
|
||||
{
|
||||
public class RuleExceptionFinder : IRuleExceptionFinder
|
||||
{
|
||||
public static Regex RuleExceptionRegex = new Regex(@"(sqllinter-(?:dis|en)able)\s*(.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private readonly IDictionary<string, IRule> Rules;
|
||||
|
||||
public RuleExceptionFinder(IDictionary<string, IRule> rules)
|
||||
{
|
||||
Rules = rules;
|
||||
}
|
||||
|
||||
public IEnumerable<IExtendedRuleException> GetIgnoredRuleList(Stream fileStream)
|
||||
{
|
||||
var ruleExceptionList = new List<IExtendedRuleException>();
|
||||
TextReader reader = new StreamReader(fileStream);
|
||||
|
||||
var lineNumber = 0;
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
lineNumber++;
|
||||
if (line.Length > Constants.MaxLineWidthForRegexEval || !line.Contains("sqllinter-"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var match = RuleExceptionRegex.Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FindIgnoredRules(ruleExceptionList, lineNumber, match);
|
||||
}
|
||||
|
||||
fileStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
foreach (var ruleException in ruleExceptionList)
|
||||
{
|
||||
if (ruleException.EndLine == 0)
|
||||
{
|
||||
ruleException.SetEndLine(lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return ruleExceptionList;
|
||||
}
|
||||
|
||||
private void FindIgnoredRules(ICollection<IExtendedRuleException> ruleExceptionList, int lineNumber, Match match)
|
||||
{
|
||||
var action = match.Groups[1].Value;
|
||||
|
||||
var disableCommand = action.Equals("sqllinter-disable", StringComparison.OrdinalIgnoreCase);
|
||||
var enableCommand = action.Equals("sqllinter-enable", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var ruleExceptionDetails = match.Groups[2].Value.Split(' ').Select(p => p.Trim()).ToList();
|
||||
var matchedFriendlyNames = ruleExceptionDetails.Intersect(Rules.Keys).ToList();
|
||||
|
||||
if (!matchedFriendlyNames.Any())
|
||||
{
|
||||
if (disableCommand)
|
||||
{
|
||||
var ruleException = new GlobalRuleException(lineNumber, 0);
|
||||
ruleExceptionList.Add(ruleException);
|
||||
}
|
||||
|
||||
if (enableCommand)
|
||||
{
|
||||
var ruleException = ruleExceptionList.OfType<GlobalRuleException>().FirstOrDefault(r => r.EndLine == 0);
|
||||
ruleException?.SetEndLine(lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var matchedFriendlyName in matchedFriendlyNames)
|
||||
{
|
||||
Rules.TryGetValue(matchedFriendlyName, out var matched);
|
||||
|
||||
if (disableCommand)
|
||||
{
|
||||
var ruleException = new RuleException(matched.GetType(), matchedFriendlyName, lineNumber, 0);
|
||||
ruleExceptionList.Add(ruleException);
|
||||
}
|
||||
|
||||
if (enableCommand)
|
||||
{
|
||||
var ruleException = ruleExceptionList.OfType<RuleException>().FirstOrDefault(r => r.RuleName == matchedFriendlyName && r.EndLine == 0);
|
||||
ruleException?.SetEndLine(lineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules.RuleViolations
|
||||
{
|
||||
public class RuleViolation : IRuleViolation
|
||||
{
|
||||
public RuleViolation(string fileName, string ruleName, string text, int startLine, int startColumn, RuleViolationSeverity severity)
|
||||
{
|
||||
FileName = fileName;
|
||||
RuleName = ruleName;
|
||||
Text = text;
|
||||
Line = startLine;
|
||||
Column = startColumn;
|
||||
Severity = severity;
|
||||
}
|
||||
|
||||
public RuleViolation(string fileName, string ruleName, int startLine, int startColumn)
|
||||
{
|
||||
FileName = fileName;
|
||||
RuleName = ruleName;
|
||||
Line = startLine;
|
||||
Column = startColumn;
|
||||
}
|
||||
|
||||
public RuleViolation(string ruleName, int startLine, int startColumn)
|
||||
{
|
||||
RuleName = ruleName;
|
||||
Line = startLine;
|
||||
Column = startColumn;
|
||||
}
|
||||
|
||||
public int Column { get; set; }
|
||||
|
||||
public string FileName { get; set; }
|
||||
|
||||
public int Line { get; set; }
|
||||
|
||||
public string RuleName { get; set; }
|
||||
|
||||
public RuleViolationSeverity Severity { get; set; }
|
||||
|
||||
public string Text { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $@"{Severity.ToString().ToUpper()}: L{Line} C{Column} {FileName} ""{Text}""";
|
||||
}
|
||||
}
|
||||
}
|
||||
80
SQLLinter/Infrastructure/Rules/SchemaQualifyRule.cs
Normal file
80
SQLLinter/Infrastructure/Rules/SchemaQualifyRule.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class SchemaQualifyRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private readonly List<string> tableAliases = new()
|
||||
{
|
||||
"INSERTED",
|
||||
"UPDATED",
|
||||
"DELETED"
|
||||
};
|
||||
|
||||
public override string Text => "Имя объекта без схемы: {0}";
|
||||
|
||||
public override void Visit(TSqlStatement node)
|
||||
{
|
||||
var childAliasVisitor = new ChildAliasVisitor();
|
||||
node.AcceptChildren(childAliasVisitor);
|
||||
tableAliases.AddRange(childAliasVisitor.TableAliases);
|
||||
}
|
||||
|
||||
public override void Visit(NamedTableReference node) => VisitTableName(node.SchemaObject, true);
|
||||
|
||||
public override void Visit(CreateTableStatement node) => VisitTableName(node.SchemaObjectName, false);
|
||||
|
||||
public override void Visit(AlterTableStatement node) => VisitTableName(node.SchemaObjectName, false);
|
||||
|
||||
public override void Visit(TruncateTableStatement node) => VisitTableName(node.TableName, false);
|
||||
|
||||
public override void Visit(DropTableStatement node)
|
||||
{
|
||||
foreach (var schemaObjectName in node.Objects)
|
||||
{
|
||||
VisitTableName(schemaObjectName, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitTableName(SchemaObjectName node, bool canHaveTableAliases)
|
||||
{
|
||||
if (node.SchemaIdentifier != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// не проверять схему во временных таблицах
|
||||
if (node.BaseIdentifier.Value.Contains('#'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// не проверять схему псевдонимов таблиц
|
||||
if (canHaveTableAliases && tableAliases.Exists(x => x.Equals(node.BaseIdentifier.Value, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node));
|
||||
}
|
||||
|
||||
public class ChildAliasVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public List<string> TableAliases { get; } = new();
|
||||
|
||||
public override void Visit(TableReferenceWithAlias node)
|
||||
{
|
||||
if (node.Alias != null)
|
||||
{
|
||||
TableAliases.Add(node.Alias.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Visit(CommonTableExpression node)
|
||||
{
|
||||
TableAliases.Add(node.ExpressionName.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
SQLLinter/Infrastructure/Rules/SelectStarRule.cs
Normal file
39
SQLLinter/Infrastructure/Rules/SelectStarRule.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class SelectStarRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private int expressionCounter;
|
||||
|
||||
public override string Text => "Ожидаются имена столбцов в SELECT";
|
||||
|
||||
public override void Visit(ExistsPredicate node)
|
||||
{
|
||||
var childVisitor = new ChildVisitor();
|
||||
node.AcceptChildren(childVisitor);
|
||||
expressionCounter += childVisitor.SelectStarExpressionCount;
|
||||
}
|
||||
|
||||
public override void Visit(SelectStarExpression node)
|
||||
{
|
||||
if (expressionCounter > 0)
|
||||
{
|
||||
expressionCounter--;
|
||||
return;
|
||||
}
|
||||
|
||||
AddViolation(node);
|
||||
}
|
||||
|
||||
public class ChildVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public int SelectStarExpressionCount { get; set; }
|
||||
|
||||
public override void Visit(SelectStarExpression node)
|
||||
{
|
||||
SelectStarExpressionCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
SQLLinter/Infrastructure/Rules/SemicolonTerminationRule.cs
Normal file
73
SQLLinter/Infrastructure/Rules/SemicolonTerminationRule.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Infrastructure.Rules.Common;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class SemicolonTerminationRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
private readonly IList<TSqlFragment> waitForStatements = new List<TSqlFragment>();
|
||||
private readonly IList<TSqlFragment> functionReturnTypeSelectStatements = new List<TSqlFragment>();
|
||||
private static Regex WhiteSpaceRegex = new Regex(@"\s", RegexOptions.Compiled);
|
||||
private static Regex AllWhiteSpaceRegex = new Regex(@"^\s$", RegexOptions.Compiled);
|
||||
|
||||
|
||||
// не принудительно завершать эти операторы точкой с запятой
|
||||
private readonly Type[] typesToSkip =
|
||||
{
|
||||
typeof(BeginEndBlockStatement),
|
||||
typeof(GoToStatement),
|
||||
typeof(IndexDefinition),
|
||||
typeof(LabelStatement),
|
||||
typeof(WhileStatement),
|
||||
typeof(IfStatement),
|
||||
typeof(CreateViewStatement)
|
||||
};
|
||||
|
||||
public override string Text => "Оператор не заканчивается точкой с запятой";
|
||||
|
||||
public override void Visit(WaitForStatement node)
|
||||
{
|
||||
waitForStatements.Add(node.Statement);
|
||||
}
|
||||
|
||||
public override void Visit(CreateFunctionStatement node)
|
||||
{
|
||||
if (node.ReturnType is SelectFunctionReturnType returnType)
|
||||
{
|
||||
functionReturnTypeSelectStatements.Add(returnType.SelectStatement);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Visit(TSqlStatement node)
|
||||
{
|
||||
if (Array.IndexOf(typesToSkip, node.GetType()) > -1 ||
|
||||
EndsWithSemicolon(node) ||
|
||||
waitForStatements.Contains(node) ||
|
||||
functionReturnTypeSelectStatements.Contains(node))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dynamicSqlColumnOffset = GetDynamicSqlColumnOffset(node);
|
||||
|
||||
var (lastToken, column) = GetLastTokenAndColumn(node);
|
||||
AddViolation(Name, Text, GetLineNumber(lastToken), column + dynamicSqlColumnOffset);
|
||||
}
|
||||
|
||||
private static (TSqlParserToken, int) GetLastTokenAndColumn(TSqlStatement node)
|
||||
{
|
||||
var lastToken = node.ScriptTokenStream[node.LastTokenIndex];
|
||||
var tabsOnLine = ColumnNumberCalculator.CountTabsBeforeToken(lastToken.Line, node.LastTokenIndex, node.ScriptTokenStream);
|
||||
var column = ColumnNumberCalculator.GetColumnNumberAfterToken(tabsOnLine, lastToken);
|
||||
|
||||
return (lastToken, column);
|
||||
}
|
||||
|
||||
private static bool EndsWithSemicolon(TSqlFragment node)
|
||||
{
|
||||
return node.ScriptTokenStream[node.LastTokenIndex].TokenType == TSqlTokenType.Semicolon
|
||||
|| node.ScriptTokenStream[node.LastTokenIndex + 1].TokenType == TSqlTokenType.Semicolon;
|
||||
}
|
||||
}
|
||||
18
SQLLinter/Infrastructure/Rules/TempTableDropRule.cs
Normal file
18
SQLLinter/Infrastructure/Rules/TempTableDropRule.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class TempTableDropRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Нежелательно использовать DROP временных таблиц: {0}";
|
||||
|
||||
public override void Visit(DropTableStatement node)
|
||||
{
|
||||
var check = node.Objects.Where(o => o.BaseIdentifier.Value.StartsWith("#"));
|
||||
if (check.Any())
|
||||
{
|
||||
AddViolation(node, string.Join(", ", check));
|
||||
}
|
||||
}
|
||||
}
|
||||
34
SQLLinter/Infrastructure/Rules/TempTableModificationRule.cs
Normal file
34
SQLLinter/Infrastructure/Rules/TempTableModificationRule.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class TempTableModificationRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Избегать ALTER/UPDATE/DELETE для временных таблиц: {0}";
|
||||
|
||||
public override void Visit(UpdateStatement node)
|
||||
{
|
||||
if (node.UpdateSpecification.Target is NamedTableReference tbl && tbl.SchemaObject.BaseIdentifier.Value.StartsWith("#"))
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(tbl.SchemaObject));
|
||||
}
|
||||
}
|
||||
|
||||
public override void Visit(DeleteStatement node)
|
||||
{
|
||||
if (node.DeleteSpecification.Target is NamedTableReference tbl && tbl.SchemaObject.BaseIdentifier.Value.StartsWith("#"))
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(tbl.SchemaObject));
|
||||
}
|
||||
}
|
||||
|
||||
public override void Visit(AlterTableStatement node)
|
||||
{
|
||||
if (node.SchemaObjectName.BaseIdentifier.Value.StartsWith("#"))
|
||||
{
|
||||
AddViolation(node, SQLHelpers.ObjectGetFullName(node.SchemaObjectName));
|
||||
}
|
||||
}
|
||||
}
|
||||
18
SQLLinter/Infrastructure/Rules/TopWithoutOrderByRule.cs
Normal file
18
SQLLinter/Infrastructure/Rules/TopWithoutOrderByRule.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class TopWithoutOrderByRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "TOP без ORDER BY может дать непредсказуемый результат";
|
||||
|
||||
public override void Visit(QuerySpecification node)
|
||||
{
|
||||
if (node.TopRowFilter != null && node.OrderByClause == null)
|
||||
{
|
||||
AddViolation(node.TopRowFilter);
|
||||
}
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
28
SQLLinter/Infrastructure/Rules/UnicodeStringRule.cs
Normal file
28
SQLLinter/Infrastructure/Rules/UnicodeStringRule.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class UnicodeStringRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
public override string Text => "Использование символов Юникода в строке, отличной от Юникода";
|
||||
|
||||
public override void Visit(StringLiteral node)
|
||||
{
|
||||
if (node.IsNational)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsAscii(node.Value))
|
||||
{
|
||||
AddViolation(Name, Text, node.StartLine, node.StartColumn);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAscii(string part)
|
||||
{
|
||||
return SQLHelpers.IsValidForEncoding(part, "windows-1251");
|
||||
}
|
||||
}
|
||||
17
SQLLinter/Infrastructure/Rules/UnionRule.cs
Normal file
17
SQLLinter/Infrastructure/Rules/UnionRule.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class UnionRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Избегать UNION, использовать UNION ALL.";
|
||||
|
||||
public override void Visit(BinaryQueryExpression node)
|
||||
{
|
||||
if (node.BinaryQueryExpressionType == BinaryQueryExpressionType.Union && !node.All)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
SQLLinter/Infrastructure/Rules/UpdateWhereRule.cs
Normal file
28
SQLLinter/Infrastructure/Rules/UpdateWhereRule.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class UpdateWhereRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Ожидается выражение WHERE для оператора UPDATE: {0}";
|
||||
|
||||
|
||||
public override void Visit(UpdateSpecification node)
|
||||
{
|
||||
if (node.WhereClause != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
string name = string.Join("", node.Target.ScriptTokenStream
|
||||
.Skip(node.Target.FirstTokenIndex)
|
||||
.Take(node.Target.LastTokenIndex - node.Target.FirstTokenIndex + 1)
|
||||
.Select(t => t.Text)
|
||||
);
|
||||
|
||||
AddViolation(node, name);
|
||||
}
|
||||
}
|
||||
64
SQLLinter/Infrastructure/Rules/UpperLowerRule.cs
Normal file
64
SQLLinter/Infrastructure/Rules/UpperLowerRule.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class UpperLowerRule : BaseRuleVisitor, IRule
|
||||
{
|
||||
|
||||
public override string Text => "Использование функций UPPER или LOWER при выполнении сравнений в операторах SELECT не требуется при запуске базы данных в режиме без учета регистра.";
|
||||
|
||||
public override void Visit(SelectStatement node)
|
||||
{
|
||||
var visitor = new ChildQueryComparisonVisitor();
|
||||
node.Accept(visitor);
|
||||
if (visitor.QueryExpressionUpperLowerFunctionFound)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChildQueryComparisonVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool QueryExpressionUpperLowerFunctionFound { get; private set; }
|
||||
|
||||
public override void Visit(QueryExpression node)
|
||||
{
|
||||
var visitor = new ChildBooleanComparisonVisitor();
|
||||
node.Accept(visitor);
|
||||
if (visitor.UpperLowerFunctionCallInComparison)
|
||||
{
|
||||
QueryExpressionUpperLowerFunctionFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ChildBooleanComparisonVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool UpperLowerFunctionCallInComparison { get; private set; }
|
||||
|
||||
public override void Visit(BooleanComparisonExpression node)
|
||||
{
|
||||
var visitor = new ChildFunctionCallVisitor();
|
||||
node.Accept(visitor);
|
||||
if (visitor.UpperLowerFound)
|
||||
{
|
||||
UpperLowerFunctionCallInComparison = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ChildFunctionCallVisitor : TSqlFragmentVisitor
|
||||
{
|
||||
public bool UpperLowerFound { get; private set; }
|
||||
|
||||
public override void Visit(FunctionCall node)
|
||||
{
|
||||
if (node.FunctionName.Value.Equals("UPPER", StringComparison.OrdinalIgnoreCase) ||
|
||||
node.FunctionName.Value.Equals("LOWER", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
UpperLowerFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
SQLLinter/Infrastructure/Rules/UserFunctionJoinRule.cs
Normal file
158
SQLLinter/Infrastructure/Rules/UserFunctionJoinRule.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using SQLLinter.Common.Helpers;
|
||||
using SQLLinter.Core;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class UserFunctionJoinRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещено использование пользовательских функций внутри запросов: {0}";
|
||||
|
||||
private bool _inQueryContext;
|
||||
|
||||
// Входим в контекст запроса
|
||||
public override void Visit(QuerySpecification node)
|
||||
{
|
||||
_inQueryContext = true;
|
||||
|
||||
// Проверяем FROM
|
||||
if (node.FromClause != null)
|
||||
{
|
||||
var tables = node.FromClause.TableReferences;
|
||||
|
||||
// Разрешаем ровно одну табличную функцию
|
||||
if (!(tables.Count == 1 && tables[0] is SchemaObjectFunctionTableReference ft && IsUserFunction(ft.SchemaObject?.BaseIdentifier?.Value)))
|
||||
{
|
||||
foreach (var tr in tables)
|
||||
CheckTableReference(tr);
|
||||
}
|
||||
}
|
||||
|
||||
base.Visit(node);
|
||||
_inQueryContext = false;
|
||||
}
|
||||
|
||||
// Скалярные функции
|
||||
public override void Visit(FunctionCall node)
|
||||
{
|
||||
if (_inQueryContext && IsUserFunction(node.FunctionName.Value))
|
||||
{
|
||||
AddViolationFunction(node);
|
||||
}
|
||||
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
// Табличные функции
|
||||
public override void Visit(SchemaObjectFunctionTableReference node)
|
||||
{
|
||||
// Проверка делается в QuerySpecification - здесь ничего не делаем
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
// Подзапросы
|
||||
public override void Visit(ScalarSubquery node)
|
||||
{
|
||||
node.QueryExpression.Accept(this);
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
public override void Visit(QueryDerivedTable node)
|
||||
{
|
||||
node.QueryExpression.Accept(this);
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
public override void Visit(QualifiedJoin node)
|
||||
{
|
||||
if (node.SearchCondition != null)
|
||||
node.SearchCondition.Accept(this);
|
||||
|
||||
if (node.FirstTableReference != null)
|
||||
CheckTableReference(node.FirstTableReference);
|
||||
if (node.SecondTableReference != null)
|
||||
CheckTableReference(node.SecondTableReference);
|
||||
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
public override void Visit(UnqualifiedJoin node)
|
||||
{
|
||||
// Проверяем APPLY
|
||||
if (node.UnqualifiedJoinType == UnqualifiedJoinType.CrossApply ||
|
||||
node.UnqualifiedJoinType == UnqualifiedJoinType.OuterApply)
|
||||
{
|
||||
if (node.SecondTableReference is SchemaObjectFunctionTableReference ft &&
|
||||
IsUserFunction(ft.SchemaObject?.BaseIdentifier?.Value))
|
||||
{
|
||||
AddViolation(ft, SQLHelpers.ObjectGetFullName(ft.SchemaObject));
|
||||
}
|
||||
}
|
||||
|
||||
// Обходим обе стороны
|
||||
if (node.FirstTableReference != null)
|
||||
CheckTableReference(node.FirstTableReference);
|
||||
if (node.SecondTableReference != null)
|
||||
CheckTableReference(node.SecondTableReference);
|
||||
|
||||
base.Visit(node);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ------------------------
|
||||
// Вспомогательные методы
|
||||
// ------------------------
|
||||
|
||||
private void AddViolationFunction(FunctionCall func)
|
||||
{
|
||||
string ident = "";
|
||||
if (func.CallTarget is MultiPartIdentifierCallTarget callTarget)
|
||||
{
|
||||
ident = SQLHelpers.ObjectGetFullName(callTarget.MultiPartIdentifier.Identifiers);
|
||||
if (!string.IsNullOrWhiteSpace(ident)) ident += ".";
|
||||
}
|
||||
|
||||
ident += "[" + func.FunctionName.Value + "]";
|
||||
AddViolation(func, ident);
|
||||
}
|
||||
|
||||
private void CheckTableReference(TableReference tr)
|
||||
{
|
||||
switch (tr)
|
||||
{
|
||||
case SchemaObjectFunctionTableReference ft:
|
||||
if (IsUserFunction(ft.SchemaObject?.BaseIdentifier?.Value))
|
||||
AddViolation(ft, SQLHelpers.ObjectGetFullName(ft.SchemaObject));
|
||||
break;
|
||||
|
||||
case QueryDerivedTable qdt:
|
||||
qdt.QueryExpression.Accept(this);
|
||||
break;
|
||||
|
||||
case QualifiedJoin qj:
|
||||
if (qj.FirstTableReference != null)
|
||||
CheckTableReference(qj.FirstTableReference);
|
||||
if (qj.SecondTableReference != null)
|
||||
CheckTableReference(qj.SecondTableReference);
|
||||
if (qj.SearchCondition != null)
|
||||
qj.SearchCondition.Accept(this);
|
||||
break;
|
||||
|
||||
case UnqualifiedJoin aj:
|
||||
if (aj.FirstTableReference != null)
|
||||
CheckTableReference(aj.FirstTableReference);
|
||||
if (aj.SecondTableReference != null)
|
||||
CheckTableReference(aj.SecondTableReference);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsUserFunction(string? functionName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(functionName) &&
|
||||
!Constants.SystemFunctions.Contains(functionName);
|
||||
}
|
||||
}
|
||||
17
SQLLinter/Infrastructure/Rules/WhereInSelectRule.cs
Normal file
17
SQLLinter/Infrastructure/Rules/WhereInSelectRule.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class WhereInSelectRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещено IN (SELECT ...), используйте EXISTS/NOT EXISTS.";
|
||||
|
||||
public override void Visit(InPredicate node)
|
||||
{
|
||||
if (node.Subquery != null)
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
SQLLinter/Infrastructure/Rules/XmlJsonParsingRule.cs
Normal file
18
SQLLinter/Infrastructure/Rules/XmlJsonParsingRule.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class XmlJsonParsingRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Запрещён парсинг XML/JSON.";
|
||||
|
||||
public override void Visit(FunctionCall node)
|
||||
{
|
||||
var fn = node.FunctionName.Value.ToUpperInvariant();
|
||||
if (fn.Contains("OPENXML") || fn.Contains("OPENJSON") || fn.Contains("NODES"))
|
||||
{
|
||||
AddViolation(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user