Files
SQLLint/SQLLinter/Common/BaseRuleVisitor.cs
2025-12-28 23:25:51 +03:00

390 lines
13 KiB
C#

using Microsoft.SqlServer.TransactSql.ScriptDom;
using System.Text;
namespace SQLLinter.Common;
public abstract class BaseRuleVisitor : TSqlFragmentVisitor, IRule
{
protected readonly List<Violation> _violations = new();
protected Dictionary<TSqlFragment, TSqlFragment?>? _parents = null;
public void SetParents(Dictionary<TSqlFragment, TSqlFragment?>? 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<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 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;
// DML
case InsertStatement:
case UpdateStatement:
case DeleteStatement:
case MergeStatement:
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;
var endLine = node.ScriptTokenStream.Where(t => t.Offset < node.StartOffset + node.FragmentLength).Max(t => t.Line) + 2;
// 1. Получаем токены для блока
var tokens = node.ScriptTokenStream
.Where(t => t.Line >= node.StartLine - 2 &&
t.Line <= endLine)
.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
};
}
/// <summary>
/// Строит список строк из токенов
/// </summary>
private List<(int LineNumber, string Text)> BuildLinesFromTokens(List<TSqlParserToken> 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;
}
/// <summary>
/// Вычисляет минимальный отступ среди непустых строк
/// </summary>
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;
}
/// <summary>
/// Нормализует строки, убирая минимальный отступ
/// </summary>
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;
}
/// <summary>
/// Вычисляет позиции ошибки (упрощенный вариант)
/// </summary>
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);
}
/// <summary>
/// Корректирует столбец после удаления отступов
/// </summary>
private int CalculateAdjustedColumn(int originalColumn, int minIndent)
{
return Math.Max(originalColumn - minIndent, 1);
}
/// <summary>
/// Получает последнюю строку ошибки
/// </summary>
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;
}
/// <summary>
/// Получает токены ошибки
/// </summary>
private List<TSqlParserToken> GetErrorTokens(TSqlFragment errorNode)
{
return errorNode.ScriptTokenStream?
.Where(t => t.Offset >= errorNode.StartOffset &&
t.Offset < errorNode.StartOffset + errorNode.FragmentLength)
.ToList() ?? new List<TSqlParserToken>();
}
}