¿Qué errores están ocultos en el código Infer.NET?


La publicación de Microsoft del código fuente para sus proyectos es una buena razón para verificarlos. Esta vez no fue la excepción, y hoy miramos los lugares sospechosos que se encuentran en el código Infer.NET. Abajo la anotación: ¡vaya al grano!

Un poco sobre el proyecto y el analizador.


Infer.NET es un sistema de aprendizaje automático desarrollado por especialistas de Microsoft. El código fuente del proyecto recientemente estuvo disponible en GitHub , que fue el motivo de su verificación. Se pueden encontrar más detalles sobre el proyecto, por ejemplo, aquí .

El proyecto se verificó utilizando el analizador estático PVS-Studio versión 6.26. Permítame recordarle que PVS-Studio está buscando errores en el código en C \ C ++ \ C # (y pronto en Java) en Windows, Linux, macOS. C # código hasta ahora estamos analizando solo en Windows. El analizador se puede descargar y probar en su proyecto.

El cheque en sí mismo fue extremadamente simple y sin problemas. Anteriormente, descargué el proyecto de GitHub, restauré los paquetes necesarios (dependencias) y me aseguré de que el proyecto se haya creado correctamente. Esto es necesario para que el analizador tenga acceso a toda la información necesaria para un análisis completo. Después de ensamblar en un par de clics, inicié el análisis de la solución a través del complemento PVS-Studio para Visual Studio.

Por cierto, este no es el primer proyecto de Microsoft probado usando PVS-Studio; hubo otros: Roslyn , MSBuild , PowerShell , CoreFX y otros .

Nota Si usted o sus conocidos están interesados ​​en analizar el código Java, puede escribirnos en soporte seleccionando "Quiero un analizador para Java". No hay una versión beta pública del analizador, pero debería estar disponible pronto. En algún lugar de un laboratorio secreto (a través del muro), los muchachos están trabajando activamente en él.

Pero suficiente conversación abstracta: veamos los problemas en el código.

¿Es esto un error o una característica?


Sugiero tratar de encontrar el error usted mismo, una tarea completamente solucionable. No hay bromas en el espíritu de lo que estaba en el artículo " Los 10 errores principales en proyectos de C ++ para 2017 ", sinceramente. Por lo tanto, no se apresure a leer la advertencia del analizador proporcionada después del fragmento de código.

private void MergeParallelTransitions() { .... if ( transition1.DestinationStateIndex == transition2.DestinationStateIndex && transition1.Group == transition2.Group) { if (transition1.IsEpsilon && transition2.IsEpsilon) { .... } else if (!transition1.IsEpsilon && !transition2.IsEpsilon) { .... if (double.IsInfinity(transition1.Weight.Value) && double.IsInfinity(transition1.Weight.Value)) { newElementDistribution.SetToSum( 1.0, transition1.ElementDistribution, 1.0, transition2.ElementDistribution); } else { newElementDistribution.SetToSum( transition1.Weight.Value, transition1.ElementDistribution, transition2.Weight.Value, transition2.ElementDistribution); } .... } 

Advertencia de PVS-Studio : V3001 Hay subexpresiones idénticas 'double.IsInfinity (transition1.Weight.Value)' a la izquierda y a la derecha del operador '&&'. Runtime Automaton.Simplification.cs 479

Como puede ver en el fragmento de código, el método está trabajando con un par de variables: transición1 y transición2 . El uso de nombres similares a veces está bastante justificado, pero vale la pena recordar que en este caso aumenta la probabilidad de cometer un error accidentalmente en algún lugar con el nombre.

Esto es lo que sucedió al verificar los números para el infinito ( double.IsInfinity ). Debido a un error, verificamos el valor de la misma variable 2 veces: transition1.Weight.Value . El valor marcado en la segunda subexpresión debía ser la variable transición2.Peso.Valor .

Otro código sospechoso similar.

 internal MethodBase ToMethodInternal(IMethodReference imr) { .... bf |= BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; .... } 

Advertencia de PVS-Studio : V3001 Hay subexpresiones idénticas 'BindingFlags.Public' a la izquierda y a la derecha de '|' operador Compilador CodeBuilder.cs 194

Al formar el valor de la variable bf , el elemento de enumeración BindingFlags.Public se usa dos veces. O este código contiene una operación de marcado adicional, o en lugar del segundo uso de BindingFlags.Public, debería haber un valor de enumeración diferente.

Por cierto, en el código fuente este código está escrito en una línea. Me parece que si está formateado en un estilo de tabla (como aquí), el problema es más fácil de detectar.

Sigamos adelante. Traigo todo el cuerpo del método y nuevamente le sugiero que encuentre el error (o tal vez el error) usted mismo.

 private void ForEachPrefix(IExpression expr, Action<IExpression> action) { // This method must be kept consistent with GetTargets. if (expr is IArrayIndexerExpression) ForEachPrefix(((IArrayIndexerExpression)expr).Target, action); else if (expr is IAddressOutExpression) ForEachPrefix(((IAddressOutExpression)expr).Expression, action); else if (expr is IPropertyReferenceExpression) ForEachPrefix(((IPropertyReferenceExpression)expr).Target, action); else if (expr is IFieldReferenceExpression) { IExpression target = ((IFieldReferenceExpression)expr).Target; if (!(target is IThisReferenceExpression)) ForEachPrefix(target, action); } else if (expr is ICastExpression) ForEachPrefix(((ICastExpression)expr).Expression, action); else if (expr is IPropertyIndexerExpression) ForEachPrefix(((IPropertyIndexerExpression)expr).Target, action); else if (expr is IEventReferenceExpression) ForEachPrefix(((IEventReferenceExpression)expr).Target, action); else if (expr is IUnaryExpression) ForEachPrefix(((IUnaryExpression)expr).Expression, action); else if (expr is IAddressReferenceExpression) ForEachPrefix(((IAddressReferenceExpression)expr).Expression, action); else if (expr is IMethodInvokeExpression) ForEachPrefix(((IMethodInvokeExpression)expr).Method, action); else if (expr is IMethodReferenceExpression) ForEachPrefix(((IMethodReferenceExpression)expr).Target, action); else if (expr is IUnaryExpression) ForEachPrefix(((IUnaryExpression)expr).Expression, action); else if (expr is IAddressReferenceExpression) ForEachPrefix(((IAddressReferenceExpression)expr).Expression, action); else if (expr is IDelegateInvokeExpression) ForEachPrefix(((IDelegateInvokeExpression)expr).Target, action); action(expr); } 

Has encontrado Estamos comprobando!

Advertencias de PVS-Studio :
  • V3003 Se usó el patrón 'if (A) {...} else if (A) {...}'. Hay una probabilidad de presencia de error lógico. Verificación de líneas: 1719, 1727. Código del compiladorRecognizer.cs 1719
  • V3003 Se usó el patrón 'if (A) {...} else if (A) {...}'. Hay una probabilidad de presencia de error lógico. Líneas de verificación: 1721, 1729. Código del compiladorRecognizer.cs 1721

Simplifique un poco el código para que los problemas sean más obvios.

 private void ForEachPrefix(IExpression expr, Action<IExpression> action) { if (....) .... else if (expr is IUnaryExpression) ForEachPrefix(((IUnaryExpression)expr).Expression, action); else if (expr is IAddressReferenceExpression) ForEachPrefix(((IAddressReferenceExpression)expr).Expression, action); .... else if (expr is IUnaryExpression) ForEachPrefix(((IUnaryExpression)expr).Expression, action); else if (expr is IAddressReferenceExpression) ForEachPrefix(((IAddressReferenceExpression)expr).Expression, action) .... } 

Expresiones condicionales y luego ramas de varios si las declaraciones están duplicadas. Quizás este código se escribió utilizando el método de copiar y pegar, por lo que surgió el problema. Ahora resulta que las ramas de los duplicados nunca se ejecutarán, ya que:

  • si la expresión condicional es verdadera, se ejecuta el cuerpo de la primera instrucción if del par correspondiente;
  • Si la expresión condicional es falsa en el primer caso, será falsa en el segundo.

Desde entonces, las ramas contienen las mismas acciones, ahora parece un código redundante que es confuso. Es posible que este sea un tipo diferente de problema: en lugar de duplicados, se deberían haber realizado otras verificaciones.

Continuamos

 public int Compare(Pair<int, int> x, Pair<int, int> y) { if (x.First < y.First) { if (x.Second >= y.Second) { // y strictly contains x return 1; } else { // No containment - order by left bound return 1; } } else if (x.First > y.First) { if (x.Second <= y.Second) { // x strictly contains y return -1; } else { // No containment - order by left bound return -1; } } .... } 

Advertencias de PVS-Studio :
  • V3004 La declaración 'then' es equivalente a la declaración 'else'. Runtime RegexpTreeBuilder.cs 1080
  • V3004 La declaración 'then' es equivalente a la declaración 'else'. Runtime RegexpTreeBuilder.cs 1093

El código parece extremadamente sospechoso, ya que contiene dos declaraciones condicionales con cuerpos idénticos de entonces y de otras ramas. En ambos casos, probablemente valga la pena devolver valores diferentes. O, si este es un comportamiento concebido, será útil eliminar las declaraciones condicionales redundantes.

Hubo ciclos interesantes. Ejemplo a continuación:

 private static Set<StochasticityPattern> IntersectPatterns(IEnumerable<StochasticityPattern> patterns) { Set<StochasticityPattern> result = new Set<StochasticityPattern>(); result.AddRange(patterns); bool changed; do { int count = result.Count; AddIntersections(result); changed = (result.Count != count); break; } while (changed); return result; } 

Advertencia de PVS-Studio : V3020 Una 'interrupción' incondicional dentro de un bucle. Compilador DefaultFactorManager.cs 474

Debido a la declaración de interrupción incondicional, se realiza exactamente una iteración del bucle, y la variable de control modificada ni siquiera se usa. En general, el código parece extraño y sospechoso.

El mismo método (copia exacta) se encontró en otra clase. Advertencia del analizador correspondiente: V3020 Una 'interrupción' incondicional dentro de un bucle. Visualizers.Windows FactorManagerView.cs 350

Por cierto, un método se encontró con una declaración de continuación incondicional en un bucle (fue encontrado por el analizador con el mismo diagnóstico), pero hubo un comentario arriba que confirma que esta es una solución temporal especial:

 // TEMPORARY continue; 

Recuerdo que no hubo tales comentarios cerca de la declaración de ruptura incondicional.

Sigamos adelante.

 internal static DependencyInformation GetDependencyInfo(....) { .... IExpression resultIndex = null; .... if (resultIndex != null) { if (parameter.IsDefined( typeof(SkipIfMatchingIndexIsUniformAttribute), false)) { if (resultIndex == null) throw new InferCompilerException( parameter.Name + " has SkipIfMatchingIndexIsUniformAttribute but " + StringUtil.MethodNameToString(method) + " has no resultIndex parameter"); .... } .... } .... } 

Advertencia de PVS-Studio : V3022 La expresión 'resultIndex == null' siempre es falsa. Compilador FactorManager.cs 382

Inmediatamente, observo que entre la declaración y la verificación anterior, el valor de la variable resultIndex puede cambiar. Sin embargo, entre las comprobaciones resultIndex! = Null y resultIndex == null, el valor ya no se puede cambiar. Por lo tanto, el resultado de la expresión resultIndex == null siempre será falso , lo que significa que nunca se lanzará una excepción.

Espero que tenga interés en encontrar errores usted mismo, sin mis sugerencias, para encontrar un problema, pero por si acaso, le propondré hacerlo nuevamente. El código del método es pequeño, lo daré en su totalidad.

 public static Tuple<int, string> ComputeMovieGenre(int offset, string feature) { string[] genres = feature.Split('|'); if (genres.Length < 1 && genres.Length > 3) { throw new ArgumentException(string.Format( "Movies should have between 1 and 3 genres; given {0}.", genres.Length)); } double value = 1.0 / genres.Length; var result = new StringBuilder( string.Format( "{0}:{1}", offset + MovieGenreBuckets[genres[0]], value)); for (int i = 1; i < genres.Length; ++i) { result.Append( string.Format( "|{0}:{1}", offset + MovieGenreBuckets[genres[i].Trim()], value)); } return new Tuple<int, string>(MovieGenreBucketCount, result.ToString()); } 

Veamos que pasa aquí. La cadena de entrada se analiza con el carácter '|'. Si la longitud de la matriz no es la esperada, se debe lanzar una excepción. Espera un segundo ... géneros . Longitud <1 & & géneros. Longitud> 3 ? Como no hay un número que se encuentre inmediatamente en el rango de valores requeridos por la expresión ( [int.MinValue..1) y (3..int.MaxValue] ), el resultado de la expresión siempre será falso . Por lo tanto, esta verificación no protege contra nada y no se lanzará la excepción esperada.

Esto es exactamente lo que advierte el analizador: V3022 Expresión 'géneros. Longitud <1 && géneros. Longitud> 3' siempre es falso. Probablemente el '||' El operador debe usarse aquí. Características del evaluador.cs 242

Operación de fisión sospechosa se reunió.

 public static void CreateTrueThetaAndPhi(....) { .... double expectedRepeatOfTopicInDoc = averageDocLength / numUniqueTopicsPerDoc; .... int cnt = Poisson.Sample(expectedRepeatOfTopicInDoc); .... } 

Advertencia de PVS-Studio : V3041 La expresión se convirtió implícitamente de tipo 'int' a tipo 'doble'. Considere utilizar un molde de tipo explícito para evitar la pérdida de una parte fraccional. Un ejemplo: doble A = (doble) (X) / Y;. LDA Utilities.cs 74

Aquí es sospechoso: se realiza una división entera (las variables averageDocLength y numUniqueTopicsPerDoc son de tipo int ), y el resultado se escribe en una variable de tipo double . Se plantea la pregunta: ¿se hace especialmente o todavía se implica la división de números reales? Si la variable esperadaRepeatOfTopicInDoc fuera del tipo int , esto aclararía las posibles preguntas.

En otros lugares, se usa el método Poisson.Sample , cuyo argumento es la variable sospechosa esperadaRepeatOfTopicInDoc , por ejemplo, como se describe a continuación.

 int numUniqueWordsPerTopic = Poisson.Sample((double)averageWordsPerTopic); 

averageWordsPerTopic es de tipo int , que ya está convertido a doble en el lugar de uso.

Y aquí hay otro lugar de uso:

 double expectedRepeatOfWordInTopic = ((double)numDocs) * averageDocLength / numUniqueWordsPerTopic; .... int cnt = Poisson.Sample(expectedRepeatOfWordInTopic); 

Tenga en cuenta que las variables tienen los mismos nombres que en el ejemplo original, solo la división de números reales se usa para inicializar el esperado RepeatOfWordInTopic (debido a la conversión explícita de numDocs al doble ).

En general, vale la pena mirar el punto de partida donde el analizador emitió una advertencia.

Pero reflexiones sobre si vale la pena editar y cómo, dejemos a los autores del código (lo saben mejor), pero vamos más allá. A la siguiente división sospechosa.

 public static NonconjugateGaussian BAverageLogarithm(....) { .... double v_opt = 2 / 3 * (Math.Log(mx * mz / Ex2 / 2) - m); if (v_opt != v) { .... } .... } 

Advertencia de PVS-Studio : V3041 La expresión se convirtió implícitamente de tipo 'int' a tipo 'doble'. Considere utilizar un molde de tipo explícito para evitar la pérdida de una parte fraccional. Un ejemplo: doble A = (doble) (X) / Y;. Tiempo de ejecución ProductExp.cs 137

El analizador volvió a detectar la operación sospechosa de la división de enteros, ya que 2 y 3 son literales numéricos enteros, y el resultado de la expresión 2/3 será 0 . Como resultado, toda la expresión toma la forma:

 double v_opt = 0 * expr; 

De acuerdo, un poco extraño. Varias veces volví a esta advertencia, tratando de encontrar algún tipo de captura, y no tratando de agregarla al artículo. El método está lleno de matemáticas y varias fórmulas (que, francamente, realmente no quería desmontar), nunca se sabe qué esperar. Además, trato de ser lo más escéptico posible sobre las advertencias que escribo en el artículo, y las describo solo después de haberlas estudiado mejor.

Pero luego me di cuenta: ¿por qué necesito un factor de 0 , escrito como 2/3 ? Así que vale la pena ver este lugar de todos modos.

 public static void WriteAttribute(TextWriter writer, string name, object defaultValue, object value, Func<object, string> converter = null) { if ( defaultValue == null && value == null || value.Equals(defaultValue)) { return; } string stringValue = converter == null ? value.ToString() : converter(value); writer.Write($"{name}=\"{stringValue}\" "); } 

Advertencia de PVS-Studio : V3080 Posible desreferencia nula. Considere inspeccionar el 'valor'. Compilador WriteHelpers.cs 78

Afirmación bastante justa del analizador basada en la condición. La desreferenciación de una referencia nula puede ocurrir en el valor de la expresión. Igual ( valor predeterminado) si valor == nulo . Dado que esta expresión es el operando derecho del operador ||, para calcularlo, el operando izquierdo debe ser falso , y para esto es suficiente que al menos una de las variables defaultValue \ value no sea nula . Como resultado, si defaultValue! = Null , y value == null :

  • defaultValue == null -> false ;
  • defaultValue == null && value == null -> false ; (no se verificó el valor)
  • value.Equals (defaultValue) -> NullReferenceException , ya que el valor es nulo .

Veamos un caso similar:

 public FeatureParameterDistribution( GaussianMatrix traitFeatureWeightDistribution, GaussianArray biasFeatureWeightDistribution) { Debug.Assert( (traitFeatureWeightDistribution == null && biasFeatureWeightDistribution == null) || traitFeatureWeightDistribution.All( w => w != null && w.Count == biasFeatureWeightDistribution.Count), "The provided distributions should be valid and consistent in the number of features."); .... } 

Advertencia de PVS-Studio : V3080 Posible desreferencia nula. Considere la posibilidad de inspeccionar 'traitFeatureWeightDistribution'. Recomendador FeatureParameterDistribution.cs 65

Desechamos el exceso, dejando solo la lógica para calcular el valor booleano, de modo que sea más fácil de entender:

 (traitFeatureWeightDistribution == null && biasFeatureWeightDistribution == null) || traitFeatureWeightDistribution.All( w => w != null && w.Count == biasFeatureWeightDistribution.Count) 

De nuevo, el operando correcto de || Se calcula solo si el resultado del cálculo de la izquierda es falso . El operando izquierdo puede ser falso , incluso cuando traitFeatureWeightDistribution == null y biasFeatureWeightDistribution! = Null . Luego, se calculará el operando correcto del operador ||, y una llamada a traitFeatureWeightDistribution.All generará una excepción ArgumentNullException .

Otra pieza interesante de código:

 public static double GetQuantile(double probability, double[] quantiles) { .... int n = quantiles.Length; if (quantiles == null) throw new ArgumentNullException(nameof(quantiles)); if (n == 0) throw new ArgumentException("quantiles array is empty", nameof(quantiles)); .... } 

PVS-Studio Advertencia : V3095 El objeto 'cuantiles' se usó antes de que se verificara como nulo. Líneas de verificación: 91, 92. Runtime OuterQuantiles.cs 91

Tenga en cuenta que primero se accede a la propiedad quantiles.Length y luego se verifica que los cuantiles sean nulos . Como resultado, si los cuantiles == nulo , el método arrojará una excepción, solo un poco mal, y un poco no donde se necesitaba. Aparentemente, desordenaron las líneas en algunos lugares.

Si usted mismo se ha ocupado de la detección de los errores anteriores, le sugiero que prepare una taza de café e intente repetir la hazaña, encontrando el error en el siguiente método. Para hacerlo un poco más interesante, cito el código completo del método.

( Enlace a tamaño completo )

Imagen 2



Está bien, está bien, eso fue una broma (¿o tuviste éxito?!). Simplifiquemos un poco la tarea:

 if (sample.Precision < 0) { precisionIsBetween = true; lowerBound = -1.0 / v; upperBound = -mean.Precision; } else if (sample.Precision < -mean.Precision) { precisionIsBetween = true; lowerBound = 0; upperBound = -mean.Precision; } else { // in this case, the precision should NOT be in this interval. precisionIsBetween = false; lowerBound = -mean.Precision; lowerBound = -1.0 / v; } 

¿Ha mejorado? El analizador emitió la siguiente advertencia para este código: V3008 A la variable 'lowerBound' se le asignan valores dos veces seguidas. Quizás esto sea un error. Líneas de verificación: 324, 323. Runtime GaussianOp.cs 324

De hecho, en la última rama más , el valor de la variable lowerBound se asigna dos veces seguidas. Aparentemente (y a juzgar por el código anterior), la variable upperBound debe estar involucrada en una de las asignaciones.

Seguimos más allá.

 private void WriteAucMatrix(....) { .... for (int c = 0; c < classLabelCount; c++) { int labelWidth = labels[c].Length; columnWidths[c + 1] = labelWidth > MaxLabelWidth ? MaxLabelWidth : labelWidth; for (int r = 0; r < classLabelCount; r++) { int countWidth = MaxValueWidth; if (countWidth > columnWidths[c + 1]) { columnWidths[c + 1] = countWidth; } } .... } 

Advertencia de PVS-Studio : V3081 El contador 'r' no se usa dentro de un bucle anidado. Considere inspeccionar el uso del contador 'c'. CommandLine ClassifierEvaluationModule.cs 459

Tenga en cuenta que el contador del bucle interno - r - no se usa en el cuerpo de este bucle. Debido a esto, resulta que durante todas las iteraciones del bucle interno se realizan las mismas operaciones en los mismos elementos; después de todo, los índices también usan el contador del bucle externo ( c ), y no el interno ( r ).

Veamos qué más se encontró interesante.

 public RegexpFormattingSettings( bool putOptionalInSquareBrackets, bool showAnyElementAsQuestionMark, bool ignoreElementDistributionDetails, int truncationLength, bool escapeCharacters, bool useLazyQuantifier) { this.PutOptionalInSquareBrackets = putOptionalInSquareBrackets; this.ShowAnyElementAsQuestionMark = showAnyElementAsQuestionMark; this.IgnoreElementDistributionDetails = ignoreElementDistributionDetails; this.TruncationLength = truncationLength; this.EscapeCharacters = escapeCharacters; } 

Advertencia de PVS-Studio : no se usa el parámetro V3117 Constructor 'useLazyQuantifier'. Runtime RegexpFormattingSettings.cs 38

El constructor no usa un parámetro: useLazyQuantifier . Esto parece particularmente sospechoso en el contexto del hecho de que una propiedad con el nombre y el tipo correspondiente se define en la clase: UseLazyQuantifier . Aparentemente, olvidaron inicializarlo a través del parámetro correspondiente.

Conocí a varios controladores de eventos potencialmente peligrosos. A continuación se muestra un ejemplo de uno de ellos:

 public class RecommenderRun { .... public event EventHandler Started; .... public void Execute() { // Report that the run has been started if (this.Started != null) { this.Started(this, EventArgs.Empty); } .... } .... } 

Advertencia de PVS-Studio : V3083 Invocación insegura del evento 'Iniciado', es posible NullReferenceException. Considere asignar un evento a una variable local antes de invocarlo. Evaluador recomendado: Run.cs 115

El hecho es que entre la verificación de la desigualdad nula y la llamada del controlador, se puede cancelar la suscripción de un evento, y si en el momento entre la verificación de nulo y la llamada de los controladores el evento no tiene suscriptores, se lanzará una NullReferenceException . Para evitar tales problemas, por ejemplo, puede guardar el enlace a la cadena de delegados en una variable local o usar el operador '?.' para llamar a los manejadores.

Además del fragmento de código anterior, había 35 de esos lugares.

Por cierto, también se cumplieron 785 advertencias de V3024 . Se emite la advertencia V3024 al comparar números reales utilizando los operadores '! =' O '=='. No me detendré aquí sobre por qué tales comparaciones no siempre son correctas: más sobre esto está escrito en la documentación, también hay un enlace a StackOverflow (esto es todo).

Teniendo en cuenta que las fórmulas y los cálculos a menudo se cumplieron, estas advertencias también pueden ser importantes, aunque se llevan al nivel 3 (ya que están lejos de ser relevantes en todos los proyectos).

Si está seguro de que estas advertencias son irrelevantes, puede eliminarlas con casi un clic , reduciendo el número total de operaciones del analizador.



Conclusión


De alguna manera resultó que durante mucho tiempo no había escrito artículos sobre la verificación de proyectos, y tocar este proceso nuevamente fue bastante agradable. Espero que también hayas aprendido algo nuevo / útil de este artículo, o al menos lo leas con interés.

Les deseo a los desarrolladores una corrección temprana de las áreas problemáticas y les recuerdo que cometer errores es normal, sin embargo, somos personas. Para esto, se necesitan herramientas adicionales como analizadores estáticos para encontrar lo que una persona se perdió, ¿verdad? De todos modos, ¡buena suerte con el proyecto y gracias por el trabajo!

Y recuerde que el beneficio máximo de un analizador estático se logra con su uso regular .

Todo lo mejor!



Si desea compartir este artículo con una audiencia de habla inglesa, utilice el enlace a la traducción: Sergey Vasiliev. ¿Qué errores acechan en el código Infer.NET?

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


All Articles