using Microsoft.SqlServer.TransactSql.ScriptDom; using System.Text; namespace SQLLinter.Common; public abstract class BaseRuleVisitor : TSqlFragmentVisitor, IRule { protected readonly List _violations = new(); protected Dictionary? _parents = null; public void SetParents(Dictionary? parents) => _parents = parents; protected TSqlFragment? GetParent(TSqlFragment node) => _parents is null ? null : _parents.TryGetValue(node, out var parent) ? parent : null; 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 fileLines, IRuleViolation ruleViolation, FileLineActions actions) { } public virtual IEnumerable Analyze(TSqlFragment fragment) { _violations.Clear(); fragment.Accept(this); return _violations; } protected void AddViolation(Violation violation) { _violations.Add(violation); } protected void AddViolation(string RuleName, string Template, int Line, int Column, params string[] param) { _violations.Add(new(RuleName, Template, Line, Column, null, param)); } protected void AddViolation(TSqlFragment node, params string[] param) { var context = FindContextBlock(node); var block = ExtractBlock(context, node); _violations.Add(new(this.Name, this.Text, GetLineNumber(node), GetColumnNumber(node), block, param)); } protected string GetText(params string[] param) { return string.Format(this.Text, param); } private TSqlFragment? FindContextBlock(TSqlFragment node) { var current = node; while (current != null) { switch (current) { // SELECT-блоки case QuerySpecification: case QueryDerivedTable: case ScalarSubquery: case QualifiedJoin: case UnqualifiedJoin: return current; // Таблицы и функции в FROM case NamedTableReference: case SchemaObjectFunctionTableReference: return current; // ---- Новые блоки для SqlDataTypeReference ---- // Определение столбца case ColumnDefinition: return current; // DECLARE @x INT case DeclareVariableStatement: return current; // Параметры процедур/функций case ProcedureParameter: return current; // CAST(x AS INT) case CastCall: case ConvertCall: return current; } current = GetParent(current); } return current; } // Вспомогательные классы public class ExtractedBlock { public string Text { get; set; } = ""; public int StartLine { get; set; } public List<(int LineNumber, string Text)> Lines { get; set; } = new(); public int ErrorStartLine { get; set; } public int ErrorStartColumn { get; set; } public int ErrorEndLine { get; set; } public int ErrorEndColumn { get; set; } public int MinIndent { get; set; } } private class LineInfo { public int OriginalLineNumber { get; set; } public string Text { get; set; } = ""; public int OriginalIndent { get; set; } public int StartOffset { get; set; } public int EndOffset { get; set; } } protected ExtractedBlock? ExtractBlock(TSqlFragment? node, TSqlFragment errorNode) { if (node == null || node.ScriptTokenStream == null) return null; // 1. Получаем токены для блока var tokens = node.ScriptTokenStream .Where(t => t.Line >= node.StartLine && t.Offset < node.StartOffset + node.FragmentLength) .ToList(); if (tokens.Count == 0) return null; // 2. Строим строки из токенов var lines = BuildLinesFromTokens(tokens); // 3. Вычисляем минимальный отступ int minIndent = CalculateMinIndent(lines); // 4. Нормализуем строки (убираем общий отступ) var normalizedLines = NormalizeLines(lines, minIndent); // 5. Вычисляем позиции ошибки var (errorStartIdx, errorStartCol, errorEndIdx, errorEndCol) = CalculateErrorPositionsSimple(errorNode, lines, normalizedLines, minIndent); // 6. Формируем результат return new ExtractedBlock { Text = string.Join("\n", normalizedLines.Select(l => l.Text)), StartLine = tokens.First().Line, Lines = normalizedLines, ErrorStartLine = errorStartIdx + 1, ErrorStartColumn = errorStartCol, ErrorEndLine = errorEndIdx + 1, ErrorEndColumn = errorEndCol }; } /// /// Строит список строк из токенов /// private List<(int LineNumber, string Text)> BuildLinesFromTokens(List tokens) { var result = new List<(int LineNumber, string Text)>(); if (tokens.Count == 0) return result; var currentLine = new StringBuilder(); int currentLineNumber = tokens[0].Line; foreach (var token in tokens) { // Если токен на новой строке, сохраняем предыдущую строку if (token.Line > currentLineNumber) { result.Add((currentLineNumber, currentLine.ToString())); currentLine.Clear(); currentLineNumber = token.Line; // Добавляем пробелы для выравнивания if (token.Column > 1) { currentLine.Append(' ', token.Column - 1); } } // Добавляем текст токена currentLine.Append(token.Text); } // Добавляем последнюю строку result.Add((currentLineNumber, currentLine.ToString())); return result; } /// /// Вычисляет минимальный отступ среди непустых строк /// private int CalculateMinIndent(List<(int LineNumber, string Text)> lines) { int minIndent = int.MaxValue; foreach (var (lineNumber, text) in lines) { if (string.IsNullOrWhiteSpace(text)) continue; int indent = 0; while (indent < text.Length && char.IsWhiteSpace(text[indent])) indent++; if (indent < minIndent) minIndent = indent; } return minIndent == int.MaxValue ? 0 : minIndent; } /// /// Нормализует строки, убирая минимальный отступ /// private List<(int LineNumber, string Text)> NormalizeLines( List<(int LineNumber, string Text)> lines, int minIndent) { var result = new List<(int LineNumber, string Text)>(); foreach (var (lineNumber, text) in lines) { string normalizedText; if (string.IsNullOrWhiteSpace(text)) { normalizedText = text; } else if (text.Length >= minIndent) { normalizedText = text.Substring(minIndent); } else { normalizedText = text; } result.Add((lineNumber, normalizedText)); } return result; } /// /// Вычисляет позиции ошибки (упрощенный вариант) /// private (int StartIdx, int StartCol, int EndIdx, int EndCol) CalculateErrorPositionsSimple( TSqlFragment errorNode, List<(int LineNumber, string Text)> originalLines, List<(int LineNumber, string Text)> normalizedLines, int minIndent) { // Находим строки ошибки int errorStartLine = errorNode.StartLine; int errorEndLine = GetErrorEndLine(errorNode); // Находим индексы в нормализованных строках int startIdx = -1; int endIdx = -1; for (int i = 0; i < normalizedLines.Count; i++) { if (normalizedLines[i].LineNumber == errorStartLine) startIdx = i; if (normalizedLines[i].LineNumber == errorEndLine) endIdx = i; } if (startIdx == -1) startIdx = 0; if (endIdx == -1) endIdx = normalizedLines.Count - 1; // Вычисляем столбцы int startCol = CalculateAdjustedColumn(errorNode.StartColumn, minIndent); int endCol; if (errorStartLine == errorEndLine) { endCol = CalculateAdjustedColumn(errorNode.StartColumn + errorNode.FragmentLength, minIndent); } else { var errorTokens = GetErrorTokens(errorNode); if (errorTokens.Any()) { var lastToken = errorTokens.Last(); endCol = CalculateAdjustedColumn(lastToken.Column + lastToken.Text.Length, minIndent); } else { endCol = startCol; } } // Ограничиваем столбцы startCol = Math.Max(startCol, 1); endCol = Math.Max(endCol, 1); return (startIdx, startCol, endIdx, endCol); } /// /// Корректирует столбец после удаления отступов /// private int CalculateAdjustedColumn(int originalColumn, int minIndent) { return Math.Max(originalColumn - minIndent, 1); } /// /// Получает последнюю строку ошибки /// private int GetErrorEndLine(TSqlFragment errorNode) { var errorTokens = errorNode.ScriptTokenStream? .Where(t => t.Offset >= errorNode.StartOffset && t.Offset < errorNode.StartOffset + errorNode.FragmentLength) .ToList(); return errorTokens?.LastOrDefault()?.Line ?? errorNode.StartLine; } /// /// Получает токены ошибки /// private List GetErrorTokens(TSqlFragment errorNode) { return errorNode.ScriptTokenStream? .Where(t => t.Offset >= errorNode.StartOffset && t.Offset < errorNode.StartOffset + errorNode.FragmentLength) .ToList() ?? new List(); } }