在开发PVS-Studio静态分析仪时,我们尝试从各个方向进行开发。因此,我们的团队正在开发IDE(Visual Studio,Rider)的插件,改善与CI的集成等。提高Unity项目分析的效率也是我们的首要目标之一。我们认为,静态分析将使使用此游戏引擎的程序员能够提高其源代码的质量并简化任何项目的工作。因此,我想增加PVS-Studio在为Unity开发的公司中的知名度。实现此想法的第一步之一是为引擎中定义的方法编写注释。这使您可以控制与带注释的方法的调用关联的代码的正确性。
介绍
注释是最重要的分析器机制之一。它们提供了有关参数,返回值和无法自动找出的方法内部功能的各种信息。同时,带注释的程序员可以根据文档和常识来假设该方法的大概内部结构及其操作的细节。
例如,如果不使用它返回的值,则调用GetComponent方法看起来有些奇怪。废话吗?一点也不。当然,这可能只是一个额外的挑战,每个人都将其遗忘和抛弃。或者可能是缺少一些重要的任务。注释可以帮助分析仪找到类似的错误以及许多其他错误。
当然,我们已经为分析器编写了很多注释。例如,带注释的System名称空间中的类方法。此外,还有一种用于自动注释某些方法的机制。您可以在此处了解更多信息。请注意,本文将详细介绍PVS-Studio的一部分,该部分负责分析C ++中的项目。但是,注释在C#和C ++的工作方式上没有明显的区别。
为Unity方法编写注释
我们努力提高使用Unity的项目代码审查的质量,因此决定注释此引擎的方法。
最初的想法是用注释将所有Unity方法普遍使用,但是其中有很多方法。因此,我们决定从最常用的类开始注释方法。
信息收集
首先,有必要准确找出哪些类比其他类更经常使用。此外,一个重要方面是提供收集注释结果的能力-分析人员在实际项目中由于书面注释而发现的新错误。因此,第一步是找到相关的开源项目。但是,事实并非如此简单。
问题在于,发现的许多项目在源代码方面都很小。如果这些错误中有错误,那么错误就不会太多了,从那里找到与Unity方法相关的任何触发器的机会就更少了。有时有些项目实际上没有使用(或根本没有使用过)Unity特定的类,尽管根据描述它们与引擎有某种联系。这样的发现完全不适合手头的任务。
当然,有时候我很幸运。例如,此集合中的宝石是MixedRealityToolkit。其中已经包含了不错的代码,这意味着所收集的有关此类项目中使用Unity方法的统计信息将更加完整。
因此,招募了20个使用引擎功能的项目。为了找到最常用的类,编写了一个基于Roslyn的实用程序来对来自Unity的方法调用进行计数。顺便说一句,这样的程序也可以称为静态分析器。毕竟,如果您考虑一下,她真的会分析源代码,而无需诉诸于启动项目本身。
书面的“分析器”使查找所发现项目中平均使用频率最高的类成为可能:
- UnityEngine.Vector3
- UnityEngine.Mathf
- UnityEngine.Debug
- UnityEngine.GameObject
- UnityEngine.Material
- UnityEditor.EditorGUILayout
- UnityEngine.Component
- UnityEngine.Object
- UnityEngine.GUILayout
- UnityEngine.Quaternion
- 等等。
当然,这完全不意味着开发人员会经常使用这些类-毕竟,基于如此少量项目的统计数据并不是特别可靠。但是,开始此信息足以确保对至少在某个地方使用的类的方法进行注释。
注解
收到必要的信息后,该自己做注释了。创建测试项目的Unity 文档和编辑器是此事的忠实助手。这对于检查文档中未指定的某些点很有必要。例如,始终不清楚是否在任何参数中传递null都会导致错误,或者程序是否可以继续运行而不会出现问题。当然,通常传递null并不是完全好的,但是在这种情况下,我们仅考虑中断执行线程的错误,或者由Unity编辑器将其记录为错误。
在进行此类检查时,发现了某些方法的有趣特征。例如,运行代码
MeshRenderer renderer = cube.GetComponent<MeshRenderer>();
Material m = renderer.material;
List<int> outNames = null;
m.GetTexturePropertyNameIDs(outNames);
导致Unity编辑器本身崩溃的事实,尽管通常在这种情况下,当前脚本的执行被中断并记录了相应的错误。当然,开发人员不太可能经常编写此代码,但是通过运行普通脚本会使Unity编辑器崩溃的事实并不是很好。至少在另外一种情况下,也会发生相同的情况:
MeshRenderer renderer = cube.GetComponent<MeshRenderer>();
Material m = renderer.material;
string keyWord = null;
bool isEnabled = m.IsKeywordEnabled(keyWord);
这些问题与Unity Editor 2019.3.10f1有关。
收集结果
注释完成后,您需要检查这将如何影响发出的警告。在为每个选定项目添加注释之前,会生成带有错误的日志,我们将其称为参考。然后,新的注释将内置到分析器中,并重新检查项目。借助注释,生成的警告列表将不同于参考列表。
使用专门针对这些需求编写的CSharpAnalyserTester程序自动执行注释测试过程。它启动对项目的分析,然后将结果日志与参考日志进行比较,并生成包含差异信息的文件。
所描述的方法还用于找出在添加新诊断或更改现有诊断时日志中出现的更改。
如前所述,很难找到Unity的大型开源项目。这是令人不愉快的,因为分析仪可能为他们产生了更多有趣的触发器。同时,参考日志和注释后形成的日志之间会有更多差异。
但是,书面注释有助于确定正在考虑的项目中的几个可疑时刻,这也是工作的令人愉快的结果。
例如,发现对GetComponent的调用有些奇怪:
void OnEnable()
{
GameObject uiManager = GameObject.Find("UIRoot");
if (uiManager)
{
uiManager.GetComponent<UIManager>();
}
}
分析器警告:V3010需要使用函数'GetComponent'的返回值。-当前的附加UIEditorWindow.cs 22
根据文档,可以得出结论,必须以某种方式使用此方法返回的值,这是合乎逻辑的。因此,在进行注释时,会对其进行标记。呼叫结果没有立即分配给任何东西,这看起来有些奇怪。
这是其他分析器触发器的另一个示例:
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);
}
}
分析仪警告:
- V3095在验证是否为null之前,已使用'this.CurrentRoom'对象。检查行:1709、1712。-当前额外的LoadBalancingClient.cs 1709
- V3125在验证了null之后使用了“ this.LocalPlayer”对象。检查行:1715、1707。-当前额外的LoadBalancingClient.cs 1715
请注意,PVS-Studio不注意将LocalPlayer传递给string.Format,因为这不会导致错误。代码看起来像是故意编写的。
在这种情况下,注释的效果不是很明显。但是,正是这些因素导致了这些积极因素的出现。出现了问题-为什么以前没有这些警告?
事实是,在DebugReturn方法中进行了多次调用,从理论上讲,这可能会影响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
}
分析器不知道所调用方法的特殊性,这意味着它不知道它们将如何影响情况。因此,PVS-Studio假定this.CurrentRoom的值在DebugReturn方法的操作过程中可能会更改,因此,将执行进一步的验证。
注释提供的信息表明,在DebugReturn内部调用的方法不会影响其他变量的值。因此,怀疑在使用变量之前先检查其是否为空。
结论
总结一下,值得一提的是,注释特定于Unity的方法无疑将使您能够使用此引擎在项目中发现更多错误。但是,所有可用方法的注释覆盖都将花费很多时间。首先注释最常用的注释会更有效。但是,要了解更经常使用哪些类,则需要具有大型代码库的合适项目。此外,大型项目可以更好地控制注释的有效性。我们将在不久的将来继续做所有这一切。
分析仪正在不断开发和完善。为Unity方法添加注释只是扩展其功能的一个示例。因此,随着时间的流逝,PVS-Studio的效率不断提高。因此,如果您还没有尝试过PVS-Studio,那么现在可以通过从相应的页面下载进行修复。在那里,您可以获得试用版的试用版钥匙,可以通过检查各种项目来熟悉分析仪的功能。
如果您想与说英语的读者分享这篇文章,请使用翻译链接:Nikita Lipilin。PVS-Studio分析仪如何开始在Unity项目中发现更多错误。