Добавьте файлы проекта.
This commit is contained in:
72
SQLLinter/Common/BaseRuleVisitor.cs
Normal file
72
SQLLinter/Common/BaseRuleVisitor.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
|
||||
namespace SQLLinter.Common;
|
||||
|
||||
public abstract class BaseRuleVisitor : TSqlFragmentVisitor, IRule
|
||||
{
|
||||
protected readonly List<Violation> _violations = new();
|
||||
|
||||
public int DynamicSqlStartColumn { get; set; }
|
||||
public int DynamicSqlStartLine { get; set; }
|
||||
public virtual string Name { get => GetDefaultRuleName(GetType().Name); }
|
||||
public abstract string Text { get; }
|
||||
public virtual RuleViolationSeverity Severity { get; set; } = RuleViolationSeverity.Info;
|
||||
|
||||
protected string GetDefaultRuleName(string className) =>
|
||||
className.Substring(className.Length - 4).ToLower() == "rule" ? className.Substring(0, className.Length - 4) : className;
|
||||
|
||||
|
||||
protected virtual int GetLineNumber(TSqlFragment node) => node.StartLine + GetDynamicSqlLineOffset();
|
||||
|
||||
protected virtual int GetLineNumber(TSqlParserToken node) => node.Line + GetDynamicSqlLineOffset();
|
||||
|
||||
private int GetDynamicSqlLineOffset() =>
|
||||
DynamicSqlStartLine > 0
|
||||
? DynamicSqlStartLine - 1
|
||||
: 0;
|
||||
|
||||
protected virtual int GetColumnNumber(TSqlFragment node) => node.StartColumn + GetDynamicSqlColumnOffset(node);
|
||||
|
||||
protected virtual int GetColumnNumber(TSqlParserToken node) => node.Column + GetDynamicSqlColumnOffset(node);
|
||||
|
||||
protected virtual int GetDynamicSqlColumnOffset(TSqlFragment node) => GetDynamicSqlColumnOffset(node.StartLine);
|
||||
|
||||
protected virtual int GetDynamicSqlColumnOffset(TSqlParserToken node) => GetDynamicSqlColumnOffset(node.Line);
|
||||
|
||||
private int GetDynamicSqlColumnOffset(int line) =>
|
||||
DynamicSqlStartLine > 0 && line == 1
|
||||
? DynamicSqlStartColumn
|
||||
: 0;
|
||||
|
||||
public virtual void FixViolation(
|
||||
List<string> fileLines, IRuleViolation ruleViolation, FileLineActions actions)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual IEnumerable<Violation> Analyze(TSqlFragment fragment)
|
||||
{
|
||||
_violations.Clear();
|
||||
fragment.Accept(this);
|
||||
return _violations;
|
||||
}
|
||||
|
||||
protected void AddViolation(Violation violation)
|
||||
{
|
||||
_violations.Add(violation);
|
||||
}
|
||||
|
||||
protected void AddViolation(string RuleName, string Message, int Line, int Column)
|
||||
{
|
||||
_violations.Add(new(RuleName, Message, Line, Column));
|
||||
}
|
||||
|
||||
protected void AddViolation(TSqlFragment node, params string[] param)
|
||||
{
|
||||
AddViolation(Name, this.GetText(param), GetLineNumber(node), GetColumnNumber(node));
|
||||
}
|
||||
|
||||
protected string GetText(params string[] param)
|
||||
{
|
||||
return string.Format(this.Text, param);
|
||||
}
|
||||
}
|
||||
53
SQLLinter/Common/FileHelpers.cs
Normal file
53
SQLLinter/Common/FileHelpers.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace SQLLinter.Common.Helpers;
|
||||
|
||||
public static class FileHelpers
|
||||
{
|
||||
public static List<string> FindFilesWithMask(List<string> paths)
|
||||
{
|
||||
return paths.SelectMany(path =>
|
||||
{
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
if (directory == null) return Enumerable.Empty<string>();
|
||||
|
||||
string pattern = Path.GetFileName(fullPath);
|
||||
|
||||
// Если маска есть в директории
|
||||
if (directory.Contains("*") || directory.Contains("?"))
|
||||
{
|
||||
var root = Path.GetPathRoot(directory);
|
||||
if (root == null) return Enumerable.Empty<string>();
|
||||
|
||||
string relative = directory.Substring(root.Length);
|
||||
|
||||
return ExpandDirectories(root, relative)
|
||||
.SelectMany(dir => Directory.EnumerateFiles(dir, pattern));
|
||||
}
|
||||
else
|
||||
{
|
||||
return Directory.Exists(directory)
|
||||
? Directory.EnumerateFiles(directory, pattern)
|
||||
: Enumerable.Empty<string>();
|
||||
}
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ExpandDirectories(string root, string relative)
|
||||
{
|
||||
string[] parts = relative.Split(Path.DirectorySeparatorChar);
|
||||
|
||||
IEnumerable<string> dirs = new[] { root };
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
dirs = part.Contains("*") || part.Contains("?")
|
||||
? dirs.SelectMany(d => Directory.Exists(d)
|
||||
? Directory.EnumerateDirectories(d, part)
|
||||
: Enumerable.Empty<string>())
|
||||
: dirs.Select(d => Path.Combine(d, part));
|
||||
}
|
||||
|
||||
return dirs.Where(Directory.Exists);
|
||||
}
|
||||
|
||||
}
|
||||
98
SQLLinter/Common/FileLineActions.cs
Normal file
98
SQLLinter/Common/FileLineActions.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
namespace SQLLinter.Common;
|
||||
|
||||
public class FileLineActions
|
||||
{
|
||||
private readonly List<string> FileLines;
|
||||
|
||||
private readonly List<IRuleViolation> RuleViolations;
|
||||
|
||||
public FileLineActions(List<IRuleViolation> ruleViolations, List<string> fileLines)
|
||||
{
|
||||
RuleViolations = ruleViolations;
|
||||
FileLines = fileLines;
|
||||
}
|
||||
|
||||
public void Insert(int index, string line)
|
||||
{
|
||||
InsertRange(index, new string[1] { line });
|
||||
}
|
||||
|
||||
public void InsertInLine(int lineIndex, int charIndex, string content)
|
||||
{
|
||||
string text = FileLines[lineIndex];
|
||||
text = text.Insert(charIndex, content);
|
||||
FileLines[lineIndex] = text;
|
||||
foreach (IRuleViolation item in RuleViolations.Where((IRuleViolation x) => x.Line == lineIndex + 1 && x.Column > charIndex))
|
||||
{
|
||||
item.Column += content.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public void InsertRange(int index, IList<string> lines)
|
||||
{
|
||||
FileLines.InsertRange(index, lines);
|
||||
foreach (IRuleViolation item in RuleViolations.Where((IRuleViolation x) => x.Line > index))
|
||||
{
|
||||
item.Line += lines.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAll(Func<string, bool> where)
|
||||
{
|
||||
for (int num = FileLines.Count - 1; num >= 0; num--)
|
||||
{
|
||||
if (where(FileLines[num]))
|
||||
{
|
||||
RemoveAt(num);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
RemoveRange(index, 1);
|
||||
}
|
||||
|
||||
public void RemoveInLine(int lineIndex, int charIndex, int length)
|
||||
{
|
||||
string text = FileLines[lineIndex];
|
||||
text = text.Remove(charIndex, length);
|
||||
FileLines[lineIndex] = text;
|
||||
foreach (IRuleViolation item in RuleViolations.Where((IRuleViolation x) => x.Column == lineIndex + 1 && x.Column > charIndex))
|
||||
{
|
||||
item.Column -= length;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveRange(int index, int count)
|
||||
{
|
||||
FileLines.RemoveRange(index, count);
|
||||
foreach (IRuleViolation item in RuleViolations.Where((IRuleViolation x) => x.Line > index))
|
||||
{
|
||||
item.Line -= count;
|
||||
}
|
||||
}
|
||||
|
||||
public void RepaceInlineAt(int lineIndex, int charIndex, string content, int? replaceLength = null)
|
||||
{
|
||||
string text = FileLines[lineIndex];
|
||||
text = text.Remove(charIndex, replaceLength ?? content.Length);
|
||||
text = text.Insert(charIndex, content);
|
||||
FileLines[lineIndex] = text;
|
||||
if (!replaceLength.HasValue || replaceLength == content.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int? num = content.Length - replaceLength;
|
||||
foreach (IRuleViolation item in RuleViolations.Where((IRuleViolation x) => x.Line == lineIndex + 1 && x.Column > charIndex + content.Length))
|
||||
{
|
||||
item.Column += num.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateLine(int lineIndex, string content)
|
||||
{
|
||||
FileLines[lineIndex] = content;
|
||||
}
|
||||
}
|
||||
94
SQLLinter/Common/FixHelpers.cs
Normal file
94
SQLLinter/Common/FixHelpers.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SQLLinter.Common.Helpers;
|
||||
|
||||
public static class FixHelpers
|
||||
{
|
||||
public class FindViolatingNodeVisitor<T> : TSqlFragmentVisitor where T : TSqlFragment
|
||||
{
|
||||
private readonly Func<T, bool> Where;
|
||||
|
||||
public List<T> Nodes = new List<T>();
|
||||
|
||||
public FindViolatingNodeVisitor(Func<T, bool> where = null)
|
||||
{
|
||||
Where = where;
|
||||
}
|
||||
|
||||
public override void Visit(TSqlFragment node)
|
||||
{
|
||||
if (node is T val && (Where == null || Where(val)))
|
||||
{
|
||||
Nodes.Add(val);
|
||||
}
|
||||
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
|
||||
public static (TReturn, TFind) FindViolatingNode<TFind, TReturn>(List<string> fileLines, IRuleViolation ruleViolation, Func<TFind, TReturn> getFragment) where TFind : TSqlFragment where TReturn : TSqlFragment
|
||||
{
|
||||
TFind val = FindNodes<TFind>(fileLines).FirstOrDefault(delegate (TFind x)
|
||||
{
|
||||
TReturn val2 = getFragment(x);
|
||||
return val2?.StartLine == ruleViolation.Line && val2?.StartColumn == ruleViolation.Column;
|
||||
});
|
||||
return (getFragment(val), val);
|
||||
}
|
||||
|
||||
public static List<T> FindNodes<T>(List<string> fileLines, Func<T, bool> where = null) where T : TSqlFragment
|
||||
{
|
||||
using StringReader input = new StringReader(string.Join("\n", fileLines));
|
||||
IList<ParseError> errors;
|
||||
TSqlFragment tSqlFragment = new TSql150Parser(initialQuotedIdentifiers: true, SqlEngineType.All).Parse(input, out errors);
|
||||
if (errors != null && errors.Any())
|
||||
{
|
||||
throw new Exception("Parsing failed. " + string.Join(". ", errors.Select((ParseError x) => x.Message)));
|
||||
}
|
||||
|
||||
FindViolatingNodeVisitor<T> findViolatingNodeVisitor = new FindViolatingNodeVisitor<T>(where);
|
||||
tSqlFragment.Accept(findViolatingNodeVisitor);
|
||||
return findViolatingNodeVisitor.Nodes;
|
||||
}
|
||||
|
||||
public static List<T> FindNodes<T>(TSqlFragment statement, Func<T, bool> where = null) where T : TSqlFragment
|
||||
{
|
||||
FindViolatingNodeVisitor<T> findViolatingNodeVisitor = new FindViolatingNodeVisitor<T>(where);
|
||||
statement.Accept(findViolatingNodeVisitor);
|
||||
return findViolatingNodeVisitor.Nodes;
|
||||
}
|
||||
|
||||
public static T FindViolatingNode<T>(List<string> fileLines, IRuleViolation ruleViolation) where T : TSqlFragment
|
||||
{
|
||||
return FindViolatingNode(fileLines, ruleViolation, (T x) => x).Item1;
|
||||
}
|
||||
|
||||
public static string GetIndent(List<string> fileLines, IRuleViolation ruleViolation)
|
||||
{
|
||||
return GetIndent(fileLines[ruleViolation.Line - 1]);
|
||||
}
|
||||
|
||||
public static string GetIndent(List<string> fileLines, TSqlStatement statement)
|
||||
{
|
||||
return GetIndent(fileLines[statement.StartLine - 1]);
|
||||
}
|
||||
|
||||
public static string GetString(TSqlFragment fragment)
|
||||
{
|
||||
return string.Join(string.Empty, from x in fragment.ScriptTokenStream.Where((TSqlParserToken x, int i) => i >= fragment.FirstTokenIndex && i <= fragment.LastTokenIndex)
|
||||
select x.Text);
|
||||
}
|
||||
|
||||
private static string GetIndent(string ifLine)
|
||||
{
|
||||
Match match = new Regex("^\\s+").Match(ifLine);
|
||||
string result = string.Empty;
|
||||
if (match.Success)
|
||||
{
|
||||
result = match.Value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
6
SQLLinter/Common/IBaseReporter.cs
Normal file
6
SQLLinter/Common/IBaseReporter.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace SQLLinter.Common;
|
||||
|
||||
public interface IBaseReporter
|
||||
{
|
||||
void Report(string message);
|
||||
}
|
||||
8
SQLLinter/Common/IReporter.cs
Normal file
8
SQLLinter/Common/IReporter.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SQLLinter.Common;
|
||||
|
||||
public interface IReporter : IBaseReporter
|
||||
{
|
||||
void ReportViolation(IRuleViolation violation);
|
||||
|
||||
void ReportViolation(string fileName, int line, int column, RuleViolationSeverity severity, string ruleName, string violationText);
|
||||
}
|
||||
17
SQLLinter/Common/IRule.cs
Normal file
17
SQLLinter/Common/IRule.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
|
||||
namespace SQLLinter.Common;
|
||||
|
||||
public interface IRule
|
||||
{
|
||||
string Name { get; }
|
||||
string Text { get; }
|
||||
|
||||
RuleViolationSeverity Severity { get; set; }
|
||||
int DynamicSqlStartColumn { get; set; }
|
||||
|
||||
int DynamicSqlStartLine { get; set; }
|
||||
|
||||
IEnumerable<Violation> Analyze(TSqlFragment fragment);
|
||||
|
||||
}
|
||||
10
SQLLinter/Common/IRuleException.cs
Normal file
10
SQLLinter/Common/IRuleException.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace SQLLinter.Common;
|
||||
|
||||
public interface IRuleException
|
||||
{
|
||||
int StartLine { get; }
|
||||
|
||||
int EndLine { get; }
|
||||
|
||||
string RuleName { get; }
|
||||
}
|
||||
16
SQLLinter/Common/IRuleViolation.cs
Normal file
16
SQLLinter/Common/IRuleViolation.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace SQLLinter.Common;
|
||||
|
||||
public interface IRuleViolation
|
||||
{
|
||||
string FileName { get; }
|
||||
|
||||
int Column { get; set; }
|
||||
|
||||
int Line { get; set; }
|
||||
|
||||
string RuleName { get; }
|
||||
|
||||
RuleViolationSeverity Severity { get; }
|
||||
|
||||
string Text { get; }
|
||||
}
|
||||
9
SQLLinter/Common/RuleViolationSeverity.cs
Normal file
9
SQLLinter/Common/RuleViolationSeverity.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SQLLinter.Common;
|
||||
|
||||
public enum RuleViolationSeverity
|
||||
{
|
||||
Off,
|
||||
Info,
|
||||
Warning,
|
||||
Critical,
|
||||
}
|
||||
59
SQLLinter/Common/SQLHelpers.cs
Normal file
59
SQLLinter/Common/SQLHelpers.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using System.Text;
|
||||
|
||||
namespace SQLLinter.Common.Helpers;
|
||||
|
||||
public static class SQLHelpers
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, что строка может быть закодирована в указанной SQL кодировке (collation).
|
||||
/// </summary>
|
||||
/// <param name="input">Строка для проверки</param>
|
||||
/// <param name="sqlEncodingName">Имя кодировки, например "windows-1251" или "iso-8859-1"</param>
|
||||
/// <returns>true, если строка полностью совместима</returns>
|
||||
public static bool IsValidForEncoding(string input, string sqlEncodingName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return true;
|
||||
|
||||
Encoding enc;
|
||||
try
|
||||
{
|
||||
// Включаем поддержку старых кодировок (ANSI, OEM)
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
enc = Encoding.GetEncoding(sqlEncodingName,
|
||||
new EncoderReplacementFallback("?"),
|
||||
new DecoderReplacementFallback("?"));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
throw new ArgumentException($"Неизвестная кодировка: {sqlEncodingName}");
|
||||
}
|
||||
|
||||
return IsValidForEncoding(input, enc);
|
||||
}
|
||||
|
||||
public static bool IsValidForEncoding(string input, Encoding enc)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return true;
|
||||
|
||||
// Пробуем закодировать и декодировать обратно
|
||||
byte[] bytes = enc.GetBytes(input);
|
||||
string roundtrip = enc.GetString(bytes);
|
||||
|
||||
// Если после кодирования/декодирования строка совпала - значит все символы поддерживаются
|
||||
return roundtrip == input;
|
||||
}
|
||||
|
||||
public static string ObjectGetFullName(SchemaObjectName name) => ObjectGetFullName(name.Identifiers);
|
||||
|
||||
|
||||
public static string ObjectGetFullName(IList<Identifier> identifiers)
|
||||
{
|
||||
return string.Join(".", identifiers.Select(i => "[" + i.Value + "]"));
|
||||
}
|
||||
|
||||
}
|
||||
4
SQLLinter/Common/Violation.cs
Normal file
4
SQLLinter/Common/Violation.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace SQLLinter.Common
|
||||
{
|
||||
public record Violation(string RuleName, string Message, int Line, int Column);
|
||||
}
|
||||
Reference in New Issue
Block a user