390 lines
13 KiB
C#
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>();
|
|
}
|
|
|
|
} |