Добавьте файлы проекта.
This commit is contained in:
209
SQLLinter/Infrastructure/Rules/DuplicateAliasRule.cs
Normal file
209
SQLLinter/Infrastructure/Rules/DuplicateAliasRule.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||
using SQLLinter.Common;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SQLLinter.Infrastructure.Rules;
|
||||
|
||||
public class DuplicateAliasRule : BaseRuleVisitor
|
||||
{
|
||||
public override string Text => "Алиасы таблиц должны быть уникальными: {0}";
|
||||
|
||||
private readonly Dictionary<int, HashSet<string>> _activeAliases = new();
|
||||
private int _dmlDepth = 0;
|
||||
private bool _insideApply = false;
|
||||
|
||||
private void EnterDmlScope(bool inherit)
|
||||
{
|
||||
_dmlDepth++;
|
||||
if (_dmlDepth == 1 || !inherit)
|
||||
{
|
||||
_activeAliases[_dmlDepth] = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
// дочерний скоуп видит алиасы родителя (для глобальной уникальности в рамках одного DML)
|
||||
var parent = _activeAliases[_dmlDepth - 1];
|
||||
_activeAliases[_dmlDepth] = new HashSet<string>(parent, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExitDmlScope()
|
||||
{
|
||||
if (_activeAliases.ContainsKey(_dmlDepth))
|
||||
_activeAliases.Remove(_dmlDepth);
|
||||
_dmlDepth--;
|
||||
}
|
||||
|
||||
private void RegisterAlias(string alias, TSqlFragment node)
|
||||
{
|
||||
if (_dmlDepth == 0) return; // вне DML не проверяем
|
||||
var set = _activeAliases[_dmlDepth];
|
||||
|
||||
if (set.Contains(alias))
|
||||
{
|
||||
AddViolation(node, "[" + alias + "]");
|
||||
}
|
||||
else
|
||||
{
|
||||
set.Add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
// Корневые DML-выражения
|
||||
public override void ExplicitVisit(SelectStatement node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
base.ExplicitVisit(node);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(InsertStatement node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
base.ExplicitVisit(node);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(UpdateStatement node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
base.ExplicitVisit(node);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(DeleteStatement node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
base.ExplicitVisit(node);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(MergeStatement node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
base.ExplicitVisit(node);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
// IF: каждая ветка - отдельный скоуп
|
||||
public override void ExplicitVisit(IfStatement node)
|
||||
{
|
||||
EnterDmlScope(false);
|
||||
node.ThenStatement.Accept(this);
|
||||
ExitDmlScope();
|
||||
|
||||
if (node.ElseStatement != null)
|
||||
{
|
||||
EnterDmlScope(false);
|
||||
node.ElseStatement.Accept(this);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
// UNION: каждая часть - отдельный скоуп
|
||||
public override void ExplicitVisit(BinaryQueryExpression node)
|
||||
{
|
||||
EnterDmlScope(true);
|
||||
node.FirstQueryExpression.Accept(this);
|
||||
ExitDmlScope();
|
||||
|
||||
EnterDmlScope(true);
|
||||
node.SecondQueryExpression.Accept(this);
|
||||
ExitDmlScope();
|
||||
|
||||
//base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
// CTE: регистрируем имя, тело - в дочернем скоупе
|
||||
public override void ExplicitVisit(CommonTableExpression node)
|
||||
{
|
||||
if (node.ExpressionName?.Value is { Length: > 0 } cteAlias)
|
||||
RegisterAlias(cteAlias, node);
|
||||
|
||||
EnterDmlScope(false);
|
||||
node.QueryExpression.Accept(this);
|
||||
ExitDmlScope();
|
||||
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
// Производная таблица: регистрируем её алиас; внутренняя QueryExpression обойдётся базой
|
||||
public override void ExplicitVisit(QueryDerivedTable node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
|
||||
|
||||
// тело подзапроса в FROM - свой локальный скоуп
|
||||
EnterDmlScope(_insideApply);
|
||||
node.QueryExpression.Accept(this);
|
||||
ExitDmlScope();
|
||||
}
|
||||
|
||||
// CROSS APPLY
|
||||
public override void ExplicitVisit(UnqualifiedJoin node)
|
||||
{
|
||||
if (node.UnqualifiedJoinType == UnqualifiedJoinType.CrossApply ||
|
||||
node.UnqualifiedJoinType == UnqualifiedJoinType.OuterApply)
|
||||
{
|
||||
var prev = _insideApply;
|
||||
_insideApply = true;
|
||||
|
||||
// обходим без нового скоупа, т.к. алиасы учитываются глобально
|
||||
node.FirstTableReference.Accept(this);
|
||||
node.SecondTableReference.Accept(this);
|
||||
|
||||
_insideApply = prev;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Табличные источники с алиасами
|
||||
public override void ExplicitVisit(NamedTableReference node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(VariableTableReference node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(SchemaObjectFunctionTableReference node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(PivotedTableReference node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
public override void ExplicitVisit(UnpivotedTableReference node)
|
||||
{
|
||||
if (node.Alias?.Value is { Length: > 0 } a)
|
||||
RegisterAlias(a, node);
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
|
||||
// Сброс состояния перед анализом скрипта
|
||||
public override void ExplicitVisit(TSqlScript node)
|
||||
{
|
||||
_dmlDepth = 0;
|
||||
_activeAliases.Clear();
|
||||
base.ExplicitVisit(node);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user