Добавьте файлы проекта.

This commit is contained in:
2025-12-07 08:52:05 +03:00
parent 95344cd7a7
commit 226b6b6b21
118 changed files with 5249 additions and 0 deletions

View 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; }
}
}
}

View 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);
}
}
}

View 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;
}
}

View 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;
}
}
}

View File

@@ -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;
}
}
}

View 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);
}
}

View 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;
}
}
}
}

View 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();
}
}

View 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);
}
}
}