Como o PVS-Studio Analyzer começou a encontrar mais erros em projetos do Unity

image1.png


Ao desenvolver o analisador estático PVS-Studio, tentamos desenvolvê-lo em várias direções. Portanto, nossa equipe está trabalhando em plugins para IDE (Visual Studio, Rider), melhorando a integração com o CI, etc. Aumentar a eficiência da análise de projetos para o Unity também é um dos nossos objetivos prioritários. Acreditamos que a análise estática permitirá que os programadores que usam esse mecanismo de jogo melhorem a qualidade do código fonte e simplifiquem o trabalho em qualquer projeto. Portanto, eu gostaria de aumentar a popularidade do PVS-Studio entre as empresas que desenvolvem para o Unity. Uma das primeiras etapas na implementação dessa idéia foi escrever anotações para os métodos definidos no mecanismo. Isso permite que você controle a correção do código associado às chamadas para métodos anotados.



Introdução



As anotações são um dos mecanismos mais importantes do analisador. Eles fornecem várias informações sobre argumentos, valor de retorno e recursos internos de métodos que não podem ser resolvidos automaticamente. Ao mesmo tempo, o programador de anotações pode assumir a estrutura interna aproximada do método e as especificidades de sua operação, com base na documentação e no senso comum.



Por exemplo, chamar o método GetComponent parece um pouco estranho se o valor retornado não for usado. Erro sem sentido? De modo nenhum. Obviamente, isso pode ser simplesmente um desafio extra, esquecido e abandonado por todos. Ou pode ser que alguma tarefa importante esteja faltando. As anotações podem ajudar o analisador a encontrar erros semelhantes e muitos outros.



Obviamente, já escrevemos muitas anotações para o analisador. Por exemplo, métodos de classes do espaço para nome System são anotados . Além disso, existe um mecanismo para anotar automaticamente alguns métodos. Você pode ler mais sobre isso aqui . Observo que este artigo fala mais sobre a parte do PVS-Studio, responsável pela análise de projetos em C ++. No entanto, não há diferença tangível em como as anotações funcionam para C # e C ++.



Escrevendo anotações para métodos Unity



Nós nos esforçamos para melhorar a qualidade da revisão de código de projetos usando o Unity e, portanto, foi decidido anotar os métodos desse mecanismo.



A idéia original era cobrir todos os métodos do Unity com anotações em geral, no entanto, havia muitos deles. Como resultado, decidimos começar anotando métodos das classes mais usadas.



Levantamento de informações



Primeiro, era necessário descobrir exatamente quais classes são usadas com mais frequência do que outras. Além disso, um aspecto importante é fornecer a capacidade de coletar resultados da anotação - novos erros que o analisador encontra em projetos reais devido a anotações escritas. Portanto, o primeiro passo foi encontrar projetos de código aberto relevantes. No entanto, não foi tão fácil fazer isso.



O problema é que muitos dos projetos encontrados foram bem pequenos em termos de código fonte. Nesse caso, se houver erros, não haverá muitos, e é ainda menos provável encontrar positivos relacionados especificamente aos métodos do Unity. Às vezes, havia projetos que praticamente não usavam (ou nem usavam) classes específicas do Unity, embora, de acordo com a descrição, estivessem de alguma forma conectados ao mecanismo. Tais descobertas foram completamente inadequadas para a tarefa.



Claro, às vezes com sorte. Por exemplo, a gema nesta coleção é o MixedRealityToolkit . O código já é decente, o que significa que as estatísticas coletadas sobre o uso dos métodos Unity em um projeto desse tipo serão mais completas.



Assim, foram recrutados 20 projetos utilizando as capacidades do mecanismo. Para encontrar as classes mais usadas, foi escrito um utilitário baseado em Roslyn que calcula as chamadas de método do Unity. A propósito, esse programa também pode ser chamado de analisador estático. Afinal, se você pensar bem, ela realmente analisa o código-fonte, sem recorrer ao lançamento do próprio projeto.



O "analisador" escrito possibilitou encontrar as classes, cuja frequência média de uso nos projetos encontrados é a mais alta:



  • UnityEngine.Vector3
  • UnityEngine.Mathf
  • UnityEngine.Debug
  • UnityEngine.GameObject
  • UnityEngine.Material
  • UnityEditor.EditorGUILayout
  • UnityEngine.Component
  • UnityEngine.Object
  • UnityEngine.GUILayout
  • UnityEngine.Quaternion
  • Etc.


Obviamente, isso não significa que essas classes sejam realmente usadas pelos desenvolvedores com tanta frequência - afinal, as estatísticas baseadas em uma amostra tão pequena de projetos não são muito confiáveis. No entanto, para começar, essas informações foram suficientes para garantir que os métodos das classes usadas em algum lugar sejam anotados.



Anotação



Depois de obter as informações necessárias, é hora de começar a anotação real. Os verdadeiros auxiliares nesse assunto foram a documentação e o editor do Unity, onde um projeto de teste foi criado. Isso foi necessário para verificar alguns pontos não especificados na documentação. Por exemplo, estava longe de estar sempre claro se passar nulo em qualquer argumento levaria a um erro ou se o programa continuaria sem problemas. É claro que passar nulo geralmente não é muito bom, mas nesse caso consideramos apenas os erros que interromperam o fluxo de execução ou foram registrados pelo editor do Unity como um erro.



Durante essas verificações, foram encontradas características interessantes do trabalho de alguns métodos. Por exemplo, executando o código



MeshRenderer renderer = cube.GetComponent<MeshRenderer>();
Material m = renderer.material;
List<int> outNames = null;
m.GetTexturePropertyNameIDs(outNames);


leva ao fato de o editor do Unity travar, embora geralmente nesses casos a execução do script atual seja interrompida e o erro correspondente seja registrado. Obviamente, é improvável que os desenvolvedores escrevam isso frequentemente, mas o fato de o editor do Unity poder ser interrompido pela execução de scripts comuns não é muito bom. O mesmo acontece em pelo menos um outro caso:



MeshRenderer renderer = cube.GetComponent<MeshRenderer>();
Material m = renderer.material;
string keyWord = null;
bool isEnabled = m.IsKeywordEnabled(keyWord);


Os problemas indicados são relevantes para o editor do Unity 2019.3.10f1.



Coletando resultados



Após a conclusão da anotação, você precisa verificar como isso afetará os avisos gerados. Antes de adicionar anotações para cada um dos projetos selecionados, é gerado um log com erros, que chamamos de referência. Em seguida, novas anotações são incorporadas ao analisador e os projetos são verificados novamente. As listas de aviso geradas, devido a anotações, diferem das listas de referência.



O procedimento de teste de anotação é realizado automaticamente usando o programa CSharpAnalyserTester especialmente criado para essas necessidades. Inicia a análise nos projetos, após os quais compara os logs resultantes com os de referência e gera arquivos contendo informações sobre as diferenças.



A abordagem descrita também é usada para descobrir quais alterações nos logs aparecem ao adicionar um diagnóstico novo ou alterar um existente.



Como observado anteriormente, era difícil encontrar grandes projetos de código aberto para o Unity. Isso é desagradável, porque o analisador poderia ter produzido gatilhos mais interessantes para eles. Ao mesmo tempo, haveria muito mais diferenças entre os logs de referência e os logs gerados após a anotação.



No entanto, as anotações escritas ajudaram a identificar vários pontos suspeitos nos projetos em consideração, o que também é um resultado agradável do trabalho.



Por exemplo, uma chamada GetComponent um tanto estranha foi encontrada :



void OnEnable()
{
  GameObject uiManager = GameObject.Find("UIRoot");

  if (uiManager)
  {
    uiManager.GetComponent<UIManager>();
  }
}


Aviso do analisador : V3010 O valor de retorno da função 'GetComponent' deve ser utilizado. - ADICIONAL NO UIEditorWindow.cs ATUAL 22



Com base na documentação , é lógico concluir que o valor retornado por esse método deve ser utilizado de alguma forma. Portanto, ao fazer anotações, foi marcado de acordo. Imediatamente, o resultado da chamada não é atribuído a nada, o que parece um pouco estranho.



E aqui está outro exemplo de gatilhos adicionais do analisador:



public void ChangeLocalID(int newID)
{
  if (this.LocalPlayer == null)                          // <=
  {
    this.DebugReturn(
      DebugLevel.WARNING, 
      string.Format(
        ...., 
        this.LocalPlayer, 
        this.CurrentRoom.Players == null,                // <=
        newID  
      )
    );
  }

  if (this.CurrentRoom == null)                          // <=
  {
    this.LocalPlayer.ChangeLocalID(newID);               // <=
    this.LocalPlayer.RoomReference = null;
  }
  else
  {
    // remove old actorId from actor list
    this.CurrentRoom.RemovePlayer(this.LocalPlayer);

    // change to new actor/player ID
    this.LocalPlayer.ChangeLocalID(newID);

    // update the room's list with the new reference
    this.CurrentRoom.StorePlayer(this.LocalPlayer);
  }
}


Avisos do analisador :



  • V3095 O objeto ' this.CurrentRoom ' foi usado antes de ser verificado em relação a nulo. Verifique as linhas: 1709, 1712. - ADICIONAL NO ATUAL LoadBalancingClient.cs 1709
  • V3125 O objeto 'this.LocalPlayer' foi usado após a verificação contra nulo. Verifique as linhas: 1715, 1707. - ADICIONAL NO ATUAL LoadBalancingClient.cs 1715


Observe que o PVS-Studio não presta atenção em passar o LocalPlayer para string.Format , pois isso não levará a um erro. E o código parece que foi escrito de propósito.



Nesse caso, o efeito das anotações não é tão óbvio. No entanto, foram eles que causaram a ocorrência desses positivos. Surge a pergunta - por que esses avisos não eram antes?



O fato é que várias chamadas são feitas no método DebugReturn , o que, em teoria, poderia afetar o valor da propriedade CurrentRoom :



public virtual void DebugReturn(DebugLevel level, string message)
{
  #if !SUPPORTED_UNITY
  Debug.WriteLine(message);
  #else
  if (level == DebugLevel.ERROR)
  {
    Debug.LogError(message);
  }
  else if (level == DebugLevel.WARNING)
  {
    Debug.LogWarning(message);
  }
  else if (level == DebugLevel.INFO)
  {
    Debug.Log(message);
  }
  else if (level == DebugLevel.ALL)
  {
    Debug.Log(message);
  }
  #endif
}


O analisador não conhece os recursos dos métodos chamados e, portanto, não se sabe como eles afetarão a situação. Portanto, o PVS-Studio assume que o valor this.CurrentRoom pode mudar durante a operação do método DebugReturn , portanto, é realizada uma verificação adicional.



As anotações forneceram informações de que os métodos chamados dentro de DebugReturn não afetarão os valores de outras variáveis. Portanto, é suspeito usar uma variável antes de verificar se ela é nula .



Conclusão



Em resumo, vale a pena dizer que a anotação de métodos específicos do Unity permitirá, sem dúvida, que você encontre mais erros diversos em projetos usando esse mecanismo. No entanto, a cobertura de anotações de todos os métodos disponíveis exigirá muito tempo. Será mais eficaz anotar em primeiro lugar os mais usados. No entanto, para entender quais classes específicas são usadas com mais frequência, você precisa de projetos adequados com uma grande base de códigos. Além disso, projetos grandes oferecem um controle muito melhor sobre o desempenho da anotação. Continuaremos a lidar com tudo isso no futuro próximo.



O analisador está sendo constantemente desenvolvido e refinado. A adição de anotações para os métodos do Unity é apenas um exemplo de expansão de seus recursos. Assim, com o tempo, a eficiência do PVS-Studio está aumentando. Portanto, se você ainda não experimentou o PVS-Studio, é hora de corrigi-lo baixando-o da página correspondente . Lá, você pode obter uma chave de avaliação para o analisador se familiarizar com seus recursos, verificando vários projetos.





Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Nikita Lipilin. Como o analisador PVS-Studio começou a encontrar ainda mais erros nos projetos do Unity .



All Articles