210 lines
6.0 KiB
C#
210 lines
6.0 KiB
C#
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);
|
||
}
|
||
}
|