Hoy publicamos la segunda parte de la traducción de material dedicado al análisis estático de grandes volúmenes de código Python del lado del servidor en Instagram.

→
La primera parteProgramadores que están cansados de linting
Dado que tenemos alrededor de un centenar de nuestras propias reglas de enlace, la contabilidad pedante de las recomendaciones emitidas por estas reglas puede resultar rápidamente en una pérdida de tiempo para los desarrolladores. Sería mejor pasar el tiempo dedicado a enderezar el estilo del código o deshacerse de patrones obsoletos para crear algo nuevo y desarrollar el proyecto.
Descubrimos que cuando los programadores ven demasiadas notificaciones procedentes de la interfaz, comienzan a ignorar todos estos mensajes. Esto también se aplica a notificaciones importantes.
Supongamos que decidimos declarar obsoleta la función
fn
y utilizar la función con un nombre mejor,
add
, en su lugar. Si no informa a los desarrolladores sobre esto, no sabrán que ya no necesitan usar la función
fn
. Peor aún, no saben qué usar en lugar de esta función. En esta situación, puede crear una regla de linter. Pero cualquier base de código grande ya contendrá muchas reglas. Como resultado, es probable que se pierda una notificación importante de linter en el montón de notificaciones de errores menores.
Linter es demasiado quisquilloso y la "señal útil" puede perderse fácilmente en el "ruido"¿Qué haremos con esto?
Puede solucionar automáticamente muchos problemas detectados por el linter. Si el linter en sí mismo se puede comparar con la documentación que aparece donde se necesita, entonces esas correcciones automáticas son un poco una refactorización del código que se ejecuta donde se necesita. Dada la gran cantidad de desarrolladores que trabajan en Instagram, es casi imposible entrenar a cada uno de ellos en nuestras mejores técnicas de escritura de código. Agregar capacidades de corrección automática de código al sistema nos permite educar a los desarrolladores sobre nuevas técnicas cuando no están al tanto de estas técnicas. Esto nos ayuda a actualizar rápidamente a los desarrolladores. Las correcciones automáticas, además, nos permitieron hacer que los programadores se enfocaran en cosas importantes, en lugar de enfocarse en cambios de código menores monótonos. En general, se puede observar que las correcciones automáticas de código son más efectivas y útiles en términos de capacitación de desarrolladores que las notificaciones simples de linter.
Entonces, ¿cómo crear un sistema para la corrección automática de código? Una pelusa basada en un árbol de sintaxis nos brinda información sobre un nodo disfuncional. Como resultado, no necesitamos crear lógica para detectar problemas, ¡ya que tenemos las reglas correspondientes para el linter! Dado que sabemos qué nodo en particular no nos conviene y dónde se encuentra su código fuente, podemos, sin correr el riesgo de estropear algo, por ejemplo, reemplazar el nombre de la función
fn
con
add
. Esto es muy adecuado para corregir violaciones individuales de las reglas que se ejecutan a medida que se detectan tales violaciones. Pero, ¿qué sucede si introducimos una nueva regla para la interfaz, lo que significa que puede haber cientos de fragmentos de código en la base de código que no cumplen con esta regla? ¿Se pueden corregir de antemano todas estas inconsistencias?
Modificaciones de código
Un codemod es solo una forma de encontrar problemas y realizar cambios en el código fuente. Los codemods están basados en guiones. Codemod puede considerarse como "refactorización de esteroides". El rango de tareas resueltas por los modos de código es extremadamente amplio: desde simples, como renombrar una variable en una función, hasta complejas, como reescribir una función para que tome un nuevo argumento. Cuando se trabaja con el codemod, se utilizan los mismos conceptos que con el linter. Pero en lugar de informar al programador sobre el problema, como lo hace el linter, el modo de código resuelve automáticamente este problema.
¿Cómo escribir un codemod? Considera un ejemplo. Aquí queremos dejar de usar
get_global
. En esta situación, puede usar el linter, pero no se sabrá cuánto tiempo llevará arreglar el código completo, además, esta tarea se distribuirá entre muchos desarrolladores. Al mismo tiempo, incluso si el proyecto usa un sistema de corrección automática de código, puede tomar algún tiempo procesar todo el código.
Queremos alejarnos del uso de get_global y usar variables de instancia en su lugarPara resolver este problema, podemos, junto con la regla de linter que lo detecta, escribir un codemod. Creemos que permitir que los patrones y API obsoletos abandonen gradualmente el código distraerá a los desarrolladores y degradará la legibilidad del código. Preferimos eliminar de inmediato el código obsoleto y no ver cómo desaparece gradualmente del proyecto.
Dado el volumen de nuestro código y la cantidad de desarrolladores activos, esto a menudo significa eliminar automáticamente los diseños obsoletos. Si podemos borrar rápidamente el código de patrones obsoletos, esto significa que podemos mantener la productividad de todos los desarrolladores de Instagram.
Entonces, ¿cómo hacer un codemod? ¿Cómo reemplazar solo el fragmento de código que nos interesa, mientras se conservan los comentarios, la sangría y todo lo demás? Existen herramientas basadas en un árbol de sintaxis específico (como lo que crea LibCST) que le permiten modificar el código con precisión quirúrgica y guardar todas las construcciones auxiliares en él. Como resultado, si necesitamos cambiar el nombre de la función de
fn
para
add
en el árbol a continuación, entonces podemos escribir
add
lugar de
fn
en el nodo
Name
, ¡y luego escribir el árbol en el disco!
El modo de código se puede hacer escribiendo el nombre agregar al nodo Nombre en lugar del nombre fn. Entonces el árbol modificado se puede escribir en el disco. Puede leer más sobre esto en la documentación de LibCST.Ahora que nos hemos familiarizado un poco con las modificaciones de código, echemos un vistazo a un ejemplo práctico. Los empleados de Instagram están trabajando arduamente para que la base del código del proyecto esté completamente escrita. Kodmody los ayuda seriamente en este asunto.
Si tenemos un cierto conjunto de funciones sin tipo que deben escribirse, ¡podemos intentar generar los tipos devueltos por la inferencia de tipos habitual! Por ejemplo, si una función devuelve valores de un solo tipo primitivo, simplemente asignamos este tipo de valor de retorno a la función. Si una función devuelve valores de un tipo lógico, por ejemplo, si compara algo con algo o verifica algo, entonces podemos asignarle el tipo de valor de retorno
bool
. Descubrimos que en el curso del trabajo práctico con la base de código de Instagram, esta es una operación bastante segura.
Descubrir los tipos de valores devueltos por funcionesPero, ¿qué sucede si la función no devuelve ningún valor explícitamente o devuelve
None
implícitamente? Si la función no devuelve nada explícitamente, se le puede asignar el tipo
None
.
Esto, a diferencia del ejemplo anterior, puede ser más peligroso debido a la existencia de patrones comunes que usan los desarrolladores. Por ejemplo, en un método de clase base, puede lanzar una excepción
NotImplemented
, y en los métodos de subclases que anulan este método, puede devolver una cadena. Es importante tener en cuenta que todas estas técnicas son heurísticas, pero los resultados de su aplicación a menudo resultan ser correctos. Como resultado, pueden considerarse útiles.
Funciones que no devuelven nadaAmpliación de módulos de código con Pyre
Vamos un paso más allá. Instagram usa Pyre, un completo sistema de verificación de tipo estático similar a mypy. El uso de Pyre nos permite verificar los tipos en una base de código. ¿Qué pasa si usamos los datos generados por Pyre para expandir las capacidades de los codemods? El siguiente es un ejemplo de tales datos. ¡Es fácil ver que hay casi todo lo que necesita para corregir automáticamente las anotaciones de tipo!
$ pyre ƛ Found 2 type errors! testing/utils.py:7:0 Missing return annotation [3]: Returning `SomeClass` but no return type is specified. testing/utils.py:10:0 Missing return annotation [3]: Returning `testing.other.SomeOtherClass` but no return type is specified.
Pyre durante el trabajo realiza un análisis detallado del orden de ejecución de cada función. Como resultado, esta herramienta a veces con una probabilidad muy alta puede suponer que una función no anotada debería regresar. Esto significa que si Pyre cree que la función devuelve un tipo simple, asignamos a esta función el tipo de retorno. Sin embargo, ahora, en potencial, también necesitamos procesar comandos de importación. Esto significa que necesitamos saber si algo se importa o declara localmente. Más adelante tocaremos brevemente este tema.
¿Qué beneficios obtenemos al agregar automáticamente información de tipo que se muestra fácilmente en el código? Bueno, los tipos son documentación! Si la función está completamente escrita, entonces el desarrollador no tendrá que leer su código para descubrir las características de su llamada y las características de usar lo que devuelve.
def get_description(page: WikiPage) -> Optional[str]: if page.draft: return None return page.metadata["description"]
Muchos de nosotros hemos encontrado un código Python similar. La base de código de Instagram también tiene algo similar. Si la función
get_description
no estuviera tipificada, entonces necesitaría buscar en varios módulos para saber qué devuelve. Al mismo tiempo, incluso si estamos hablando de funciones más simples, cuyos tipos de valores de retorno son fáciles de derivar, sus variantes tipificadas se perciben más fácilmente que las no tipificadas.
Además, Pyre no verifica el funcionamiento correcto del cuerpo de la función si la función no está completamente anotada. En el siguiente ejemplo, la llamada a
some_function
fallará. Sería bueno saber sobre esto antes de que el código entre en producción.
def some_function(in: int) -> bool: return in > 0 def some_other_function(): if some_function("bla"):
En este caso, podemos descubrir un error similar después de que el código haya entrado en producción. El hecho es que
some_other_function
no tiene una anotación de tipo de retorno. Si lo anotáramos utilizando nuestros mecanismos heurísticos utilizando el tipo deducido automáticamente
None
, entonces habríamos descubierto un problema con los tipos antes de que pudiera causar algún problema. Esto, por supuesto, es un ejemplo artificial, pero en Instagram estos problemas son graves. Si tiene millones de líneas de código, entonces, en el proceso de revisión de código, puede perderse cosas que parecen completamente obvias en un ejemplo simple.
En Instagram, los métodos anteriores basados en tipos deducidos automáticamente permitieron escribir alrededor del 10% de las funciones. Como resultado, las personas ya no tenían que editar manualmente miles y miles de funciones. Las ventajas del código escrito son obvias, pero esto, en el contexto de nuestra conversación, conduce a otra ventaja importante. Una base de código completamente tipada abre posibilidades aún mayores para procesar código usando codemods.
Si confiamos en las anotaciones de tipo, significa que Pyre puede abrir posibilidades adicionales para nosotros. Veamos nuevamente el ejemplo en el que cambiamos el nombre de las funciones. ¿Qué sucede si la entidad que estamos renombrando está representada por un método de clase y no por una función global?
La función es un método de clase.Si combina la información de tipo recibida de Pyre y el modo de código que cambia el nombre de las funciones, puede, inesperadamente, hacer correcciones en el lugar donde se llama la función y donde se declara. En este ejemplo, dado que sabemos lo que está en el lado izquierdo de la construcción
a.fn
, también sabemos que es seguro cambiar esta construcción a
a.add
.
Análisis estático más avanzado
Python tiene cuatro tipos de ámbitos: ámbito global, ámbito de clase y nivel de función, ámbito anidadoEl análisis del alcance nos permite utilizar codemods aún más potentes. ¿Recuerdas uno de los ejemplos anteriores, donde hablamos sobre el hecho de que agregar anotaciones de tipo también puede significar la necesidad de trabajar con comandos de importación? Si el sistema analiza el alcance, esto significa que podemos saber qué tipos utilizados en el archivo están presentes gracias a los comandos de importación, los que se declaran localmente y los que faltan. Del mismo modo, si sabe que una variable global se superpone con un argumento de función, puede evitar cambiar accidentalmente el nombre de dicho argumento al cambiar el nombre de una variable global.
Resumen
En nuestra búsqueda para corregir todos los errores en el código de Instagram, entendimos una cosa. Consiste en el hecho de que la búsqueda del código que debe corregirse a menudo es más importante que la corrección misma. Los programadores a menudo tienen que resolver tareas simples, como renombrar funciones, agregar argumentos a métodos o dividir módulos en partes. Todo esto es común, pero el tamaño de nuestra base de código significa que una persona no podrá encontrar todas las líneas que necesitan ser cambiadas. Por eso es tan importante combinar las capacidades de los codemods con un análisis estático confiable. Esto nos permite encontrar con mayor confianza las partes del código que deben cambiarse, lo que significa que nos permite hacer que los modos de código sean más seguros y potentes.
Estimados lectores! ¿Usas mods de código?
