Buscamos y analizamos errores en el código Orchard CMS

Cuadro 6

Este artículo es el resultado de volver a verificar el proyecto Orchard utilizando el analizador estático PVS-Studio. Orchard es un sistema de gestión de contenido de código abierto que forma parte de Outercurve Foundation, una galería de proyectos ASP.NET sin fines de lucro. La prueba es interesante porque el proyecto y el analizador han crecido significativamente desde el primer análisis. Nuevos positivos y errores interesantes te están esperando.

Sobre Orchard CMS

Probamos Orchard con PVS-Studio hace tres años. Desde entonces, el analizador C # se ha desarrollado enormemente: mejoramos el análisis del flujo de datos, desarrollamos el análisis interprocedial, agregamos nuevos diagnósticos y reparamos una serie de falsos positivos. Además, el análisis mostró que todos los comentarios del artículo anterior fueron corregidos. Esto significa que se logra el objetivo: el código ha mejorado.

Quiero creer que los desarrolladores prestarán atención a este artículo y harán los cambios necesarios. Aún mejor, si introduce la práctica del uso regular de PVS-Studio. Permítame recordarle que para proyectos de código abierto proporcionamos una versión gratuita de la licencia. Por cierto, hay otras opciones que son adecuadas para proyectos cerrados.

El código del proyecto Orchard se puede descargar desde aquí , como lo hice yo. Una descripción completa del proyecto se puede encontrar aquí . Si aún no tiene nuestro analizador PVS-Studio, puede descargar una versión de prueba desde aquí . Usé la versión de PVS-Studio versión 7.05 Beta. Compartiré los resultados de su trabajo. Espero que esté de acuerdo con la utilidad de usar PVS-Studio. Lo principal es usar el analizador regularmente .

Resultados de validación

Te daré algunos números del último artículo para que no tengas que cambiar para comparar.

“Todos los archivos (3739 piezas) con la extensión .cs participaron en la última verificación. En total, contenían 214.564 líneas de código. Según los resultados de la auditoría, se recibieron 137 advertencias. En el primer nivel (alto) de confianza, se recibieron 39 advertencias. En el segundo nivel (promedio), se recibieron 60 advertencias ".

Por el momento, el proyecto tiene 2767 archivos .cs, es decir, el proyecto se ha convertido en menos de mil archivos. A juzgar por esta disminución en el número de archivos y el cambio del nombre del repositorio, se asignó un núcleo del proyecto ( commit 966 ). Hay 108.287 líneas de código en el núcleo y el analizador emitió 153 advertencias, 33 en un nivel alto, 70 en promedio. Por lo general, no consideramos advertencias del tercer nivel, no rompí la tradición.

Advertencia de PVS-Studio : V3110 Posible recursión infinita dentro del método 'TryValidateModel'. PrefixedModuleUpdater.cs 48

public bool TryValidateModel(object model, string prefix) { return TryValidateModel(model, Prefix(prefix)); } 

Como en el artículo anterior, abrimos la lista de errores con recursión infinita. Es difícil decir qué quería hacer exactamente el desarrollador en este caso. Pero noté que el método TryValidateModel tiene una sobrecarga de argumento único que se ve así:

 public bool TryValidateModel(object model) { return _updateModel.TryValidateModel(model); } 

Supongo que, como con el método sobrecargado, el desarrollador quería llamar a los métodos a través de _updateModel. El compilador no vio un error, _updateModel es del tipo IUpdateModel y la clase actual también implementa esta interfaz. Dado que no hay una sola condición en el método para proteger contra una StackOverflowException , es posible que no se haya llamado al método ni una sola vez, pero no contaría con él. Si la suposición es correcta, la versión corregida se verá así:

 public bool TryValidateModel(object model, string prefix) { return _updateModel.TryValidateModel(model, Prefix(prefix)); } 

Advertencia de PVS-Studio: V3008 A la variable 'contenido' se le asignan valores dos veces seguidas. Quizás esto sea un error. Líneas de verificación: 197, 190. DynamicCacheTagHelper.cs 197

 public override async Task ProcessAsync(....) { .... IHtmlContent content; .... try { content = await output.GetChildContentAsync(); } finally { _cacheScopeManager.ExitScope(); } content = await ProcessContentAsync(output, cacheContext); .... } 

El analizador vio dos asignaciones al contenido de la variable local . GetChildContentAsync es un método de la biblioteca que no es lo suficientemente común como para que podamos decidirlo y anotarlo. Entonces, desafortunadamente, ni nosotros ni el analizador sabemos nada sobre el objeto devuelto del método y los efectos secundarios. Pero definitivamente podemos decir que asignar un resultado en el contenido no tiene sentido sin uso. Quizás esto no sea un error en absoluto, solo una operación adicional. No pude llegar a una conclusión inequívoca sobre la forma de solucionarlo. Dejaré esta decisión a los desarrolladores.

Advertencia de PVS-Studio : V3080 Posible desreferencia nula. Considere inspeccionar 'itemTag'. CoreShapes.cs 92

 public async Task<IHtmlContent> List(....string ItemTag....) { .... string itemTagName = null; if (ItemTag != "-") { itemTagName = string.IsNullOrEmpty(ItemTag) ? "li" : ItemTag; } var index = 0; foreach (var item in items) { var itemTag = String.IsNullOrEmpty(itemTagName) ? null : ....; .... itemTag.InnerHtml.AppendHtml(itemContent); listTag.InnerHtml.AppendHtml(itemTag); ++index; } return listTag; } 

En este código, el analizador detectó una etiqueta de elemento de desreferencia peligrosa. Un buen ejemplo de la diferencia entre un analizador estático y una persona que realiza pruebas. El método tiene un parámetro llamado ItemTag y una variable local llamada itemTag . Como sabes, ¡la diferencia es enorme para el compilador! Estas son dos variables diferentes, aunque relacionadas. La conexión pasa por la tercera variable, itemTagName. El escenario para lanzar una excepción es este: el argumento ItemTag es "-", el itemTagName no tiene asignado un valor, seguirá siendo una referencia nula, y si es una referencia nula, el itemTag local también se convertirá en una referencia nula. Creo que es mejor lanzar una excepción aquí después de verificar la cadena.

 public async Task<IHtmlContent> List(....string ItemTag....) { .... string itemTagName = null; if (ItemTag != "-") { itemTagName = string.IsNullOrEmpty(ItemTag) ? "li" : ItemTag; } var index = 0; foreach (var item in items) { var itemTag = ....; if(String.IsNullOrEmpty(itemTag)) throw .... .... itemTag.InnerHtml.AppendHtml(itemContent); listTag.InnerHtml.AppendHtml(itemTag); ++index; } return listTag; } 

Advertencia de PVS-Studio: V3095 El objeto 'remoteClient' se usó antes de que se verificara como nulo. Verifique las líneas: 49, 51. ImportRemoteInstanceController.cs 49

 public async Task<IActionResult> Import(ImportViewModel model) { .... var remoteClient = remoteClientList.RemoteClients.FirstOrDefault(....); var apiKey = Encoding.UTF8.GetString(....(remoteClient.ProtectedApiKey)); if (remoteClient == null || ....) { .... } .... } 

El analizador vio la desreferenciación del RemoteClient y la comprobación de null a continuación. Esto es realmente una potencial NullReferenceException , porque el método FirstOrDefault puede devolver el valor predeterminado ( nulo para los tipos de referencia). Supongo que, para arreglar el código, simplemente transfiera el cheque anterior:

 public async Task<IActionResult> Import(ImportViewModel model) { .... var remoteClient = remoteClientList.RemoteClients.FirstOrDefault(....); if (remoteClient != null) var apiKey = UTF8.GetString(....remoteClient.ProtectedApiKey); else if (....) { .... } .... } 

Sin embargo, podría valer la pena reemplazar FirstOrDefault con First y eliminar la verificación por completo.

Desde PVS-Studio 7.05 Beta:

Por el momento, hemos anotado todos los métodos orDefault de LINQ . Esta información será utilizada por nuevos diagnósticos, notando la desreferenciación del resultado de llamar a estos métodos sin verificar. Para cada método orDefault hay un análogo que genera una excepción si no se encuentra un elemento adecuado. Esta excepción ayudará más a comprender el problema que la excepción NullReferenceException abstracta.

No puedo dejar de compartir el resultado de los diagnósticos desarrollados en este proyecto. 27 lugares potencialmente peligrosos. Aquí hay algunos de ellos:

ContentTypesAdminNodeNavigationBuilder.cs 71:

 var treeBuilder = treeNodeBuilders.Where(....).FirstOrDefault(); await treeBuilder.BuildNavigationAsync(childNode, builder, treeNodeBuilders); 

ListPartDisplayDriver.cs 217:

 var contentTypePartDefinition = ....Parts.FirstOrDefault(....); return contentTypePartDefinition.Settings....; 

ContentTypesAdminNodeNavigationBuilder.cs 113:

 var typeEntry = node.ContentTypes.Where(....).FirstOrDefault(); return AddPrefixToClasses(typeEntry.IconClass); 

Advertencia de PVS-Studio : V3080 Posible desreferencia nula del valor de retorno del método. Considere inspeccionar: CreateScope (). SetupService.cs 136

 public async Task<string> SetupInternalAsync(SetupContext context) { .... using (var shellContext = await ....) { await shellContext.CreateScope().UsingAsync(....); } .... } 

Entonces, el analizador observó la desreferenciación del resultado de llamar al método CreateScope . El método CreateScope es muy pequeño, lo daré en su totalidad:

 public ShellScope CreateScope() { if (_placeHolder) { return null; } var scope = new ShellScope(this); // A new scope can be only used on a non released shell. if (!released) { return scope; } scope.Dispose(); return null; } 

Como puede ver, puede devolver nulo en dos casos. El analizador no puede decir por qué rama pasará el programa, por lo tanto, marca el código como sospechoso. En mi código, inmediatamente agregaría un cheque nulo .

Tal vez juzgue parcial, pero todos los métodos asincrónicos deben estar asegurados contra NullReferenceException lo mejor posible. La depuración de tales cosas es un placer muy dudoso.

En este caso, el método CreateScope tiene cuatro llamadas, y en dos hay una comprobación, pero en las otras dos falta. Además, un par sin verificación es similar a copiar y pegar (una clase, un método, desreferenciado para llamar a UsingAsync). Ya hice la primera llamada de este par y, por supuesto, la segunda llamada también tiene una advertencia de analizador similar:

V3080 Posible desreferencia nula del valor de retorno del método. Considere inspeccionar: CreateScope (). SetupService.cs 192

Advertencia de PVS-Studio: V3127 Se encontraron dos fragmentos de código similares. Quizás, este es un error tipográfico y se debe usar la variable 'AccessTokenSecret' en lugar de 'ConsumerSecret' TwitterClientMessageHandler.cs 52

 public async Task ConfigureOAuthAsync(HttpRequestMessage request) { .... if (!string.IsNullOrWhiteSpace(settings.ConsumerSecret)) settings.ConsumerSecret = protrector.Unprotect(settings.ConsumerSecret); if (!string.IsNullOrWhiteSpace(settings.ConsumerSecret)) settings.AccessTokenSecret = protrector.Unprotect(settings.AccessTokenSecret); .... } 

Clásico copiar y pegar. ConsumerSecret se verificó dos veces y AccessTokenSecret , no una. Obviamente, necesitas arreglar:

 public async Task ConfigureOAuthAsync(HttpRequestMessage request) { .... if (!string.IsNullOrWhiteSpace(settings.ConsumerSecret)) settings.ConsumerSecret = protrector.Unprotect(settings.ConsumerSecret); if (!string.IsNullOrWhiteSpace(settings.AccessTokenSecret)) settings.AccessTokenSecret = protrector.Unprotect(settings.AccessTokenSecret); .... } 

Advertencia de PVS-Studio: V3139 Dos o más ramificaciones de casos realizan las mismas acciones. SerialDocumentExecuter.cs 23

Otro error de copiar y pegar. Para que quede más claro, daré toda la clase, ya que es pequeña.

 public class SerialDocumentExecuter : DocumentExecuter { private static IExecutionStrategy ParallelExecutionStrategy = new ParallelExecutionStrategy(); private static IExecutionStrategy SerialExecutionStrategy = new SerialExecutionStrategy(); private static IExecutionStrategy SubscriptionExecutionStrategy = new SubscriptionExecutionStrategy(); protected override IExecutionStrategy SelectExecutionStrategy(....) { switch (context.Operation.OperationType) { case OperationType.Query: return SerialExecutionStrategy; case OperationType.Mutation: return SerialExecutionStrategy; case OperationType.Subscription: return SubscriptionExecutionStrategy; default: throw ....; } } } 

El analizador consideró dos ramas de caso idénticas sospechosas. De hecho, hay tres entidades en la clase, dos regresan cuando circulan, una no. Si esto está planeado y se abandona la tercera opción, puede eliminarla combinando las dos ramas de la siguiente manera:

 switch (context.Operation.OperationType) { case OperationType.Query: case OperationType.Mutation: return SerialExecutionStrategy; case OperationType.Subscription: return SubscriptionExecutionStrategy; default: throw ....; } 

Si se trata de un error de copiar y pegar, debe corregir el campo devuelto de esta manera:

 switch (context.Operation.OperationType) { case OperationType.Query: return ParallelExecutionStrategy; case OperationType.Mutation: return SerialExecutionStrategy; case OperationType.Subscription: return SubscriptionExecutionStrategy; default: throw ....; } 

O viceversa. No estoy familiarizado con el proyecto y no puedo correlacionar los nombres de los tipos de operaciones y estrategias.

 switch (context.Operation.OperationType) { case OperationType.Query: return SerialExecutionStrategy; case OperationType.Mutation: return ParallelExecutionStrategy; case OperationType.Subscription: return SubscriptionExecutionStrategy; default: throw ....; } 

Advertencia de PVS-Studio : V3080 Posible desreferencia nula. Considere inspeccionar 'solicitud'. GraphQLMiddleware.cs 148

 private async Task ExecuteAsync(HttpContext context....) { .... GraphQLRequest request = null; .... if (HttpMethods.IsPost(context.Request.Method)) { .... } else if (HttpMethods.IsGet(context.Request.Method)) { .... request = new GraphQLRequest(); .... } var queryToExecute = request.Query; .... } 

En el primer bloque if , la variable de solicitud obtiene un valor distinto de nulo en varios lugares, pero en todas partes con condiciones anidadas. No cité todas estas condiciones, ya que el ejemplo habría sido demasiado engorroso. Las primeras condiciones que comprueban el tipo http del método IsGet o IsPost son suficientes . La clase Microsoft.AspNetCore.Http.HttpMethods tiene nueve métodos estáticos para verificar el tipo de solicitud. Esto significa que si, por ejemplo, una solicitud Eliminar o Establecer ingresa al método ExecuteAsync , se generará una NullReferenceException . Incluso si en este momento estos métodos no son compatibles, es mejor hacer una verificación con una excepción. Después de todo, los requisitos para el sistema pueden cambiar. Ejemplo de verificación a continuación:

 private async Task ExecuteAsync(HttpContext context....) { .... if (request == null) throw ....; var queryToExecute = request.Query; .... } 

Advertencia de PVS-Studio : V3080 Posible desreferencia nula del valor de retorno del método. Considere inspeccionar: Obtenga <ContentPart> (...). ContentPartHandlerCoordinator.cs 190

La mayoría de los desencadenadores de diagnóstico V3080 son más fáciles de ver en el entorno de desarrollo. Necesita navegación de método, resaltado de tipo, una atmósfera IDE alentadora. Trato de mantener los ejemplos lo más cortos posible para que se sienta más cómodo leyendo. Pero si no me funciona, o si desea verificar su nivel de programación y resolverlo por su cuenta, le aconsejo que observe cómo funciona este diagnóstico en cualquier proyecto abierto o propio.

 public override async Task LoadingAsync(LoadContentContext context) { .... context.ContentItem.Get<ContentPart>(typePartDefinition.Name) .Weld(fieldName, fieldActivator.CreateInstance()); .... } 

El analizador jura en esta línea. Veamos el método Get:

 public static TElement Get<TElement>(this ContentElement contentElement....) where TElement : ContentElement { return (TElement)contentElement.Get(typeof(TElement), name); } 

Causa su sobrecarga. Miremosla:

 public static ContentElement Get(this ContentElement contentElement....) { .... var elementData = contentElement.Data[name] as JObject; if (elementData == null) { return null; } .... } 

Resulta que si al recibir un elemento de Datos por el indexador de nombres obtenemos una entidad de un tipo incompatible con JObject , entonces el método Get devolverá nulo . No me aventuraré a juzgar la probabilidad de esto, ya que estos tipos son de la biblioteca Newtonsoft.Json , y tengo un poco de experiencia con eso. Pero el desarrollador sospechó que el artículo deseado podría no serlo. Por lo tanto, no se olvide de esta posibilidad cuando se refiera a los resultados de lectura. Agregaría un lanzamiento de excepción al primer Get si pensamos que el nodo debería ser, o verifique antes de desreferenciar, si la ausencia del nodo no cambia la lógica general (se toma el valor predeterminado, por ejemplo).

Opcion uno:

 public static ContentElement Get(this ContentElement contentElement....) { .... var elementData = contentElement.Data[name] as JObject; if (elementData == null) { throw.... } .... } 

Opción dos:

 public override async Task LoadingAsync(LoadContentContext context) { .... context.ContentItem.Get<ContentPart>(typePartDefinition.Name) ?.Weld(fieldName, fieldActivator.CreateInstance()); .... } 

Advertencia de PVS-Studio : V3080 Posible desreferencia nula. Considere inspeccionar los "resultados". ContentQueryOrchardRazorHelperExtensions.cs 19

 public static async Task<IEnumerable<ContentItem>> ContentQueryAsync(....) { var results = await orchardHelper.QueryAsync(queryName, parameters); .... foreach (var result in results) { .... } .... } 

Un ejemplo bastante simple en relación con el anterior. El analizador considera que el resultado de llamar a QueryAsync puede ser una referencia nula. Aquí está el método:

 public static async Task<IEnumerable> QueryAsync(....) { .... var query = await queryManager.GetQueryAsync(queryName); if (query == null) { return null; } .... } 

Aquí GetQueryAsync es un método de interfaz, no puede estar seguro de cada implementación. Además, en el mismo proyecto existe tal opción:

 public async Task<Query> GetQueryAsync(string name) { var document = await GetDocumentAsync(); if(document.Queries.TryGetValue(name, out var query)) { return query; } return null; } 

El análisis del método GetDocumentAsync se complica por la abundancia de llamadas a funciones externas y accesos a caché. Detengámonos en el hecho de que vale la pena agregar la verificación. Además, el método es asíncrono.

 public static async Task<IEnumerable<ContentItem>> ContentQueryAsync(....) { var results = await orchardHelper.QueryAsync(queryName, parameters); if(results == null) throw ....; .... foreach (var result in results) { .... } .... } 

Cuadro 14


Conclusión

¡No puedo dejar de notar la alta calidad del código! Sí, hubo otras deficiencias que no mencioné en este artículo, pero se consideraron las más graves. Por supuesto, esto no significa que un cheque cada tres años sea suficiente. El beneficio máximo se logra con el uso regular de un analizador estático, ya que es este enfoque el que le permite detectar y solucionar problemas en las primeras etapas, cuando el costo y la complejidad de la edición son mínimos.

Aunque las comprobaciones únicas no son tan efectivas como sea posible, le insto a que descargue y pruebe PVS-Studio en su proyecto. ¿Qué sucede si puede encontrar algo interesante?



Si desea compartir este artículo con una audiencia de habla inglesa, utilice el enlace a la traducción: Alexander Senichkin. Escaneando el código de Orchard CMS en busca de errores .

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


All Articles