您是否经历过编写代码的困惑?您的代码库是否存在着前后不一致的现象?在每次审核代码时,您是否会感到焦虑?如果您对这些问题的任何一项回答为“是”的话,那么静态代码分析应该能够为您提供帮助。
所谓静态代码分析,是指在执行代码之前,对其进行分析的过程。通过集成静态代码分析器,开发人员可以优化整个工作流程。
本文将向您介绍什么是JavaScript静态代码分析,为什么要使用它,以及如何在项目中通过快速设置来实现它。
什么是静态代码分析?
顾名思义,静态代码分析是对处于静态、或非执行状态的代码进行分析。它在效果上等同于让另一名开发人员去阅读和审查您的代码。当然,其效率可能不及自动化工具。
与测试有何不同?
无论是单元测试、功能测试、集成测试、视觉测试、还是回归测试,所有这些类型的测试都是通过运行代码,将结果与已知的预期输出状态进行比较,以检查是否一切正常。可以说,测试是将您的代码视为黑盒子,通过为其提供输入,来验证执行后的输出,以确保代码能够按照预期运行。
然而,静态代码分析主要是针对程序代码的可读性、一致性、错误处理、类型检查、以及缩进排版等方面进行分析。可以说,静态分析并非关注代码能否提供预期的输出,而是关注代码本身的编写方式。它是对源代码质量(而非功能性)的分析。
总而言之,测试着眼于检查代码是否有效,而静态分析则会检查代码是否编写正确。在理想情况下,您应该在项目中,让测试和静态分析相辅相成。
为什么要使用静态代码分析?
从程序格式化小工具,到漏洞扫描器,甚至程序审查器,任何能够读取源代码,对其进行解析,并提出改进建议的工具,都属于静态代码分析器。下面,让我们看看有哪些工作场景会用到静态代码分析。
全面审查
人是会犯错的。据统计,只有15%的JSHint(一种流行的JavaScript代码审查工具)代码库的安装过程,能够顺利通过。正所谓“当局者迷,旁观者清”,“第二双眼睛”往往可以发现您在自己的代码中,那些永远不会察觉到的问题。据此,开发人员不但可以了解项目中的某些内置功能,而且能够提出更好、更便捷的实现方法。
那么,我们是否能引入更多的“眼睛”、更全面的“扫描”呢?通常,静态分析器是由庞大的开源社区提供支持的。这就意味着,所有为该工具做出贡献的人,都可以间接地检查了您的代码,以发现任何遗漏的错误。
例子:
如下代码段展示了一个让用户挑选水果的例子。如果用户不做选择,则默认为“Mango”。
- let fruits = ['Apple', 'Banana', 'Cherry', 'Mango']
- function getFruit(index) {
- index = index || 3 // Everybody likes mangoes
- return fruits[index]
- }
只要用户的输入不为0,那么各种测试都能顺利通过。
- getFruit() // Mango
- getFruit(2) // Cherry
- getFruit(0) // Mango (expected Apple!)
而由于0的存在,用户无法选择Apple。因此针对null和undefined之类的虚假值(falsy value),您应该改用null-coalescing(空值合并)运算符--??(请参见如下代码段)。
- let fruits = ['Apple', 'Banana', 'Cherry', 'Mango']
- function getFruit(index) {
- index = index ?? 3 // Everybody likes mangoes
- return fruits[index]
- }
风格统一
每个开发人员都可以采用自己的风格去编写代码。但是,在许多开发人员需要协同工作时,通过风格指南的一致性来约束大家的编程,就显得极为重要了。不过,我们不可能要求开发人员记住指南中的数百条规则,并依靠人工进行逐行对照与检查。因此,我们需要依靠自动化来完成。
每一种语言都有自己的代码校验器(lint),例如:JavaScript有ESLint(https://eslint.org/)、Python有Black(https://black.readthedocs.io/en/stable/)、而Ruby有RuboCop(https://github.com/rubocop-hq/rubocop)。这些校验器可以确保您的代码遵循相关的风格规则,进而让代码更加整洁。例如,RuboCop便可以通过检测和修复错误,以确保函数与变量名具有更好的原子一致性。
例子:
如下JavaScript代码段,旨在从列表中打印水果的名称。同时,该列表在整个程序中应保持不变。
- var fruits = ['Apple', 'Banana', 'Cherry', 'Mango']
- console.log(fruits[0])
通过配置,ESLint可以确保尽可能地使用常量,以避免对代码产生副作用。
- const fruits = ['Apple', 'Banana', 'Cherry', 'Mango']
- console.log(fruits[0])
如上述代码所示,倘若我们用let和const两个关键字来替换var,则会让代码更加易于调试。
立即发现问题
以测试为驱动的开发实践,往往强调编写各种用于测试的用例。但是,为了尽可能地覆盖所有输入,开发人员需要花费时间和精力去编写测试用例。最终,测试用例不但十分臃肿,而且需要花费数小时,才能完成大型代码库的构建。
而静态代码分析器则不会遇到此类问题。您不需要编写测试用例,即可导入整个预设的代码库。同时,由于无需执行代码,因此静态分析器的运行速度会更快。实际上,许多代码校验器都能够与代码编辑器相集成,并在您输入代码时,实时地突显代码中的问题。
例子:
大多数静态分析器,尤其是linters和formatter,不仅会指出代码问题,而且可以解决问题。同时,诸如Python的Black和JavaScript的ESLint还能够与IDE相集成。在用户保存代码时,它们便可以自动修复当前已编辑的文件。这种实时提高代码质量的方式,已广受开发者的欢迎。
例子:
ESLint带有一个可用来修复常见问题的参数—fix,可用来修复:不必要的分号、尾部空格、以及多余的逗号。在如下代码段中,一个·代表着一个空格。
- var fruits = [
- 'Apple',
- 'Banana',
- 'Cherry',··
- 'Mango'
- ];
而在运行了带有—fix的ESLint后,代码会变成:
- const fruits = [
- 'Apple',
- 'Banana',
- 'Cherry',
- 'Mango',
- ]
修复依赖项
在构建应用程序时,您不可避免地会用到由其他开发人员所构建的框架和工具。当然,其他开发人员也可能会用到更多第三方的框架。如此下去,就算是一个简单的Vue.js应用,也可能在其node_modules/的目录中,被放置了成千上万个软件包。
然而,在这种“叠罗汉”式的依赖链中,您的应用程序会受到最薄弱的依赖性的制约。而作为另一种静态分析器的漏洞扫描程序,正好可以通过强大的漏洞签名数据库的优势,对依赖关系树中的每个依赖项进行检查。而所有被发现的漏洞,都能够被扫描程序通过一条命令,来予以更新和修复。
例子:
GitHub会使用Dependabot来扫描依赖项。而npm会使用npm audit命令来扫描漏洞。Dependabot和npm audit在它们的新版本中,都提供了带有自动化漏洞修复功能的更新包。
自动化重复性事务
相对于繁琐地手动遍历与审查代码,各种linter、formatters、以及拼写检查器(spell checkers)能够大幅简化整个过程。那么它们又是怎么实现的呢?首先,预提交的钩子(pre-commit hook)将确保在检入VCS(版本控制系统)之前,对代码已进行校验和格式化。其次,构建管道或GitHub工作流形式的项目级自动化,会在每次提交时检测代码的质量,并突显代码自身的问题。最后,由于代码的各种小问题已经被处理了,因此审查人员可以去关注代码逻辑等其他问题。可见,软件自动化不但可以减少人工的工作量,还能够提高审查的速度,以及遍历的覆盖率。
项目示例
下面,让我们来逐步创建一个项目示例。首先,我们为项目创建一个新的目录。在该目录中,我们通过npm init向导(如下图所示),逐步完成Node.js包的初始化。
- $ mkdir wuphf.com
- $ cd wuphf.com
- $ npm init
我们使用如下简单的命令来安装ESLint。
- $ npm install eslint
我们运行如下命令,来激活ESLint向导。
- $ ./node_modules/.bin/eslint --init
该向导会通过与您的互动,完成在项目中如何使用ESLint的相关问题。此本例中,请选择Airbnb的规则集。在完成设置后,该目录中将出现一个.eslintrc.js的文件。
下面的代码段展示了一个控制台应用。在此,我们可以自定义规则,并关闭针对它的各种警告。
- module.exports = {
- env: {
- es2021: true,
- node: true,
- },
- extends: [
- 'airbnb-base',
- ],
- parserOptions: {
- ecmaVersion: 12,
- },
- overrides: [
- {
- files: ['*.js'],
- rules: {
- 'no-console': 'off',
- },
- },
- ],
- };
最后,我们将此文件提交到版本控制系统中,以便ESLint持续扫描项目中的所有JS文件。此外,我建议您通过安装Husky(https://typicode.github.io/husky/#/),以实现在每次提交之前,运行一次代码校验的作业,以保证不会有不良的代码被检入VCS中。
使用DeepSource来实现自动化
作为一个静态代码分析器,DeepSource(https://deepsource.io/)可以在代码库中查找问题,并自动修复它们。目前,它可以支持大多数主流编程语言,并能够集成到GitHub、GitLab和Bitbucket中。为了在项目中设置DeepSource,您可以将名为.deepsource.toml的文件放置在存储库的根目录中,以便对项目进行持续扫描。
原文标题:Beginner's Guide to JavaScript Static Code Analysis,作者:Dhruv Bhanushali