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> _activeAliases = new(); private int _dmlDepth = 0; private bool _insideApply = false; private void EnterDmlScope(bool inherit) { _dmlDepth++; if (_dmlDepth == 1 || !inherit) { _activeAliases[_dmlDepth] = new HashSet(StringComparer.OrdinalIgnoreCase); } else { // дочерний скоуп видит алиасы родителя (для глобальной уникальности в рамках одного DML) var parent = _activeAliases[_dmlDepth - 1]; _activeAliases[_dmlDepth] = new HashSet(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); } }