Translate

Mostrando las entradas con la etiqueta Roslyn. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Roslyn. Mostrar todas las entradas

lunes, 22 de abril de 2024

Usando Roslyn ScriptEngine


La idea es correr C# como un lenguaje script y esto lo podemos hacer con Roslyn. 

Primero, instalamos las dependencias: 


dotnet add package Microsoft.CodeAnalysis.Scripting --version 4.9.2

dotnet add package Microsoft.CodeAnalysis.CSharp.Scripting --version 4.9.2


Y con eso ya estamos, podemos hacer esto: 


using System;

using Microsoft.CodeAnalysis.CSharp.Scripting;

using Microsoft.CodeAnalysis.Scripting;


class Program

{

    static async System.Threading.Tasks.Task Main(string[] args)

    {

        try

        {

            // Creamos el script

            string code = @"

                using System;


                public class MyClass

                {

                    public void MyMethod()

                    {

                        Console.WriteLine(""Hello from C#!"");

                    }

                }";


            // Creamos las opciones (usamos las por defecto) 

            ScriptOptions options = ScriptOptions.Default;


            // Creamos el motor que ejecuta el script

            var script = CSharpScript.Create(code, options);


            // corremos el script

            var result = await script.RunAsync();


            // Y chequeamos si hubo error. 

            if (result.Exception != null)

            {

                Console.WriteLine("Script execution failed: " + result.Exception.Message);

            }

        }

        catch (Exception ex)

        {

            Console.WriteLine("Error executing script: " + ex.Message);

        }

    }

}

Y listo! 

Lo unico malo es que este lenguaje C# no es completo, por ejemplo no se pueden utiliza namespace. Pero para hacer cosas chicas, esta bueno. 

Dejo link: 

viernes, 16 de febrero de 2024

Chequeando "TODOs" con Roslyn


Vamos a buscar // TODO: ... con Roslyn 

Para empezar en una aplicación de consola simple agreguemos algunos paquetes. Microsoft.CodeAnalysis.CSharp y Microsoft.CodeAnalysis.CSharp.Workspaces son lo que necesitamos.

Y hacemos esto: 


const int ExitOK = 0;

const int ExitError = 99;

const int ExitIssueFound = 1;


static async Task<int> MainAsync(string[] args)

{

var workspace = await GetWorkspace().ConfigureAwait(false);

if (workspace == null)

return ExitError;

using (workspace)

{

var issueFound = false;

foreach (var project in workspace.CurrentSolution.Projects)

{

foreach (var document in project.Documents)

{

var documentWritten = false;

var root = await document.GetSyntaxRootAsync().ConfigureAwait(false);

foreach (var item in root.DescendantTrivia().Where(x => x.IsKind(SyntaxKind.SingleLineCommentTrivia)))

{

var match = Regex.Match(item.ToFullString(), @"//\s?TODO:\s*(.*)");

if (match.Success)

{

issueFound = true;

var text = match.Groups[1].Value;

if (!documentWritten)

{

documentWritten = true;

Console.WriteLine(MinimizePath(document.FilePath));

}

var position = item.GetLocation().GetMappedLineSpan();

var line = position.StartLinePosition.Line;

Console.WriteLine($"\tL{line}:\t{text}");

}

}

}

}

return issueFound ? ExitIssueFound : ExitOK;

}

}


static async Task<Workspace> GetWorkspace()

{

var workspace = MSBuildWorkspace.Create();

var solution = Directory.EnumerateFiles(Environment.CurrentDirectory, "*.sln", SearchOption.TopDirectoryOnly).FirstOrDefault();

if (solution != null)

{

await workspace.OpenSolutionAsync(solution).ConfigureAwait(false);

return workspace;

}

var project = Directory.EnumerateFiles(Environment.CurrentDirectory, "*.csproj", SearchOption.TopDirectoryOnly).FirstOrDefault();

if (project != null)

{

await workspace.OpenProjectAsync(project).ConfigureAwait(false);

return workspace;

}

return null;

}


static string MinimizePath(string path)

{

return path.Remove(0, Environment.CurrentDirectory.Length + 1);

}


lunes, 22 de mayo de 2023

Test de arquitectura con Roslyn parte 2

Siguiento la idea del post anterior, de testear cosas con roslyn. Vamos a testear que en todo el proyecto, todos los atributos privados comiencen con _ (guión bajo) 

Bueno para esto primero necesitamos 3 dependencias más : 

    <PackageReference Include="Microsoft.Build.Locator" Version="1.5.5" />

    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.5.0" />

    <PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.5.0" />


Locator es para localizar el sdk y las librerías, pienso no estoy muy seguro que tenemos que hacer esto porque estamos en un test (pero corrijanme si estoy equivocado) 

Workspace porque vamos abrir un workspace y msbuild para compilar. (esto esta explicado en este post)

Entonces nuestro test nos queda de la siguiente manera : 

    [Test]

    public async Task The_Field_Private_Should_Start_With_Underscore()

    {

        // Arrange

        string projectPath = @"C:\projects\hat\ArchTestWithRoslyn\test\test.csproj";

        MSBuildLocator.RegisterDefaults();

        using (var workspace = MSBuildWorkspace.Create())

        {

            var project = await workspace.OpenProjectAsync(projectPath);

            var compilation = await project.GetCompilationAsync();

            foreach (var syntaxTree in compilation.SyntaxTrees)

            {

                var nodeRoot = syntaxTree.GetRoot();

                var fields = nodeRoot.DescendantNodes()

                    .OfType<FieldDeclarationSyntax>()

                    .Where(field => field.Modifiers

                                        .Any(modify => 

                                            modify.Kind().Equals(SyntaxKind.PrivateKeyword))

                    && field.Declaration.Variables

                        .Any(aVar => !aVar.Identifier.ValueText.StartsWith("_"))

                    );

                // Assert

                Assert.IsTrue(!fields.Any());

            }

        }

    }


Este test lo que hace es crear un workspace, importar el proyecto (podemos importar soluciones si quisieramos) y luego obtiene el o los arboles sintacticos y luego busca si existe algun atributo privado que no comience con "_".  


Y listo!! 

viernes, 19 de mayo de 2023

Emit API de Roslyn

Hasta ahora, hemos analizado principalmente cómo podemos usar Roslyn para analizar y manipular el código fuente. Ahora veremos cómo finalizar el proceso de compilación emitiéndolo en el disco o en la memoria. Para comenzar, solo intentaremos emitir una compilación simple al disco y verificar si tuvo éxito o no.

var tree = CSharpSyntaxTree.ParseText(@"

using System;

public class C

{

    public static void Main()

    {

        Console.WriteLine(""Hello World!"");

        Console.ReadLine();

    }   

}");


var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);

var compilation = CSharpCompilation.Create("MyCompilation",

    syntaxTrees: new[] { tree }, references: new[] { mscorlib });


//Emitting to file is available through an extension method in the Microsoft.CodeAnalysis namespace

var emitResult = compilation.Emit("output.exe", "output.pdb");


//If our compilation failed, we can discover exactly why.

if(!emitResult.Success)

{

    foreach(var diagnostic in emitResult.Diagnostics)

    {

        Console.WriteLine(diagnostic.ToString());

    }

}


Después de ejecutar este código, podemos ver que nuestro ejecutable y .pdb se han emitido a Debug/bin/. Podemos hacer doble clic en output.exe y ver que nuestro programa se ejecuta como se esperaba. El archivo .pdb es opcional. Escribir el archivo .pdb en el disco puede llevar bastante tiempo y, a menudo, vale la pena omitir este argumento a menos que realmente se necesite.

A veces es posible que no queramos emitir a disco. Es posible que solo queramos compilar el código, enviarlo a la memoria y luego ejecutarlo desde la memoria. Para esto, la API de script probablemente tenga más sentido de usar. Aún así, vale la pena conocer nuestras opciones.

var tree = CSharpSyntaxTree.ParseText(@"

using System;

public class MyClass

{

    public static void Main()

    {

        Console.WriteLine(""Hello World!"");

        Console.ReadLine();

    }   

}");


var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);

var compilation = CSharpCompilation.Create("MyCompilation",

    syntaxTrees: new[] { tree }, references: new[] { mscorlib });


//Emit to stream

var ms = new MemoryStream();

var emitResult = compilation.Emit(ms);


//Load into currently running assembly. Normally we'd probably

//want to do this in an AppDomain

var ourAssembly = Assembly.Load(ms.ToArray());

var type = ourAssembly.GetType("MyClass");


//Invokes our main method and writes "Hello World" 🙂

type.InvokeMember("Main", BindingFlags.Default | BindingFlags.InvokeMethod, null, null, null);


Finalmente, ¿qué pasa si queremos influir en cómo se compila nuestro código? Es posible que queramos permitir código no seguro, marcar advertencias como errores o retrasar la firma del ensamblado. Todas estas opciones se pueden personalizar pasando un objeto CSharpCompilationOptions a CSharpCompilation.Create(). Echaremos un vistazo a cómo podemos interactuar con algunas de estas propiedades a continuación.


var tree = CSharpSyntaxTree.ParseText(@"

using System;

public class MyClass

{

    public static void Main()

    {

        Console.WriteLine(""Hello World!"");

        Console.ReadLine();

    }   

}");


//We first have to choose what kind of output we're creating: DLL, .exe etc.

var options = new CSharpCompilationOptions(OutputKind.ConsoleApplication);

options = options.WithAllowUnsafe(true);                                //Allow unsafe code;

options = options.WithOptimizationLevel(OptimizationLevel.Release);     //Set optimization level

options = options.WithPlatform(Platform.X64);                           //Set platform


var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);

var compilation = CSharpCompilation.Create("MyCompilation",

    syntaxTrees: new[] { tree },

    references: new[] { mscorlib },

    options: options);   


En total hay unas veinticinco opciones diferentes disponibles para la personalización. Básicamente, cualquier opción que tiene la página de propiedades del proyecto de Visual Studio debería estar disponible aquí.

Hay algunos parámetros opcionales disponibles en Compilation.Emit() que vale la pena analizar. 

xmlDocPath: genera automáticamente documentación XML basada en los comentarios de documentación presentes en sus clases, métodos, propiedades, etc.

manifestResources: le permite incrustar manualmente recursos como cadenas e imágenes dentro del ensamblaje emitido. 

win32ResourcesPath: ruta del archivo desde el que se leerán los recursos Win32 de la compilación (en formato RES). 


miércoles, 17 de mayo de 2023

SymbolVisitor

SymbolVisitor es el análogo de SyntaxVisitor, pero se aplica a nivel de símbolo. Desafortunadamente, a diferencia de SyntaxWalker y CSharpSyntaxRewriter, cuando usamos SymbolVisitor debemos construir el código de andamiaje para visitar todos los nodos.

Para enumerar simplemente todos los tipos disponibles para una compilación, podemos usar lo siguiente.


public class NamedTypeVisitor : SymbolVisitor

{

    public override void VisitNamespace(INamespaceSymbol symbol)

    {

        Console.WriteLine(symbol);

        

        foreach(var childSymbol in symbol.GetMembers())

        {

            //We must implement the visitor pattern ourselves and 

            //accept the child symbols in order to visit their children

            childSymbol.Accept(this);

        }

    }


    public override void VisitNamedType(INamedTypeSymbol symbol)

    {

        Console.WriteLine(symbol);

        

        foreach (var childSymbol in symbol.GetTypeMembers())

        {

            //Once againt we must accept the children to visit 

            //all of their children

            childSymbol.Accept(this);

        }

    }

}


//Now we need to use our visitor

var tree = CSharpSyntaxTree.ParseText(@"

class MyClass

{

    class Nested

    {

    }

    void M()

    {

    }

}");


var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);

var compilation = CSharpCompilation.Create("MyCompilation",

    syntaxTrees: new[] { tree }, references: new[] { mscorlib });


var visitor = new NamedTypeVisitor();

visitor.Visit(compilation.GlobalNamespace);


Para visitar todos los métodos disponibles para una compilación dada podemos utilizar los siguientes:


public class MethodSymbolVisitor : SymbolVisitor

{

    //NOTE: We have to visit the namespace's children even though

    //we don't care about them. 😦

    public override void VisitNamespace(INamespaceSymbol symbol)

    {

        foreach(var child in symbol.GetMembers())

        {

            child.Accept(this);

        }

    }

    

    //NOTE: We have to visit the named type's children even though

    //we don't care about them. 😦

    public override void VisitNamedType(INamedTypeSymbol symbol)

    {

        foreach(var child in symbol.GetMembers())

        {

            child.Accept(this);

        }

    }


    public override void VisitMethod(IMethodSymbol symbol)

    {

        Console.WriteLine(symbol);

    }

}

Es importante tener en cuenta cómo  se debe estructurar el código para poder visitar todos los símbolos que interesan. Si estoy interesado en visitar símbolos de métodos, no quiero tener que escribir código que visite espacios de nombres y tipos.

Con suerte, en algún momento obtendremos una clase SymbolWalker que podamos usar para separar nuestra implementación del código transversal. 

¿Cómo obtenemos una lista de todos los tipos disponibles para una compilación? así :


public class CustomSymbolFinder

{

    public List<INamedTypeSymbol> GetAllSymbols(Compilation compilation)

    {

        var visitor = new FindAllSymbolsVisitor();

        visitor.Visit(compilation.GlobalNamespace);

        return visitor.AllTypeSymbols;

    }


    private class FindAllSymbolsVisitor : SymbolVisitor

    {

        public List<INamedTypeSymbol> AllTypeSymbols { get; } = new List<INamedTypeSymbol>();


        public override void VisitNamespace(INamespaceSymbol symbol)

        {

            Parallel.ForEach(symbol.GetMembers(), s => s.Accept(this));

        }


        public override void VisitNamedType(INamedTypeSymbol symbol)

        {

            AllTypeSymbols.Add(symbol);

            foreach (var childSymbol in symbol.GetTypeMembers())

            {

                base.Visit(childSymbol);

            }

        }

    }

}

La clase SymbolVisitor probablemente sea apropiada para usos únicos durante la compilación o para visitar un subconjunto de símbolos disponibles. Por lo menos, vale la pena ser consciente de ello.


jueves, 11 de mayo de 2023

Scripting API de Roslyn

Para instalar la API de Scripting en el proyecto simplemente hay que ejecutar:

Install-Package Microsoft.CodeAnalysis.Scripting -Pre

CSharpScript.EvaluateAsync es probablemente la forma más sencilla de empezar a evaluar expresiones. 


var result = await CSharpScript.EvaluateAsync("5 + 5");

Console.WriteLine(result); // 10


result = await CSharpScript.EvaluateAsync(@"""sample""");

Console.WriteLine(result); // sample


result = await CSharpScript.EvaluateAsync(@"""sample"" + "" string""");

Console.WriteLine(result); // sample string


result = await CSharpScript.EvaluateAsync("int x = 5; int y = 5; x"); //Note the last x is not contained in a proper statement

Console.WriteLine(result); // 5

No todos los scripts devuelven un único valor. Para secuencias de comandos más complejos, es posible que deseemos realizar un seguimiento del estado o inspeccionar diferentes variables. CSharpScript.RunAsync crea y devuelve un objeto ScriptState que nos permite hacer exactamente esto. 


var state = CSharpScript.RunAsync(@"int x = 5; int y = 3; int z = x + y;""");

ScriptVariable x = state.Variables["x"];

ScriptVariable y = state.Variables["y"];


Console.Write($"{x.Name} : {x.Value} : {x.Type} "); // x : 5

Console.Write($"{y.Name} : {y.Value} : {y.Type} "); // y : 3


También podemos mantener el estado de nuestro script y continuar aplicándole cambios con ScriptState.ContinueWith():


var state = CSharpScript.RunAsync(@"int x = 5; int y = 3; int z = x + y;""").Result;

state = state.ContinueWithAsync("x++; y = 1;").Result;

state = state.ContinueWithAsync("x = x + y;").Result;


ScriptVariable x = state.Variables["x"];

ScriptVariable y = state.Variables["y"];


Console.Write($"{x.Name} : {x.Value} : {x.Type} "); // x : 7

Console.Write($"{y.Name} : {y.Value} : {y.Type} "); // y : 1



Podemos agregar referencias a los archivos DLL que nos gustaría usar. Usamos ScriptOptions para proporcionar nuestro script con las MetadataReferences adecuadas.

ScriptOptions scriptOptions = ScriptOptions.Default;

//Add reference to mscorlib
var mscorlib = typeof(System.Object).Assembly;
var systemCore = typeof(System.Linq.Enumerable).Assembly;
scriptOptions = scriptOptions.AddReferences(mscorlib, systemCore);
//Add namespaces
scriptOptions = scriptOptions.AddNamespaces("System");
scriptOptions = scriptOptions.AddNamespaces("System.Linq");
scriptOptions = scriptOptions.AddNamespaces("System.Collections.Generic");

var state = await CSharpScript.RunAsync(@"var x = new List(){1,2,3,4,5};", scriptOptions);
state = await state.ContinueWithAsync("var y = x.Take(3).ToList();");

var y = state.Variables["y"];
var yList = (List)y.Value;
foreach(var val in yList)
{
  Console.Write(val + " "); // Prints 1 2 3
}

Este material es sorprendentemente amplio. El espacio de nombres Microsoft.CodeAnalysis.Scripting está lleno de tipos.


lunes, 8 de mayo de 2023

SyntaxAnnotation

Puede ser complicado realizar un seguimiento de los nodos al aplicar cambios a los árboles de sintaxis. Cada vez que "cambiamos" un árbol, en realidad estamos creando una copia del mismo con nuestros cambios aplicados a ese nuevo árbol. En el momento en que hacemos eso, cualquier pieza de sintaxis a la que tuviéramos referencias anteriormente se vuelve inválida en el contexto del nuevo árbol.

¿Qué significa esto en la práctica? Es difícil hacer un seguimiento de los nodos de sintaxis cuando cambiamos los árboles de sintaxis.

Una pregunta reciente de Stack Overflow se refirió a esto. ¿Cómo podemos obtener el símbolo de una clase que acabamos de agregar a un documento? Podemos crear una nueva declaración de clase, pero en el momento en que la agregamos al documento, perdemos el rastro del nodo. Entonces, ¿cómo podemos realizar un seguimiento de la clase para que podamos obtener el símbolo una vez que la hayamos agregado al documento?

La respuesta: usar una anotación de sintaxis

Una SyntaxAnnotation es básicamente una pieza de metadatos que podemos adjuntar a una pieza de sintaxis. A medida que manipulamos el árbol, la anotación se adhiere a esa parte de la sintaxis, lo que facilita su búsqueda.


AdhocWorkspace workspace = new AdhocWorkspace();

Project project = workspace.AddProject("SampleProject", LanguageNames.CSharp);


//Attach a syntax annotation to the class declaration

var syntaxAnnotation = new SyntaxAnnotation();

var classDeclaration = SyntaxFactory.ClassDeclaration("MyClass")

.WithAdditionalAnnotations(syntaxAnnotation);


var compilationUnit = SyntaxFactory.CompilationUnit().AddMembers(classDeclaration);


Document document = project.AddDocument("SampleDocument.cs", compilationUnit);

SemanticModel semanticModel = document.GetSemanticModelAsync().Result;


//Use the annotation on our original node to find the new class declaration

var changedClass = document.GetSyntaxRootAsync().Result.DescendantNodes().OfType<ClassDeclarationSyntax>()

.Where(n => n.HasAnnotation(syntaxAnnotation)).Single();

var symbol = semanticModel.GetDeclaredSymbol(changedClass);



Hay un par de sobrecargas disponibles al crear una SyntaxAnnotation. Podemos especificar Tipo y Datos para adjuntarlos a piezas de sintaxis. Los datos se utilizan para adjuntar información adicional a una parte de la sintaxis que nos gustaría recuperar más tarde. Tipo es un campo que podemos usar para buscar anotaciones de sintaxis.

Entonces, en lugar de buscar la instancia exacta de nuestra anotación en cada nodo, podríamos buscar anotaciones según su tipo:

AdhocWorkspace workspace = new AdhocWorkspace();
Project project = workspace.AddProject("Test", LanguageNames.CSharp);

string annotationKind = "SampleKind";
var syntaxAnnotation = new SyntaxAnnotation(annotationKind);
var classDeclaration = SyntaxFactory.ClassDeclaration("MyClass")
.WithAdditionalAnnotations(syntaxAnnotation);

var compilationUnit = SyntaxFactory.CompilationUnit().AddMembers(classDeclaration);

Document document = project.AddDocument("Test.cs", compilationUnit);
SemanticModel semanticModel = await document.GetSemanticModelAsync();
var newAnnotation = new SyntaxAnnotation("test");

//Just search for the Kind instead
var root = await document.GetSyntaxRootAsync();
var changedClass = root.GetAnnotatedNodes(annotationKind).Single();

var symbol = semanticModel.GetDeclaredSymbol(changedClass);


Esta es solo una de las pocas formas diferentes de lidiar con los árboles inmutables de Roslyn. Probablemente no sea el más fácil de usar si está realizando varios cambios y necesita realizar un seguimiento de varios nodos de sintaxis. (Si ese es el caso, recomendaría el DocumentEditor). Dicho esto, es bueno tenerlo en cuenta para que pueda usarlo cuando tenga sentido.

domingo, 7 de mayo de 2023

Test de arquitectura con Roslyn

Supongamos que queremos testear diferentes reglas que debe seguir el código general, por ejemplo "todos los nombres de los parametros de los metodos de los servicios deben finalizar con DTO" 

Para esto podemos utilizar Roslyn, veamos un ejemplo: 


     [Test]

    public void The_Parameters_Of_Service_Methods_Should_End_With_DTO()

    {

        // Arrange

        var program = @" public class ModelClassExample

                         {

                         } 


                         public class ExampleService

                         {

                             public bool Test1Method() => true;

                             public bool Test2Method(ModelClassExample example) => true;

                         }";


        var nodeRoot = GetNodeRoot(program);

        var classes = nodeRoot.DescendantNodes()

            .OfType<ClassDeclarationSyntax>()

            .Where(clazz => clazz.Identifier

                                .ToString().EndsWith("Service")

                            && clazz.DescendantNodes()

                                .OfType<MethodDeclarationSyntax>()

                                .Any(method => method.ParameterList.Parameters

                                    .Any(

                                    parameter => !parameter.Identifier.ToString().EndsWith("DTO")

                                )));

        

        // Assert

        Assert.IsTrue(!classes.Any());

    }


Este test no termina bien. Veamos que hace. 

Primero utilizo una variable de tipo string "program" donde pongo mi código, en un ejemplo más real podria leer el archivo que tiene el código o mejor aun traerme todo el projecto con Roslyn. 

Segundo genero el SyntaxTree y retorno el nodo root con esta función que hice : 

    private SyntaxNode GetNodeRoot(string program) {

        var tree = CSharpSyntaxTree.ParseText(program);

        return tree.GetRoot();

    }

Tercero, busco las clases que su nombre terminan en "Service" y me fijo en esas clases si hay un metodo que tenga un parametro que no termine en "DTO". 

Por ultimo chequeo que no haya ninguna clase que rompa la regla, en este caso si la hay porque en mi código, si hay una clase que finaliza en service y tiene un metodo que tiene un parametro que no finaliza en DTO. 

 


jueves, 4 de mayo de 2023

Edición de documentos con DocumentEditor

Una desventaja de la inmutabilidad de Roslyn es que a veces puede resultar complicado aplicar varios cambios a un documento o árbol de sintaxis. La inmutabilidad significa que cada vez que aplicamos cambios a un árbol de sintaxis, se nos proporciona un árbol de sintaxis completamente nuevo. De forma predeterminada, no podemos comparar nodos entre árboles, entonces, ¿qué hacemos cuando queremos realizar varios cambios en un árbol de sintaxis?

Roslyn nos da cuatro opciones:

  • Use CSharpSyntaxRewriter
  • Usar anotaciones 
  • Usar TrackNodes()
  • Usar el DocumentEditor

El Editor de documentos nos permite realizar múltiples cambios en un documento y obtener el documento resultante después de que se hayan aplicado los cambios. El DocumentEditor es una clase que hereda de SyntaxEditor.

Usaremos el DocumentEditor para cambiar:


char key = Console.ReadKey();

if(key == 'A')

{

    Console.WriteLine("You pressed A");

}

else

{

    Console.WriteLine("You didn't press A");

}


a:


char key = Console.ReadKey();

if(key == 'A')

{

    LogConditionWasTrue();

    Console.WriteLine("You pressed A");

}

else

{

    Console.WriteLine("You didn't press A");

    LogConditionWasFalse();

}


Usaremos DocumentEditor para insertar simultáneamente una invocación antes de la primera Console.WriteLine() y para insertar otra después de la segunda.

 Por lo general, obtendrá un documento de un espacio de trabajo :


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

var workspace = new AdhocWorkspace();

var projectId = ProjectId.CreateNewId();

var versionStamp = VersionStamp.Create();

var projectInfo = ProjectInfo.Create(projectId, versionStamp, "NewProject", "projName", LanguageNames.CSharp);

var newProject = workspace.AddProject(projectInfo);

var sourceText = SourceText.From(@"

class C

{

    void M()

    {

        char key = Console.ReadKey();

        if (key == 'A')

        {

            Console.WriteLine(""You pressed A"");

        }

        else

        {

            Console.WriteLine(""You didn't press A"");

        }

    }

}");

var document = workspace.AddDocument(newProject.Id, "NewFile.cs", sourceText);

var syntaxRoot = await document.GetSyntaxRootAsync();

var ifStatement = syntaxRoot.DescendantNodes().OfType<IfStatementSyntax>().Single();


var conditionWasTrueInvocation =

SyntaxFactory.ExpressionStatement(

    SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("LogConditionWasTrue"))

    .WithArgumentList(

                    SyntaxFactory.ArgumentList()

                    .WithOpenParenToken(

                        SyntaxFactory.Token(

                            SyntaxKind.OpenParenToken))

                    .WithCloseParenToken(

                        SyntaxFactory.Token(

                            SyntaxKind.CloseParenToken))))

            .WithSemicolonToken(

                SyntaxFactory.Token(

                    SyntaxKind.SemicolonToken));


var conditionWasFalseInvocation =

SyntaxFactory.ExpressionStatement(

    SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("LogConditionWasFalse"))

    .WithArgumentList(

                    SyntaxFactory.ArgumentList()

                    .WithOpenParenToken(

                        SyntaxFactory.Token(

                            SyntaxKind.OpenParenToken))

                    .WithCloseParenToken(

                        SyntaxFactory.Token(

                            SyntaxKind.CloseParenToken))))

            .WithSemicolonToken(

                SyntaxFactory.Token(

                    SyntaxKind.SemicolonToken));


//Finally… create the document editor

var documentEditor = await DocumentEditor.CreateAsync(document);

//Insert LogConditionWasTrue() before the Console.WriteLine()

documentEditor.InsertBefore(ifStatement.Statement.ChildNodes().Single(), conditionWasTrueInvocation);

//Insert LogConditionWasFalse() after the Console.WriteLine()

documentEditor.InsertAfter(ifStatement.Else.Statement.ChildNodes().Single(), conditionWasFalseInvocation);


var newDocument = documentEditor.GetChangedDocument();


Todos los métodos familiares de SyntaxNode están aquí. Podemos Insertar, Reemplazar y Eliminar nodos como mejor nos parezca, todo basado en nodos en nuestro árbol de sintaxis original. Mucha gente encuentra este enfoque más intuitivo que construir un CSharpSyntaxRewriter completo.

¿Cómo podemos saber qué tipos de nodos son compatibles entre sí? No creo que haya una buena respuesta aquí. Esencialmente, tenemos que aprender qué nodos son compatibles nosotros mismos. Como de costumbre, Syntax Visualizer y Roslyn Quoter son las mejores herramientas para determinar qué tipo de nodos debe crear.

Vale la pena señalar que DocumentEditor expone el SemanticModel de su documento original. Es posible que necesite esto cuando edite el documento original y tome decisiones sobre lo que le gustaría cambiar.

También vale la pena señalar que el SyntaxEditor subyacente expone un SyntaxGenerator que puede usar para crear nodos de sintaxis sin depender de SyntaxFactory, que es más detallado.

miércoles, 3 de mayo de 2023

Introducción a el modelo semantico de Roslyn

Hasta este punto, hemos estado trabajando con código C# en un nivel puramente sintáctico. Podemos encontrar declaraciones de propiedades, pero no podemos rastrear referencias a esta propiedad dentro de nuestro código fuente. Podemos identificar invocaciones, pero no podemos decir qué se está invocando. Y Dios nos ayude si queremos tratar de resolver los problemas realmente difíciles como la resolución de sobrecarga.

La capa semántica es donde realmente brilla el poder de Roslyn. El modelo semántico de Roslyn puede responder todas las preguntas difíciles de tiempo de compilación que podamos tener. Sin embargo, este poder tiene un costo. Consultar el modelo semántico suele ser más costoso que consultar los árboles de sintaxis. Esto se debe a que solicitar un modelo semántico a menudo desencadena una compilación.

Hay 3 formas diferentes de solicitar el modelo semántico:

1. Document.GetSemanticModel()

2. Compilation.GetSemanticModel(SyntaxTree)

3. Diferentes contextos como AnalysisContexts, CodeBlockStartAnalysisContext.SemanticModel y SemanticModelAnalysisContext.SemanticModel

Para evitar el problema de configurar nuestro propio espacio de trabajo, simplemente crearemos compilaciones para árboles de sintaxis individuales de la siguiente manera:


var tree = CSharpSyntaxTree.ParseText(@"

public class MyClass 

{

int MyMethod() { return 0; }

}");


var Mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);

var compilation = CSharpCompilation.Create("MyCompilation",

syntaxTrees: new[] { tree }, references: new[] { Mscorlib });

var model = compilation.GetSemanticModel(tree);


Antes de continuar, vale la pena tomarse un momento para analizar los Símbolos. Los programas de C# se componen de elementos únicos, como tipos, métodos, propiedades, etc. Los símbolos representan casi todo lo que el compilador sabe sobre cada uno de estos elementos únicos.

En un nivel alto, cada símbolo contiene información sobre:

  • Donde estos elementos se declaran en fuente o metadatos (Puede haber venido de un ensamblado externo)
  • Dentro de qué espacio de nombres y tipo existe este símbolo
  • Varias verdades acerca de que el símbolo es abstracto, estático, sellado, etc.

También se puede descubrir otra información más dependiente del contexto. Cuando se trata de métodos, IMethodSymbol nos permite determinar:

  • Si el método oculta un método base.
  • El símbolo que representa el tipo de retorno del método.
  • El método de extensión del que se sobreescribio este símbolo.

El modelo semántico es nuestro puente entre el mundo de la sintaxis y el mundo de los símbolos.

SemanticModel.GetDeclaredSymbol() acepta la sintaxis de declaración y proporciona el símbolo correspondiente.

SemanticModel.GetSymbolInfo() acepta la sintaxis de expresión (p. ej., InvocationExpressionSyntax) y devuelve un símbolo. Si el modelo no pudo resolver con éxito un símbolo, proporciona símbolos candidatos que pueden servir como mejores conjeturas.

A continuación, recuperamos el símbolo de un método a través de su sintaxis de declaración. Luego recuperamos el mismo símbolo, pero a través de una invocación (InvocaciónExpresiónSyntax) en su lugar.


var tree = CSharpSyntaxTree.ParseText(@"

public class MyClass {

int Method1() { return 0; }

void Method2()

{

int x = Method1();

}

}

}");


var Mscorlib = PortableExecutableReference.CreateFromAssembly(typeof(object).Assembly);

var compilation = CSharpCompilation.Create("MyCompilation",

syntaxTrees: new[] { tree }, references: new[] { Mscorlib });

var model = compilation.GetSemanticModel(tree);


//Looking at the first method symbol

var methodSyntax = tree.GetRoot().DescendantNodes().OfType<MethodDeclarationSyntax>().First();

var methodSymbol = model.GetDeclaredSymbol(methodSyntax);


Console.WriteLine(methodSymbol.ToString());         //MyClass.Method1()

Console.WriteLine(methodSymbol.ContainingSymbol);   //MyClass

Console.WriteLine(methodSymbol.IsAbstract);         //false


//Looking at the first invocation

var invocationSyntax = tree.GetRoot().DescendantNodes().OfType<InvocationExpressionSyntax>().First();

var invokedSymbol = model.GetSymbolInfo(invocationSyntax).Symbol; //Same as MyClass.Method1


Console.WriteLine(invokedSymbol.ToString());         //MyClass.Method1()

Console.WriteLine(invokedSymbol.ContainingSymbol);   //MyClass

Console.WriteLine(invokedSymbol.IsAbstract);         //false


Console.WriteLine(invokedSymbol.Equals(methodSymbol)); //true


Una instancia de SemanticModel almacena en caché símbolos locales e información semántica. Por lo tanto, es mucho más eficiente usar una sola instancia de SemanticModel cuando se hacen varias preguntas sobre un árbol de sintaxis, porque la información de la primera pregunta se puede reutilizar. Esto también significa que mantener una instancia de SemanticModel durante mucho tiempo puede evitar que una cantidad significativa de memoria se recopile como basura.

Esencialmente, Roslyn le permite hacer el equilibrio entre la memoria y la computación. Al consultar el modelo semántico de forma repetitiva, puede ser de su interés mantener una instancia del mismo, en lugar de solicitar un nuevo modelo de una compilación o documento.

jueves, 27 de abril de 2023

Trabajando con Workspaces con Roslyn

Hasta este punto, simplemente hemos estado construyendo árboles de sintaxis a partir de cadenas. Este enfoque funciona bien cuando se crean muestras cortas, pero a menudo nos gustaría trabajar con soluciones completas. 

Los Workspaces son el nodo raíz de una jerarquía de C# que consta de una solución, proyectos secundarios y documentos secundarios. Un principio fundamental dentro de Roslyn es que la mayoría de los objetos son inmutables. Esto significa que no podemos aferrarnos a una referencia a una solución y esperar que esté actualizada para siempre. En el momento en que se realice un cambio, esta solución quedará obsoleta y se habrá creado una nueva solución actualizada. Los espacios de trabajo son nuestro nodo raíz. A diferencia de las soluciones, los proyectos y los documentos, no dejarán de ser válidos y siempre contendrán una referencia a la solución actual más actualizada. Hay cuatro variantes de Workspace a considerar:

Workspaces:  La clase base abstracta para todos los demás espacios de trabajo. Es un poco falso afirmar que es una variante del espacio de trabajo, ya que nunca tendrás una instancia de ella. En cambio, esta clase sirve como una especie de API en torno a la cual se pueden crear implementaciones de espacios de trabajo reales. Puede ser tentador pensar en áreas de trabajo únicamente dentro del contexto de Visual Studio. Después de todo, para la mayoría de los desarrolladores de C#, esta es la única forma en que hemos tratado las soluciones y los proyectos. Sin embargo, Workspace está destinado a ser agnóstico en cuanto a la fuente física de los archivos que representa. Las implementaciones individuales pueden almacenar los archivos en el sistema de archivos local, dentro de una base de datos o incluso en una máquina remota. Uno simplemente hereda de esta clase y anula las implementaciones vacías de Workspace como mejor le parezca.

MSBuildWorkspace:  Un espacio de trabajo creado para manejar archivos de solución (.sln) y proyecto (.csproj, .vbproj) de MSBuild. Desafortunadamente, actualmente no puede escribir en archivos .sln, lo que significa que no podemos usarlo para agregar proyectos o crear nuevas soluciones.

El siguiente ejemplo muestra cómo podemos iterar sobre todos los documentos en una solución:


string solutionPath = @"C:\Users\…\PathToSolution\MySolution.sln";

var msWorkspace = MSBuildWorkspace.Create();


var solution = msWorkspace.OpenSolutionAsync(solutionPath).Result;

foreach (var project in solution.Projects)

{

foreach (var document in project.Documents)

{

Console.WriteLine(project.Name + "\t\t\t" + document.Name);

}

}


AdhocWorkspace : Un espacio de trabajo que permite agregar archivos de solución y proyecto manualmente. Se debe tener en cuenta que la API para agregar y eliminar elementos de la solución es diferente dentro de AdhocWorkspace en comparación con los otros espacios de trabajo. En lugar de llamar a TryApplyChanges(), se proporcionan métodos para agregar proyectos y documentos en el nivel del espacio de trabajo. Este espacio de trabajo está destinado a aquellos que solo necesitan una forma rápida y sencilla de crear un espacio de trabajo y agregarle proyectos y documentos.


var workspace = new AdhocWorkspace();

string projName = "NewProject";

var projectId = ProjectId.CreateNewId();

var versionStamp = VersionStamp.Create();

var projectInfo = ProjectInfo.Create(projectId, versionStamp, projName, projName, LanguageNames.CSharp);

var newProject = workspace.AddProject(projectInfo);

var sourceText = SourceText.From("class A {}");

var newDocument = workspace.AddDocument(newProject.Id, "NewFile.cs", sourceText);


foreach (var project in workspace.CurrentSolution.Projects)

{

foreach (var document in project.Documents)

{

Console.WriteLine(project.Name + "\t\t\t" + document.Name);

}

}

VisualStudioWorkspace : El espacio de trabajo activo consumido dentro de los paquetes de Visual Studio. Como este espacio de trabajo está estrechamente integrado con Visual Studio, es difícil proporcionar un pequeño ejemplo sobre cómo usar este espacio de trabajo. 



lunes, 24 de abril de 2023

Analisis del flujo de control

El análisis de flujo de control se utiliza para comprender los diversos puntos de entrada y salida dentro de un bloque de código y para responder preguntas sobre accesibilidad. Si estamos analizando un método, es posible que nos interesen todos los puntos return . Si estamos analizando un bucle for, es posible que nos interesen todos los lugares en los que hacemos break o continue.

Activamos el análisis de flujo de control a través de un método de SemanticModel. Esto nos devuelve una instancia de ControlFlowAnalysis que expone las siguientes propiedades:

EntryPoints: el conjunto de declaraciones dentro de la región que son el destino de las sucursales fuera de la región.

ExitPoints: el conjunto de declaraciones dentro de una región que salta a ubicaciones fuera de la región.

EndPointIsReachable: indica si una región se completa normalmente. Devuelve verdadero si y solo si se puede llegar al final de la última declaración o si toda la región no contiene declaraciones.

StartPointIsReachable: indica si una región puede comenzar normalmente.

ReturnStatements: el conjunto de declaraciones de devolución dentro de una región.

Succeeded: devuelve verdadero si y solo si el análisis fue exitoso. El análisis puede fallar si la región no abarca correctamente una sola expresión, una sola declaración o una serie contigua de declaraciones dentro del bloque adjunto.


Uso básico de la API:


var tree = CSharpSyntaxTree.ParseText(@"

    class C

    {

        void M()

        {

            for (int i = 0; i < 10; i++)

            {

                if (i == 3)

                    continue;

                if (i == 8)

                    break;

            }

        }

    }

");


var Mscorlib = PortableExecutableReference.CreateFromAssembly(typeof(object).Assembly);

var compilation = CSharpCompilation.Create("MyCompilation",

    syntaxTrees: new[] { tree }, references: new[] { Mscorlib });

var model = compilation.GetSemanticModel(tree);


var firstFor = tree.GetRoot().DescendantNodes().OfType<ForStatementSyntax>().Single();

ControlFlowAnalysis result = model.AnalyzeControlFlow(firstFor.Statement);


Console.WriteLine(result.Succeeded);            //True

Console.WriteLine(result.ExitPoints.Count());    //2 – continue, and break


Alternativamente, podemos especificar dos declaraciones y analizar las declaraciones entre los dos. El siguiente ejemplo demuestra esto y el uso de EntryPoints:


var tree = CSharpSyntaxTree.ParseText(@"

class C

{

    void M(int x)

    {

        L1: ; // 1

        if (x == 0) goto L1;    //firstIf

        if (x == 1) goto L2;

        if (x == 3) goto L3;

        L3: ;                   //label3

        L2: ; // 2

        if(x == 4) goto L3;

    }

}

");


var Mscorlib = PortableExecutableReference.CreateFromAssembly(typeof(object).Assembly);

var compilation = CSharpCompilation.Create("MyCompilation",

syntaxTrees: new[] { tree }, references: new[] { Mscorlib });

var model = compilation.GetSemanticModel(tree);


//Choose first and last statements

var firstIf = tree.GetRoot().DescendantNodes().OfType<IfStatementSyntax>().First();

var label3 = tree.GetRoot().DescendantNodes().OfType<LabeledStatementSyntax>().Skip(1).Take(1).Single();


ControlFlowAnalysis result = model.AnalyzeControlFlow(firstIf, label3);

Console.WriteLine(result.EntryPoints);      //1 – Label 3 is a candidate entry point within these statements

Console.WriteLine(result.ExitPoints);       //2 – goto L1 and goto L2 and candidate exit points


En el ejemplo anterior, vemos un ejemplo de una posible etiqueta de punto de entrada L3. Que yo sepa, las etiquetas son los únicos puntos de entrada posibles.

Finalmente, veremos cómo responder preguntas sobre la accesibilidad. A continuación, no se puede alcanzar ni el punto inicial ni el punto final:

var tree = CSharpSyntaxTree.ParseText(@"

    class C

    {

        void M(int x)

        {

            return;

            if(x == 0)                                  //-+     Start is unreachable

                System.Console.WriteLine(""Hello"");    // |

            L1:                                            //-+    End is unreachable

        }

    }

");


var Mscorlib = PortableExecutableReference.CreateFromAssembly(typeof(object).Assembly);

var compilation = CSharpCompilation.Create("MyCompilation",

    syntaxTrees: new[] { tree }, references: new[] { Mscorlib });

var model = compilation.GetSemanticModel(tree);


//Choose first and last statements

var firstIf = tree.GetRoot().DescendantNodes().OfType<IfStatementSyntax>().Single();

var label1 = tree.GetRoot().DescendantNodes().OfType<LabeledStatementSyntax>().Single();


ControlFlowAnalysis result = model.AnalyzeControlFlow(firstIf, label1);

Console.WriteLine(result.StartPointIsReachable);    //False

Console.WriteLine(result.EndPointIsReachable);      //False


En general, la API de flujo de control parece mucho más intuitiva que la API de análisis de flujo de datos. Requiere menos conocimiento de la especificación de C# y es sencillo trabajar con él. 


jueves, 20 de abril de 2023

CSharpSyntaxRewriter

En el post anterior hablamos de CSharpSyntaxWalker y cómo podríamos navegar el árbol de sintaxis con el patrón de visitor. Ahora vamos un paso más allá con CSharpSyntaxRewriter y "modificamos" el árbol sintactico a medida que lo recorremos. Es importante tener en cuenta que en realidad no estamos mutando el árbol sintactico original, ya que los árboles de Roslyn son inmutables. En su lugar, CSharpSyntaxRewriter crea un nuevo árbol como resultado de nuestros cambios.

CSharpSyntaxRewriter puede visitar todos los nodos, tokens o trivias dentro de un árbol sintactico. Al igual que CSharpSyntaxVisitor, podemos elegir de forma selectiva qué fragmentos de sintaxis nos gustaría visitar. Hacemos esto sobreescribiendo varios métodos y devolviendo lo siguiente:

  • El nodo, token o trivia original, sin cambios.
  • Nulo, que indica que se debe eliminar el nodo, el token o la trivia.
  • Un nuevo nodo de sintaxis, token o trivia.

Al igual que con la mayoría de las API, CSharpSyntaxRewriter se comprende mejor a través de ejemplos. Una pregunta reciente sobre Stack Overflow preguntó ¿Cómo puedo eliminar los puntos y comas redundantes en el código con SyntaxRewriter?

Roslyn trata todos los puntos y coma redundantes como parte de un nodo EmptyStatementSyntax. A continuación, demostramos cómo resolver el caso base: un punto y coma innecesario en una línea propia.


public class EmtpyStatementRemoval : CSharpSyntaxRewriter

{

    public override SyntaxNode VisitEmptyStatement(EmptyStatementSyntax node)

    {

        //Simply remove all Empty Statements

        return null;

    }

}


public static void Main(string[] args)

{

    //A syntax tree with an unnecessary semicolon on its own line

    var tree = CSharpSyntaxTree.ParseText(@"

    public class Sample

    {

       public void Foo()

       {

          Console.WriteLine();

          ;

        }

    }");


    var rewriter = new EmtpyStatementRemoval();

    var result = rewriter.Visit(tree.GetRoot());

    Console.WriteLine(result.ToFullString());

}


La salida de este programa produce un programa simple sin ningún punto y coma redundante.


public class Sample

{

   public void Foo()

   {

      Console.WriteLine();

    }

}

Sin embargo, cuando hay trivia inicial o final, esta trivia se elimina. Esto significa que se eliminarán los comentarios por encima y por debajo del punto y coma. Al construir un EmptyStatementSyntax con un token faltante en lugar de un punto y coma, podemos eliminar el punto y coma del árbol original:

public class EmtpyStatementRemoval : CSharpSyntaxRewriter

{

    public override SyntaxNode VisitEmptyStatement(EmptyStatementSyntax node)

    {

        //Construct an EmptyStatementSyntax with a missing semicolon

        return node.WithSemicolonToken(

            SyntaxFactory.MissingToken(SyntaxKind.SemicolonToken)

                .WithLeadingTrivia(node.SemicolonToken.LeadingTrivia)

                .WithTrailingTrivia(node.SemicolonToken.TrailingTrivia));

    }

}


public static void Main(string[] args)

{

    var tree = CSharpSyntaxTree.ParseText(@"

    public class Sample

    {

       public void Foo()

       {

          Console.WriteLine();

          #region SomeRegion

          //Some other code

          #endregion

          ;

        }

    }");


    var rewriter = new EmtpyStatementRemoval();

    var result = rewriter.Visit(tree.GetRoot());

    Console.WriteLine(result.ToFullString());

}


El resultado de este enfoque es:


public class Sample

{

   public void Foo()

   {

      Console.WriteLine();

      #region SomeRegion

      //Some other code

      #endregion

    }

}

Este enfoque tiene el efecto secundario de dejar una línea en blanco donde haya un punto y coma redundante. Dicho esto, creo que probablemente valga la pena el intercambio, ya que de lo contrario no parece haber una forma de retener las trivias. En última instancia, la trivia solo se puede conservar si se adjunta a un nodo y luego se devuelve ese nodo.

La única forma de eliminar el nodo y conservar las trivias es construir un nodo de reemplazo. El mejor candidato para el reemplazo probablemente sea un EmptyStatementSyntax al que le falte un punto y coma.

Esto también podría indicar una limitación con CSharpSyntaxRewriter. Parece que debería ser más fácil eliminar nodos, manteniendo sus trivias.

domingo, 16 de abril de 2023

Analizando el flujo de datos con Roslyn

Esta API se puede usar para inspeccionar cómo se leen y escriben las variables dentro de un bloque de código determinado. Tal vez le gustaría crear una extensión de Visual Studio que capture y registre todas las asignaciones a una determinada variable. Se puede usar la API de análisis de flujo de datos para encontrar las declaraciones y un reescritor para registrarlas.

Para demostrar las capacidades de esta API, podemos analizar el bucle for en el siguiente código:


var tree = CSharpSyntaxTree.ParseText(@"

public class Sample

{

   public void Foo()

   {

        int[] outerArray = new int[10] { 0, 1, 2, 3, 4, 0, 1, 2, 3, 4};

        for (int index = 0; index < 10; index++)

        {

             int[] innerArray = new int[10] { 0, 1, 2, 3, 4, 0, 1, 2, 3, 4 };

             index = index + 2;

             outerArray[index – 1] = 5;

        }

   }

}");

 

var Mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);

 

var compilation = CSharpCompilation.Create("MyCompilation",

    syntaxTrees: new[] { tree }, references: new[] { Mscorlib });

var model = compilation.GetSemanticModel(tree);

 

var forStatement = tree.GetRoot().DescendantNodes().OfType<ForStatementSyntax>().Single();

DataFlowAnalysis result = model.AnalyzeDataFlow(forStatement);


En este punto, tenemos acceso a un objeto DataFlowAnalysis.


Quizás la propiedad más importante de este objeto es Succeeded. Esto le indica si el análisis de flujo de datos se completó correctamente. En mi experiencia, la API ha sido bastante buena para lidiar con código semánticamente inválido. Ni las invocaciones a métodos faltantes ni el uso de variables no declaradas parecían hacer tropezar. La documentación señala que si la región analizada no abarca una sola expresión o declaración, es probable que el análisis falle.

El objeto DataFlowAnalysis expone una API bastante rica. Expone información sobre direcciones inseguras, variables locales capturadas por métodos anónimos y mucho más.

En nuestro caso, estamos interesados en las siguientes propiedades:

  • DataFlowAnalysis.AlwaysAssigned: el conjunto de variables locales para las que siempre se asigna un valor dentro de una región.
  • DataFlowAnalysis.ReadInside: el conjunto de variables locales que se leen dentro de una región.
  • DataFlowAnalysis.WrittenOutside: el conjunto de variables locales que se escriben fuera de una región.
  • DataFlowAnalysis.WrittenInside: el conjunto de variables locales que se escriben dentro de una región.
  • DataFlowAnalysis.VariablesDeclared: el conjunto de variables locales que se declaran dentro de una región. Tenga en cuenta que la región debe estar delimitada por el cuerpo de un método o el inicializador de un campo, por lo que los símbolos de parámetros nunca se incluyen en el resultado.


Los resultados del análisis son los siguientes:


AlwaysAssigned: index

index siempre se asigna a, ya que está contenido en el inicializador del bucle for, que se ejecuta incondicionalmente.

WrittenInside: index, innerArray

Tanto index como innerArray están claramente escritos dentro del bucle.

Un punto importante es que externalArray no. Mientras estamos mutando la matriz, no estamos mutando la referencia contenida dentro de la variable outsideArray. Por lo tanto, no aparece en esta lista.

WrittenOutside: outerArray, this

outsideArray está claramente escrito fuera del bucle for.

Sin embargo, me sorprendió que esto apareciera como un símbolo de parámetro dentro de la lista de WriteOutside. Parece como si esto se pasara como un parámetro a la clase y su miembro, lo que significa que también aparece aquí. Esto parece ser por diseño, aunque sospecho que la mayoría de los consumidores de esta API se sorprenderán y probablemente ignoren este valor.

ReadInside: index, outerArray

Está claro que el valor del índice se lee dentro del ciclo.

Me sorprendió que se considere que outsideArray se "lee" dentro del ciclo, ya que no estamos leyendo su valor directamente. Supongo que, técnicamente, primero debemos leer el valor de externalArray para calcular el desplazamiento y recuperar la dirección correcta para el elemento dado de la matriz. Así que estamos realizando una especie de "lectura implícita" dentro del ciclo aquí.


VariablesDeclared: index, innerArray

Esto es bastante sencillo. index se declara dentro del inicializador de bucle e innerArray dentro del cuerpo del bucle for.

La rareza general de la API de análisis de flujo de datos hace que no le vea mucha utilidad, se les ocurre un lugar para usarla?

martes, 11 de abril de 2023

CSharpSyntaxWalker

En el post anterior, exploramos diferentes enfoques para separar partes del árbol de sintaxis. Este enfoque funciona bien cuando solo está interesado en partes específicas de la sintaxis (métodos, clases, declaración de lanzamiento, etc.). Es excelente para identificar ciertas partes del árbol de sintaxis para una mayor investigación.

Sin embargo, a veces nos gustaría operar en todos los nodos y tokens dentro de un árbol. Alternativamente, el orden en que visita estos nodos puede ser importante. Quizás esté intentando convertir C# en VB.Net. O tal vez le gustaría analizar un archivo C# y generar un archivo HTML estático con la coloración correcta. Ambos programas requerirían que visitáramos todos los nodos y tokens dentro de un árbol de sintaxis en el orden correcto.

La clase abstracta CSharpSyntaxWalker nos permite construir nuestro propio objeto que recorre la sintaxis y que puede visitar todos los nodos, tokens y trivia. Simplemente podemos heredar de CSharpSyntaxWalker y sobreescribir el método Visit() para visitar todos los nodos dentro del árbol.


public class CustomWalker : CSharpSyntaxWalker

{

    static int Tabs = 0;

    public override void Visit(SyntaxNode node)

    {

        Tabs++;

        var indents = new String('\t', Tabs);

        Console.WriteLine(indents + node.Kind());

        base.Visit(node);

        Tabs—;

    }

}


static void Main(string[] args)

{

    var tree = CSharpSyntaxTree.ParseText(@"

        public class MyClass

        {

            public void MyMethod()

            {

            }

            public void MyMethod(int n)

            {

            }

       ");

    

    var walker = new CustomWalker();

    walker.Visit(tree.GetRoot());

}


Este breve ejemplo contiene una implementación de CSharpSyntaxWalker llamada CustomWalker. CustomWalker sobreescribe el método Visit() e imprime el tipo de nodo que se está visitando actualmente. Es importante tener en cuenta que CustomWalker.Visit() también llama al método base.Visit(SyntaxNode). Esto permite que CSharpSyntaxWalker visite todos los nodos secundarios del nodo actual.


La salida para este programa:


Podemos ver claramente los distintos nodos del árbol de sintaxis y su relación entre sí. Hay dos MethodDeclarations hermanos que comparten la misma ClassDeclaration principal.

Este ejemplo anterior solo visita los nodos de un árbol de sintaxis, pero también podemos modificar CustomWalker para visitar tokens y trivias. La clase abstracta CSharpSyntaxWalker tiene un constructor que nos permite especificar la profundidad con la que queremos visitar.

Podemos modificar el ejemplo anterior para imprimir los nodos y sus tokens correspondientes en cada profundidad del árbol de sintaxis.


public class DeeperWalker : CSharpSyntaxWalker

{

    static int Tabs = 0;

    //NOTE: Make sure you invoke the base constructor with 

    //the correct SyntaxWalkerDepth. Otherwise VisitToken() will never get run.

    public DeeperWalker() : base(SyntaxWalkerDepth.Token)

    {

    }

    public override void Visit(SyntaxNode node)

    {

        Tabs++;

        var indents = new String('\t', Tabs);

        Console.WriteLine(indents + node.Kind());

        base.Visit(node);

        Tabs—;

    }


    public override void VisitToken(SyntaxToken token)

    {

        var indents = new String('\t', Tabs);

        Console.WriteLine(indents + token);

        base.VisitToken(token);

    }

}


Es importante pasar el argumento SyntaxWalkerDepth adecuado a CSharpSyntaxWalker. De lo contrario, nunca se llama al método VisitToken().


El resultado cuando usamos este CSharpSyntaxWalker:



La muestra anterior y esta comparten el mismo árbol de sintaxis. La salida contiene los mismos nodos de sintaxis, pero agregamos los tokens de sintaxis correspondientes para cada nodo.

En los ejemplos anteriores, visitamos todos los nodos y todos los tokens dentro de un árbol de sintaxis. Sin embargo, a veces solo nos gustaría visitar ciertos nodos, pero en el orden predefinido que proporciona CSharpSyntaxWalker. Afortunadamente, la API nos permite filtrar los nodos que nos gustaría visitar según su sintaxis.

En lugar de visitar todos los nodos como hicimos en ejemplos anteriores, lo siguiente solo visita los nodos ClassDeclarationSyntax y MethodDeclarationSyntax. Es extremadamente simple, simplemente imprime la concatenación del nombre de la clase con el nombre del método.


public class ClassMethodWalker : CSharpSyntaxWalker

{

    string className = String.Empty;

    public override void VisitClassDeclaration(ClassDeclarationSyntax node)

    {

        className = node.Identifier.ToString();

        base.VisitClassDeclaration(node);

    }


    public override void VisitMethodDeclaration(MethodDeclarationSyntax node)

    {

        string methodName = node.Identifier.ToString();

        Console.WriteLine(className + '.' + methodName);

        base.VisitMethodDeclaration(node);

    }

}


static void Main(string[] args)

{

    var tree = CSharpSyntaxTree.ParseText(@"

    public class MyClass

    {

        public void MyMethod()

        {

        }

    }

    public class MyOtherClass

    {

        public void MyMethod(int n)

        {

        }

    }

   ");


    var walker = new ClassMethodWalker();

    walker.Visit(tree.GetRoot());

}


Esta muestra simplemente genera:

MiClase.MiMétodo

MiOtraClase.MiMétodo


CSharpSyntaxWalker actúa como una gran API para analizar árboles de sintaxis. Permite lograr mucho sin recurrir al modelo semántico y forzar una compilación (posiblemente) costosa. Siempre que sea importante inspeccionar los árboles de sintaxis y el orden, CSharpSyntaxWalker suele ser lo que está buscando.


miércoles, 5 de abril de 2023

Analizando el Syntax Trees con LINQ


La idea principal sobre  Syntax Trees es que dada una cadena que contiene código C#, el compilador crea una representación de árbol (llamada Árbol de sintaxis) de la cadena. El poder de Roslyn es que nos permite consultar este árbol de sintaxis con LINQ.

Aquí hay una muestra en la que usamos Roslyn para crear un árbol de sintaxis a partir de una cadena. Debemos agregar referencias a Microsoft.CodeAnalysis y Microsoft.CodeAnalysis.CSharp. 


using Microsoft.CodeAnalysis.CSharp;

using Microsoft.CodeAnalysis.CSharp.Syntax;


var tree = CSharpSyntaxTree.ParseText(@"

    public class MyClass

    {

        public void MyMethod()

        {

        }

    }");


var syntaxRoot = tree.GetRoot();

var MyClass = syntaxRoot.DescendantNodes().OfType<ClassDeclarationSyntax>().First();

var MyMethod = syntaxRoot.DescendantNodes().OfType<MethodDeclarationSyntax>().First();


Console.WriteLine(MyClass.Identifier.ToString());

Console.WriteLine(MyMethod.Identifier.ToString());


Primero comenzamos analizando una cadena que contiene código C# y obteniendo la raíz de este árbol de sintaxis. Desde este punto, es extremadamente fácil recuperar elementos que nos gustaría usar LINQ. Dada la raíz del árbol, miramos todos los objetos descendientes y los filtramos por su tipo. Si bien solo hemos usado ClassDeclarationSyntax y MethodDeclarationSyntax, hay piezas correspondientes de sintaxis para cualquier característica de C#.

Intellisense de Visual Studio es extremadamente valioso para explorar los diversos tipos de sintaxis de C# que podemos usar.

Podemos componer expresiones LINQ más avanzadas como cabría esperar:


var tree = CSharpSyntaxTree.ParseText(@"

    public class MyClass

    {

        public void MyMethod()

        {

        }

        public void MyMethod(int n)

        {

        }

    }");


var syntaxRoot = tree.GetRoot();

var MyMethod = syntaxRoot.DescendantNodes().OfType<MethodDeclarationSyntax>()

    .Where(n => n.ParameterList.Parameters.Any()).First();


//Find the type that contains this method

var containingType = MyMethod.Ancestors().OfType<TypeDeclarationSyntax>().First();


Console.WriteLine(containingType.Identifier.ToString());

Console.WriteLine(MyMethod.ToString());


Arriba, comenzamos buscando todos los métodos y luego filtramos por aquellos que aceptan parámetros. Luego tomamos este método y avanzamos hacia arriba a través del árbol con el método Ancestors(), buscando el primer tipo que contiene este método.

Existen algunas limitaciones en el tipo de información que puede descubrir a un nivel puramente sintáctico y para superarlas debemos hacer uso del modelo semántico de Roslyn.

martes, 4 de abril de 2023

Roslyn, Compiler-as-a-Service parte 2

Los principales tipos de Syntax Nodes se derivan de la clase base SyntaxNode. El conjunto de Syntax Nodes no es extensible. Estas clases forman la construcción para crear declaraciones, sentencias, cláusulas y expresiones. No son terminales, lo que significa que tienen subnodos por los que se puede navegar a través de propiedades o métodos escritos.


/* tree is given after parsing the Hello World example with the

Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText method */

var syntaxRoot = tree.GetRoot();

var Demo = syntaxRoot.DescendantNodes().OfType<ClassDeclarationSyntax>().First();


Los Syntax Tokens son terminales y no pueden ser padres de otros nodos o tipos de tokens. Representan identificadores, palabras clave, literales y puntuaciones. Ofrece propiedades para acceder a los valores analizados desde el código fuente de entrada.

Syntax Trivia representan porciones de texto insignificantes, que pueden aparecer como espacios en blanco, comentarios y directivas de preprocesador en cualquier posición dentro de la entrada de origen. No forman parte de la sintaxis del lenguaje normal y no se agregarán como elementos secundarios de los nodos de sintaxis. Tampoco tienen un nodo principal. La clase base se llama SyntaxTrivia.

Los nodos, tokens y trivias contienen Spans para el posicionamiento y la cantidad de ocurrencias de caracteres para asociar la ubicación correcta desde la fuente de entrada. Esto se puede utilizar para la depuración o la determinación de información de errores.

Los Kinds o tipos se utilizan como propiedades para nodos, tokens y trivias para distinguir los tipos correspondientes y proporcionar las conversiones correctas. La clase SyntaxKind se puede representar como un tipo de enumeración en el lenguaje de destino C# o VB.

Al analizar el código fuente, pueden ocurrir errores, que pueden ubicarse y marcarse como incorrectos o incompletos, por ejemplo, falta un token o no es válido. El analizador puede omitir tokens no válidos y continuar buscando el siguiente token válido para continuar con el análisis. Los tokens omitidos se adjuntarán como Trivia Node of Kind SkippedToken.

Roslyn proporciona una rica API para crear unidades de compilación AST por código. Utiliza el patrón de fábrica y proporciona muchas clases y métodos estáticos o enumeraciones, que se pueden usar a través del encadenamiento para construir estas soluciones. El encadenamiento es un diseño de llamada de método fluido utilizado en lenguajes de programación orientados a objetos, donde cada método devuelve un objeto, lo que permite llamar a la siguiente declaración sin requerir variables que almacenen los resultados intermedios. El siguiente código cortado muestra un ejemplo de cómo usar la API de Roslyn para hacer una declaración de clase simple.

var tree = SyntaxFactory.CompilationUnit().AddMembers(

    SyntaxFactory.NamespaceDeclaration(

        SyntaxFactory.IdentifierName("Example")).AddMembers(

            SyntaxFactory.ClassDeclaration("Demo").AddMembers(

                SyntaxFactory.MethodDeclaration(

                    SyntaxFactory.PredefinedType(

                        SyntaxFactory.Token(SyntaxKind.VoidKeyword)), "Main")

                    .AddModifiers(SyntaxFactory.Token(SyntaxKind.StaticKeyword))

                    .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))

                    .WithBody(SyntaxFactory.Block()).AddBodyStatements(

                         SyntaxFactory.ReturnStatement())

           )

     )

);

Aunque la representación del árbol sintáctico del código fuente de entrada ofrece muchas funciones, solo analiza las estructuras léxica y sintáctica. Para cubrir completamente todos los aspectos de los lenguajes de programación también es necesario definir su semántica, que marca su comportamiento. Las designaciones de variables locales y miembro pueden superponerse entre sí y diferir entre sus alcances y las reglas de acción deben integrarse dentro de esos cierres. Para ello, Roslyn utiliza símbolos. Un símbolo representa un elemento, que lleva los metadatos recibidos de la fuente de entrada. Se puede acceder a estos a través de la tabla de símbolos proporcionada, también representada en una estructura de árbol, comenzando desde el elemento raíz, que es el espacio de nombres global. Los símbolos se derivan de la interfaz ISymbol, mientras que el compilador proporciona las propiedades y los métodos. Los símbolos ofrecen espacios de nombres, tipos y miembros entre el código fuente y los metadatos y sus conceptos de lenguaje son similares a la API de Reflection utilizada por el sistema de tipos CLR. También es importante saber que el acceso al árbol del modelo semántico desencadena una compilación, lo que significa que es más costoso en comparación con el acceso al árbol sintáctico.

Cada símbolo contiene información sobre

  • la ubicación de la declaración (en el fuente o en los metadatos)
  • en qué espacio de nombres o tipo existe este símbolo
  • la información si el símbolo es abstracto, estático, sellado, etc.

El siguiente fragmento de código muestra cómo agregar información semántica al ejemplo de Hello World declarado anteriormente.


/* tree is given after parsing the Hello World example with the Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText method */ 

var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly. Location); 

var compilation = CSharpCompilation.Create("DemoCompilation", syntaxTrees: new[] { tree }, references: new[] { mscorlib }); 

var model = compilation.GetSemanticModel(tree)


Roslyn es una rica plataforma de servicio de compilación que permite un acceso profundo al marco .NET de Microsoft y sus subconjuntos de lenguajes admitidos. Aunque Roslyn ofrece una variedad de posibilidades de bajo nivel para construir AST, se abstrae muy bien de estas capas y proporciona una API limpia e intuitiva. 


Roslyn, Compiler-as-a-Service

 


Roslyn expone el análisis del código del compilador para desarrolladores de C# y Visual Basic a través de tres capas principales de API. 

La capa Compiler Pipeline maneja el análisis de nivel inferior, el análisis de símbolos y el procesamiento de metadatos, incluidos los enlaces y la emisión de IL. La capa Compiler API ofrece acceso a la representación AST (abstract syntax tree) de los datos procesados y funciona como una abstracción de nivel superior para los desarrolladores de C# o Visual Basic. La capa Language Service proporciona un conjunto de herramientas de nivel superior para operar en los datos de la API del Compilador, incluidas las operaciones de navegación, la información y las posibilidades de formato.

Compiler Pipeline: Compiler Pipeline procesa la fuente de entrada en diferentes fases, comenzando con el proceso de análisis, donde la fuente se tokeniza en las reglas de lenguaje gramatical correspondientes. Posteriormente, los metadatos se analizan para extraer los símbolos requeridos, también llamada fase de declaración. El Binder hace coincidir los símbolos con los identificadores correspondientes y, al final, el Emisor construye un conjunto completo.

Compiler API: en equivalencia con las fases descritas anteriormente de Compiler Pipeline, Compiler API ofrece un modelo de objeto representativo en el lenguaje de destino actual para acceder a la información necesaria. La representación de la fase Parser es traducida por la Syntax Tree API como modelo AST, seguida de modelos de metadatos para los símbolos y enlaces, y la Emit API se abstrae del productor de código de bytes IL correspondiente.

Language Service: El Servicio Lingüístico se adapta a las capas y fases subyacentes. Por ejemplo, utiliza la tabla de símbolos para el Examinador de objetos o las funciones de navegación, y la representación del árbol de sintaxis para las funciones de esquematización y formato.

Roslyn está construido como un conjunto de APIs de dos capas, la capa de API del compilador y la capa de API del espacio de trabajo. La capa del compilador contiene los modelos de objetos que consisten en la información extraída sobre las fases individuales y los datos sintácticos y semánticos analizados desde el fuente. Ofrece una instantánea inmutable de una única invocación de un compilador. Esta capa es independiente de cualquier componente de Visual Studio. Esto permite que las herramientas de diagnóstico definidas por el usuario se conecten al proceso de compilación real para obtener información sobre errores y advertencias. La API del espacio de trabajo resuelve las dependencias del proyecto y utiliza la capa del compilador para proporcionar modelos de objetos para soluciones de análisis y refactorización.

La API del árbol de sintaxis es el componente central del marco de Roslyn. Vincula todas las funciones proporcionadas a las clases de modelos de objetos abstractos, conocidos como AST. El análisis de código, la refactorización, los componentes IDE, la información de origen, las construcciones gramaticales y muchas más funciones convergen en un punto, que es la API del árbol sintáctico. También es importante saber que todos los resultados analizados se pueden revertir a su texto de origen original (completamente de ida y vuelta). Además, el árbol de sintaxis es seguro para subprocesos e inmutable, lo que permite que múltiples usuarios interactúen simultáneamente sin interferir. Esto también significa que no se pueden enviar modificaciones en los árboles de sintaxis. Los métodos de fábricación proporcionan instantáneas adicionales del árbol de sintaxis para operar indirectamente con ellos, a costa de poca sobrecarga de memoria. Para ilustrar los principales componentes y características de la API del árbol sintáctico, el siguiente programa Hello World escrito en C# servirá como una ilustración práctica.


using System;

    namespace Demo {

        class Program {

            static void Main() {

                Console.WriteLine("Hello World!");

            }

        }

}

Roslyn también brinda posibilidades para visualizar secciones de código existentes como un árbol de sintaxis utilizando el SDK de la plataforma del compilador .NET, que está disponible como un paquete NuGet para Visual Studio. Después de instalar el paquete adicional, Visual Studio agrega un nuevo elemento de menú en la pestaña Ver/Otras ventanas/Visualizador de sintaxis. El visualizador de sintaxis crea el siguiente árbol de sintaxis a partir del código mostrado anteriormente, como se muestra en la figura 


También es posible ver una representación visual del árbol de sintaxis como se muestra en la figura. 


La leyenda en el lado derecho proporciona una descripción general de los elementos del árbol de sintaxis resultantes de un procedimiento de análisis.

Cada árbol de sintaxis de Roslyn consta de los siguientes elementos:

Nodos de sintaxis para declaraciones, sentencias, cláusulas y expresiones 

Tokens de sintaxis para representar terminales, los fragmentos de código más pequeños 

Sintaxis Trivia para espacios en blanco, comentarios y directivas de preprocesador, que son los elementos de código insignificantes que se pueden omitir después del análisis

Spanns para el posicionamiento del texto y la cantidad de caracteres utilizados por un nodo, token o trivia 

Tipos para identificar el elemento de sintaxis exacto correspondiente, que se puede convertir en enumeraciones específicas del lenguaje

Los errores de sintaxis, que no se ajustan a la gramática y se pueden adjuntar elementos de diagnóstico adicionales para el desarrollo o la depuración