SemanticCzęść NR.4

Znając już Syntax Tree API oraz API kompilacyjne powstaje pytanie, jak można uzyskać pewne semantyczne informacje na temat kodu, używając obu API.

Przykładowo chcę uzyskać informację, czy podane wyrażenie wewnątrz metody jest wyrażeniem stałym. Jeśli nim nie jest, to zapewne jest ono powiązane z działaniem specyficznej metody operacyjnej jak znak “dodawania”.

2+2

Może też wyrażenie wewnątrz metody jest po prostu odniesieniem do zmiennej. Jak to sprawdzić.

Aby sprawdzić kod pod kątem takiej semantyki, trzeba skorzystać z mechanizmu zwanego “Semantic Model”.

Oto kod zawarty w pliku “Code.txt”. Zawiera on wywołania metody Append instancji string builder.

using System;

class Hello
{
    public void Say(string s, int x, char c,double d)
    {
        StringBuilder sb = new StringBuilder();

        sb.Append(s);
        sb.Append(x);
        sb.Append(c);
        sb.Append(d);

        sb.Append("ff");
        sb.Append(1111);
        sb.Append('C');
        sb.Append(3.12);

        sb.Append(x * 2 + 1);
        sb.Append(s + x);
        sb.Append(c + 'c');
        sb.Append(d + x);

        Console.WriteLine(null);
    }
}

class GG 
{
    public void Bar()
    {
        // inside
    }
}

class G2 {

    private int AGE;
}

Mam też zadanie bonusowe. W tym kodzie są błędy, czy umiesz je znaleźć? Odpowiedź znajduje się na końcu wpisu.

Korzystając z Vistora uzyskamy wszystkie wyrażenia znajdujące wewnątrz wywołania metody Append.

Później do oceny, czym dane wyrażenie jest, będzie nam potrzebny Semantic Model.

Jak go uzyskać.  Musimy napisać kod kompilacyjny. Tym razem w nim nie będziemy tworzyć dll-ki, ale jest on potrzebny do utworzenia instancji SemanticModel.

string code = "";

using (StreamReader sr = new StreamReader("Code.txt"))
{
    code = sr.ReadToEnd();
}

var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

var mscorlib = MetadataReference.
    CreateFromAssembly(typeof(object).Assembly);

var options = new CSharpCompilationOptions
    (OutputKind.DynamicallyLinkedLibrary);

var comp = CSharpCompilation.Create("MyDemoCode")
    .AddSyntaxTrees(tree)
    .AddReferences(mscorlib)
    .WithOptions(options);

//var r = comp.Emit("demo.exe");

var model = comp.GetSemanticModel(tree);

Mamy więc już nasz model. Czas utworzyć naszego odwiedzającego kod w pliku Code.txt.

Tym razem przeciążam metodę “VisitInvocationExpression”. Jeśli wyrażenie istnieje wewnątrz inwokacji oraz identyfikator inwokacji ma nazwę “Append”, to jego zawartość jest dodawana do listy.

public class StringBulderWalker : CSharpSyntaxWalker
{
    public StringBulderWalker()
    {
        Arguments = new List<ExpressionSyntax>();
    }

    public List<ExpressionSyntax> Arguments { get; private set; }

    public override void VisitInvocationExpression(InvocationExpressionSyntax node)
    {
        var mem = node.Expression as MemberAccessExpressionSyntax;

        if (mem != null)
        {
            var type = mem.Expression as IdentifierNameSyntax;

            if (type != null && 
                mem.Name.Identifier.Text == "Append")
            {
                if (node.ArgumentList.Arguments.Count == 1)
                {
                    var arg = node.ArgumentList.Arguments.Single().Expression;

                    Arguments.Add(arg);

                    return;
                }
            }
        }

        base.VisitInvocationExpression(node);
    }
}

Jakbyś zapomniał, użycie odwiedzającego wygląda tak:

var walker2 = new StringBulderWalker();
walker2.Visit(root);

Instancja SemanticModel ma dużo ciekawych metod, w tym wpisie użyje tylko dwóch z nich.

“GetConstantValue” i “GetSymbolInfo”

SemanticModel

Dla każdego wyrażenia, które zdobiliśmy wcześniej użyjemy tych metod.

Najpierw sprawdzimy, które wyrażenia są stałymi.

foreach (var item in walker2.Arguments)
{
 
    var value = model.GetConstantValue(item);

    if (value.HasValue)
    {
        Console.WriteLine(item + " has constant value  of type "  + value.Value?.GetType().Name);
    }
    else
    {
        Console.WriteLine(item + " does not have constant value  ");
    }
}

Oto wynik:

GetConstantValue

A teraz sprawdzimy czym te wyrażenia są:

Console.WriteLine(); 
foreach (var item in walker2.Arguments) 
{ 
    var sym = model.GetSymbolInfo(item); 

    if (sym.Symbol != null) 
    { 
        Console.WriteLine(item + " is bound to " + sym.Symbol + " of type " + sym.Symbol.Kind); 
    } 
    else 
    { 
        Console.WriteLine(item + " isn't bound"); 
    } 
} 

s, x ,c d są zmiennymi. Stałe nie są powiązane z niczym. Wyrażenia matematyczne są powiązane z odpowiednimi metodami operatorów.

GetSymbolInfo

Co jeszcze SemanticModel potrafi? Potrafi  po indeksie znaku w pliku z kodem określić, na co ten wskazuje.

var cursor = code.IndexOf("inside");

var bar = model.GetEnclosingSymbol(cursor);

for (var sym = bar; sym != null; sym = sym.ContainingSymbol)
{
    Console.WriteLine(sym);
}

ContainingSymbol

Tekst “inside” znajduje się wewnątrz metody “GG.Bar()”.

A oto też mały bonus niezwiązany SemanticModel. Pisałem wcześniej, że plik Code.txt zawiera błędy. Mając instancje obiektu kompilacji mogę zobaczyć jakie są to błędy.

var diag = comp.GetDiagnostics();

foreach (var item in diag)
{
    if (item.WarningLevel == 3)
    {
        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.WriteLine(item);
    }

    if (item.WarningLevel == 0)
    {
        Console.ForegroundColor = ConsoleColor.Magenta;
        Console.WriteLine(item);
    }
}

Oto błędy i ostrzeżenia kodu zawartego w pliku Code.txt.

Debuging