QML 静态分析 2 - 自定义过程

本章展示了如何通过扩展上一章中创建的插件,将自定义分析过程添加到 qmllint 中。为了演示目的,我们将创建一个插件,用于检查 Text 元素的文本属性是否分配了 "Hello world!"。

为了做到这一点,我们创建了一个新类,该类继承自 ElementPass

注意:插件可以注册两种类型的分析过程,ElementPassesPropertyPasses。在本教程中,我们仅考虑更简单的 ElementPass

class HelloWorldElementPass : public QQmlSA::ElementPass
{
public:
    HelloWorldElementPass(QQmlSA::PassManager *manager);
    bool shouldRun(const QQmlSA::Element &element) override;
    void run(const QQmlSA::Element &element) override;
private:
    QQmlSA::Element m_textType;
};

由于我们的 HelloWorldElementPass 应该分析 Text 元素,我们需要一个对 Text 类型的引用。我们可以使用 resolveType 函数来获取它。由于我们不想不断重新解析类型,我们会在构造函数中仅执行一次,并将类型存储在一个成员变量中。

HelloWorldElementPass::HelloWorldElementPass(QQmlSA::PassManager *manager)
    : QQmlSA::ElementPass(manager)
{
    m_textType = resolveType("QtQuick", "Text");
}

我们的分析过程的实际逻辑发生在两个函数中:shouldRunrun。它们将运行在 qmllint 分析的文件中的所有元素上。

在我们的 shouldRun 方法中,我们检查当前元素是否是 Text 的子类,并检查它是否在文本属性上有绑定。

bool HelloWorldElementPass::shouldRun(const QQmlSA::Element &element)
{
    if (!element.inherits(m_textType))
        return false;
    if (!element.hasOwnPropertyBindings(u"text"_s))
        return false;
    return true;
}

只有通过那里检查的元素才会通过我们的过程由其 run 方法进行分析。虽然也可以在 run 中本身完成所有检查,但通常更优选将关注点分开——从性能和代码可读性两个方面来看。

在我们的 run 函数中,我们检索对文本属性的绑定。如果绑定的值是字符串字面量,我们检查它是否是我们期望的问候。

void HelloWorldElementPass::run(const QQmlSA::Element &element)
{
    auto textBindings = element.ownPropertyBindings(u"text"_s);
    for (const auto &textBinding: textBindings) {
        if (textBinding.bindingType() != QQmlSA::BindingType::StringLiteral)
            continue;
        if (textBinding.stringValue() != u"Hello world!"_s)
            emitWarning("Incorrect greeting", helloWorld, textBinding.sourceLocation());
    }
}

注意:大多数情况下,属性只会分配一个绑定。但是,例如,属性可能会有一个字面量绑定和一个 Behavior 分配到同一个属性。

最后,我们需要创建我们过程的一个实例,并将其与 PassManager 注册。这是通过将以下内容添加到我们插件中的 registerPasses 函数来完成的。

    manager->registerElementPass(std::make_unique<HelloWorldElementPass>(manager));

registerPasses函数中。

我们可以通过在以下命令上对示例文件进行 qmllint 调用来测试我们的插件:

qmllint -P /path/to/the/directory/containing/the/plugin --Plugin.HelloWorld.hello-world info test.qml

如果test.qml看起来像

import QtQuick

Item {
     id: root

     property string greeting: "Hello"

     component MyText : Text {}

     component NotText : Item {
         property string text
     }

     Text { text: "Hello world!" }
     Text { text: root.greeting }
     Text { text: "Goodbye world!" }
     NotText {
         text: "Does not trigger"
          MyText { text: "Goodbye world!" }
     }
}

我们将得到以下输出:

Info: test.qml:22:26: Incorrect greeting [Plugin.HelloWorld.hello-world]
          MyText { text: "Goodbye world!" }
                         ^^^^^^^^^^^^^^^^
Info: test.qml:19:19: Incorrect greeting [Plugin.HelloWorld.hello-world]
     Text { text: "Goodbye world!" }

我们可以在此做一些观察:

  • 第一个 Text 包含预期的问候,因此没有警告
  • 第二个 文本 在运行时会有错误的警告("Hello" 而不是 "Hello world"。但是,这无法由 qmllint(一般情况下)检测到,因为没有进行字面绑定,而是对另一个属性的绑定。因为我们只检查字面绑定,所以我们简单地跳过这个绑定。
  • 对于第三个 文本 元素中的字面绑定,我们正确地警告了错误的问候。
  • 因为 非文本 不从 文本 继承,分析将跳过它,因为 继承 检查会丢弃它。
  • 自定义的 MyText 元素从 文本 继承,因此我们看到了预期的警告。

总之,我们看到了扩展 qmllint 的步骤,以及了解到静态检查的限制。

© 2024 Qt 公司有限。此处包含的文档贡献是各自所有者的版权。此处提供的文档是根据由自由软件基金会发布的 GNU 自由文档许可证 1.3 版本 的条款提供的。Qt 及其相关标志是芬兰以及/或世界其他地方 Qt 公司的商标。所有其他商标是各自所有者的财产。