Come l'analizzatore PVS-Studio ha iniziato a trovare ancora più errori nei progetti Unity

Come l'analizzatore PVS-Studio ha iniziato a trovare ancora più errori nei progetti Unity

Quando sviluppiamo l'analizzatore statico PVS-Studio, cerchiamo di svilupparlo in varie direzioni. Pertanto, il nostro team sta lavorando sui plug-in per l'IDE (Visual Studio, Rider), migliorando l'integrazione con CI e così via. Anche aumentare l'efficienza dell'analisi dei progetti nell'ambito di Unity è uno dei nostri obiettivi prioritari. Riteniamo che l'analisi statica consentirà ai programmatori che utilizzano questo motore di gioco di migliorare la qualità del codice sorgente e semplificare il lavoro su qualsiasi progetto. Pertanto, vorremmo aumentare la popolarità di PVS-Studio tra le aziende che sviluppano sotto Unity. Uno dei primi passi nell'implementazione di questa idea è stato scrivere annotazioni per i metodi definiti nel motore. Ciò consente a uno sviluppatore di controllare la correttezza del codice relativo alle chiamate di metodi annotati.

Introduzione

Le annotazioni sono uno dei meccanismi più importanti dell'analizzatore. Forniscono varie informazioni su argomenti, valori restituiti e funzionalità interne dei metodi che non possono essere trovati nella modalità automatica. Allo stesso tempo, lo sviluppatore che annota un metodo può assumere la sua struttura interna approssimativa e le caratteristiche del suo funzionamento, sulla base della documentazione e del buon senso.

Ad esempio, chiamando GetComponent il metodo sembra alquanto strano se il valore restituito non viene utilizzato. Un bug insignificante? In nessun modo. Certo, questa potrebbe essere semplicemente una chiamata ridondante, dimenticata e abbandonata da tutti. Oppure può darsi che qualche incarico importante sia stato omesso. Le annotazioni possono aiutare l'analizzatore a trovare errori simili e molti altri.

Naturalmente, abbiamo già scritto molte annotazioni per l'analizzatore. Ad esempio, metodi di classe dal Sistema namespace sono annotati. Inoltre, esiste un meccanismo per annotare automaticamente alcuni metodi. Puoi leggere qui in dettaglio a riguardo. Nota che questo articolo spiega di più sulla parte di PVS-Studio che è responsabile dell'analisi dei progetti in C++. Tuttavia, non vi è alcuna differenza evidente nel modo in cui le annotazioni funzionano per C# e C++.

Scrittura di annotazioni per metodi Unity

Ci sforziamo di migliorare la qualità del controllo del codice dei progetti che utilizzano Unity, motivo per cui abbiamo deciso di annotare i metodi di questo motore.

L'idea iniziale era quella di coprire tutti i metodi Unity con le annotazioni, tuttavia ce ne sono state molte. Di conseguenza, abbiamo deciso di iniziare annotando i metodi delle classi più comunemente utilizzate.

Raccolta di informazioni

Innanzitutto, abbiamo dovuto scoprire quali classi vengono utilizzate più spesso di altre. Inoltre, un aspetto importante era garantire la possibilità di raccogliere i risultati delle annotazioni, nuovi errori che l'analizzatore troverà nei progetti reali grazie alle annotazioni scritte. Pertanto, il primo passo è stato cercare progetti open source appropriati. Tuttavia, questo non è stato così facile da fare.

Il problema è che molti dei progetti trovati erano piuttosto piccoli in termini di codice sorgente. Se ci sono errori in tali progetti, il loro numero è piccolo. Per non parlare del fatto che è meno probabile trovare in essi alcuni avvertimenti relativi ai metodi di Unity. Occasionalmente, sono emersi alcuni progetti che quasi non hanno utilizzato (o non hanno utilizzato affatto) classi specifiche di Unity, sebbene siano state descritte come correlate al motore in un modo o nell'altro. Tali reperti erano completamente inadatti al compito da svolgere.

Certo, in alcuni casi sono stato fortunato. Ad esempio, la gemma in questa raccolta è MixedRealityToolkit. C'è già molto codice al suo interno, il che significa che le statistiche raccolte sull'uso dei metodi Unity in un progetto del genere saranno più complete.

Quindi, c'erano 20 progetti che utilizzavano le capacità del motore. Per trovare le classi utilizzate più di frequente, è stata scritta un'utilità basata su Roslyn che conta le chiamate di metodo da Unity. Questo programma, tra l'altro, può anche essere chiamato analizzatore statico. Dopotutto, se ci pensi, analizza davvero il codice sorgente, senza eseguire il progetto stesso.

L'"analizzatore" scritto ci ha permesso di trovare classi la cui frequenza media di utilizzo nei progetti trovati era la più alta:

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

Naturalmente, questo non significa che queste classi siano effettivamente utilizzate molto spesso dagli sviluppatori – dopotutto, le statistiche basate su un insieme così piccolo di progetti non sono particolarmente affidabili. Tuttavia, per cominciare, queste informazioni erano sufficienti per assicurarsi che le classi dei metodi annotati fossero utilizzate almeno da qualche parte.

Annotazione

Dopo aver ottenuto le informazioni necessarie, è il momento di eseguire l'annotazione vera e propria. La documentazione e l'editor Unity, dove è stato creato il progetto di test, sono stati in questo caso degli aiutanti affidabili. È stato necessario verificare alcuni punti non specificati nella documentazione. Ad esempio, non era sempre chiaro se passare null poiché qualsiasi argomento porterebbe a un errore o se il programma continuerebbe a funzionare senza problemi. Ovviamente, passando null di solito non è una buona pratica, ma in questo caso abbiamo considerato come errori solo gli errori che hanno interrotto il flusso di esecuzione o sono stati registrati dall'editor di Unity.

Durante questi controlli sono state rilevate caratteristiche interessanti di alcuni metodi. Ad esempio, eseguendo il codice

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

provoca l'arresto anomalo dell'editor Unity stesso, anche se di solito in questi casi l'esecuzione dello script corrente viene interrotta e viene registrato l'errore corrispondente. Ovviamente, è improbabile che gli sviluppatori scrivano spesso cose del genere, ma il fatto che l'editor Unity possa andare in crash eseguendo script regolari non è piacevole. La stessa cosa accade in almeno un altro caso:

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

Questi problemi sono rilevanti per l'editor di Unity 2019.3.10f1.

Raccolta dei risultati

Dopo che l'annotazione è stata completata, è necessario verificare in che modo ciò influirà sugli avvisi emessi. Prima di aggiungere annotazioni, viene generato un log degli errori per ciascuno dei progetti selezionati, che chiamiamo log di riferimento. Quindi le nuove annotazioni vengono incorporate nell'analizzatore e i progetti vengono nuovamente controllati. Gli elenchi di avvisi generati differiranno da quelli di riferimento a causa delle annotazioni.

La procedura di test delle annotazioni viene eseguita automaticamente utilizzando il programma CSharpAnalyserTester scritto appositamente per queste esigenze. Esegue analisi sui progetti, quindi confronta i log risultanti con quelli di riferimento e genera file contenenti informazioni sulle differenze.

L'approccio descritto viene utilizzato anche per scoprire quali modifiche vengono visualizzate nei registri quando viene aggiunta una nuova diagnostica o ne viene modificata una esistente.

Come notato in precedenza, è stato difficile trovare grandi progetti aperti nell'ambito di Unity. Questo è spiacevole, poiché l'analizzatore sarebbe in grado di produrre avvisi più interessanti per loro. Allo stesso tempo, ci sarebbero molte più differenze tra i log di riferimento e i log generati dopo l'annotazione.

Tuttavia, le annotazioni scritte hanno aiutato a identificare diversi punti sospetti nei progetti in esame, che è anche un risultato favorevole del lavoro.

Ad esempio, una chiamata un po' strana di GetComponent è stato trovato:

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

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

Avviso sull'analizzatore :V3010 È necessario utilizzare il valore di ritorno della funzione 'GetComponent'. - AGGIUNTIVO IN UIEditorWindow.cs 22

CORRENTE

Sulla base della documentazione, è logico concludere che il valore restituito da questo metodo debba essere utilizzato in qualche modo. Pertanto, è stato contrassegnato di conseguenza quando annotato. In questo caso, il risultato della chiamata non viene assegnato a nulla, il che sembra un po' strano.

Ecco un altro esempio di ulteriori avvisi dell'analizzatore:

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);
  }
}

Avvisi dell'analizzatore :

  • V3095 L'oggetto 'this.CurrentRoom' è stato utilizzato prima di essere verificato rispetto a null. Linee di controllo:1709, 1712. - AGGIUNTIVO IN CORRENTE LoadBalancingClient.cs 1709
  • V3125 L'oggetto 'this.LocalPlayer' è stato utilizzato dopo che è stato verificato rispetto a null. Righe di controllo:1715, 1707. - AGGIUNTIVO IN CORRENTE LoadBalancingClient.cs 1715

Nota che PVS-Studio non presta attenzione al passaggio di LocalPlayer in string.Format , poiché ciò non causerà un errore. E sembra che il codice sia stato scritto intenzionalmente.

In questo caso, l'impatto delle annotazioni non è così evidente. Tuttavia, sono la ragione di questi inneschi. Quindi ecco che arriva la domanda:perché non c'erano tali avvisi prima?

Il fatto è che il DebugReturn metodo effettua diverse chiamate, che in teoria potrebbero influenzare il valore della CurrentRoom proprietà:

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
}

L'analizzatore non sa come funzionano i metodi chiamati, quindi non sa come influenzeranno la situazione. Ad esempio, PVS-Studio presuppone che il valore di this.CurrentRoom potrebbe essere cambiato durante il DebugReturn metodo, quindi il controllo viene eseguito successivamente.

Le annotazioni fornivano anche le informazioni che i metodi chiamavano all'interno di DebugReturn non influirà sui valori di altre variabili. Pertanto, utilizzare una variabile prima di verificarla per null può essere considerato sospetto.

Conclusione

Per riassumere, annotare metodi specifici di Unity ti consentirà senza dubbio di trovare più errori nei progetti che utilizzano questo motore. Tuttavia, l'annotazione di tutti i metodi disponibili richiederà molto tempo. È più efficiente annotare prima quelli usati più di frequente. Tuttavia, per capire quali classi vengono utilizzate più spesso, sono necessari progetti adatti con una base di codice ampia. Inoltre, i progetti di grandi dimensioni consentono un controllo molto migliore sull'efficacia dell'annotazione. Continueremo a fare tutto questo nel prossimo futuro.

L'analizzatore viene costantemente sviluppato e perfezionato. L'aggiunta di annotazioni ai metodi Unity è solo un esempio di estensione delle sue capacità. Così, nel tempo, l'efficienza di PVS-Studio aumenta. Quindi, se non hai ancora provato PVS-Studio, è il momento di risolverlo scaricandolo dalla pagina corrispondente. Lì puoi anche ottenere una chiave di prova per l'analizzatore per familiarizzare con le sue capacità controllando vari progetti.