Translate

lunes, 22 de mayo de 2023

Por qué son importantes los test de arquitectura.


En el desarrollo de aplicaciones de software, la arquitectura juega un papel fundamental. Define la estructura y el diseño de la aplicación, proporcionando una base sólida sobre la cual se construyen todas las funcionalidades. A medida que las aplicaciones se vuelven más complejas, es esencial garantizar que la arquitectura sea sólida y cumpla con los requisitos deseados. Aquí es donde entran en juego los test de arquitectura.  

¿Qué son los test de arquitectura? Los test de arquitectura son una práctica que busca evaluar la robustez y la calidad de la arquitectura de una aplicación. Se centran en verificar que los componentes clave de la arquitectura funcionen correctamente y cumplan con los requisitos esperados. A diferencia de los test unitarios, que se centran en probar unidades individuales de código, los test de arquitectura se enfocan en la estructura global de la aplicación y en cómo interactúan sus diferentes componentes. 

¿Porque es importante verificar la arquitectura? Principalmente porque a medida que desarrollamos podemos no respetar los principios que definimos desde la concepción de la aplicación. Los test de arquitectura nos permiten mantener esos principios a lo largo del tiempo verificando si se cumplen commit a commit.  

Además, permiten:  

  • Identificación temprana de problemas: Los test de arquitectura permiten identificar problemas potenciales en la fase inicial del desarrollo. Al evaluar la arquitectura antes de implementar todas las funcionalidades, es posible detectar problemas de diseño, cuellos de botella de rendimiento, dependencias no deseadas, entre otros. Esto ayuda a reducir el costo y el esfuerzo necesario para corregir problemas en etapas más avanzadas del desarrollo. 
  • Mejora de la calidad y mantenibilidad: Una arquitectura sólida es fundamental para garantizar la calidad y mantenibilidad a largo plazo de una aplicación. Los test de arquitectura ayudan a validar que la estructura de la aplicación cumpla con principios de diseño y buenas prácticas. Esto facilita la comprensión del código, la reutilización de componentes y la realización de cambios sin afectar negativamente otras partes del sistema. 
  • Alineación con los requisitos: Los test de arquitectura permiten asegurar que la arquitectura esté alineada con los requisitos del negocio y las necesidades de los usuarios. Al realizar pruebas exhaustivas de la arquitectura, se pueden identificar brechas en los requisitos y asegurar que todas las funcionalidades esperadas estén cubiertas. Esto ayuda a evitar costosos cambios posteriores debido a requisitos omitidos o malinterpretados. 
  • Rendimiento y escalabilidad: Las aplicaciones modernas deben ser capaces de manejar grandes volúmenes de datos y un alto número de usuarios concurrentes. Los test de arquitectura pueden ayudar a identificar cuellos de botella de rendimiento y evaluar la escalabilidad de la aplicación. Esto permite tomar decisiones informadas sobre el uso de tecnologías adecuadas, la optimización de consultas y la distribución de la carga de trabajo, asegurando un rendimiento óptimo incluso en condiciones de alta demanda. 
  • Gestión de riesgos: Los test de arquitectura ayudan a mitigar los riesgos asociados con el desarrollo de aplicaciones complejas. Permiten identificar y abordar problemas de seguridad, integridad de datos y fiabilidad del sistema. Al probar y validar la arquitectura en diferentes escenarios y condiciones, se pueden detectar posibles vulnerabilidades o debilidades y tomar medidas para corregirlas antes de que se conviertan en problemas reales. 

¿Y cómo podemos probar nuestra arquitectura fácilmente? Para ello existen varios frameworks y herramientas disponibles para realizar test de arquitectura en las plataformas Java y .NET algunos de los más utilizados son: 

Java: 

  • Arquillian: Es un framework de pruebas de integración que se enfoca en la creación de escenarios de prueba realistas para aplicaciones Java. Proporciona soporte para la ejecución de pruebas en diferentes entornos y contenedores. 
  • JUnit: Aunque principalmente utilizado para test unitarios, JUnit también puede ser utilizado para realizar pruebas de integración y pruebas de arquitectura. Permite la ejecución de pruebas en paralelo y la creación de casos de prueba complejos. 

.NET: 

  • NUnit: Similar a JUnit, NUnit es un framework de pruebas para la plataforma .NET. Ofrece una amplia gama de funcionalidades para realizar pruebas de arquitectura, incluyendo aserciones, configuraciones y la ejecución de pruebas en paralelo. 
  • SpecFlow: Esta herramienta se basa en el concepto de BDD (Behavior-Driven Development) y permite escribir pruebas de aceptación en un lenguaje natural fácilmente comprensible. Es especialmente útil para probar la arquitectura desde la perspectiva del comportamiento esperado del sistema. 

ArchUnit es otro framework relevante para realizar test de arquitectura en aplicaciones Java y .NET. ArchUnit(en java) y NArchiUnit(en .NET) es un framework de código abierto que permite definir y verificar reglas arquitectónicas en el código fuente de una aplicación. Proporciona una forma declarativa de definir restricciones sobre la estructura y el diseño de la arquitectura, como reglas de dependencia entre paquetes, convenciones de nomenclatura, restricciones de visibilidad, entre otros aspectos.  

Al utilizar ArchUnit, los desarrolladores pueden escribir pruebas que verifiquen automáticamente si la arquitectura de la aplicación cumple con las reglas definidas. Esto permite garantizar la coherencia y la integridad de la arquitectura, evitando que se introduzcan violaciones arquitectónicas en el código. Además, ArchUnit ofrece una sintaxis expresiva y flexible que facilita la definición de reglas específicas para cada proyecto.  

Para que los test unitarios sean útiles es importante, correrlos regularmente y para esto herramientas como un servidor de integración continua son fundamentales.  

Los servidores de integración continua desempeñan un papel importante en la ejecución automatizada de los test de arquitectura. Estos servidores, como Jenkins, Bamboo o Azure DevOps, permiten la configuración y ejecución de pipelines de integración continua, que incluyen la compilación, pruebas y despliegue automatizado de la aplicación. Al utilizar un servidor de integración continua, los test de arquitectura se pueden ejecutar de manera regular y sistemática, lo que garantiza que cualquier cambio o actualización en la aplicación se someta a pruebas de arquitectura antes de ser desplegado en producción. 

Conclusión: Los test de arquitectura son fundamentales para garantizar una base sólida y robusta en el desarrollo de aplicaciones de software. Ayudan a identificar problemas tempranamente, mejorar la calidad y mantenibilidad, alinear la arquitectura con los requisitos, garantizar el rendimiento y escalabilidad, y mitigar riesgos. Utilizar frameworks específicos y aprovechar los servidores de integración continua facilita su implementación y automatización. No programar estos test puede tener consecuencias negativas, como problemas de escalabilidad, falta de mantenibilidad, vulnerabilidades de seguridad y dificultad para cumplir requisitos. En resumen, los test de arquitectura son una práctica esencial para asegurar el éxito y la calidad de las aplicaciones. 


 

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.


viernes, 12 de mayo de 2023

La Guía de Google Cloud que te ayudará a innovar y crecer.

 

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.


String Interpolation en Java


La interpolación de cadenas es una forma sencilla y precisa de inyectar valores variables en una cadena. Permite a los usuarios incrustar referencias de variables directamente en literales de cadena procesados. Java carece de soporte nativo para la interpolación de cadenas en comparación con lenguajes como Scala.

Sin embargo, existen algunos enfoques para lograr este comportamiento en Java.

Primero, tenemos el operador "+". Podemos usar el operador "+" para concatenar nuestras variables y valores de cadena. La variable se reemplaza por su valor, por lo que logramos la interpolación o concatenación de cadenas:


@Test

public void givenTwoString_thenInterpolateWithPlusSign() {

    String EXPECTED_STRING = "String Interpolation in Java with some Java examples.";

    String first = "Interpolation";

    String second = "Java";

    String result = "String " + first + " in " + second + " with some " + second + " examples.";

    assertEquals(EXPECTED_STRING, result);

}


Como podemos ver en el ejemplo anterior, con este operador el String resultante contiene los valores de las variables “interpolados” con otros valores de string. Dado que puede ajustarse para satisfacer necesidades específicas, este método de concatenación de cadenas se encuentra entre los más sencillos y valiosos. Cuando usamos el operador, no necesitamos poner el texto entre comillas.

Otro enfoque es usar el método format() de la clase String. Al contrario del operador "+", en este caso necesitamos usar marcadores de posición para obtener el resultado esperado en la interpolación de cadenas:

@Test

public void givenTwoString_thenInterpolateWithFormat() {

    String EXPECTED_STRING = "String Interpolation in Java with some Java examples.";

    String first = "Interpolation";

    String second = "Java";

    String result = String.format("String %s in %s with some %s examples.", first, second, second);

    assertEquals(EXPECTED_STRING, result);

}

Además, podemos hacer referencia a un argumento específico si queremos evitar repeticiones de variables en nuestra llamada de formato:

@Test

public void givenTwoString_thenInterpolateWithFormatStringReference() {

    String EXPECTED_STRING = "String Interpolation in Java with some Java examples.";

    String first = "Interpolation";

    String second = "Java";

    String result = String.format("String %1$s in %2$s with some %2$s examples.", first, second);

    assertEquals(EXPECTED_STRING, result);

}

Nuestro siguiente enfoque es la clase StringBuilder. Instanciamos un objeto StringBuilder y luego llamamos a la función append() para construir el String. En el proceso, nuestras variables se agregan a la cadena resultante:


@Test

public void givenTwoString_thenInterpolateWithStringBuilder() {

    String EXPECTED_STRING = "String Interpolation in Java with some Java examples.";

    String first = "Interpolation";

    String second = "Java";

    StringBuilder builder = new StringBuilder();

    builder.append("String ")

      .append(first)

      .append(" in ")

      .append(second)

      .append(" with some ")

      .append(second)

      .append(" examples.");

    String result = builder.toString();

    assertEquals(EXPECTED_STRING, result);

}


Como podemos ver en el ejemplo de código anterior, podemos interpolar las cadenas con el texto necesario encadenando la función de agregar, que acepta el parámetro como una variable (en este caso, dos cadenas).

El uso de la clase MessageFormat es un método menos conocido para obtener la interpolación de cadenas en Java. Con MessageFormat, podemos crear mensajes concatenados sin preocuparnos por el idioma subyacente. Es un método estándar para crear mensajes orientados al usuario. Toma una colección de objetos, formatea las cadenas contenidas dentro y las inserta en el patrón en las ubicaciones adecuadas.

El método de formato de MessageFormat es casi idéntico al método de formato de String, excepto por cómo se escriben los marcadores de posición. Índices como {0}, {1}, {2}, etc., representan el marcador de posición en esta función:


@Test

public void givenTwoString_thenInterpolateWithMessageFormat() {

    String EXPECTED_STRING = "String Interpolation in Java with some Java examples.";

    String first = "Interpolation";

    String second = "Java";

    String result = MessageFormat.format("String {0} in {1} with some {1} examples.", first, second);

    assertEquals(EXPECTED_STRING, result);

}

En cuanto al rendimiento, StringBuilder solo agrega texto a un búfer dinámico; sin embargo, MessageFormat analiza el formato dado antes de agregar los datos. Como resultado, StringBuilder supera a MessageFormat en términos de eficiencia.

Finalmente, tenemos StringSubstitutor de Apache Commons. En el contexto de esta clase, los valores se sustituyen por variables incluidas dentro de una Cadena. Esta clase toma un fragmento de texto y reemplaza todas las variables. La definición predeterminada de una variable es ${variableName}. Se pueden usar constructores y métodos de conjuntos para modificar el prefijo y los sufijos. La resolución de los valores de las variables normalmente implica el uso de un mapa. Sin embargo, podemos resolverlos utilizando los atributos del sistema o proporcionando un solucionador de variables especializado:

@Test

public void givenTwoString_thenInterpolateWithStringSubstitutor() {

    String EXPECTED_STRING = "String Interpolation in Java with some Java examples.";

    String baseString = "String ${first} in ${second} with some ${second} examples.";

    String first = "Interpolation";

    String second = "Java";

    Map<String, String> parameters = new HashMap<>();

    parameters.put("first", first);

    parameters.put("second", second);

    StringSubstitutor substitutor = new StringSubstitutor(parameters);

    String result = substitutor.replace(baseString);

    assertEquals(EXPECTED_STRING, result);

}

De nuestro ejemplo de código, podemos ver que creamos un Map. Los nombres de las claves son los mismos que los nombres de las variables que reemplazaremos en el String. Luego pondremos el valor correspondiente para cada clave en el Map. A continuación, lo pasaremos como argumento constructor a la clase StringSubstitutor. Finalmente, el objeto instanciado llama a la función replace(). Esta función recibe como argumento el texto con los marcadores de posición. Como resultado, obtenemos un texto interpolado.

Como vemos no es tarea fácil interpolar strings en java actualmente, pero tenemos la esperanza que en java 21 vendrá string template. Esta funcionalidad nos permitirá hacer lo siguiente : 

String name = "Joan";

String info = STR."My name is \{name}";

assert info.equals("My name is Joan");   // true

Otro ejemplo puede ser : 

// Embedded expressions can be strings

String firstName = "Bill";

String lastName  = "Duck";

String fullName  = STR."\{firstName} \{lastName}"; // "Bill Duck"

String sortName  = STR."\{lastName}, \{firstName}"; // "Duck, Bill"

// Embedded expressions can perform arithmetic

int x = 10, y = 20;

String s = STR."\{x} + \{y} = \{x + y}"; // "10 + 20 = 30"

Que opinan?


Dejo link: https://openjdk.org/jeps/430



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.

Primeros pasos con Phoenix parte 9

 


Seguimos la idea del post anterior: Phoenix adopta el diseño de enchufe o plugs para funcionalidad componible. 

Por ejemplo los Endpoints estan organizados por plugs: 

defmodule HelloWeb.Endpoint do

  ...


  plug :introspect

  plug HelloWeb.Router


Los conectores Endpoint predeterminados hacen bastante trabajo. Aquí están en orden:

Plug.Static: sirve activos estáticos. Dado que este complemento viene antes que el registrador, las solicitudes de activos estáticos no se registran.

Phoenix.LiveDashboard.RequestLogger: configura el registrador de solicitudes para Phoenix LiveDashboard, esto le permitirá tener la opción de pasar un parámetro de consulta para transmitir registros de solicitudes o habilitar/deshabilitar una cookie que transmite registros de solicitudes desde su tablero.

Plug.RequestId: genera un ID de solicitud único para cada solicitud.

Plug.Telemetry: agrega puntos de instrumentación para que Phoenix pueda registrar la ruta de la solicitud, el código de estado y la hora de la solicitud de forma predeterminada.

Plug.Parsers: analiza el cuerpo de la solicitud cuando hay disponible un analizador conocido. De forma predeterminada, este complemento puede manejar contenido codificado en URL, de varias partes y JSON (con Jason). El cuerpo de la solicitud se deja intacto si el tipo de contenido de la solicitud no se puede analizar.

Plug.MethodOverride: convierte el método de solicitud en PUT, PATCH o DELETE para solicitudes POST con un parámetro _method válido.

Plug.Head: convierte las solicitudes HEAD en solicitudes GET y elimina el cuerpo de la respuesta

Plug.Session: un complemento que configura la gestión de sesiones. Tenga en cuenta que fetch_session/2 aún debe llamarse explícitamente antes de usar la sesión, ya que este complemento solo configura cómo se obtiene la sesión.

En el medio del endpoint, también hay un bloque condicional:


  if code_reloading? do

    socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket

    plug Phoenix.LiveReloader

    plug Phoenix.CodeReloader

    plug Phoenix.Ecto.CheckRepoStatus, otp_app: :hello

  end


Este bloque solo se ejecuta en desarrollo. Permite:

  • recarga en vivo: si cambia un archivo CSS, se actualizan en el navegador sin actualizar la página;
  • recarga de código, para que podamos ver los cambios en nuestra aplicación sin reiniciar el servidor;
  • verificar el estado del repositorio, lo que garantiza que nuestra base de datos esté actualizada y, de lo contrario, genera un error legible y procesable.


En el enrutador, podemos declarar enchufes dentro de tuberías:


defmodule HelloWeb.Router do

  use HelloWeb, :router

  pipeline :browser do

    plug :accepts, ["html"]

    plug :fetch_session

    plug :fetch_live_flash

    plug :put_root_layout, {HelloWeb.LayoutView, :root}

    plug :protect_from_forgery

    plug :put_secure_browser_headers

    plug HelloWeb.Plugs.Locale, "en"

  end

  scope "/", HelloWeb do

   pipe_through :browser

    get "/", PageController, :index

  end


Las rutas se definen dentro de los ámbitos y los ámbitos pueden canalizarse a través de múltiples canalizaciones. Una vez que una ruta coincide, Phoenix invoca todos los complementos definidos en todas las tuberías asociadas a esa ruta. Por ejemplo, acceder a "/" se canalizará a través de la canalización del navegador, invocando en consecuencia todos sus conectores.

Como veremos en la guía de enrutamiento, las tuberías en sí mismas son tapones. Allí, también discutiremos todos los complementos en la tubería :browser.

Finalmente, los controladores también son enchufes, por lo que podemos hacer:


defmodule HelloWeb.PageController do

  use HelloWeb, :controller

  plug HelloWeb.Plugs.Locale, "en"


En particular, los complementos del controlador brindan una función que nos permite ejecutar complementos solo dentro de ciertas acciones. Por ejemplo, puedes hacer:

defmodule HelloWeb.PageController do

  use HelloWeb, :controller

 plug HelloWeb.Plugs.Locale, "en" when action in [:index]


Y el complemento solo se ejecutará para la acción de índice.

Al cumplir con el contrato de conexión, convertimos una solicitud de aplicación en una serie de transformaciones explícitas. No se detiene ahí. Para ver realmente qué tan efectivo es el diseño de Plug, imaginemos un escenario en el que necesitamos verificar una serie de condiciones y luego redirigir o detenernos si una condición falla. Sin enchufe, terminaríamos con algo como esto:



defmodule HelloWeb.MessageController do

  use HelloWeb, :controller

  def show(conn, params) do

    case Authenticator.find_user(conn) do

      {:ok, user} ->

        case find_message(params["id"]) do

          nil ->

            conn |> put_flash(:info, "That message wasn't found") |> redirect(to: ~p"/")

          message ->

            if Authorizer.can_access?(user, message) do

              render(conn, :show, page: message)

            else

              conn |> put_flash(:info, "You can't access that page") |> redirect(to: ~p"/")

            end

        end

      :error ->

        conn |> put_flash(:info, "You must be logged in") |> redirect(to: ~p"/")


    end

  end

end


¿Observa cómo unos pocos pasos de autenticación y autorización requieren anidamiento y duplicación complicados? Mejoremos esto con un par de enchufes.


defmodule HelloWeb.MessageController do

  use HelloWeb, :controller

  plug :authenticate

  plug :fetch_message

  plug :authorize_message

  def show(conn, params) do

    render(conn, :show, page: conn.assigns[:message])

  end


  defp authenticate(conn, _) do

   case Authenticator.find_user(conn) do

      {:ok, user} ->

        assign(conn, :user, user)

      :error ->

        conn |> put_flash(:info, "You must be logged in") |> redirect(to: ~p"/") |> halt()

    end

  end

  defp fetch_message(conn, _) do

    case find_message(conn.params["id"]) do

      nil ->

        conn |> put_flash(:info, "That message wasn't found") |> redirect(to: ~p"/") |> halt()

      message ->

        assign(conn, :message, message)

    end

  end


  defp authorize_message(conn, _) do

    if Authorizer.can_access?(conn.assigns[:user], conn.assigns[:message]) do

      conn

    else

      conn |> put_flash(:info, "You can't access that page") |> redirect(to: ~p"/") |> halt()

    end

  end

end


Para que todo esto funcione, convertimos los bloques de código anidados y usamos halt(conn) cada vez que llegamos a una ruta de error. La funcionalidad halt(conn) es esencial aquí: le dice a Plug que no se debe invocar el siguiente plug.

Al final del día, al reemplazar los bloques de código anidados con una serie aplanada de transformaciones de complemento, podemos lograr la misma funcionalidad de una manera mucho más componible, clara y reutilizable.


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. 



Clean

 


Clean es un lenguaje de programación funcional de propósito general, diseñado para hacer que la programación sea más segura, eficiente y fácil de entender. Clean es un lenguaje de programación de alto nivel que ofrece muchas características avanzadas de programación funcional, como la inferencia de tipos, el orden superior y la recursividad estructural.

Características principales de Clean:

  • Sintaxis clara: Clean ofrece una sintaxis clara y legible que permite una fácil comprensión del código y facilita el mantenimiento y la depuración.
  • Inferencia de tipos: Clean utiliza la inferencia de tipos, lo que significa que el compilador puede determinar el tipo de datos de una variable sin que el programador tenga que especificarlo explícitamente.
  • Recursividad estructural: Clean ofrece una técnica de recursividad estructural que garantiza la terminación de una función recursiva y previene la ocurrencia de bucles infinitos.
  • Orden superior: Clean admite el uso de funciones de orden superior, lo que significa que las funciones pueden tomar otras funciones como argumentos y devolver funciones como resultados.
  • Gestión automática de memoria: Clean maneja automáticamente la gestión de memoria, lo que significa que el programador no tiene que preocuparse por la asignación y desasignación de memoria.


Ventajas de Clean:

  • Facilita el mantenimiento y la depuración: La sintaxis clara y legible de Clean facilita el mantenimiento y la depuración del código.
  • Seguridad: Clean garantiza que los programas sean seguros mediante la eliminación de errores comunes de programación, como la asignación de memoria no válida y la división por cero.
  • Eficiencia: Clean es un lenguaje de programación muy eficiente que utiliza técnicas avanzadas de optimización de código para maximizar el rendimiento.
  • Escalabilidad: Clean es un lenguaje de programación escalable que se adapta bien a proyectos grandes y complejos.


En resumen, Clean es un lenguaje de programación funcional que ofrece muchas características avanzadas de programación funcional, como la inferencia de tipos, el orden superior y la recursividad estructural. Clean es un lenguaje de programación seguro, eficiente, escalable y fácil de entender que es ideal para proyectos de desarrollo de software de alta calidad.


Veamos un ejemplo : 


module Main

import StdEnv


factorial :: Int -> Int

factorial n = if n == 0 then 1 else n * factorial (n - 1)


Start w = writeln (show (factorial 5))


En este ejemplo, la función factorial toma un argumento entero n y calcula el factorial de n utilizando una definición recursiva. Si n es cero, la función devuelve 1. De lo contrario, la función multiplica n por el factorial de n-1. La función show convierte el resultado en una cadena y la función writeln imprime la cadena en la salida estándar.

El programa principal es la función Start, que utiliza writeln para imprimir el resultado del cálculo del factorial de 5. El tipo w en la declaración Start w indica que w no se utiliza en el programa y se puede ignorar.

Dejo link : https://clean-lang.org/


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. 


Primeros pasos con Phoenix parte 8


Plug vive en el corazón de la capa HTTP de Phoenix. Interactuamos con los enchufes o Plug en cada paso del ciclo de vida de una request, y los componentes principales de Phoenix, como end points, enrutadores y controladores, son solo enchufes internamente. 

Plug es una especificación para módulos componibles entre aplicaciones web. También es una capa de abstracción para adaptadores de conexión de diferentes servidores web. La idea básica de Plug es unificar el concepto de "conexión" sobre la que operamos. Esto difiere de otras capas de middleware HTTP como Rack, donde la solicitud y la respuesta están separadas en la pila de middleware.

En el nivel más simple, la especificación Plug viene en dos formas: complementos de funciones y complementos de módulos.

Para actuar como un enchufe, una función necesita: aceptar una estructura de conexión (%Plug.Conn{}) como primer argumento y opciones de conexión como segundo;

Cualquier función que cumpla con estos dos criterios servirá. Por ejemplo:


def introspect(conn, _opts) do

  IO.puts """

  Verb: #{inspect(conn.method)}

  Host: #{inspect(conn.host)}

  Headers: #{inspect(conn.req_headers)}

  """


  conn

end


Esta función hace lo siguiente:

  1. Recibe una conexión y opciones (que no usamos)
  2. Imprime alguna información de conexión en el terminal.
  3. Devuelve la conexión

Veamos esta función en acción agregándola a nuestro end point en lib/hello_web/endpoint.ex. Podemos conectarlo en cualquier lugar, así que hagámoslo insertando plug :introspect justo antes de delegar la solicitud al enrutador:

defmodule HelloWeb.Endpoint do

  ...


  plug :introspect

  plug HelloWeb.Router


  def introspect(conn, _opts) do

    IO.puts """

    Verb: #{inspect(conn.method)}

    Host: #{inspect(conn.host)}

    Headers: #{inspect(conn.req_headers)}

    """


    conn

  end

end


Los complementos de función se conectan pasando el nombre de la función como un átomo. Para probar esto, vamos al navegador y buscamos http://localhost:4000. Debería ver algo como esto impreso en su terminal de shell:


Verb: "GET"

Host: "localhost"

Headers: [...]


Nuestro enchufe simplemente imprime información de la conexión. Aunque nuestro complemento inicial es muy simple, puede hacer prácticamente cualquier cosa que desee dentro de él. 

Ahora veamos la otra variante de enchufe, los enchufes de módulo.

Los conectores de módulo son otro tipo de conector que nos permite definir una transformación de conexión en un módulo. El módulo solo necesita implementar dos funciones:

  • init/1 que inicializa cualquier argumento u opción que se pase a call/2
  • call/2 que realiza la transformación de la conexión. call/2 es solo un complemento de función como vimos anteriormente

Para ver esto en acción, escribamos un complemento de módulo que coloque la clave y el valor :locale en la conexión para uso posterior en otros complementos, acciones de controlador y nuestras vistas. Coloquemos el contenido a continuación en un archivo llamado lib/hello_web/plugs/locale.ex:


defmodule HelloWeb.Plugs.Locale do

  import Plug.Conn


  @locales ["en", "fr", "de"]


  def init(default), do: default


  def call(%Plug.Conn{params: %{"locale" => loc}} = conn, _default) when loc in @locales do

    assign(conn, :locale, loc)

  end


  def call(conn, default) do

    assign(conn, :locale, default)

  end

end


Para probarlo, agreguemos este complemento de módulo a nuestro enrutador, agregando el complemento HelloWeb.Plugs.Locale, "en" a nuestra tubería de navegación en lib/hello_web/router.ex:


defmodule HelloWeb.Router do

  use HelloWeb, :router


  pipeline :browser do

    plug :accepts, ["html"]

    plug :fetch_session

    plug :fetch_flash

    plug :protect_from_forgery

    plug :put_secure_browser_headers

    plug HelloWeb.Plugs.Locale, "en"

  end

  ...


En la devolución de llamada init/1, pasamos una configuración regional predeterminada para usar si no hay ninguna presente en los parámetros. También utilizamos la coincidencia de patrones para definir varios encabezados de función call/2 para validar la configuración regional en los parámetros, y recurrimos a "en" si no hay ninguna coincidencia. assign/3  es parte del módulo Plug.Conn y es cómo almacenamos valores en la estructura de datos de conn.

Para ver la asignación en acción, vayamos a la plantilla en lib/hello_web/components/layouts/home.html.heex y agreguamos el siguiente código después del cierre de la etiqueta </h1>:


<p>Locale: <%= @locale %></p>


Si vamos a http://localhost:4000/ y debería ver la configuración regional exhibida. Y si vamos a http://localhost:4000/?locale=fr y debería ver la asignación cambiada a "fr". Alguien puede usar esta información junto con Gettext para proporcionar una aplicación web totalmente internacionalizada.

Eso es todo lo que hay que hacer con Plug. Phoenix adopta el diseño de enchufe de las transformaciones componibles en toda la pila.