¿Qué pasa si no hay un analizador estático para su idioma favorito?

Bueno, si tu idioma favorito es ruso, inglés, etc., entonces este está en otro centro . Y si el lenguaje de programación o marcado, entonces, por supuesto, escriba usted mismo el analizador. A primera vista, esto es muy difícil, pero, afortunadamente, hay herramientas multilingües listas para usar que son relativamente fáciles de agregar soporte para un nuevo idioma. Hoy mostraré cómo agregar soporte de lenguaje Modelica al analizador PMD con una cantidad de tiempo bastante pequeña.


Por cierto, ¿sabe qué puede degradar la calidad de la base de código obtenida de una secuencia de solicitudes de extracción ideales? El hecho de que los programadores de terceros copiaron fragmentos de código de proyecto existente en sus parches en lugar de una abstracción competente. Debe admitir que, hasta cierto punto, es aún más difícil detectar una banalidad que el código de mala calidad: es de alta calidad e incluso se depura con cuidado, por lo que la verificación local no es suficiente aquí, debe tener en cuenta toda la base del código, pero esto no es fácil para una persona ... Entonces: si agregar soporte completo para Modelica (sin crear reglas específicas) al estado "puede ejecutar comprobaciones primitivas" me tomó alrededor de una semana, ¡luego el soporte para el detector de copiar y pegar a menudo se puede agregar en un día!


¿Qué más es Modelica?


Modelica es, como su nombre indica, un lenguaje para escribir modelos de sistemas físicos. De hecho, no solo físico: es posible describir procesos químicos, el comportamiento cuantitativo de las poblaciones animales, etc., que se describe mediante sistemas de ecuaciones diferenciales de la forma der(X) = f(X) , donde X es el vector de las incógnitas. Piezas de código imperativo también son compatibles. Las ecuaciones diferenciales parciales no se admiten explícitamente, pero es posible dividir el área de estudio en partes (como probablemente habríamos hecho en un lenguaje de propósito general), y luego escribir las ecuaciones para cada elemento, reduciendo el problema al anterior. El truco de Modelka es que la solución a este der(X) = f(X) recae en el compilador: solo puede cambiar el solucionador en la configuración , la ecuación no tiene que ser lineal, etc. En resumen, hay algunas ventajas (escribí la fórmula del libro de texto, y funcionó), y los contras (con más abstracción, tenemos menos control). Una introducción a Modelika es el tema de un artículo separado (que ya ha aparecido varias veces en Habré), e incluso un ciclo completo, hoy me interesa como abierto y tener varias implementaciones, pero, por desgracia, sigue siendo un estándar húmedo.


Además, Modelika, por un lado, tiene una tipificación estática (que nos ayudará a escribir un análisis significativo más rápido), por otro lado, al crear instancias de un modelo, no se requiere que el compilador verifique completamente la biblioteca completa (por lo tanto, un analizador estático es muy útil para atrapar “durmiendo” errores) Finalmente, a diferencia de algunos C ++, para los cuales hay una nube de analizadores estáticos y compiladores con hermosos, y lo más importante detallado, ver plantillas de C ++ diagnóstico de errores, compiladores Los modelos aún generan periódicamente un error interno del compilador, lo que significa que hay espacio para ayudar al usuario incluso con un analizador bastante simple.


¿Qué es el PMD?


Voy a responder cancion en bicicleta Una vez que quería hacer una pequeña solicitud de extracción en el entorno de desarrollo para OpenModelica. Al ver cómo se procesa el guardado del modelo en otra parte del código, noté una pequeña y no muy clara pieza interior de cuatro líneas de código que admitía algún tipo de invariante. No entiendo con qué tipo de editor interno interactúa, pero me doy cuenta de que desde el punto de vista de este código, mi tarea es completamente idéntica, simplemente lo puse en una función para poder reutilizarlo y no romperlo. Menteiner dijo que es maravilloso, solo entonces reemplace este código con una llamada a la función en los veinte lugares restantes ... Decidí no involucrarme aún, e hice otra copia, notando que de alguna manera tendría que peinar todo de una vez sin mezclarlo con el parche actual En Google, encontré el Detector de copiar y pegar (CPD), parte del analizador estático PMD, que admite incluso más idiomas que el analizador en sí. Habiéndolo establecido en la base del código OMEdit, esperaba ver esas dos docenas de piezas de cuatro líneas. Simplemente no los vi (cada uno de ellos simplemente no superó el umbral en el número de tokens), pero vi, por ejemplo, la repetición de casi cincuenta líneas de código C ++. Como ya dije, es poco probable que el medidor simplemente copie una pieza gigantesca de otro archivo. Pero podría fácilmente perderse esto en relaciones públicas, porque el código, por definición, ¡ya cumplía con todos los estándares del proyecto! Cuando compartí la observación con el medidor, él estuvo de acuerdo en que sería necesario limpiar como parte de una tarea separada.


En consecuencia, el Detector de errores de programa (PMD) es un analizador estático fácilmente extensible. Tal vez no calcule el conjunto de valores que puede tomar una variable (aunque quién sabe ...), pero para agregarle reglas, ¡ni siquiera necesita conocer Java y cambiar su código de alguna manera! El hecho es que lo primero que él, y no es sorprendente, es construir archivos AST con códigos fuente. ¿Y cómo se ve el árbol de análisis fuente? ¡Al árbol de análisis XML! Por lo tanto, puede describir las reglas simplemente como solicitudes XPath, para las cuales coincide, luego emitimos una advertencia. ¡Incluso tienen un depurador gráfico para las reglas! Las reglas más complejas, por supuesto, se pueden escribir directamente en Java como visitantes para el AST.


Consecuencia : PMD se puede usar no solo para las reglas duras y universales que los programadores de Java severos se han comprometido con el código del analizador, sino también para el estilo de codificación local, ¡incluso si inserta su propio juego de reglas local.xml en cada repositorio!


Nivel 1: busque copiar y pegar automáticamente


En principio, agregar soporte para un nuevo lenguaje en CPD es a menudo muy simple. No veo ningún sentido en volver a contar la documentación "cómo hacerlo": es muy clara, estructurada y paso a paso. Para volver a contar tal cosa, solo juega en un teléfono dañado. Mejor describo lo que te espera (TLDR: está bien) :


  • El analizador (tanto PMD como CPD) se desarrolla en el github en el repositorio pmd / pmd
  • El depurador de reglas visuales se ha movido a un repositorio pmd / pmd-designer separado. Tenga en cuenta que el sobrenombre de jar terminado se incrusta automáticamente en la distribución binaria PMD , que Gradle recopilará para usted en el repositorio anterior, no necesita clonar especialmente pmd-designer para esto.
  • El proyecto tiene una documentación del desarrollador . El que leí fue muy detallado. Es cierto, un poco anticuado, pero esto es tratado por la segunda solicitud de extracción :)

Te advertiré que estoy desarrollando en Ubuntu. En Windows, también debería funcionar perfectamente, tanto en términos de calidad como en el sentido de una forma ligeramente diferente de iniciar herramientas.


Entonces, para agregar un nuevo idioma al CPD, solo necesita ...


  • ATENCIÓN: si desea soporte completo para PMD antes del lanzamiento de PMD 7, entonces es mejor pasar directamente al nivel 2, ya que el soporte normal para el camino fácil a través de la gramática Antlr terminada aparecerá, según los rumores, en la versión 7, pero por ahora solo pasará tiempo (aunque y un poquito ...)
  • Bifurca el repositorio pmd / pmd .
  • Encuentre en antlr / grammars-v4 una gramática preparada para su idioma; por supuesto, si el idioma es interno, debe escribirlo usted mismo, pero para Modelika, por ejemplo, se encontró. Aquí, por supuesto, debe cumplir con los trámites de las licencias: no soy un abogado, pero al menos necesito especificar la fuente desde donde copié.
  • Después de eso, debe crear el pmd-<your language name> , agregarlo a Gradle y colocar el archivo de gramática allí. Además, después de leer dos páginas de documentación no estresante, vuelva a hacer el script de ensamblaje del módulo para Go, un par de clases para cargar el módulo a través de la reflexión, bueno, hay una pequeña cosa ...
  • Corrija la salida de referencia en una de las pruebas, porque ahora CPD admite un idioma más. ¿Cómo encuentras esta prueba? Muy fácil: quiere romper la construcción .
  • BENEFICIOS! Es realmente simple siempre que haya una gramática preparada

Ahora, estando en la raíz del repositorio pmd, puede escribir ./mvnw clean verify , mientras que en pmd-dist/target obtendrá, entre otras cosas, distribución binaria en forma de archivo zip que necesita descomprimir y ejecutar usando ./bin/run.sh cpd --minimum-tokens 100 --files /path/to/source/dir --language <your language name> del directorio desempaquetado. En principio, puede hacer ../mvnw clean verify desde su nuevo módulo, lo que acelerará drásticamente el ensamblaje, pero luego deberá colocar correctamente el apodo del jar ensamblado en la distribución binaria desempaquetada (por ejemplo, ensamblado una vez después de registrar un nuevo módulo).


Nivel 2: encontrar errores y violaciones de la guía de estilo


Como dije, se promete soporte completo para Antlr en PMD 7 . Si usted, como yo, no quiere esperar a la liberación del mar, tendrá que obtener una descripción de la gramática del idioma en formato JJTree de alguna parte. Tal vez pueda anular el soporte de un analizador arbitrario usted mismo: la documentación dice que esto es posible, pero no dicen cómo exactamente ... Acabo de tomar modelica.g4 del mismo repositorio con gramáticas para Anltr como base, y volver a convertirlo manualmente en JJTree. Naturalmente, si la gramática resultó ser un procesamiento de la existente, nuevamente, indique la fuente, verifique el cumplimiento de las licencias y. etc.


Por cierto, para una persona que conoce bien todo tipo de generadores de analizadores, es poco probable que esto sea una sorpresa. Antes de eso, realmente utilicé solo mis propios escritores regulares y combinadores de analizador en Scala. Por lo tanto, lo obvio, de hecho, me entristeció al principio: AST, por supuesto, obtendré de modelica.g4 , pero no se ve muy claro y "utilizable": habrá nubes de nodos adicionales en él, y si no miras los tokens , pero solo en nodos, no siempre está claro dónde, por ejemplo, termina la rama y luego comienza.


Nuevamente, no volveré a contar la documentación en JJTree y un buen tutorial ; esta vez, sin embargo, no porque el original brille con detalle y claridad, sino porque yo mismo no lo descubrí por completo, pero la documentación se retransmitió incorrectamente, pero con confianza, obviamente peor que no volver a contar. Será mejor que deje una pequeña pista, descubierta en el camino:


  • En primer lugar, el código de descripción del analizador JavaCC supone inserciones Java que se inscribirán en el analizador generado
  • No se confunda con el hecho de que al construir un AST, una sintaxis como [ Expression() ] significa opcional, y en el contexto de describir tokens, elegir un carácter, como en una expresión regular. Hasta donde entiendo la explicación de los desarrolladores de PMD, estas son construcciones similares que tienen un significado tan diferente: legado, señor ...
  • Para el nodo raíz (en mi caso, StoredDefinition ), debe especificar su tipo en lugar de void (es decir, ASTStoredDefiniton )
  • Usando la sintaxis #void después del nombre del nodo, puede ocultarlo del árbol analizado (es decir, solo afectará cuál es la fuente correcta y cuál no, y cómo se anidarán los otros nodos)
  • Usando una construcción del formulario void SimpleExpression() #SimpleExpression(>1) podemos decir que el nodo debe mostrarse en el AST resultante, si tiene más de un descendiente. Esto es muy conveniente cuando se describen expresiones con muchos operadores con diferentes prioridades: es decir, desde el punto de vista del analizador, la constante 1 solitaria será algo así como LogicExpression(AdditiveExpression(MultiplicativeExperssion(Constant(1)))) - ingrese todos los n niveles de prioridades de operación , pero el código del analizador solo obtendrá Constant(1)
  • El nodo tiene una image variable estándar (ver setImage getImage , setImage ), que generalmente setImage la "esencia" de este nodo: por ejemplo, para un nodo correspondiente al nombre de una variable local, es lógico copiar el token coincidente con el identificador en la image (por defecto, todos los tokens de los árboles se tirarán, por lo que vale la pena copiar el significado que contienen, en cualquier caso, si es algo variable y no solo palabras clave)
  • LOOKAHEAD : bueno, esta es una canción separada, incluso un capítulo separado en la documentación está dedicada a ella
    • En términos generales, en JavaCC, si va a un nodo, no puede devolverlo e intentar analizarlo de manera diferente, pero puede mirar hacia adelante con anticipación y decidir si desea ir o no.
    • en el caso más simple, al ver una advertencia de JavaCC, solo dices en el encabezado LOOKAHEAD = n y obtienes errores de análisis misteriosos, porque en el caso general, al parecer, no puede resolver todos los problemas (bueno, excepto que al configurar unos pocos miles de millones de tokens, realmente obtienes una vista previa de todo, pero no el hecho de que funciona de esa manera ... .)
    • delante del nombre del nodo incrustado, puede indicar explícitamente sobre la base de cuántos tokens aquí definitivamente puede tomar la decisión final
    • si en el caso general no existe un número fijo de tokens, puede decir "vaya aquí, si anteriormente, a partir de este punto, logramos hacer coincidir dicho prefijo, y luego la descripción habitual del subárbol"
    • tenga cuidado: en el caso general, JavaCC no puede verificar la exactitud de las directivas LOOKAHEAD : confía en usted, así que al menos LOOKAHEAD la prueba matemática de por qué es suficiente esa anticipación ...

Ahora que tiene una descripción de la gramática del idioma en formato JJTree, estos simples 14 pasos lo ayudarán a agregar compatibilidad con el idioma. La mayoría de ellos tienen la forma "crear una clase similar a la implementación para Java o VM, pero adaptada". Notaré solo las características típicas, algunas de ellas aparecerán en la documentación principal si aceptan mi solicitud de extracción de documentación :


  • Al comentar la eliminación de todos los archivos generados en el script de ensamblaje alljavacc.xml (que está en su nuevo módulo), puede transferirlos al árbol de origen desde las fuentes de target/generated-sources . Pero mejor no. Probablemente, solo se cambiará una pequeña parte, por lo que es mejor eliminar solo unos pocos: vieron la necesidad de cambiar la implementación predeterminada, se copiaron en el árbol de origen, se agregaron a la lista de archivos eliminados, se reconstruyeron, y ahora administran el archivo, específicamente este archivo . De lo contrario, será difícil averiguar qué se ha cambiado exactamente, y el soporte difícilmente se puede llamar agradable.
  • ahora que tiene una implementación del modo PMD "principal", también puede colgar fácilmente en su analizador JJTree un enlace para CPD, similar a Java u otra implementación disponible
  • Recuerde implementar un método que devuelva el nombre de host para las consultas XPath. En la implementación predeterminada, se obtiene una recursión infinita (el nombre del nodo es a través de toString y viceversa), o algo más, en general, debido a esto, tampoco es posible mirar el árbol en PMD Designer, y sin esta depuración la gramática es realmente triste
  • parte de los registros de componentes se realiza agregando archivos de texto de puntos de entrada de nombre de clase totalmente calificados a META-INF/services
  • lo que puede describirse declarativamente en las reglas (por ejemplo, una descripción detallada de la verificación y ejemplos de errores) no se describe en el código, sino en la category/<language name>/<ruleset>.xml ; en cualquier caso, deberá registrar sus reglas allí
  • ... pero al implementar las pruebas, aparentemente, se utiliza activamente algún mecanismo de autodescubrimiento, quizás de cosecha propia,
    • si le dicen, "agregue una prueba trivial para cada versión del idioma" - mejor no discuta, dicen "No lo necesito, funciona así" - tal vez este es el mecanismo de auto descubrimiento
    • si ve una prueba para una regla específica con un cuerpo de clase que contiene solo un comentario // no additional unit tests , entonces estas no son pruebas, solo se encuentran en los recursos en forma de descripción XML de los datos de entrada y las reacciones esperadas del analizador, de inmediato: algunas correctas y algunos ejemplos incorrectos

Una búsqueda pequeña pero importante: terminó el Diseñador PMD


Quizás pueda depurar todo sin un visualizador. Pero por que? En primer lugar, terminarlo es muy simple. En segundo lugar, ayudará mucho a sus usuarios que no estén familiarizados con Java: son fáciles y simples (si esto se aplica a XPath), bueno, o al menos sin recompilar, PMD podrá describir patrones simples de lo que no les gusta (en el caso más simple). - una guía de estilo como "el nombre de un paquete de modelo siempre comienza con una p minúscula").


A diferencia de otros errores que son visibles de inmediato, los problemas con PMD Designer son bastante insidiosos: parece que ya entendió que la inscripción de Java en el lado derecho del menú no es un botón, sino una lista desplegable de la selección de idioma O_o, en la que ya apareció Modelica, porque un nuevo módulo con registro de puntos de entrada ha aparecido en el classpath. Pero aquí puede elegir su idioma, descargar un archivo de prueba y ver AST. Y parece ser una victoria, pero de alguna manera es en blanco y negro, y el subárbol resaltado podría resaltarse en el texto, aunque no, hay un resaltado, pero se actualiza de forma torcida, y, sin embargo, ¿cómo no adivinaron para resaltar las coincidencias encontradas con XPath ... Ya estimando la cantidad de trabajo, piensa en la próxima solicitud de extracción, pero luego decide accidentalmente cambiar el idioma a Java y descargar algún código fuente del PMD en sí ... ¡Oh! ¡Está coloreado! .. ¡Y el resaltado del subárbol funciona! Uh ... y resulta que normalmente resalta las coincidencias encontradas y escribe fragmentos de texto en el cuadro a la derecha de la solicitud ... Parece que cuando ocurre una excepción en el código JavaFX mientras se procesa la interfaz, interrumpe la representación, pero no se imprime en la consola ...


En general, solo necesita agregar una clase ma-a-scarlet para resaltar la sintaxis basada en expresiones regulares. En mi caso, era net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.ModelicaSyntaxHighlighter , que debe registrarse en la clase AvailableSyntaxHighlighters . Tenga en cuenta que estos dos cambios ocurren en el repositorio pmd-designer , cuyo artefacto del ensamblaje debe colocarse en su distribución binaria.


Al final, se parece a esto (GIF tomado de README en el repositorio de PMD Designer):


PMD Designer en el trabajo


Subtotal


Si ha completado todos estos niveles, ahora tiene:


  • detector de copiar y pegar
  • motor de reglas
  • visualizador para depurar AST y llevarlo a una forma conveniente para el análisis (como ya hemos visto, ¡no todas las gramáticas del mismo idioma son igualmente útiles!)
  • el mismo visualizador para depurar las reglas XPath que sus usuarios pueden escribir sin recompilar PMD y, en general, conocimiento de Java (XPath, por supuesto, tampoco es BÁSICO, pero es al menos un lenguaje de consulta estándar y no local)

Espero que también comprenda el hecho de que la gramática ahora es una API estable para su implementación de soporte de idiomas: no la cambie (o más bien la función de convertir la fuente a AST descrita por él) a menos que sea absolutamente necesario, y si ha cambiado, notifíquelo como un cambio importante, y luego los usuarios se molestarán: lo más probable es que no todos escriban pruebas para sus reglas, pero es muy triste cuando las reglas verificaron el código y luego se detuvieron sin previo aviso, casi como una copia de seguridad, que de repente se rompió, y hace un año ...


La historia no termina ahí: al menos algunas reglas útiles tienen que ser escritas.


Pero eso no es todo: PMD admite de forma nativa ámbitos y declaraciones. Cada nodo AST tiene un alcance asociado: el cuerpo de la clase, función, bucle ... ¡Todo el archivo, en el peor de los casos! Y en cada ámbito hay una lista de definiciones (declaraciones) que contiene directamente. Como en otros casos, se propone implementar por analogía con otros idiomas, por ejemplo, Modelika (pero al momento de escribir, la lógica en mi solicitud de extracción es, francamente, sin formato). scopes declarations visitor, - ScopeAndDeclarationFinder , — , , , - , read-only AST. , .


 public class ModelicaHandler extends AbstractLanguageVersionHandler { // ... @Override public VisitorStarter getSymbolFacade() { return new VisitorStarter() { @Override public void start(Node rootNode) { new SymbolFacade().initializeWith((ASTStoredDefinition) rootNode); } }; } } 

Conclusión


PMD . , «» Clang Static Analyzer , . , CPD ( ), .

Source: https://habr.com/ru/post/474920/


All Articles