Comprobación del código fuente de Roslyn

PVS-Studio vs Roslyn

De vez en cuando volvemos a los proyectos que hemos verificado previamente utilizando PVS-Studio, lo que da como resultado sus descripciones en varios artículos. Dos razones hacen que estos regresos sean emocionantes para nosotros. En primer lugar, la oportunidad de evaluar el progreso de nuestro analizador. En segundo lugar, monitorear los comentarios de los autores del proyecto sobre nuestro artículo y el informe de errores, que generalmente les proporcionamos. Por supuesto, los errores pueden corregirse sin nuestra participación. Sin embargo, siempre es bueno cuando nuestros esfuerzos ayudan a mejorar un proyecto. Roslyn no fue la excepción. El artículo anterior sobre la verificación de este proyecto data del 23 de diciembre de 2015. En vista del progreso que nuestro analizador ha hecho desde entonces, es bastante largo. Dado que el núcleo C # del analizador PVS-Studio se basa en Roslyn, nos da un interés adicional en este proyecto. Como resultado, estamos tan interesados ​​como la mostaza sobre la calidad del código de este proyecto. Ahora probémoslo una vez más y descubramos algunos problemas nuevos e interesantes (pero esperemos que nada significativo) que PVS-Studio pueda encontrar.

Es probable que muchos de nuestros lectores conozcan bien a Roslyn (o .NET Compiler Platform). En resumen, es un conjunto de compiladores de código abierto y una API para el análisis de código de los lenguajes C # y Visual Basic .NET de Microsoft. El código fuente del proyecto está disponible en GitHub .

No daré una descripción detallada de esta plataforma y recomendaré revisar el artículo de mi colega Sergey Vasiliev " Introducción a Roslyn y su uso en el desarrollo de programas " a todos los lectores interesados. A partir de este artículo, puede descubrir no solo las características de la arquitectura de Roslyn, sino también cómo usamos exactamente esta plataforma.

Como mencioné anteriormente, han pasado más de 3 años desde que mi colega Andrey Karpov escribió el último artículo sobre el cheque de Roslyn " Lanzamiento de Año Nuevo PVS-Studio 6.00: Escaneo de Roslyn ". Desde entonces, el analizador C # PVS-Studio tenía muchas características nuevas. En realidad, el artículo de Andrey era un caso de prueba, ya que en ese momento el analizador C # acababa de agregarse en PVS-Studio. A pesar de esto, logramos detectar errores en el proyecto Roslyn, que sin duda fue de alta calidad. Entonces, ¿qué ha cambiado en el analizador para el código C # en este momento que nos permitirá realizar un análisis más profundo?

Desde entonces, tanto el núcleo como la infraestructura se han desarrollado. Agregamos soporte para Visual Studio 2017 y Roslyn 2.0, y una integración profunda con MSBuild. El artículo de mi colega Paul Eremeev " Soporte de Visual Studio 2017 y Roslyn 2.0 en PVS-Studio: a veces no es tan fácil de usar como parece " describe nuestro enfoque de integración con MSBuild y los motivos de esta decisión.

Por el momento, estamos trabajando activamente para pasar a Roslyn 3.0 de la misma manera que inicialmente admitimos Visual Studio 2017. Requiere el uso de nuestro propio conjunto de herramientas, incluido en el distributivo PVS-Studio como un "trozo", que es un MSBuild vacío archivo .exe A pesar del hecho de que parece una "muleta" (MSBuild API no es muy amigable para reutilizar en proyectos de terceros debido a la baja portabilidad de las bibliotecas), este enfoque ya nos ha ayudado a superar relativamente fácilmente múltiples actualizaciones de Roslyn en términos de Visual Studio 2017. Hasta ahora, estaba ayudando (incluso con algunos desafíos) a pasar por la actualización de Visual Studio 2019 y mantener una compatibilidad y un rendimiento completos para sistemas con versiones anteriores de MSBuild.

El núcleo del analizador también ha experimentado una serie de mejoras. Una de las características principales es un análisis interprocedural completo con consideración de los valores de los métodos de entrada y salida, evaluando (dependiendo de estos parámetros) el alcance de las ramas de ejecución y los puntos de retorno.

Estamos en camino de completar la tarea de monitorear los parámetros dentro de los métodos (por ejemplo, desreferencias potencialmente peligrosas) junto con guardar sus anotaciones automáticas. Para un diagnóstico que utiliza un mecanismo de flujo de datos, esto permitirá tener en cuenta situaciones peligrosas que se producen al pasar un parámetro en un método. Antes de esto, al analizar lugares tan peligrosos, no se generaba una advertencia, ya que no podíamos conocer todos los valores de entrada posibles en dicho método. Ahora podemos detectar el peligro, ya que en todos los lugares de llamar a este método, estos parámetros de entrada se tendrán en cuenta.

Nota: puede leer acerca de los mecanismos básicos del analizador, como el flujo de datos y otros en el artículo " Tecnologías utilizadas en el analizador de código PVS-Studio para encontrar errores y vulnerabilidades potenciales ".

El análisis interprocedural en PVS-Studio C # no está limitado ni por los parámetros de entrada, ni por la profundidad. La única limitación son los métodos virtuales en las clases, abiertos para la herencia, así como también para entrar en recursión (el análisis se detiene cuando se topa con una llamada repetida del método ya evaluado). Al hacerlo, el método recursivo en sí mismo se evaluará eventualmente suponiendo que se desconoce el valor de retorno de su recursividad.

Otra gran característica nueva en el analizador C # se ha convertido en tener en cuenta la posible desreferencia de un puntero potencialmente nulo. Antes de eso, el analizador se quejó de una posible excepción de referencia nula, asegurándose de que en todas las ramas de ejecución el valor de la variable sea nulo. Por supuesto, estuvo mal en algunos casos, por eso el diagnóstico V3080 había llamado previamente referencia nula potencial.

Ahora el analizador es consciente del hecho de que la variable podría ser nula en una de las ramas de ejecución (por ejemplo, bajo una cierta condición if ). Si nota el acceso a dicha variable sin una verificación, emitirá la advertencia V3080, pero con un nivel de certeza más bajo, que si ve nulo en todas las ramas. Junto con el análisis interprocedural mejorado, dicho mecanismo permite encontrar errores que son muy difíciles de detectar. Aquí hay un ejemplo: imagine una larga cadena de llamadas a métodos, la última de las cuales no le es familiar. Bajo ciertas circunstancias, devuelve nulo en el bloque catch , pero no se ha protegido de esto, como simplemente no lo ha sabido. En este caso, el analizador solo se queja cuando ve exactamente la asignación nula. En nuestra opinión, distingue cualitativamente nuestro enfoque de la característica de C # 8.0 como referencia de tipo anulable, que, de hecho, se limita a establecer verificaciones para nulo para cada método. Sin embargo, sugerimos la alternativa: realizar comprobaciones solo en lugares donde realmente puede ocurrir nulo, y nuestro analizador ahora puede buscar tales casos.

Por lo tanto, no demoremos demasiado el punto principal y pasemos a la toma de culpa: analicemos los resultados de la verificación de Roslyn. Primero, consideremos los errores encontrados debido a las características descritas anteriormente. En resumen, esta vez hubo muchas advertencias para el código de Roslyn. Creo que está relacionado con el hecho de que la plataforma está evolucionando muy activamente (en este punto, la base de código es de aproximadamente 2,770,000 líneas excluidas las vacías), y no hemos analizado este proyecto por mucho tiempo. Sin embargo, no hay tantos errores críticos, mientras que son de mayor interés para el artículo. Como de costumbre, excluí las pruebas del cheque, hay muchas en Roslyn.

Comenzaré con los errores V3080 del nivel medio de certeza, en el que el analizador ha detectado un posible acceso por referencia nula, pero no en todos los casos posibles (ramificaciones de código).

Posible desreferencia nula - Media

V3080 Posible desreferencia nula. Considere inspeccionar 'actual'. CSharpSyntaxTreeFactoryService.PositionalSyntaxReference.cs 70

private SyntaxNode GetNode(SyntaxNode root) { var current = root; .... while (current.FullSpan.Contains(....)) // <= { .... var nodeOrToken = current.ChildThatContainsPosition(....); .... current = nodeOrToken.AsNode(); // <= } .... } public SyntaxNode AsNode() { if (_token != null) { return null; } return _nodeOrParent; } 

Consideremos el método GetNode . El analizador sugiere que el acceso por referencia nula es posible en la condición del bloque while . A la variable se le asigna un valor en el cuerpo del bloque while , que es el resultado del método AsNode . En algunos casos, este valor será nulo . Un buen ejemplo de análisis interprocedural en acción.

Ahora consideremos un caso similar, en el que el análisis interprocedural se realizó a través de dos llamadas a métodos.

V3080 Posible desreferencia nula. Considere inspeccionar 'directorio'. CommonCommandLineParser.cs 911

 private IEnumerable<CommandLineSourceFile> ExpandFileNamePattern(string path, string baseDirectory, ....) { string directory = PathUtilities.GetDirectoryName(path); .... var resolvedDirectoryPath = (directory.Length == 0) ? // <= baseDirectory : FileUtilities.ResolveRelativePath(directory, baseDirectory); .... } public static string GetDirectoryName(string path) { return GetDirectoryName(path, IsUnixLikePlatform); } internal static string GetDirectoryName(string path, bool isUnixLike) { if (path != null) { .... } return null; } 

La variable de directorio en el cuerpo del método ExpandFileNamePattern obtiene el valor del método GetDirectoryName (string) . Eso, a su vez, devuelve el resultado del método sobrecargado GetDirectoryName (string, bool) cuyo valor puede ser nulo . Dado que el directorio variable se usa sin una comprobación preliminar de nulo en el cuerpo del método ExpandFileNamePattern , podemos proclamar que el analizador es correcto al emitir la advertencia. Esta es una construcción potencialmente insegura.

Otro fragmento de código con el error V3080, más precisamente, con dos errores, emitido para una sola línea de código. El análisis interprocedural no era necesario aquí.

V3080 Posible desreferencia nula. Considere la posibilidad de inspeccionar 'spanStartLocation'. TestWorkspace.cs 574

V3080 Posible desreferencia nula. Considere inspeccionar 'spanEndLocationExclusive'. TestWorkspace.cs 574

 private void MapMarkupSpans(....) { .... foreach (....) { .... foreach (....) { .... int? spanStartLocation = null; int? spanEndLocationExclusive = null; foreach (....) { if (....) { if (spanStartLocation == null && positionInMarkup <= markupSpanStart && ....) { .... spanStartLocation = ....; } if (spanEndLocationExclusive == null && positionInMarkup <= markupSpanEndExclusive && ....) { .... spanEndLocationExclusive = ....; break; } .... } .... } tempMappedMarkupSpans[key]. Add(new TextSpan( spanStartLocation.Value, // <= spanEndLocationExclusive.Value - // <= spanStartLocation.Value)); } } .... } 

Las variables spanStartLocation y spanEndLocationExclusive son del tipo int anulables y se inicializan por null . Más adelante en el código se les pueden asignar valores, pero solo bajo ciertas condiciones. En algunos casos, su valor sigue siendo nulo . Después de eso, se accede a estas variables por referencia sin verificación preliminar de nulo, lo que indica el analizador.

El código de Roslyn contiene muchos de estos errores, más de 100. A menudo, el patrón de estos errores es el mismo. Hay algún tipo de método general, que potencialmente devuelve nulo . El resultado de este método se usa en muchos lugares, a veces a través de docenas de llamadas a métodos intermedios o verificaciones adicionales. Es importante comprender que estos errores no son fatales, pero potencialmente pueden conducir al acceso por referencia nula. Si bien la detección de tales errores es bastante desafiante. Es por eso que en algunos casos uno debería considerar la refactorización de código, en cuyo caso si nulo regresa, el método arrojará una excepción. De lo contrario, puede proteger su código solo con verificaciones generales, lo cual es bastante agotador y, a veces, poco confiable. De todos modos, está claro que cada caso específico requiere una solución basada en las especificaciones del proyecto.

Nota Sucede que en un punto dado no hay tales casos (datos de entrada), cuando el método devuelve nulo y no hay un error real. Sin embargo, dicho código aún no es confiable, porque todo puede cambiar al introducir algunos cambios en el código.

Para abandonar el tema V3080 , echemos un vistazo a los errores obvios de alto nivel de certeza, cuando el acceso por referencia nula es el más probable o incluso inevitable.

Posible desreferencia nula - Alta

V3080 Posible desreferencia nula. Considere inspeccionar 'collectionType.Type'. AbstractConvertForToForEachCodeRefactoringProvider.cs 137

 public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { .... var collectionType = semanticModel.GetTypeInfo(....); if (collectionType.Type == null && collectionType.Type.TypeKind == TypeKind.Error) { return; } .... } 

Debido al error tipográfico en la condición ( && se usa en lugar del operador || ), el código funciona de manera diferente a la prevista y el acceso a la variable collectionType.Type se ejecutará cuando sea nulo . La condición debe corregirse de la siguiente manera:

 if (collectionType.Type == null || collectionType.Type.TypeKind == TypeKind.Error) .... 

Por cierto, las cosas pueden desarrollarse de otra manera: en la primera parte de la condición, los operadores == y ! = Están en mal estado . Entonces el código correcto se vería así:

 if (collectionType.Type != null && collectionType.Type.TypeKind == TypeKind.Error) .... 

Esta versión del código es menos lógica, pero también corrige el error. La solución final recae en la decisión de los autores del proyecto.

Otro error similar.

V3080 Posible desreferencia nula. Considere inspeccionar 'acción'. TextViewWindow_InProc.cs 372

 private Func<IWpfTextView, Task> GetLightBulbApplicationAction(....) { .... if (action == null) { throw new InvalidOperationException( $"Unable to find FixAll in {fixAllScope.ToString()} code fix for suggested action '{action.DisplayText}'."); } .... } 

El error se produce al generar el mensaje para la excepción. Le sigue el intento de acceder a la propiedad action.DisplayText a través de la variable action , que se sabe que es nula .

Aquí viene el último error V3080 del nivel alto.

V3080 Posible desreferencia nula. Considere inspeccionar 'tipo'. ObjectFormatterHelpers.cs 91

 private static bool IsApplicableAttribute( TypeInfo type, TypeInfo targetType, string targetTypeName) { return type != null && AreEquivalent(targetType, type) || targetTypeName != null && type.FullName == targetTypeName; } 

El método es bastante pequeño, así que lo cito por completo. La condición en el bloque de retorno es incorrecta. En algunos casos, al acceder a type.FullName , puede producirse una excepción. Usaré paréntesis para aclararlo (no cambiarán el comportamiento):

 return (type != null && AreEquivalent(targetType, type)) || (targetTypeName != null && type.FullName == targetTypeName); 

Según la precedencia de las operaciones, el código funcionará exactamente así. En caso de que la variable de tipo sea nula , entraremos en el check de else, donde usaremos la referencia de tipo nulo, después de haber verificado la variable targetTypeName para null . El código puede repararse, por ejemplo, de la siguiente manera:

 return type != null && (AreEquivalent(targetType, type) || targetTypeName != null && type.FullName == targetTypeName); 

Creo que es suficiente para revisar los errores de V3080. Ahora es el momento de ver otras cosas interesantes que el analizador PVS-Studio logró encontrar.

Error tipográfico

V3005 La variable 'SourceCodeKind' se asigna a sí misma. DynamicFileInfo.cs 17

 internal sealed class DynamicFileInfo { .... public DynamicFileInfo( string filePath, SourceCodeKind sourceCodeKind, TextLoader textLoader, IDocumentServiceProvider documentServiceProvider) { FilePath = filePath; SourceCodeKind = SourceCodeKind; // <= TextLoader = textLoader; DocumentServiceProvider = documentServiceProvider; } .... } 

Debido al error en el nombre de las variables, se realizó un error tipográfico en el constructor de la clase DynamicFileInfo . Al campo SourceCodeKind se le asigna su propio valor en lugar de utilizar el parámetro sourceCodeKind . Para minimizar la probabilidad de tales errores, le recomendamos que utilice el prefijo de subrayado para los nombres de los parámetros en tales casos. Aquí hay un ejemplo de una versión corregida del código:

 public DynamicFileInfo( string _filePath, SourceCodeKind _sourceCodeKind, TextLoader _textLoader, IDocumentServiceProvider _documentServiceProvider) { FilePath = _filePath; SourceCodeKind = _sourceCodeKind; TextLoader = _textLoader; DocumentServiceProvider = _documentServiceProvider; } 

Inadvertencia

V3006 El objeto fue creado pero no se está utilizando. Podría faltar la palabra clave 'throw': lanzar una nueva InvalidOperationException (FOO). ProjectBuildManager.cs 61

 ~ProjectBuildManager() { if (_batchBuildStarted) { new InvalidOperationException("ProjectBuilderManager.Stop() not called."); } } 

Bajo cierta condición, el destructor debe lanzar una excepción, pero no sucede mientras el objeto de excepción simplemente se crea. Se perdió la palabra clave de lanzamiento . Aquí está la versión corregida del código:

 ~ProjectBuildManager() { if (_batchBuildStarted) { throw new InvalidOperationException("ProjectBuilderManager.Stop() not called."); } } 

El problema con los destructores en C # y el lanzamiento de excepciones de ellos es un tema para otra discusión, que está más allá del alcance de este artículo.

Cuando el resultado no es importante

Los métodos, que recibieron el mismo valor en todos los casos, activaron un cierto número de advertencias V3009 . En algunos casos, puede no ser crítico o el valor de retorno simplemente no se verifica en el código de llamada. Me salté tales advertencias. Pero algunos fragmentos de código parecían sospechosos. Aquí está uno de ellos:

V3009 Es extraño que este método siempre devuelva uno y el mismo valor de 'verdadero'. GoToDefinitionCommandHandler.cs 62

 internal bool TryExecuteCommand(....) { .... using (context.OperationContext.AddScope(....)) { if (....) { return true; } } .... return true; } 

El método TryExecuteCommand devuelve nada más que verdadero . Al hacerlo, en el código de llamada, el valor devuelto participa en algunas comprobaciones.

 public bool ExecuteCommand(....) { .... if (caretPos.HasValue && TryExecuteCommand(....)) { .... } .... } 

Es difícil decir exactamente en qué medida ese comportamiento es peligroso. Pero si el resultado no es necesario, tal vez el tipo del valor de retorno debería cambiarse a vacío y uno debería hacer pequeñas modificaciones en el método de llamada. Esto hará que el código sea más legible y seguro.

Advertencias de analizador similares:

  • V3009 Es extraño que este método siempre devuelva uno y el mismo valor de 'verdadero'. CommentUncommentSelectionCommandHandler.cs 86
  • V3009 Es extraño que este método siempre devuelva uno y el mismo valor de 'verdadero'. RenameTrackingTaggerProvider.RenameTrackingCommitter.cs 99
  • V3009 Es extraño que este método siempre devuelva uno y el mismo valor de 'verdadero'. JsonRpcClient.cs 138
  • V3009 Es extraño que este método siempre devuelva uno y el mismo valor de 'verdadero'. AbstractFormatEngine.OperationApplier.cs 164
  • V3009 Es extraño que este método siempre devuelva uno y el mismo valor de 'falso'. TriviaDataFactory.CodeShapeAnalyzer.cs 254
  • V3009 Es extraño que este método siempre devuelva uno y el mismo valor de 'verdadero'. ObjectList.cs 173
  • V3009 Es extraño que este método siempre devuelva uno y el mismo valor de 'verdadero'. ObjectList.cs 249

Comprobado lo incorrecto

V3019 Posiblemente, una variable incorrecta se compara con nulo después de la conversión de tipo usando la palabra clave 'as'. Verifique las variables 'value', 'valueToSerialize'. RoamingVisualStudioProfileOptionPersister.cs 277

 public bool TryPersist(OptionKey optionKey, object value) { .... var valueToSerialize = value as NamingStylePreferences; if (value != null) { value = valueToSerialize.CreateXElement().ToString(); } .... } 

La variable de valor se convierte al tipo NamingStylePreferences . El problema está en el control que sigue a esto. Incluso si la variable de valor no es nula, no garantiza que la conversión de tipos haya sido exitosa y valueToSerialize no contenga nulo . Posible lanzamiento de la excepción NullReferenceException . El código debe corregirse de la siguiente manera:

 var valueToSerialize = value as NamingStylePreferences; if (valueToSerialize != null) { value = valueToSerialize.CreateXElement().ToString(); } 

Otro error similar:

V3019 Posiblemente, una variable incorrecta se compara con nulo después de la conversión de tipo usando la palabra clave 'as'. Verifique las variables 'columnState', 'columnState2'. StreamingFindUsagesPresenter.cs 181

 private void SetDefinitionGroupingPriority(....) { .... foreach (var columnState in ....) { var columnState2 = columnState as ColumnState2; if (columnState?.Name == // <= StandardTableColumnDefinitions2.Definition) { newColumns.Add(new ColumnState2( columnState2.Name, // <= ....)); } .... } .... } 

La variable columnState se convierte al tipo ColumnState2 . Sin embargo, el resultado de la operación, que es la variable columnState2, no se verifica para nulo adicional. En cambio, la variable columnState se verifica utilizando el operador nulo condicional. ¿Por qué es peligroso este código? Al igual que en el ejemplo anterior, la conversión con el operador as puede fallar y la variable será nula, lo que dará como resultado una excepción. Por cierto, un error tipográfico puede ser el culpable aquí. Observe la condición en el bloque if .

Tal vez, en lugar de columnState? .Name, el autor quería escribir columnState2? .Name . Es muy probable, considerando nombres de variables bastante defectuosos columnState y columnState2.

Cheques redundantes

Se emitió una gran cantidad de advertencias (más de 100) en construcciones no críticas pero potencialmente inseguras relacionadas con verificaciones redundantes. Por ejemplo, este es uno de ellos.

V3022 La expresión 'navInfo == null' siempre es falsa. AbstractSyncClassViewCommandHandler.cs 101

 public bool ExecuteCommand(....) { .... IVsNavInfo navInfo = null; if (symbol != null) { navInfo = libraryService.NavInfoFactory.CreateForSymbol(....); } if (navInfo == null) { navInfo = libraryService.NavInfoFactory.CreateForProject(....); } if (navInfo == null) // <= { return true; } .... } public IVsNavInfo CreateForSymbol(....) { .... return null; } public IVsNavInfo CreateForProject(....) { return new NavInfo(....); } 

Puede ser que no haya un error real aquí. Es solo una buena razón para demostrar que el "análisis interprocedural + análisis de flujo de datos" funciona en un remolque. El analizador sugiere que la segunda verificación navInfo == null es redundante. De hecho, antes de eso, el valor asignado a navInfo se obtendrá del método libraryService.NavInfoFactory.CreateForProject , que construirá y devolverá un nuevo objeto de la clase NavInfo . De ninguna manera volverá nulo . Aquí surge la pregunta, ¿por qué el analizador no emitió una advertencia para la primera verificación navInfo == null ? Hay algunas razones En primer lugar, si la variable de símbolo es nula , el valor navInfo seguirá siendo una referencia nula también. En segundo lugar, incluso si navInfo obtiene el valor del método ibraryService.NavInfoFactory.CreateForSymbol , este valor también puede ser nulo . Por lo tanto, la primera verificación navInfo == null es realmente necesaria.

Controles insuficientes

Ahora la situación inversa de lo discutido anteriormente. Se activaron varias advertencias V3042 para el código, en el que es posible el acceso por referencia nula. Incluso uno o dos cheques pequeños podrían haber solucionado todo.

Consideremos otro fragmento de código interesante, que tiene dos de esos errores.

V3042 Posible NullReferenceException. El '?.' y '.' Los operadores se utilizan para acceder a los miembros del objeto 'receptor' Binder_Expressions.cs 7770

V3042 Posible NullReferenceException. El '?.' y '.' Los operadores se utilizan para acceder a los miembros del objeto 'receptor' Binder_Expressions.cs 7776

 private BoundExpression GetReceiverForConditionalBinding( ExpressionSyntax binding, DiagnosticBag diagnostics) { .... BoundExpression receiver = this.ConditionalReceiverExpression; if (receiver?.Syntax != // <= GetConditionalReceiverSyntax(conditionalAccessNode)) { receiver = BindConditionalAccessReceiver(conditionalAccessNode, diagnostics); } var receiverType = receiver.Type; // <= if (receiverType?.IsNullableType() == true) { .... } receiver = new BoundConditionalReceiver(receiver.Syntax, 0, // <= receiverType ?? CreateErrorType(), hasErrors: receiver.HasErrors) // <= { WasCompilerGenerated = true }; return receiver; } 

La variable del receptor puede ser nula. El autor del código sabe de esto, ya que utiliza el operador nulo condicional en la condición del bloque if para acceder al receptor ? . Sintaxis . Además, la variable del receptor se usa sin ninguna comprobación para acceder a receptor.Tipo , receptor.Sintaxis y receptor.TieneErrores . Estos errores deben corregirse:

 private BoundExpression GetReceiverForConditionalBinding( ExpressionSyntax binding, DiagnosticBag diagnostics) { .... BoundExpression receiver = this.ConditionalReceiverExpression; if (receiver?.Syntax != GetConditionalReceiverSyntax(conditionalAccessNode)) { receiver = BindConditionalAccessReceiver(conditionalAccessNode, diagnostics); } var receiverType = receiver?.Type; if (receiverType?.IsNullableType() == true) { .... } receiver = new BoundConditionalReceiver(receiver?.Syntax, 0, receiverType ?? CreateErrorType(), hasErrors: receiver?.HasErrors) { WasCompilerGenerated = true }; return receiver; } 

También debemos asegurarnos de que el constructor admite obtener valores nulos para sus parámetros o debemos realizar una refactorización adicional.

Otros errores similares:

  • V3042 Posible NullReferenceException. El '?.' y '.' Los operadores se utilizan para acceder a los miembros del objeto 'contienenType' SyntaxGeneratorExtensions_Negate.cs 240
  • V3042 Posible NullReferenceException. El '?.' y '.' Los operadores se utilizan para acceder a los miembros del objeto 'expresión' ExpressionSyntaxExtensions.cs 349
  • V3042 Posible NullReferenceException. El '?.' y '.' Los operadores se utilizan para acceder a los miembros del objeto 'expresión' ExpressionSyntaxExtensions.cs 349

Error en la condicion

V3057 La función ' Subcadena ' podría recibir el valor '-1' mientras se espera un valor no negativo. Inspeccione el segundo argumento. CommonCommandLineParser.cs 109

 internal static bool TryParseOption(....) { .... if (colon >= 0) { name = arg.Substring(1, colon - 1); value = arg.Substring(colon + 1); } .... } 

En caso de que la variable de dos puntos sea ​​0, lo cual está bien de acuerdo con la condición del código, el método Substring arrojará una excepción. Esto tiene que ser arreglado:

 if (colon > 0) 

Posible error tipográfico

El parámetro V3065 't2' no se utiliza dentro del cuerpo del método. CSharpCodeGenerationHelpers.cs 84

 private static TypeDeclarationSyntax ReplaceUnterminatedConstructs(....) { .... var updatedToken = lastToken.ReplaceTrivia(lastToken.TrailingTrivia, (t1, t2) => { if (t1.Kind() == SyntaxKind.MultiLineCommentTrivia) { var text = t1.ToString(); .... } else if (t1.Kind() == SyntaxKind.SkippedTokensTrivia) { return ReplaceUnterminatedConstructs(t1); } return t1; }); .... } 

La expresión lambda acepta dos parámetros: t1 y t2. Sin embargo, solo se usa t1. Parece sospechoso, teniendo en cuenta el hecho de lo fácil que es cometer un error al usar variables con tales nombres.

Inadvertencia

V3083 Invocación insegura del evento 'TagsChanged', NullReferenceException es posible. Considere asignar un evento a una variable local antes de invocarlo. PreviewUpdater.Tagger.cs 37

 public void OnTextBufferChanged() { if (PreviewUpdater.SpanToShow != default) { if (TagsChanged != null) { var span = _textBuffer.CurrentSnapshot.GetFullSpan(); TagsChanged(this, new SnapshotSpanEventArgs(span)); // <= } } } 

El evento TagsChanged se invoca de forma insegura. Entre la comprobación de nulo y la invocación del evento, alguien puede darse de baja del mismo, luego se lanzará una excepción. Además, se realizan otras operaciones en el cuerpo del bloque if justo antes de invocar el evento. Llamé a este error "Inadvertencia", porque este evento se maneja con más cuidado en otros lugares, de la siguiente manera:

 private void OnTrackingSpansChanged(bool leafChanged) { var handler = TagsChanged; if (handler != null) { var snapshot = _buffer.CurrentSnapshot; handler(this, new SnapshotSpanEventArgs(snapshot.GetFullSpan())); } } 

El uso de una variable de controlador adicional evita el problema. En el método OnTextBufferChanged, uno tiene que hacer ediciones para manejar el evento de manera segura.

Rangos de intersección

V3092 Las intersecciones de rango son posibles dentro de expresiones condicionales. Ejemplo: if (A> 0 && A <5) {...} más if (A> 3 && A <9) {...}. ILBuilderEmit.cs 677

 internal void EmitLongConstant(long value) { if (value >= int.MinValue && value <= int.MaxValue) { .... } else if (value >= uint.MinValue && value <= uint.MaxValue) { .... } else { .... } } 

Para una mejor comprensión, permítanme reescribir este código, cambiando los nombres de las constantes con sus valores reales:

 internal void EmitLongConstant(long value) { if (value >= -2147483648 && value <= 2147483648) { .... } else if (value >= 0 && value <= 4294967295) { .... } else { .... } } 

Probablemente, no hay un error real, pero la condición parece extraña. Su segunda parte ( si no ) se ejecutará solo para el rango de 2147483648 + 1 a 4294967295.

Otro par de advertencias similares:

  • V3092 Las intersecciones de rango son posibles dentro de expresiones condicionales. Ejemplo: if (A> 0 && A <5) {...} más if (A> 3 && A <9) {...}. LocalRewriter_Literal.cs 109
  • V3092 Las intersecciones de rango son posibles dentro de expresiones condicionales. Ejemplo: if (A> 0 && A <5) {...} más if (A> 3 && A <9) {...}. LocalRewriter_Literal.cs 66

Más sobre cheques para nulo (o falta de ellos)

Un par de errores V3095 en la comprobación de una variable para nulo justo después de su uso. El primero es ambiguo, consideremos el código.

V3095 El objeto 'displayName' se usó antes de que se verificara como nulo. Líneas de verificación: 498, 503. FusionAssemblyIdentity.cs 498

 internal static IAssemblyName ToAssemblyNameObject(string displayName) { if (displayName.IndexOf('\0') >= 0) { return null; } Debug.Assert(displayName != null); .... } 

Se supone que la referencia tee puede ser nula. Para esto, se realizó la verificación Debug.Assert . No está claro por qué va después de usar una cadena. También debe tenerse en cuenta que para configuraciones diferentes de Debug, el compilador eliminará Debug.Assert . ¿Significa que obtener una referencia nula solo es posible para Debug? Si no es así, ¿por qué el autor realizó la comprobación de string.IsNullOrEmpty (string) , por ejemplo. Es la pregunta a los autores del código.

El siguiente error es más evidente.

V3095 El objeto 'scriptArgsOpt' se usó antes de que se verificara como nulo. Líneas de verificación: 321, 325. CommonCommandLineParser.cs 321

 internal void FlattenArgs(...., List<string> scriptArgsOpt, ....) { .... while (args.Count > 0) { .... if (parsingScriptArgs) { scriptArgsOpt.Add(arg); // <= continue; } if (scriptArgsOpt != null) { .... } .... } } 

Creo que este código no necesita ninguna explicación. Déjame darte la versión fija:

 internal void FlattenArgs(...., List<string> scriptArgsOpt, ....) { .... while (args.Count > 0) { .... if (parsingScriptArgs) { scriptArgsOpt?.Add(arg); continue; } if (scriptArgsOpt != null) { .... } .... } } 

En el código de Roslyn, hubo 15 errores más similares:

  • V3095 El objeto 'LocalFunctions' se usó antes de que se verificara contra nulo. Líneas de verificación: 289, 317. ControlFlowGraphBuilder.RegionBuilder.cs 289
  • V3095 El objeto 'resolution.OverloadResolutionResult' se usó antes de que se verificara como nulo. Líneas de verificación: 579, 588. Binder_Invocation.cs 579
  • V3095 El objeto 'resolution.MethodGroup' se usó antes de que se verificara como nulo. Líneas de verificación: 592, 621. Binder_Invocation.cs 592
  • V3095 El objeto 'TouchFilesLogger' se usó antes de que se verificara como nulo. Líneas de verificación: 111, 126. CSharpCompiler.cs 111
  • V3095 El objeto 'newExceptionRegionsOpt' se usó antes de que se verificara contra nulo. Líneas de verificación: 736, 743. AbstractEditAndContinueAnalyzer.cs 736
  • V3095 El objeto 'símbolo' se usó antes de que se verificara como nulo. Líneas de verificación: 422, 427. AbstractGenerateConstructorService.Editor.cs 422
  • V3095 El objeto '_state.BaseTypeOrInterfaceOpt' se usó antes de que se verificara como nulo. Líneas de verificación: 132, 140. AbstractGenerateTypeService.GenerateNamedType.cs 132
  • V3095 El objeto 'elemento' se usó antes de que se verificara como nulo. Líneas de verificación: 232, 233. ProjectUtil.cs 232
  • V3095 El objeto 'idiomas' se usó antes de que se verificara como nulo. Líneas de verificación: 22, 28. ExportCodeCleanupProvider.cs 22
  • V3095 El objeto 'memberType' se usó antes de que se verificara contra nulo. Líneas de verificación: 183, 184. SyntaxGeneratorExtensions_CreateGetHashCodeMethod.cs 183
  • V3095 El objeto 'validTypeDeclarations' se usó antes de que se verificara contra nulo. Líneas de verificación: 223, 228. SyntaxTreeExtensions.cs 223
  • V3095 El objeto 'texto' se usó antes de que se verificara como nulo. Líneas de verificación: 376, 385. MSBuildWorkspace.cs 376
  • V3095 El objeto 'nameOrMemberAccessExpression' se usó antes de que se verificara como nulo. Líneas de verificación: 206, 223. CSharpGenerateTypeService.cs 206
  • V3095 El objeto 'simpleName' se usó antes de que se verificara como nulo. Líneas de verificación: 83, 85. CSharpGenerateMethodService.cs 83
  • V3095 El objeto 'opción' se usó antes de que se verificara como nulo. Líneas de verificación: 23, 28. OptionKey.cs 23

Consideremos los errores de V3105 . Aquí, el operador nulo condicional se usa al inicializar la variable, pero además la variable se usa sin verificaciones para nulo .

Dos advertencias indican el siguiente error:

V3105 La variable 'documentId' se usó después de que se asignó a través del operador condicional nulo. NullReferenceException es posible. CodeLensReferencesService.cs 138

V3105 La variable 'documentId' se usó después de que se asignó a través del operador condicional nulo. NullReferenceException es posible. CodeLensReferencesService.cs 139

 private static async Task<ReferenceLocationDescriptor> GetDescriptorOfEnclosingSymbolAsync(....) { .... var documentId = solution.GetDocument(location.SourceTree)?.Id; return new ReferenceLocationDescriptor( .... documentId.ProjectId.Id, documentId.Id, ....); } 

La variable documentId se puede inicializar por nulo . Como resultado, la creación de un objeto ReferenceLocationDescriptor generará una excepción. El código debe ser arreglado:

 return new ReferenceLocationDescriptor( .... documentId?.ProjectId.Id, documentId?.Id, ....); 

Los desarrolladores también deben cubrir la posibilidad de que las variables, pasadas a un constructor, sean nulas.

Otros errores similares en el código:

  • V3105 La variable 'símbolo' se usó después de que se asignó a través del operador condicional nulo. NullReferenceException es posible. SymbolFinder_Hierarchy.cs 44
  • V3105 La variable 'símbolo' se usó después de que se asignó a través del operador condicional nulo. NullReferenceException es posible. SymbolFinder_Hierarchy.cs 51

Prioridades y paréntesis

V3123 Quizás el operador '?:' Funciona de una manera diferente a la esperada. Su prioridad es menor que la prioridad de otros operadores en su condición. Edit.cs 70

 public bool Equals(Edit<TNode> other) { return _kind == other._kind && (_oldNode == null) ? other._oldNode == null : _oldNode.Equals(other._oldNode) && (_newNode == null) ? other._newNode == null : _newNode.Equals(other._newNode); } 

La condición en el bloque de retorno no se evalúa según lo previsto por el desarrollador. Se supuso que la primera condición será _kind == other._kin d, (es por eso que después de esta condición hay un salto de línea), y después de eso, los bloques de condiciones con el operador " ? " Se evaluarán en secuencia. De hecho, la primera condición es _kind == other._kind && (_oldNode == null) . Esto se debe al hecho de que el operador && tiene mayor prioridad que el operador " ? ". Para solucionar esto, un desarrollador debe tomar todas las expresiones del operador " ? " Entre paréntesis:

 return _kind == other._kind && ((_oldNode == null) ? other._oldNode == null : _oldNode.Equals(other._oldNode)) && ((_newNode == null) ? other._newNode == null : _newNode.Equals(other._newNode)); 

Con esto concluye mi descripción de los errores encontrados.

Conclusión

A pesar de la gran cantidad de errores, que logré encontrar, en términos del tamaño del código del proyecto Roslyn (2,770,000 líneas), no es demasiado. Como Andrey escribió en un artículo anterior, también estoy listo para reconocer la alta calidad de este proyecto.

Me gustaría señalar que tales comprobaciones de código ocasionales no tienen nada que ver con la metodología del análisis estático y son casi inútiles. El análisis estático debe aplicarse con regularidad, y no caso por caso. De esta forma, muchos errores se corregirán en las primeras etapas y, por lo tanto, el costo de corregirlos será diez veces menor. Esta idea se expone con más detalle en esta pequeña nota , por favor, échale un vistazo.

Puede comprobar usted mismo algunos errores tanto en este proyecto como en otro. Para hacer esto, solo necesita descargar y probar nuestro analizador.

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


All Articles