Добавлено формирование детальной ошибки
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using System.Text;
|
||||
|
||||
namespace SQLLinter.Common;
|
||||
|
||||
@@ -6,6 +7,16 @@ public abstract class BaseRuleVisitor : TSqlFragmentVisitor, IRule
|
||||
{
|
||||
protected readonly List<Violation> _violations = new();
|
||||
|
||||
protected Dictionary<TSqlFragment, TSqlFragment?> _parents
|
||||
= new Dictionary<TSqlFragment, TSqlFragment?>();
|
||||
|
||||
public void SetParents(Dictionary<TSqlFragment, TSqlFragment?> parents)
|
||||
=> _parents = parents;
|
||||
|
||||
protected TSqlFragment? GetParent(TSqlFragment node)
|
||||
=> _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); }
|
||||
@@ -57,16 +68,314 @@ public abstract class BaseRuleVisitor : TSqlFragmentVisitor, IRule
|
||||
|
||||
protected void AddViolation(string RuleName, string Template, int Line, int Column, params string[] param)
|
||||
{
|
||||
_violations.Add(new(RuleName, Template, Line, Column, param));
|
||||
_violations.Add(new(RuleName, Template, Line, Column, null, param));
|
||||
}
|
||||
|
||||
protected void AddViolation(TSqlFragment node, params string[] param)
|
||||
{
|
||||
_violations.Add(new(this.Name, this.Text, GetLineNumber(node), GetColumnNumber(node), 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
|
||||
};
|
||||
}
|
||||
|
||||
/// <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>();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user