Analizador PVS-Studio RunUO check

Foto 1

Este artículo está dedicado a verificar el proyecto RunUO utilizando el analizador estático PVS-Studio. RunUO es un emulador de software de servidor para Ultima Online, un juego que una vez se ganó los corazones de muchos fanáticos de MMORPG.

Introducción


RunUO es un emulador de software de servidor para MMORPG Ultima Online. El objetivo de este proyecto es crear un software estable que pueda competir con los servidores oficiales de EA Games. RunUO se creó en 2002, pero no pierde relevancia y se utiliza activamente hasta el día de hoy.

El propósito de la verificación del proyecto es popularizar el tema del análisis estático. Verificamos varios proyectos: juegos ( ejemplo ), bibliotecas ( ejemplo ), mensajeros ( ejemplo ), navegadores ( ejemplo ) y mucho más ( ejemplo , ejemplo , ejemplo) para atraer la atención de una audiencia diversa. Con estos artículos, intentamos llamar la atención sobre la importancia de utilizar el análisis estático en el desarrollo. El análisis estático hace que el código sea más confiable y seguro. Además, con su uso regular, puede encontrar y eliminar errores en las primeras etapas. Esto ahorra tiempo y esfuerzo a los desarrolladores, porque nadie quiere pasar 50 horas buscando el error que el analizador puede encontrar.

También ayudamos a la comunidad de código abierto. A través de artículos con errores encontrados, contribuimos al desarrollo de código abierto. Sin embargo, en los artículos no analizamos todas las advertencias. Algunas advertencias parecían demasiado aburridas para nosotros para entrar en el artículo, algunas resultaron ser falsos positivos, etc. Por lo tanto, estamos listos para proporcionar una licencia gratuita para proyectos de código abierto. Además, lo que nos pareció aburrido para el artículo puede parecer bastante interesante para los desarrolladores del proyecto que se está probando, porque los desarrolladores del proyecto aún son más conscientes de qué problemas son más críticos.

Piezas de código que llamaron la atención al examinar el informe del analizador


Advertencia de PVS-Studio: V3010 Se requiere utilizar el valor de retorno de la función 'Intern'. BasePaintedMask.cs 49

public static string Intern( string str )
{
  if ( str == null )
    return null;
  else if ( str.Length == 0 )
    return String.Empty;

  return String.Intern( str );
}

public BasePaintedMask( string staffer, int itemid )
                            : base( itemid + Utility.Random( 2 ) )
{
  m_Staffer = staffer;

  Utility.Intern( m_Staffer );
}

El valor de retorno del método Intern () no se tiene en cuenta en ninguna parte, como lo indica el analizador. Quizás esto sea un error o un código redundante.

Advertencia de PVS-Studio: V3017 Se detectó un patrón: (el elemento es BasePotion) || ((el artículo es BasePotion) && ...). La expresión es excesiva o contiene un error lógico. Cleanup.cs 137

public static bool IsBuggable( Item item )
{
  if ( item is Fists )
    return false;

  if ( item is ICommodity || item is Multis.BaseBoat
    || item is Fish || item is BigFish
    || item is BasePotion || item is Food || item is CookableFood
    || item is SpecialFishingNet || item is BaseMagicFish
    || item is Shoes || item is Sandals
    || item is Boots || item is ThighBoots
    || item is TreasureMap || item is MessageInABottle
    || item is BaseArmor || item is BaseWeapon
    || item is BaseClothing
    || ( item is BaseJewel && Core.AOS )
    || ( item is BasePotion && Core.ML )
  {
    ....
  }
}

Hay subexpresiones que se pueden simplificar. Los escribiré para que quede más claro:
if (item is BasePotion || ( item is BasePotion && Core.ML ))

Supongamos, artículo es BasePotion = true , entonces la condición es verdadera, a pesar de la Core.ML . Si el artículo es BasePotion = false , entonces la condición es falsa, una vez más, independientemente del valor Core.ML . Este código suele ser simplemente redundante, sin embargo, hay situaciones peores cuando el programador cometió un error y escribió la variable incorrecta en la segunda subexpresión.

Advertencia de PVS-Studio: V3031 Se puede simplificar una comprobación excesiva. El '||' El operador está rodeado de expresiones opuestas 'bPlayerOnly' y '! bPlayerOnly'. BaseCreature.cs 3005

public virtual double GetFightModeRanking( Mobile m,
                                           FightMode acqType,
                                           bool bPlayerOnly )
{
  if ( ( bPlayerOnly && m.Player ) ||  !bPlayerOnly )
  {
    ....
  }
  ....
}

Este código es redundante o erróneo. Su problema es que en ambos lados de '||' son diferentes en el sentido de subexpresión. Si lo cortamos así:

if ( m.Player || !bPlayerOnly )

entonces nada cambiará.

Advertencia de PVS-Studio: V3001 Hay subexpresiones idénticas 'deed is SmallBrickHouseDeed' a la izquierda y a la derecha de '||' operador. RealEstateBroker.cs 132

public int ComputePriceFor( HouseDeed deed )
{
  int price = 0;

  if ( deed is SmallBrickHouseDeed ||    // <=
       deed is StonePlasterHouseDeed ||
       deed is FieldStoneHouseDeed ||
       deed is SmallBrickHouseDeed ||    // <=
       deed is WoodHouseDeed ||
       deed is WoodPlasterHouseDeed ||
       deed is ThatchedRoofCottageDeed )
      ....
}

Creo que no vale la pena explicar nada aquí, el siguiente o el código incorrecto o redundante.

Advertencia de PVS-Studio: V3067 Es posible que se haya olvidado o comentado el bloque 'else', alterando así las lógicas de operación del programa. BaseHouse.cs 1558

private void SetLockdown( Item i, bool locked, bool checkContains )
{
  if ( m_LockDowns == null )
    return;

  #region Mondain's Legacy
  if ( i is BaseAddonContainer )
    i.Movable = false;
  else
  #endregion

  i.Movable = !locked;
  i.IsLockedDown = locked;

  ....
}

Una advertencia bastante rara. El analizador encontró sospechoso el formato del código después de la directiva #endregion. Si no lee el código, parece una línea

i.Movable = !locked;

se ejecutará en cualquier caso, independientemente de la variable i . Quizás las llaves se olvidaron aquí ... En general, los desarrolladores deberían verificar este código.

Advertencia de PVS-Studio: V3043 La lógica operativa del código no se corresponde con su formato. La declaración está sangrada a la derecha, pero siempre se ejecuta. Es posible que falten llaves. Earthquake.cs 57

public override void OnCast()
{
  if ( Core.AOS )
  {
    damage = m.Hits / 2;

    if ( !m.Player )
      damage = Math.Max( Math.Min( damage, 100 ), 15 );
      damage += Utility.RandomMinMax( 0, 15 );            // <=

  }
  else
  {
    ....
  }
}

Es posible que falten llaves en este código. Esta conclusión puede hacerse debido al extraño formato del código en el cuerpo de if (! M.Player) .

Advertencia de PVS-Studio: V3083 Invocación insegura del evento 'ServerStarted', NullReferenceException es posible. Considere asignar un evento a una variable local antes de invocarlo. EventSink.cs 921

public static void InvokeServerStarted()
{
  if ( ServerStarted != null )
    ServerStarted();
}

Este método utiliza una llamada potencialmente insegura al controlador de eventos RefreshStarted , como indica el analizador.

¿Por qué es peligroso? Imagina esta situación. El evento ServerStarted solo tiene un suscriptor. Y en el momento entre verificar nulo y llamar directamente al controlador de eventos ServerStarted () en otro hilo, se realizó una cancelación de suscripción de este evento. Esto arrojará una NullReferenceException .

La forma más fácil de prevenir esta situación es proporcionar una llamada de evento segura utilizando el operador '?.':

public static void InvokeServerStarted()
{
  ServerStarted?.Invoke();
}

Advertencia de PVS-Studio: V3054 Bloqueo de doble verificación potencialmente inseguro. Use variables volátiles o primitivas de sincronización para evitar esto. Item.cs 1624
private Packet m_RemovePacket;
....
private object _rpl = new object();
public Packet RemovePacket
{
  get
  {
    if (m_RemovePacket == null)
    {
      lock (_rpl)
      {
        if (m_RemovePacket == null)
        {
          m_RemovePacket = new RemoveItem(this);
          m_RemovePacket.SetStatic();
        }
      }
    }

    return m_RemovePacket;
  }
}

La advertencia del analizador está asociada con el uso inseguro del patrón de bloqueo doblemente verificado. Como puede ver en el código anterior, se aplicó un bloqueo de doble verificación para implementar el patrón generativo: Loners. Cuando intentamos obtener una instancia de la clase Packet accediendo a la propiedad RemovePacket , el captador comprueba la igualdad del campo m_RemovePacket para cero. Si se aprueba la verificación, ingresamos al cuerpo de la declaración de bloqueo , donde se inicializa el campo m_RemovePacket . Una situación interesante surge en el momento en que el hilo principal ya ha inicializado la variable m_RemovePacket a través del constructor, pero aún no ha llamado al métodoSetStatic (). Teóricamente, otro hilo puede acceder a la propiedad RemovePacket en el momento muy inconveniente. La comprobación de m_RemovePacket para la igualdad a cero ya no fallará, y el hilo de llamada recibirá un enlace a un objeto incompleto listo para usar. Para resolver este problema, puede crear una variable intermedia de la clase Packet en el cuerpo de la instrucción de bloqueo , inicializarla mediante una llamada de constructor y el método SetStatic (), y luego asignarla a la variable m_RemovePacket . En este caso, el cuerpo de la declaración de bloqueo podría verse así:

lock (_rpl)
{
  if (m_RemovePacket == null)
  {
    Packet instance = new RemoveItem(this);
    instance.SetStatic();
    m_RemovePacket = instance;
  }
}

El problema parece estar solucionado y el código funcionará como se esperaba. Pero esto no es así.

Hay un punto más: el analizador no solo sugiere usar la palabra clave volátil . En la versión de lanzamiento del programa, el compilador puede realizar la optimización y reordenar las líneas de la llamada al método SetStatic () y asignar la variable de instancia al campo m_RemovePacket (desde el punto de vista del compilador, la semántica del programa no será violada). Y nuevamente volvemos al mismo punto donde comenzamos: la posibilidad de obtener la variable no inicializada m_RemovePacket. Es imposible decir con exactitud cuándo puede ocurrir este reordenamiento y si ocurrirá en absoluto: puede verse afectado por la versión CLR, la arquitectura del procesador utilizado, etc. Todavía vale la pena protegerse de tal escenario, y una de las soluciones (sin embargo, no la más productiva) será usar la palabra clave volátil . Una variable que se declarará con el modificador volátil no estará sujeta a permutaciones durante la optimización por parte del compilador. Un código finalmente arreglado puede verse así:

private volatile Packet m_RemovePacket;
....
private object _rpl = new object();
public Packet RemovePacket
{
  get
  {
    if (m_RemovePacket == null)
    {
      lock (_rpl)
      {
        if (m_RemovePacket == null)
        {
          Packet instance = new RemoveItem(this);
          instance.SetStatic();
          m_RemovePacket = instance;
        }
      }
    }

    return m_RemovePacket;
  }
}

A veces, usar un campo volátil puede ser indeseable debido a la sobrecarga de acceder a dicho campo. No nos detendremos en este tema en detalle, simplemente notando que en el ejemplo en consideración, se necesita un registro de campo atómico solo una vez (cuando se accede por primera vez a la propiedad), sin embargo, declarar el campo como volátil hará que el compilador haga su lectura y escritura atómica eso puede no ser óptimo en términos de rendimiento.

Por lo tanto, otro enfoque para corregir esta advertencia del analizador para evitar una sobrecarga adicional al declarar un campo volátil es usar el tipo Lazy <T> para respaldar el campo m_RemovePacketen lugar de doble control de bloqueo. En este caso, el cuerpo getter se puede reemplazar por el método inicializador, que se pasará al constructor de la instancia Lazy <T> :

private Lazy<Packet> m_RemovePacket = new Lazy<Packet>(() =>
  {
    Packet instance = new RemoveItem(this);
    instance.SetStatic();
    return instance;
  }, LazyThreadSafetyMode.ExecutionAndPublication);

....
public Packet RemovePacket
{
  get
  {
    return m_RemovePacket.Value;
  }
}

El método de inicializador se llamará una vez, cuando se acceda al tipo Lazy por primera vez, y la seguridad de transmisión en caso de acceder a la propiedad desde varios subprocesos al mismo tiempo estará garantizada por el tipo Lazy <T> (el modo de seguridad de subprocesos se controla mediante el segundo parámetro del constructor Lazy ).

Advertencia de PVS-Studio: V3131 Se verifica la compatibilidad de la expresión 'específica' con el tipo 'IAxe', pero se convierte al tipo 'Elemento'. HarvestTarget.cs 61

protected override void OnTarget( Mobile from, object targeted )
{
  ....
  else if ( m_System is Lumberjacking &&
            targeted is IAxe && m_Tool is BaseAxe )
  {
    IAxe obj = (IAxe)targeted;
    Item item = (Item)targeted;
    ....
  }
  ....
}

Se comprobó que la variable objetivo pertenecía al tipo IAxe , pero nadie verificó la pertenencia al artículo , como informa el analizador.

Advertencia de PVS-Studio: V3070 La variable no inicializada 'Cero' se usa al inicializar la variable 'm_LastMobile'. Serial.cs 29
public struct Serial : IComparable, IComparable<Serial>
{
  private int m_Serial;

  private static Serial m_LastMobile = Zero;                // <=
  private static Serial m_LastItem = 0x40000000;

  public static Serial LastMobile { .... }
  public static Serial LastItem { .... }

  public static readonly Serial MinusOne = new Serial( -1 );
  public static readonly Serial Zero = new Serial( 0 );     // <=
  ....
  private Serial( int serial )
  {
    m_Serial = serial;
  }
  ....
}

Como tal, no hay ningún error aquí, sin embargo, escribir esto no es muy bueno. Debido a la asignación de m_LastMobile al valor de un cero sin inicializar , se creará una estructura con el constructor predeterminado Serial () , lo que conducirá a la inicialización de m_Serial = 0 . Y esto, a su vez, es equivalente a llamar a la nueva serie (0) . De hecho, los desarrolladores tienen la suerte de que el número de serie sea ​​igual a 0 , si hubiera habido algún otro valor, esto conduciría a un error.

Advertencia de PVS-Studio: V3063 Una parte de la expresión condicional siempre es verdadera si se evalúa: m_Serial <= 0x7FFFFFFF. Serial.cs 83

public bool IsItem
{
  get
  {
    return ( m_Serial >= 0x40000000 && m_Serial <= 0x7FFFFFFF );
  }
}

0x7FFFFFFF es el valor máximo posible que puede contener Int32 . Por lo tanto, cualquiera que sea el valor que tenga la variable m_Serial , en cualquier caso será menor o igual a 0x7FFFFFFF .

Advertencia de PVS-Studio: V3004 La declaración 'then' es equivalente a la declaración 'else'. Serialization.cs 1571

public override void WriteDeltaTime( DateTime value )
{
  ....
  try 
  { 
    d = new TimeSpan( ticks-now ); 
  }
  catch 
  {
    if( ticks < now ) 
      d = TimeSpan.MaxValue; 
    else 
      d = TimeSpan.MaxValue;
  }
  ....
}

El analizador advierte sobre un código sospechoso en el que las ramas verdadera y falsa de la declaración if coinciden completamente. Quizás una de las ramas debería tener TimeSpan.MinValue. Se encontró el mismo código en varios lugares:

V3004 La declaración 'then' es equivalente a la declaración 'else'. Item.cs 2103

public virtual void Serialize( GenericWriter writer )
{
  ....
  
  if( ticks < now ) 
    d = TimeSpan.MaxValue; 
  else 
    d = TimeSpan.MaxValue;
  
  ....
}

V3004 La declaración 'then' es equivalente a la declaración 'else'. Serialization.cs 383

public override void WriteDeltaTime( DateTime value )
{
  ....
  
  if( ticks < now ) 
    d = TimeSpan.MaxValue; 
  else 
    d = TimeSpan.MaxValue;
  
  ....
}

Usé "Esto" por una razón, es muy probable que, entre otras cosas, no podría funcionar sin copiar y pegar, estos fragmentos de código se ven muy dolorosos.

Advertencia PVS-Studio: V3051 Un tipo de molde excesivo. El objeto ya es del tipo 'Elemento'. Mobile.cs 11237

public Item Talisman
{
  get
  {
    return FindItemOnLayer( Layer.Talisman ) as Item;
  }
}
public Item FindItemOnLayer( Layer layer )
{
  ....
}

Esta advertencia analizador puede obtenerse si se hace un uso excesivo de la que el operador . No hay error en esta sección del código, pero tampoco tiene sentido convertir el objeto en su propio tipo. Advertencia de

PVS-Studio: V3148 El valor potencial 'nulo' de conversión de 'toSet' a un tipo de valor puede conducir a NullReferenceException. Properties.cs 502

public static string ConstructFromString( .... )
{
  object toSet;
  bool isSerial = IsSerial( type );

  if ( isSerial ) // mutate into int32
    type = m_NumericTypes[4];

  ....
  else if ( value == null )
  {
    toSet = null;
  }
  ....

  if ( isSerial ) // mutate back
    toSet = (Serial)((Int32)toSet);

  constructed = toSet;
  return null;
}

En esta sección del código nos interesa el caso en que el valor de la variable es nulo . Entonces la variable toSet también es nula . Además, si la variable esSerial == verdadero , toSet se convierte en Int32 , lo que conducirá a NRE .

Puede corregir este código agregando, por ejemplo, 0 de forma predeterminada:

toSet = (Serial)((Int32)(toSet ?? 0));

Advertencia de PVS-Studio: V3031 Se puede simplificar una comprobación excesiva. El '||' El operador está rodeado de expresiones opuestas 'pack == null' y 'pack! = null'. BODBuyGump.cs 64
public override void OnResponse(Server.Network.NetState sender, RelayInfo info)
{
  ....
  if ( (pack == null) ||
       ((pack != null) &&
        (!pack.CheckHold(
                m_From,
                item,
                true,
                true,
                0,
                item.PileWeight + item.TotalWeight)) ) )
  {
    pv.SayTo(m_From, 503204);
    m_From.SendGump(new BOBGump(m_From, m_Book, m_Page, null));
  }
  ....
}

Este código se puede simplificar, ya que el analizador informa:

if ((pack == null) || ((pack != null) && (!pack.CheckHold(....))))

A la izquierda y a la derecha del operador '||' hay expresiones que son opuestas en significado. Aquí el paquete de verificación ! = Nulo es redundante, porque antes de eso se comprueba la condición opuesta paquete == nulo , y estas expresiones están separadas por el operador '||'. Esta línea se puede acortar de la siguiente manera:

if (pack == null || !pack.CheckHold(....))

PVS-Studio Advertencia: V3080 Posible desreferencia nula. Considere inspeccionar 'ganador'. CTF.cs 1302

private void Finish_Callback()
{
  ....
  CTFTeamInfo winner = ( teams.Count > 0 ? teams[0] : null );

  .... 

  m_Context.Finish( m_Context.Participants[winner.TeamID] as Participant );
}

Digamos equipos . El conteo es 0 . Entonces ganador = nulo. Y más adelante en el código, se accede a la propiedad winner.TeamID sin verificar null , lo que dará como resultado el acceso por una referencia nula.

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;. StormsEye.cs 87

public static void Gain( Mobile from, Skill skill ) 
{
  ....
  if ( from.Player && 
     ( skills.Total / skills.Cap ) >= Utility.RandomDouble())
  ....
}

Este fragmento de código contiene la operación de dividir las variables skill.Total y skills.Cap , que son de tipo int , y el resultado se convierte implícitamente en type double , como informa el analizador.

Advertencia de PVS-Studio: V3085 El nombre del campo 'typeofObject' en un tipo anidado es ambiguo. El tipo externo contiene un campo estático con un nombre idéntico. PropsGump.cs 744

private static Type typeofObject = typeof( object );
....
private class GroupComparer : IComparer
{
  ....
  private static Type typeofObject = typeof( Object );
  ....
}

La variable typeofObject se creó en esta sección de código en una clase anidada . Su problema es que en la clase externa hay una variable con el mismo nombre, y pueden ocurrir errores debido a esto. Es mejor no permitir esto para reducir la probabilidad de tales errores debido a la falta de atención.

Advertencia de PVS-Studio: los accesores de propiedades V3140 utilizan diferentes campos de respaldo. WallBanner.cs 77

private bool m_IsRewardItem;

[CommandProperty( AccessLevel.GameMaster )]
public bool IsRewardItem
{
  get{ return m_IsRewardItem; }
  set{ m_IsRewardItem = value; InvalidateProperties(); }
}

private bool m_East;

[CommandProperty( AccessLevel.GameMaster )]
public bool East
{
  get{ return m_East; }
  set{ m_IsRewardItem = value; InvalidateProperties(); }
}

Y aquí hay un error que apareció debido a copiar y pegar. El método de acceso establecido de la propiedad Este era asignar un valor para m_East , no m_IsRewardItem . Advertencias de

PVS-Studio:

V3012 El operador '?:', Independientemente de su expresión condicional, siempre devuelve el mismo valor: 0xe7f. TreasureChestLevel2.cs 52

V3012 El operador '?:', Independientemente de su expresión condicional, siempre devuelve el mismo valor: 0xe77. TreasureChestLevel2.cs 57

private void SetChestAppearance()
{
  bool UseFirstItemId = Utility.RandomBool();

  switch( Utility.RandomList( 0, 1, 2, 3, 4, 5, 6, 7 ) )
  {
    ....
    case 6:// Keg
      this.ItemID = ( UseFirstItemId ? 0xe7f : 0xe7f );
      this.GumpID = 0x3e;
      break;

    case 7:// Barrel
      this.ItemID = ( UseFirstItemId ? 0xe77 : 0xe77 );
      this.GumpID = 0x3e;
      break;
  }
}

Algún tipo de ilusión de elección :) Independientemente del valor de UseFirstItemId , this.ItemID será 0xe7f en el primer caso o 0xe77 en el segundo.

Advertencia PVS-Studio: V3066 Posible orden incorrecto de argumentos pasados ​​al método 'OnSwing': 'defensor' y 'atacante'. BaseWeapon.cs 1188

public virtual int AbsorbDamageAOS( Mobile attacker,
                                    Mobile defender,
                                    int damage )
{
  ....
  if ( weapon != null )
  {
    defender.FixedParticles(0x3779,
                            1,
                            15,
                            0x158B,
                            0x0,
                            0x3,
                            EffectLayer.Waist);
    weapon.OnSwing( defender, attacker );
  }
  ....
}

public virtual TimeSpan OnSwing( Mobile attacker, Mobile defender )
{
  return OnSwing( attacker, defender, 1.0 );
}

Al analizador le pareció sospechoso que el método OnSwing () pasara argumentos en el orden inverso. Quizás esto se deba a un error.

Advertencia de PVS-Studio: V3092 Las intersecciones de rango son posibles dentro de las expresiones condicionales. Ejemplo: if (A> 0 && A <5) {...} más if (A> 3 && A <9) {...}. HouseFoundation.cs 1883

public static bool IsFixture( int itemID )
{
  ....
  else if( itemID >= 0x319C && itemID < 0x31B0 ) 
    return true;
  // ML doors
  else if( itemID == 0x2D46 ||
           itemID == 0x2D48 ||
           itemID == 0x2FE2 ||
           itemID == 0x2FE4 )
    return true;
  else if( itemID >= 0x2D63 && itemID < 0x2D70 )
    return true;
  else if( itemID >= 0x319C && itemID < 0x31AF ) 
    return true;
  ....
}

Los rangos probados bajo condiciones se cruzan. Esto le pareció sospechoso al analizador. Incluso si este código funciona correctamente, vale la pena arreglarlo. Imagine una situación en la que necesitáramos reescribir el cuerpo del último si es así, si la condición es verdadera, el método devuelve falso . Si itemID es igual a, digamos, 0x319C , el método aún devolverá verdadero . Esto, a su vez, implicará la pérdida de tiempo para buscar errores.

Conclusión


RunUO se creó hace mucho tiempo, se ha realizado mucho trabajo, sin embargo, con el ejemplo de este proyecto puede ver los beneficios de usar análisis estático en proyectos con un historial. El analizador de 543 mil líneas de código de proyecto emitió alrededor de 500 advertencias (sin contar el nivel bajo), la mayoría de las cuales no aparecieron en el artículo debido a su uniformidad. Para familiarizarse con el resultado del análisis en más detalle, sugiero usar una licencia gratuita para proyectos de código abierto .



Si desea compartir este artículo con una audiencia de habla inglesa, utilice el enlace a la traducción: Ekaterina Nikiforova. RunUO Check con el analizador PVS-Studio .

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


All Articles