Статьи

Круг вокруг Рослин CTP: синтаксический анализ и анализ потока

Сколько раз вы видели в обзорах кода кусок кода, который вызывает метод, например Dictionary <K, V> .TryGetValue , и игнорирует возвращаемое значение? Мы собираемся найти все подобные вызовы и выдать предупреждение.

Мы собираемся извлечь из SyntaxWalker (а не SyntaxRewriter ), потому что мы не будем переписывать, просто обнаруживать проблемы * . Есть два основных случая, которые мы должны рассмотреть:

  • Метод вызывается без сохранения его результата в локальной переменной или использования его как части выражения. Два примера:
    dict.TryGetValue(1, out r);
    for (int.TryParse(s, out i); ; ) …
  • Метод вызывается, и его результат присваивается локальной переменной, но эта локальная переменная никогда не считывается в остальной части метода.

Скелет нашего SyntaxWalker выглядит следующим образом — это должно показаться знакомым, за исключением метода VisitMethodDeclaration , который не возвращает значение (помните, мы не занимаемся переписыванием — просто проверяем).

class MyIgnoredBooleanReturnValueLocator : SyntaxWalker
{
    private readonly SemanticModel _semanticModel;
    private readonly HashSet<SyntaxKind> _assignExprs;

    public MyIgnoredBooleanReturnValueLocator(
        SemanticModel model)
    {
        _semanticModel = model;
        _assignExprs = new HashSet<SyntaxKind>(
            new[] {
                SyntaxKind.AndAssignExpression,
                SyntaxKind.AssignExpression,
                SyntaxKind.ExclusiveOrAssignExpression,
                SyntaxKind.OrAssignExpression
            });
    }

    protected override void VisitMethodDeclaration(
        MethodDeclarationSyntax node)
    {
    }
} 

Почему мы посещаем объявление метода? Весь анализ представляется уместным на уровне тела метода — это облегчит ответы на такие вопросы, как «читается ли эта локальная переменная в остальной части метода?». Для начала нам нужно посмотреть на все вызовы методов в методе, который мы посещаем:

foreach (InvocationExpressionSyntax invocation in
         node.DescendentNodes()
             .OfType<InvocationExpressionSyntax>())
{
    SemanticInfo methodInfo =
        _semanticModel.GetSemanticInfo(
            invocation.Expression);
    MethodSymbol methodSym = (MethodSymbol)
        methodInfo.Symbol;

    Symbol localVariableAssigned;
    SyntaxToken localVariableInitialized;
    bool needTrack = CheckInvocation(
        invocation,
        out localVariableAssigned,
        out localVariableInitialized);
    if (!needTrack)
        continue;
    //...
}

Метод CheckInvocation проверяет, соответствует ли вызов метода одному из обнаруженных нами подозрительных шаблонов. Это требует некоторой работы, поскольку древовидные структуры этих шаблонов весьма различны. У нас есть следующие четыре случая:

  1. Родителем InvocationExpressionSyntax является ExpressionStatementSyntax , представляющий случай, когда метод вызывается без обработки результата как выражения вообще.
  2. Родителем InvocationExpressionSyntax является ForStatementSyntax , и в этом случае нам нужно проверить, является ли вызов метода прямым потомком коллекции Initializers или Incrementors в ForStatementSyntax .
  3. Родителем InvocationExpressionSyntax является BinaryExpressionSyntax , и в этом случае свойство Left должно быть IdentifierNameSyntax, представляющим локальную переменную, а ExpressionKind должен быть одним из выражений присваивания (=, && = и т. Д.). Это представляет локальную переменную, которой присваивается возвращаемое значение метода.
  4. Родителем InvocationExpressionSyntax является EqualsValueClauseSyntax, чьим родителем является VariableDeclaratorSyntax , представляющий локальную переменную, которая инициализируется возвращаемым значением метода.

В случаях № 3 и № 4 мы хотим вернуть вызывающей стороне локальную переменную для отслеживания. Здесь есть еще одно осложнение: в случае № 3 у нас есть символ, представляющий локальную переменную, о которой следует говорить, тогда как в случае № 4 у нас есть только SyntaxToken, представляющий идентификатор локальной переменной. С учетом сказанного, вот код:

private bool CheckInvocation(
    InvocationExpressionSyntax invocation,
    out Symbol localVariableAssigned,
    out SyntaxToken localVariableInitialized)
{
    localVariableAssigned = null;
    localVariableInitialized = NoToken;

    SemanticInfo info = _semanticModel.GetSemanticInfo(
        invocation);
    MethodSymbol methodSymbol = (MethodSymbol)info.Symbol;
    if (methodSymbol.ReturnType.SpecialType !=
        SpecialType.System_Boolean)
        return false;
    if (!methodSymbol.Name.Contains("Try"))
        return false;

    if (invocation.Parent is ExpressionStatementSyntax)
    {
        WARN("Invocation of {0} ignores its return value.",
             methodSymbol.Name);
        return false;
    }
    ForStatementSyntax forStmt =
        invocation.Parent as ForStatementSyntax;
    if (forStmt != null)
    {
        if (forStmt.Initializers.Contains(invocation) ||
            forStmt.Incrementors.Contains(invocation))
        {
            WARN("Invocation of {0} as part of the 'for'" +
                 "statement ignores its return value.",
                 methodSymbol.Name);
            return false;
        }
    }
    BinaryExpressionSyntax binaryExpr =
        invocation.Parent as BinaryExpressionSyntax;
    if (binaryExpr != null &&
        _assignExprs.Contains(binaryExpr.Kind))
    {
        IdentifierNameSyntax id =
            binaryExpr.Left as IdentifierNameSyntax;
        if (id != null)
        {
            Symbol symbol =
                _semanticModel.GetSemanticInfo(id).Symbol;
            if (symbol.Kind == SymbolKind.Local)
            {
                localVariableAssigned = symbol;
                return true;
            }
        }
    }
    EqualsValueClauseSyntax equalsClause =
        invocation.Parent as EqualsValueClauseSyntax;
    if (equalsClause != null)
    {
        VariableDeclaratorSyntax varDecl =
            equalsClause.Parent as
                         VariableDeclaratorSyntax;
        if (varDecl != null)
        {
            SyntaxToken localVar = varDecl.Identifier;
            localVariableInitialized = localVar;
            return true;
        }
    }

    return false;
}

В последних двух случаях вызывающая сторона должна отслеживать локальную переменную, которая инициализируется или присваивается в остальной части метода, и определять, используется ли она. Для простоты (и краткости!) Мы будем игнорировать случай, когда переменная перезаписывается перед ее чтением или считывается условно — это не означает, что эти случаи не могут быть обработаны, по крайней мере, частично ** .

Как мы собираемся выяснить, читается ли переменная? Наивным подходом было бы изучить каждый возможный синтаксический узел, который мог бы прочитать переменную. Это было бы очень сложно и подвержено ошибкам. Вместо этого Roslyn предлагает API анализа потока данных, который может отвечать на вопросы типа «эта переменная читается / записывается в этом блоке?» или «какие переменные записаны за пределами этого региона?». (Чтобы быть справедливым, есть также API анализа потока управления, не показанный здесь, который может ответить на такие вопросы, как «во всех местах, где контроль покидает этот блок?» И «каковы все целевые местоположения, в которые элемент управления поступает в этом блок?».)

Возвращаясь к нашему методу VisitMethodDeclaration , мы можем дополнить код отслеживания следующим:

RegionDataFlowAnalysis flow =
    _semanticModel.AnalyzeRegionDataFlow(
        TextSpan.FromBounds(invocation.FullSpan.End,
                            node.Span.End));
if (localVariableAssigned != null)
{
    if (!flow.ReadInside.Contains(localVariableAssigned))
    {
        WARN("The local variable {0} assigned the " +
             "return value of {1} is never read.",
             localVariableAssigned.Name, methodSym.Name);
    }
}
else if (localVariableInitialized != NoToken)
{
    VariableDeclaratorSyntax varDecl =
        (VariableDeclaratorSyntax)invocation.Parent.Parent;
    if (!flow.ReadInside.Any(
         sym => sym.Name ==
             localVariableInitialized.ValueText &&
         sym.Kind == SymbolKind.Local &&
         sym.Locations.Any(loc =>
             loc.SourceSpan.IntersectsWith(varDecl.Span))))
    {
        WARN("The local variable {0} initialized with " +
             "the return value of {1} is never read.",
             localVariableInitialized.ValueText,
             methodSym.Name);
    }
}

На этом мы завершаем анализ синтаксиса — мы с абсолютной уверенностью обнаружили несколько случаев, когда возвращаемое значение метода игнорируется. Есть некоторые случаи, которые мы не обнаруживаем, но, надеюсь, они составляют небольшое меньшинство.


* Хотя было бы интересно автоматически вставить код, который проверяет возвращаемое значение и выдает соответствующее исключение, в большинстве случаев это, вероятно, плохая идея 🙂

** Попытка решить общую проблему с абсолютной точностью просто невозможна . Теоретики компиляторов просто не могут упустить возможность обсудить неразрешимую проблему, поэтому здесь идет речь:

Проблема остановки может быть сведена к решению языка

L = { <x, P> : переменная x читается при каждом выполнении программы P }

Сокращение происходит следующим образом. Учитывая вход <T, w>, мы строим следующую программу P :

  1. int x = 0
  2. запустить T на W
  3. печать (х)

Теперь P считывает значение x тогда, когда P достигает строки # 3, если T останавливается на w . Это завершает сокращение, показывая, что язык неразрешим (потому что проблема остановки неразрешима), и оставляет только эвристику, о которой можно говорить.