这个标题通俗易懂,并且符合中文的口语表达习惯。它简洁地传达了文章的主要内容,即如何使用Tree Sitter工具从代码中提取有价值的信息,并利用这些信息来改善开发指标和管理代码库的健康状况。
照片由 Maxim Berg 拍摄,在 Unsplash 上分享
特别是在软件开发领域,这是一个不断变化的领域。这些变化可能来自团队内部,随着团队对问题理解的加深并不断迭代解决方案而自然而然地产生,也可以来自外部因素,例如新推荐的技术范式(比如团队使用的框架)。
为了让工程团队保持这些更改的低开销,需要有一种衡量代码库健康的手段。这些通常被称为开发指标,通过提取源代码中的“事实”来编制,这些“事实”可以量化,并用来推动团队调整该数量。
例如,几年前有一个团队创建了一个基于 React 的产品,并当时采用了一种被认为是最佳实践的状态管理模式——Redux 和 HOC。几年后,最佳实践发生了变化,Hook 和 Context 取代了较旧的模式。
面对这一变化,团队可以采取几种不同的回应方式:
- 他们可以专注于重新调整代码库以符合新模式,而以牺牲进行其他代码更改(如功能开发)为代价。
- 他们可以抵制新模式,坚持他们熟悉的编程范式,接受由于这种选择,可能在某个时候无法再更新他们依赖的库。
- 他们可以制定一个计划,在进行其他工作的同时逐步调整代码库,在开发其他功能时重构所接触的现有代码。
据我观察,通常是后两种方法被采用,而第二种方法比我期望的更常见。采用这种方法的团队基本上都在回避问题,直到他们不得不面对变化,或者他们的解决方案需要大改,因为比行业标准落后了。
更懂得管理“技术债务”的团队会选择第三个选择。这些团队清楚代码永远无法达到“完美”的状态,并开发工具来应对代码与内部及外部模式之间的变化。
其中一个工具就是所谓的开发度量。
构建开发指标体系为了创建一个Dev Metrics指标,你需要能够量化你的代码,但如何将一堆指令转化为可以在图表上展示的数字呢?——你需要定义一些代码模式,并看看你的代码有多少遵循这些模式。
一种模式可以是确保每一行代码末尾都有分号那么简单,也可以是更复杂的要求,比如检查代码是否使用了外部资源中的相同值,以确保它们保持一致。
要创建一个模式,你需要能够分析代码。以下是几种不同的方法:
文本匹配技术一种方法是匹配代码中的文本模式,通常使用正则表达式(正则表达式,简称Regex)。这种方法对于检查或提取简单的字符串值很有帮助,但如果代码编写不一致,可能会出错,导致假阳性或假阴性结果。
这种方式的好处在于速度快,如果你能容忍一些不精确,它能迅速带来很多好处。但这也意味着权衡,你越是追求正则表达式的准确性,它就会变得越复杂,读起来和维护起来也就越困难。
比如说,下面的检查用于确认某个特定方法是否被调用到。
^.*\(.*\).*=>.*cy\..*\(.*$
它将与以下代码完美匹配:
const someUseOfCypress = () => cy.find(".selector")
但是如果我们以后需要在那行代码前面插入一行来记录想要的某条信息,那么就会破坏了之前的检查。
const someUseOfCypress = () => {
cy.log("这里将会检查某个事项")
cy.find(".selector") // 这里查找具有相应选择器的元素
}
然后我们就得修改正则来处理 inline arrow function 以及 multi-line arrow function,这样会变得非常难以阅读。
代码校验另一种方法是使用例如eslint或Grit之类的代码格式检查工具,根据代码的语法定义一个模式。这些工具会保存代码的表示形式,可以查询并用于生成报告的输出。
要重用上面提到的检查方法,在Grit中,这会像是检查常量、变量或函数声明体内是否使用了“cy”实例的方法。
引擎 marzano 版本 0.1
使用 JavaScript 语言
其中可以是 {
词法声明语句($declarations) 其中 $declarations <: 包含 成员表达式(对象: 'cy'),
变量声明语句($declarations) 其中 $declarations <: 包含 成员表达式(对象: 'cy'),
函数声明语句($body) 其中 $body <: 包含 成员表达式(对象: 'cy')
}
代码审查工具通常让你为匹配其规则的代码定义一个严重性级别,你可以将 INFO
级别用来让工具报告匹配的内容,而不会把匹配该模式的代码当作错误。
根据你使用的代码检查工具提供的选项,你可能需要采取额外的步骤来获取计数来支持你的开发指标。这可能涉及阅读工具运行时输出的行数,或者解析序列化后的结果。
Grit 提供了 JSON 输出格式,这使得它成为收集开发指标的好工具。你可以将其输出通过管道传递给类似的工具,如 JQ,或者将其作为子进程运行并解析 stderr 中的 JSON 数据以进行进一步计算。
# 获取特定 GritQL 检查匹配项的数量,其中 "CHECK_NAME" 表示检查名称
grit check --json 2>&1 | jq '.results[] | select(.localname == "CHECK_NAME") | length'
语法检查比文本匹配更准确,因为它更能适应代码结构的变化,不会因函数中的换行或额外的代码行而影响匹配。
使用结构化代码检查工具的缺点是,你只能收集代码语法符合预定义模式程度的指标,你无法直接从代码中提取值,除非进行额外的处理步骤。
抽象语法树(AST)遍历操作另一种方法是利用代码静态分析工具使用的基础技术——抽象语法树(AST)遍历技术,来查询和提取代码中的值。代码静态分析工具Grit就是基于Tree Sitter开发的。
与 linting 不同,linting 让你定义一个查询并获得匹配项的数量或列表,遍历 AST 则允许你提取符合谓词条件的节点并进一步对它们进行计算。
如果我们再次使用之前的检查方法示例,我们可以用一个 Tree Sitter 来匹配该方法,并且还可以通过节点获取传递给参数(@selector
)的值。
(
(调用表达式
function: (
成员表达式
object: (标识符) @实例
property: (属性标识符) @方法
)
参数: (
参数 (字符串 (字符串片段) @选择器)
)
)
(@实例等于 "cy")
)
我们可以使用这些值进行更深入的分析。例如,我们检查方法传递的值是否在值的外部表格中,这样操作后,我们还可以生成一个关于缺失值的报告。
我正好遇到了这种情况。
基于 Tree Sitter 提取代码中的数据我用Python写了我的脚本,不过我认为过程在所有编程语言中都大同小异。
为了进行分析,我需要安装Tree Sitter和Tree Sitter语法文件,因为我需要分析的代码是用TypeScript编写的。我需要安装tree-sitter
和tree-sitter-typescript
来分析代码。
安装了库之后,我需要弄清楚如何在我的源代码上运行查询。不过,Tree Sitter的文档在这方面的帮助不大,幸运的是,tree-sitter
库的Github上有一些示例代码展示了Python库如何查询字节字符串。所以我参照这些示例从磁盘读取文件为字节字符串,这方法奏效了。
# 导入库
from tree_sitter import Language, Parser
import tree_sitter_typescript
# 初始化解析器
language = Language(tree_sitter_typescript.language_typescript())
parser = Parser(language)
query = language.query("""
(
(call_expression
function: (
member_expression
object: (identifier) @instance
property: (property_identifier) @method
)
arguments: (
arguments (string (string_fragment) @selectors)
)
)
(#eq? @instance "cy")
)
""")
# 要么读取文件作为字节字符串,要么创建一个字节字符串
source_code = b"""
const someUseOfCypress = () => {
cy.log("This is going to check something")
cy.find(".selector")
}
"""
# 执行查询
source_code_tree = parser.parse(source_code)
captures = query.captures(source_code_tree)
一旦我有了运行查询的方法,我就需要编写查询了。这是一个很大的学习步骤,因为Tree Sitter的S-表达式语法使用的是波兰表示法,这总是让我感到头疼。
实际上,在构建查询时,你会创建一个用于匹配节点的表达式,并且你可以嵌套这些表达式以确保只匹配那些具有满足条件的子节点的节点。你可以捕获这些匹配的节点,并在外层表达式中进一步检查这些节点。
该查询的输出将是一对键值,键是捕获,值是匹配查询的抽象语法树节点列表。
你可以遍历节点的树来提取抽象语法树中的值,在这种情况下,我需要提取该方法调用时传递的参数。
# 获取捕获结果
captures = query.captures(source_code_tree)
selector_nodes = captures["selectors"]
# 对于每个捕获,遍历树以获取值
selectors = []
for selector_node in selector_nodes:
cursor = selector_node.walk()
if cursor.node:
selector_name_node = cursor.node.child(1)
if selector_name_node:
selector_name_text_node = selector_name_node.child(1)
if selector_name_text_node and selector_name_text_node.text:
selectors.append(selector_name_text_node.text.decode("utf-8"))
我就可以把这些值保存到一个集合里,并与另一个集合对比,看看少了哪些值。
代码中的选择器 = set(选择器)
外部资源中的选择器 = set(外部资源中的选择器)
外部资源中缺少的选择器 = (代码中的选择器 - 外部资源中的选择器)
源代码中缺少的选择器 = (外部资源中的选择器 - 代码中的选择器)
一些坑
AST 遍历并不完美无缺,当你分析一个从其他文件导入常量或类型的文件时,你将无法访问这些定义,除非进行额外处理,例如进行额外的静态处理或动态运行时处理。
// 由于 Tree sitter 不了解 SomeFancyType,因此无法解析 'typedArg' 的具体属性,但它知道 cy.find() 方法使用了 typedArg 的 selector 属性
import SomeFancyType from './types'
const someUseOfCypress = (typedArg: SomeFancyType) => {
cy.log("这将检查某个内容", typedArg)
cy.find(typedArg.selector)
}
有这样一些系统能够做到这一点,这些系统会解析你的整个代码库并创建关于代码的“事实”。通常为了保持高性能,这些系统不存储抽象语法树(AST,Abstract Syntax Tree),而是存储关于代码最常访问的事实。
所以你需要评估一下需求,看看AST是否足够,还是需要更大一些的东西。
总结通过使用 Tree Sitter,我能够提取代码中使用的数值,并将其与非技术同事使用的电子表格里的数据进行对比检查,提供有价值的反馈意见,以确保所有人都了解添加到代码或电子表格中的新分析事件名称,确保所有人都能跟上变化。
我已经能够为此开发指标,这样我们就能找出不同步的情况,进而找出减少这种不同步情况发生的方法。
通过了解Tree Sitter的工作方式,并从代码中提取“事实”,我开始学会如何作为开发者为非技术人员创建查询,以回答他们关于代码功能的问题。
共同学习,写下你的评论
评论加载中...
作者其他优质文章