如今,每个人都知道云服务。 许多公司已经打入这个市场领域,并创建了自己的各种用途的云服务。 最近,我们的团队也对将PVS-Studio代码分析器集成到其中的这些服务感兴趣。 通常,我们的普通读者已经猜到了这次我们将检查哪种类型的项目。 选择取决于华为云服务的代码。
引言
如果您关注PVS-Studio团队的帖子,您可能已经注意到我们最近一直在深入研究云技术。 我们已经发表了几篇有关该主题的文章:
就在我为即将到来的文章寻找不寻常的项目时,我收到了一封包含
华为工作机会的电子邮件。 在收集了有关该公司的一些信息之后,事实证明他们拥有自己的云服务,但主要的是这些服务的源代码可在GitHub上获得。 这是本文选择该公司的主要原因。 正如一位中国圣人所说:“事故并非偶然。”
让我给您一些有关我们分析仪的细节。 PVS-Studio是一种静态分析器,用于用C,C ++,C#和Java编写的程序源代码中的错误检测。 该分析仪可在Windows,Linux和macOS上运行。 除了用于经典开发环境(例如Visual Studio或IntelliJ IDEA)的插件之外,该分析仪还可以集成到SonarQube和Jenkins中:
项目分析
在对本文进行一些研究时,我发现华为拥有一个
开发人员中心,其中提供了可用的信息,手册和云服务的来源。 各种各样的编程语言被用来创建这些服务,但是诸如Go,Java和Python之类的语言最为流行。
由于我专门研究Java,因此选择了符合我的知识和技能的项目。 您可以在GitHub存储库
huaweicloud中的文章中分析项目源。
要分析项目,我只需要做几件事:
- 从资源库中获取项目;
- 使用Java启动分析器说明,并在每个项目上运行分析。
在分析了项目之后,我们仅选择了三个我们要注意的项目。 这是因为事实证明,其余Java项目的大小过小。
项目分析结果(警告数量和文件数量):
很少有警告可以告诉我们有关代码质量的信息,更重要的是,因为并非所有警告都指向实际错误。 这是由于这样的事实,即分析器有时缺乏将正确代码与错误代码区分开的信息。 因此,我们会根据用户的信息,每天对分析仪的诊断进行调整。 欢迎您阅读以下文章:“
静态分析器与误报作斗争的方式,以及为什么要这样做 。”
在分析项目时,我只选择了最热门的警告,本文将对此进行讨论。
字段初始化顺序
V6050存在类初始化周期。 在初始化“ LOG”之前出现“ INSTANCE”的初始化。 UntrustedSSL.java(32),UntrustedSSL.java(59),UntrustedSSL.java(33)
public class UntrustedSSL { private static final UntrustedSSL INSTANCE = new UntrustedSSL(); private static final Logger LOG = LoggerFactory.getLogger(UntrustedSSL.class); .... private UntrustedSSL() { try { .... } catch (Throwable t) { LOG.error(t.getMessage(), t);
如果
UntrustedSSL类构造函数中有任何异常,则使用
LOG记录器将有关此异常的信息记录在
catch块中。 但是,由于静态字段的初始化顺序,在初始化
INSTANCE字段时,尚未初始化
LOG 。 因此,如果您在构造函数中记录有关异常的信息,则将导致
NullPointerException 。 此异常是另一个异常
ExceptionInInitializerError的原因,如果在初始化静态字段时发生
异常 ,则抛出该异常。 您需要解决的问题是将
LOG初始化放在
INSTANCE初始化之前
。不起眼的错字
V6005变量'this.metricSchema'被分配给它自己。 OpenTSDBSchema.java(72)
public class OpenTSDBSchema { @JsonProperty("metric") private List<SchemaField> metricSchema; .... public void setMetricsSchema(List<SchemaField> metricsSchema) { this.metricSchema = metricSchema;
两种方法都设置
metricSchema字段,但是方法的名称之间用一个's'符号表示区别。 程序员根据方法的名称来命名这些方法的参数。 结果,在分析器指向的行中,向其自身分配了
metricSchema字段,并且未使用
metricsSchema方法的参数。
V6005变量“挂起”已分配给自身。 SuspendTransferTaskRequest.java(77)
public class SuspendTransferTaskRequest { .... private boolean suspend; .... public void setSuspend(boolean suspend) { suspend = suspend; } .... }
这是一个与粗心有关的琐碎错误,因此
会将suspend参数分配给它自己。 结果,将不会像隐式那样为
suspend字段分配获得的参数的值。 正确的版本:
public void setSuspend(boolean suspend) { this.suspend = suspend; }
条件预先确定
经常发生的是,
V6007规则在警告数量方面
居于领先地位。
V6007表达式“ firewallPolicyId == null”始终为false。 FirewallPolicyServiceImpl.java(125)
public FirewallPolicy removeFirewallRuleFromPolicy(String firewallPolicyId, String firewallRuleId) { checkNotNull(firewallPolicyId); checkNotNull(firewallRuleId); checkState(!(firewallPolicyId == null && firewallRuleId == null), "Either a Firewall Policy or Firewall Rule identifier must be set"); .... }
在此方法中,用
checkNotNull方法检查参数是否为
空 :
@CanIgnoreReturnValue public static <T> T checkNotNull(T reference) { if (reference == null) { throw new NullPointerException(); } else { return reference; } }
通过
checkNotNull方法检查参数后,可以100%确保传递给此方法的参数不等于
null 。 由于
removeFirewallRuleFromPolicy方法的两个参数均由
checkNotNull方法检查,因此进一步检查是否为
null毫无意义。 但是,将重新检查
firewallPolicyId和
firewallRuleId参数为
null的表达式作为第一个参数传递给
checkState方法。
也为
firewallRuleId发出类似的警告:
- V6007表达式“ firewallRuleId == null”始终为false。 FirewallPolicyServiceImpl.java(125)
V6007表达式'filteringParams!= Null'始终为true。 NetworkPolicyServiceImpl.java(60)
private Invocation<NetworkServicePolicies> buildInvocation(Map<String, String> filteringParams) { .... if (filteringParams == null) { return servicePoliciesInvocation; } if (filteringParams != null) {
在此方法中,如果
filteringParams参数为
null ,则该方法返回一个值。 这就是分析仪指向的检查始终为真的原因,这反过来意味着该检查没有意义。
还有13个类似的类:
- V6007表达式'filteringParams!= Null'始终为true。 PolicyRuleServiceImpl.java(58)
- V6007表达式'filteringParams!= Null'始终为true。 GroupServiceImpl.java(58)
- V6007表达式'filteringParams!= Null'始终为true。 ExternalSegmentServiceImpl.java(57)
- V6007表达式'filteringParams!= Null'始终为true。 L3policyServiceImpl.java(57)
- V6007表达式'filteringParams!= Null'始终为true。 PolicyRuleSetServiceImpl.java(58)
- 等等...
空参考
V6008可能会取消引用'm.blockDeviceMapping'。 NovaServerCreate.java(390)
@Override public ServerCreateBuilder blockDevice(BlockDeviceMappingCreate blockDevice) { if (blockDevice != null && m.blockDeviceMapping == null) { m.blockDeviceMapping = Lists.newArrayList(); } m.blockDeviceMapping.add(blockDevice);
在此方法中,如果
blockDevice参数为
null,则不会初始化
m.blockDeviceMapping参考字段。 仅在此方法中初始化此字段,因此从
m.blockDeviceMapping字段调用
add方法时,将发生
NullPointerException 。
V6008函数'<init>'中'FileId.get(path)'的潜在空引用。 TrackedFile.java(140),TrackedFile.java(115)
public TrackedFile(FileFlow<?> flow, Path path) throws IOException { this(flow, path, FileId.get(path), ....); }
TrackedFile类的构造函数接收静态
FileId.get(路径)方法的结果作为第三个参数。 但是此方法可以返回
null :
public static FileId get(Path file) throws IOException { if (!Files.exists(file)) { return null; } .... }
在通过
this调用的构造函数中,
id参数直到第一次使用才改变:
public TrackedFile(...., ...., FileId id, ....) throws IOException { .... FileId newId = FileId.get(path); if (!id.equals(newId)) { .... } }
如我们所见,如果将
null作为第三个参数传递给该方法,则会发生异常。
这是另一种类似的情况:
- V6008可能会取消引用“缓冲区”。 PublishingQueue.java(518)
V6008可能会取消引用“ dataTmpFile”。 CacheManager.java(91)
@Override public void putToCache(PutRecordsRequest putRecordsRequest) { .... if (dataTmpFile == null || !dataTmpFile.exists()) { try { dataTmpFile.createNewFile();
再次NPE。 条件运算符中的许多检查允许零对象
dataTmpFile进一步取消引用。 我认为这里有两种错别字,支票实际上应该是这样的:
if (dataTmpFile != null && !dataTmpFile.exists())
子字符串和负数
V6009 “子字符串”功能可以接收“ -1”值,而预期为非负值。 检查参数:2. RemoveVersionProjectIdFromURL.java(37)
@Override public String apply(String url) { String urlRmovePojectId = url.substring(0, url.lastIndexOf("/")); return urlRmovePojectId.substring(0, urlRmovePojectId.lastIndexOf("/")); }
含义是此方法将URL作为字符串获取,而未经任何方式的验证。 稍后,使用
lastIndexOf方法将该字符串截断几次。 如果方法
lastIndexOf在字符串中找不到匹配项,它将返回-1。 这将导致
StringIndexOutOfBoundsException ,因为
substring方法的参数必须为非负数。 为了使方法正确运行,必须添加输入参数验证或检查
lastIndexOf方法的结果是否为非负数。
以下是一些其他类似的片段:
- V6009“子字符串”功能可以接收“ -1”值,而预期为非负值。 检查参数:2. RemoveProjectIdFromURL.java(37)
- V6009“子字符串”功能可以接收“ -1”值,而预期为非负值。 检查参数:2. RemoveVersionProjectIdFromURL.java(38)
被遗忘的结果
V6010需要使用功能“ concat”的返回值。 AKSK.java(278)
public static String buildCanonicalHost(URL url) { String host = url.getHost(); int port = url.getPort(); if (port > -1) { host.concat(":" + Integer.toString(port)); } return host; }
在编写此代码时,其作者没有考虑到
concat方法的调用不会由于
String类型对象的不变性而更改
主机字符串。 为了使方法正确运行,必须将
concat方法的结果分配给
if块中的
主机变量。 正确的版本:
if (port > -1) { host = host.concat(":" + Integer.toString(port)); }
未使用的变量
V6021不使用变量“ url”。 TriggerV2Service.java(95)
public ActionResponse deleteAllTriggersForFunction(String functionUrn) { checkArgument(!Strings.isNullOrEmpty(functionUrn), ....); String url = ClientConstants.FGS_TRIGGERS_V2 + ClientConstants.URI_SEP + functionUrn; return deleteWithResponse(uri(triggersUrlFmt, functionUrn)).execute(); }
在此方法中,初始化后不使用
url变量。 很有可能,必须将
url变量作为第二个参数而不是
functionUrn传递给
uri方法,因为
functionUrn变量会参与
url变量的初始化。
参数未使用构造函数
V6022在构造函数主体内部未使用参数'returnType'。 HttpRequest.java(68)
public class HttpReQuest<R> { .... Class<R> returnType; .... public HttpRequest(...., Class<R> returnType)
在此构造函数中,程序员忘记使用
returnType参数,并将其值分配给
returnType字段。 这就是为什么当
从此构造方法创建的对象中调用
getReturnType方法时,默认情况下将返回
null的原因 。 但最有可能的是,程序员打算获取该对象,该对象先前已传递给构造函数。
相同的功能
V6032方法“启用”的主体完全等同于另一种方法“禁用”的主体,这很奇怪。 ServiceAction.java(32),ServiceAction.java(36)
public class ServiceAction implements ModelEntity { private String binary; private String host; private ServiceAction(String binary, String host) { this.binary = binary; this.host = host; } public static ServiceAction enable(String binary, String host) {
拥有两个相同的方法不是一个错误,但是两个方法执行相同操作的事实至少很奇怪。 查看上述方法的名称,我们可以假定它们应该执行相反的操作。 实际上,这两种方法都做同样的事情-创建并返回
ServiceAction对象。 最有可能的是,
disable方法是通过复制
enable方法的代码创建的,但是方法的主体保持不变。
忘了检查主要的东西
V6060在对null进行验证之前,已使用“ params”参考。 DomainService.java(49),DomainService.java(46)
public Domains list(Map<String, String> params) { Preconditions.checkNotNull(params.get("page_size"), ....); Preconditions.checkNotNull(params.get("page_number"), ....); Invocation<Domains> domainInvocation = get(Domains.class, uri("/domains")); if (params != null) {
在这种方法中,作者决定检查
Map类型的结构的内容是否为
null 。 为此,从
params参数调用
get方法两次。
get方法的结果传递给
checkNotNull方法。 一切似乎合乎逻辑,但事实并非如此! 在
if中检查
params参数是否为
null 。 此后,预期输入参数可能为
null ,但在此检查之前,已经从
params中调用了
get方法两次
。 如果将
null作为参数传递给此方法,则首次调用
get方法时,将引发异常。
在其他三个地方也发生类似的情况:
- V6060在对null进行验证之前,已使用“ params”参考。 DomainService.java(389),DomainService.java(387)
- V6060在对null进行验证之前,已使用“ params”参考。 DomainService.java(372),DomainService.java(369)
- V6060在对null进行验证之前,已使用“ params”参考。 DomainService.java(353),DomainService.java(350)
结论
当今的大公司离不开使用云服务。 大量的人使用这些服务。 按照这种观点,即使是很小的服务错误也可能导致许多人面临问题,并导致公司蒙受更多损失,以弥补该错误的不利后果。 始终应考虑人为缺陷,尤其是因为本文所述每个人迟早都会犯错。 这个事实证实了所有可能工具的使用,以提高代码质量。
PVS-Studio一定会通知华为公司有关其云服务的检查结果,以便华为开发人员可以继续使用它们,因为本文(
1,2 )所介绍的一次性使用静态代码分析无法完全演示所有的优点。 您可以在
此处下载PVS-Studio分析仪。