开发人员在编写代码时有无数的方法来犯错误。但是,有时我们可以在开发人员犯错方面找到明显而有趣的模式。让我们谈谈该代码作为错别字的“磁铁”。
作者:Andrey Karpov
研究背景
为了测试和推广PVS-Studio静态代码分析仪,我们检查了各种开源项目。如果发现错误,我们将向项目的作者报告有关它们的信息。我们collect errors并写下articles about最有趣的案例。
查看所有这些错误时,我注意到错别字的各种重复模式。除少数例外,它们独立于编程语言。至少,它们通常是用C,C ++,C#和Java编写的代码。在本文中,我将概述我现在注意到的7种模式:
- 最后一行效果
- 最危险的memset函数
- 错误的比较功能
- 错误的复制功能
- 与数据和时间有关的错误
- 不幸的数字:0、1、2
- 逐个错误
这些误差模式很明显的事实表明它们很普遍。知道如何避免潜在危险的编码或如何在代码审查过程中更有效地查找错误是有用的。换句话说,您将了解哪些代码会吸引错误以及如何更仔细地查看代码。 PVS-Studio确实可以检测到很多错误,但无法检测到它们。因此,对它们的更多关注并不是不合适的。
最后一行效应
这是模式的描述:编写同一类型的代码块时,您可以在最后一个块中犯错。
让我以Godot Engine Project(C ++)的代码示例进行解释。
String SoftBody::get_configuration_warning() const {
....
Transform t = get_transform();
if ((ABS(t.basis.get_axis(0).length() - 1.0) > 0.05 ||
ABS(t.basis.get_axis(1).length() - 1.0) > 0.05 ||
ABS(t.basis.get_axis(0).length() - 1.0) > 0.05)) {
if (!warning.empty())
....
}
这是一个经典的复制纸可能错误。开发人员不想重新输入类似的代码行。这就是为什么他们只是加倍代码行:
ABS(t.basis.get_axis(0).length() - 1.0) > 0.05
之后,他们在第二行中将0替换为1,但他们忘记了最后一行中替换0替换为2。当我们单独查看此类代码片段时,发现如此简单的错误似乎令人惊讶。但这正是这种模式的样子。
这是LLVM(C ++)项目的单元测试中的另一个示例。
TEST(RegisterContextMinidump, ConvertMinidumpContext_x86_64) {
MinidumpContext_x86_64 Context;
....
Context.rax = 0x0001020304050607;
Context.rbx = 0x08090a0b0c0d0e0f;
....
Context.eflags = 0x88898a8b;
Context.cs = 0x8c8d;
Context.fs = 0x8e8f;
Context.gs = 0x9091;
Context.ss = 0x9293; // <=
Context.ds = 0x9495;
Context.ss = 0x9697; // <=
llvm::ArrayRef<uint8_t> ContextRef(reinterpret_cast<uint8_t *>(&Context),
sizeof(Context));
....
}
让我们看一下用代码注释突出显示的代码行。此代码几乎是使用复制纸编写的,但是错误仍在最后一行中。当反复将值写入SS寄存器而不是ES寄存器时,开发人员匆匆忙忙地做了错字。
注意。另一个有趣的事情是,该错误显示了静态分析器如何补充单元测试。单位测试还可能包含导致某些未经测试或未完全测试的方案的错误。但是,为单位测试编写单元测试是不切实际的。代码分析仪将在这里为您提供帮助。
为了避免这样的印象:此类错误仅在C和C ++语言中很常见,我将向您展示Java代码的示例。我在Intellij Idea Community Edition项目(Java)中找到了以下错别字。
private static boolean isBeforeOrAfterKeyword(String str, boolean trimKeyword) {
return (trimKeyword ? LoadingOrder.BEFORE_STR.trim() :
LoadingOrder.BEFORE_STR).equalsIgnoreCase(str) ||
(trimKeyword ? LoadingOrder.AFTER_STR.trim() :
LoadingOrder.AFTER_STR).equalsIgnoreCase(str) ||
LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase(str) || // <=
LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase(str); // <=
}
在这里,我们注意到一个明显的模式。您可以在旧文章"The last line effect"中看到其他类似的示例。
为什么我以这样的方式命名这种模式?来自登山界的“最后仪表效应” - 这是我的灵感来源。我读过的观察是,登山者经常在攀登结束时犯错。原因不是因为他们很累 - 他们的集中程度较低。他们错误地认为自己已经到达了山顶,很快就可以休息,并享受旅途的尽头。登山者失去集中度,急于完成攀登并做错了什么。
编写代码时,开发人员可以体验同一件事。编写相同类型的代码块很无聊。当我们谈论比较功能时,我们将在下面证明这一点。因此,开发人员急于完成一份单调的工作。在编写最后的代码行时,他们很高兴自己完成了常规,并可能开始考虑下一步写什么。
无论如何,集中度下降,开发人员犯了错误。好吧,他们可以随机犯错,但是观察表明,在同一类型的最后一个代码块中更有可能发生。
代码审查也会发生相同的故事。回顾相同类型的代码很无聊。这就是为什么浓度迅速下降的原因,开发人员不小心检查最后的代码行或根本不检查它们。这是代码中发生错误的方式,并且可能会忽略。当您发现这些错误时,它们的简单性会令人惊讶。
提示:查看代码时,您应该更加注意最后的代码行。
继续阅读单击here