CodeQL 文档

优化查询以考虑边缘情况

你可以通过添加条件来改进 CodeQL 查询生成的结果,以消除由常见边缘情况引起的误报。

概述

本主题介绍了如何开发 C++ 查询。此示例介绍了递归谓词并演示了用于优化查询的典型工作流程。有关学习为 C/C++ 代码编写查询的可用主题的完整概述,请参阅“用于 C 和 C++ 的 CodeQL”。

查找每个私有字段并检查初始化

编写一个查询来检查构造函数是否初始化了所有私有字段看起来像是一个简单的问题,但有几个边缘情况需要考虑。

基本查询

我们可以从查看类中的每个私有字段并检查该类中的每个构造函数是否初始化它们开始。一旦你熟悉了 C++ 库,这并不难做到。

import cpp

from Constructor c, Field f
where f.getDeclaringType() = c.getDeclaringType() and f.isPrivate()
    and not exists(Assignment a | a = f.getAnAssignment() and a.getEnclosingFunction() = c)
select c, "Constructor does not initialize fields $@.", f, f.getName()
  1. f.getDeclaringType() = c.getDeclaringType() 断言字段和构造函数都是同一个类的成员。
  2. f.isPrivate() 检查字段是否为私有。
  3. not exists(Assignment a | a = f.getAnAssignment() and a.getEnclosingFunction() = c) 检查构造函数中是否存在对该字段的赋值。

这段代码看起来相当完整,但当你在一个项目上测试它时,会有一些包含我们遗漏的示例的结果。

优化 1 - 排除由列表初始化的字段

你可能会发现,结果中包含通过构造函数初始化列表而不是通过赋值语句初始化的字段。例如,以下类

class BoxedInt {
public:
  BoxedInt(int value) : m_value(value) {}

private:
  int m_value;
};

这些可以通过添加一个额外的条件来排除,以检查这种特殊的构造函数专用赋值形式。

import cpp

from Constructor c, Field f
where f.getDeclaringType() = c.getDeclaringType() and f.isPrivate()
    and not exists(Assignment a | a = f.getAnAssignment() and a.getEnclosingFunction() = c)
    // check for constructor initialization lists as well
    and not exists(ConstructorFieldInit i | i.getTarget() = f and i.getEnclosingFunction() = c)
select c, "Constructor does not initialize fields $@.", f, f.getName()

优化 2 - 排除由外部库初始化的字段

当你测试修改后的查询时,你可能会发现,来自外部库中的类的字段被过度报告。这通常是因为头文件声明了一个在未分析的源文件中定义的构造函数(外部库通常从分析中排除)。当分析源代码时,CodeQL 数据库将填充一个 Constructor 条目,该条目没有主体。因此,这个 constructor 不包含任何赋值,因此查询报告说,由构造函数初始化的任何字段都是“未初始化的”。没有特别的理由怀疑这些情况,我们可以通过定义一个条件来排除这些情况,以排除没有主体的构造函数

import cpp

from Constructor c, Field f
where f.getDeclaringType() = c.getDeclaringType() and f.isPrivate()
    and not exists(Assignment a | a = f.getAnAssignment() and a.getEnclosingFunction() = c)
    // check for constructor initialization lists as well
    and not exists(ConstructorFieldInit i | i.getTarget() = f and i.getEnclosingFunction() = c)
    // ignore cases where the constructor source code is not available
    and exists(c.getBlock())
select c, "Constructor does not initialize fields $@.", f, f.getName()

这是一个相当精确的查询 - 它报告的大部分结果都很有趣。但是,你可以进行进一步的优化。

优化 3 - 排除间接初始化的字段

你可能还想考虑由构造函数调用的方法,这些方法会对字段赋值,甚至考虑由这些方法调用的方法。作为一个具体示例,考虑以下类。

class BoxedInt {
public:
  BoxedInt(int value) {
    setValue(value);
  }

  void setValue(int value) {
    m_value = value;
  }

private:
  int m_value;
};

可以通过创建一个递归谓词来排除这种情况。递归谓词给定一个函数和一个字段,然后检查该函数是否对该字段进行赋值。该谓词在它被赋予的所有函数调用的函数上运行自身。通过将构造函数传递给该谓词,我们可以检查构造函数调用的所有函数中是否对字段进行了赋值,然后对所有函数调用的函数执行相同的操作,一直向下遍历函数调用树。有关更多信息,请参阅 QL 语言参考中的“递归”。

import cpp

predicate getSubAssignment(Function c, Field f){
    exists(Assignment a | a = f.getAnAssignment() and a.getEnclosingFunction() = c)
    or exists(Function fun | c.calls(fun) and getSubAssignment(fun, f))
}
from Constructor c, Field f
where f.getDeclaringType() = c.getDeclaringType() and f.isPrivate()
    // check for constructor initialization lists as well
    and not exists(ConstructorFieldInit i | i.getTarget() = f and i.getEnclosingFunction() = c)
    // check for initializations performed indirectly by methods called
    // as a result of the constructor being called
    and not getSubAssignment(c, f)
    // ignore cases where the constructor source code is not available
    and exists(c.getBlock())
select c, "Constructor does not initialize fields $@.", f, f.getName()

优化 4 - 简化查询

最后,我们可以使用传递闭包运算符来简化查询。在这个查询的最终版本中,c.calls*(fun) 解析为一组所有函数,这些函数分别是 c 本身,被 c 调用,被 c 调用函数调用,等等。这消除了完全创建新谓词的必要性。有关更多信息,请参阅 QL 语言参考中的“传递闭包”。

import cpp

from Constructor c, Field f
where f.getDeclaringType() = c.getDeclaringType() and f.isPrivate()
    // check for constructor initialization lists as well
    and not exists(ConstructorFieldInit i | i.getTarget() = f and i.getEnclosingFunction() = c)
    // check for initializations performed indirectly by methods called
    // as a result of the constructor being called
    and not exists(Function fun, Assignment a |
      c.calls*(fun) and a = f.getAnAssignment() and a.getEnclosingFunction() = fun)
    // ignore cases where the constructor source code is not available
    and exists(c.getBlock())
select c, "Constructor does not initialize fields $@.", f, f.getName()
  • ©GitHub, Inc.
  • 条款
  • 隐私