写了一个 vscode 插件:自动添加可选链

前几天有朋友问了我个问题:

是否可以通过 eslint 插件实现自动把属性访问变成可选链的方式。

这当然是可以的,我们来实现下:

arduino 复制代码
mkdir auto-optional-chain

cd auto-optional-chain

npm init -y

创建项目,新建 package.json

安装 eslint 的包

css 复制代码
npm install --save eslint

然后在 src/index.js 写这样一段代码:

javascript 复制代码
const { ESLint } = require("eslint");

const engine = new ESLint({
    fix: false,
    overrideConfig: {
        parser: "@babel/eslint-parser",
        rules: {
            'semi': ['error', 'never']
        }
    },
    useEslintrc: false
});

async function main() {
    const results = await engine.lintText(`
        function handleRes(data) {
            const res = data.a.b.c + data.e.f.g;

        }
    `)
    
    console.log(results);
}
main();

eslint 一般我们用的是命令行的方式,当然,它也有 api 的方式。

我们 new 了 ESLint 的对象,指定配置,不自动 fix。

然后用 lintText 来检查一段代码,打印结果。

这里我们就用了一个 rule ,也就是检查末尾分号的,设置为不加分号。

这里用到了 @babel/eslint-parser,安装一下:

javascript 复制代码
npm install @babel/eslint-parser

然后创建 babel 配置文件:

然后跑一下:

可以看到确实有一个错误。

展开是这样的:

在第 3 行第 48 列有一个额外的分号。

但这样看起来太费劲了,我们把它格式化一下:

javascript 复制代码
const formatter = await engine.loadFormatter('stylish');
const resultText = formatter.format(results);
console.log(resultText);

eslint 内置了一些 formatter 用它格式化一下再打印:

这种错误格式就是我们经常见的那种了。

然后我们再把 fix 改为 true,也就是自动修复:

打印下 result[0].output,也就是第一个错误自动修复后的结果:

可以看到,末尾分号被去掉了。

这就是 eslint 的 api 方式的用法。

下面我们来写一个自动添加可选链的插件。

新建 src/auto-optional-chain.js

javascript 复制代码
module.exports = {
    meta: {
        docs: {
            description: "自动添加可选链"
        },

        fixable: true
    },

    create(context) {
        return {
           BlockStatement(node) {
           }
       }
   }
};

meta 部分是指定这个插件的元信息,比如文档、是否可以自动 fix 等。

create 部分是插件的实现逻辑,指定对什么节点做什么处理。

那我们要处理的是什么 AST 节点呢?

可以用 astexplorer.net 看一下:

选择 javascript,用 @babel/parser 解析,在后边可以看到 parse 出的 AST。

可以看到,这种 data.name 的语法叫做 MemberExpression 成员表达式。

如果多个 . 的话就是 MemberExpression 嵌套了:

那 data?.name 呢?

可以看到,叫做 OptionalMemberExpression

也就是说我们找到 MemberExpression,给它报个错,然后 fix 的时候修复为可选链的方式就好了。

也就是这样:

javascript 复制代码
module.exports = {
    meta: {
        docs: {
            description: "自动添加可选链"
        },

        fixable: true
    },

    create(context) {
        return {
            MemberExpression(node) {
                context.report({
                    node,
                    loc: {
                        line: 111,
                        column: 222
                    },
                    message: '应该用可选链'
                })
            }
       }
   }
};

指定 rulePaths 也就是去哪里找 rule,然后配置这个 rule 为 error 级别:

测试下:

可以看到,确实是报了 6 个错误。

只不过现在的位置不太对。

拿到 . 的位置需要用 token 相关的 api。

也就是这样:

javascript 复制代码
module.exports = {
    meta: {
        docs: {
            description: "自动添加可选链"
        },

        fixable: true
    },

    create(context) {
        const sourceCode = context.getSourceCode();

        return {
            MemberExpression(node) {
                const tokens = sourceCode.getTokens(node);

                context.report({
                    node,
                    loc: {
                        line: 111,
                        column: 222
                    },
                    message: '应该用可选链'
                })
            }
       }
   }
};

我们断点调试下:

创建 launch.json

创建 node 类型的调试配置:

在代码里打个断点:

点击调试启动:

代码会在断点处断住:

可以看到有 7 个 token,分别是 data 和 . 和 a 和 b 和 . 和 c

那我们取哪个 . 的 loc 呢?

可以看到,第一次断住是这样的:

第二次是这样的:

第三次是这样的:

也就是说 data.a.b.c 是从右向左解析的,所以我们要拿到的是最后一个 . 的 token 的位置。

取倒数第二个,可以用数组的 at 方法:

也就是这样:

javascript 复制代码
module.exports = {
    meta: {
        docs: {
            description: "自动添加可选链"
        },

        fixable: true
    },

    create(context) {
        const sourceCode = context.getSourceCode();

        return {
            MemberExpression(node) {
                const tokens = sourceCode.getTokens(node);

                context.report({
                    node,
                    loc: tokens.at(-2).loc,
                    message: '应该用可选链'
                })
            }
       }
   }
};

现在的位置就都对了:

光报错意义不大,我们再实现自动 fix。

eslint 的 fix 是基于字符串替换实现的,它提供了一个 fixer api。

打断点看看:

很明显,这里比较适合用 insertTextBefore 来做。

也就是这样:

javascript 复制代码
context.report({
    node,
    loc: dotToken.loc,
    message: '应该用可选链',
    fix: fixer => {
        return fixer.insertTextBefore(dotToken, '?')
    }
})

但要注意,fix 之后会再次 lint,这时候拿到的 token 就这样了:

这种应该不再 fix,直接跳过:

javascript 复制代码
module.exports = {
    meta: {
        docs: {
            description: "自动添加可选链"
        },

        fixable: true
    },

    create(context) {
        const sourceCode = context.getSourceCode();

        return {
            MemberExpression(node) {
                const tokens = sourceCode.getTokens(node);

                const dotToken = tokens.at(-2);

                if(dotToken.value === '?.'){
                    return;
                }

                context.report({
                    node,
                    loc: dotToken.loc,
                    message: '应该用可选链',
                    fix: fixer => {
                        return fixer.insertTextBefore(dotToken, '?')
                    }
                })
            }
       }
   }
};

测试下:

修复后的代码是对的。

只要项目里用到了这个 rule,开启自动 fix 就可以自动加上可选链。

但这样其实有个问题:不是所有的 data.xxx 都需要变成可选链的方式,而现在这个 eslint rule 是把所有的 data.xxx 都自动 fix 了。

也可以写个 babel 插件来做这件事情,不修改源代码,只是在编译的时候做:

javascript 复制代码
const { transformSync } = require('@babel/core');

function autoOptionalPlugin() {
    return {
        visitor: {
            MemberExpression(path, state) {
                const text = path.toString();

                path.replaceWithSourceString(text.replace(/\./g, '?.'));
            }
        }
    }
}

const res = transformSync(`
    function handleRes(data) {
        const res = data.a.b.c + data.e.f.g;

    }
`, {
    plugins: [autoOptionalPlugin]
});

console.log(res.code);

用 transformSync 来编译源代码为目标代码,过程中调用 autoOptionalPlugin。

插件里处理 MemberExpression,拿到代码对应的字符串,然后把 . 改成 ?. 再替换回去。

效果和 eslint 插件是一样的:

babel 插件的好处是不修改源码,可以在编译过程中无感的做这件事情。

那我如果就是想把代码改了,但是还不能全部改,而是我选中哪部分就自动修复哪部分代码呢?

这种就要用 vscode 插件来做了。

安装 vscode 插件的脚手架:

css 复制代码
npm install -g yo generator-code

生成 vscode 插件项目:

css 复制代码
yo code 

生成的项目是这样的:

它已经配置好了调试配置,点击就可以调试:

它会启动一个新的 vscode 窗口,然后输入 hello world 命令,右下角会有提示框:

这就代表 vscode 插件运行成功了。

然后想一下我们的插件要做成什么样子:

选中一段代码,右键菜单里会有转换为可选链的选项,点击就可以转换。

或者选中之后,按快捷键也可以转换。

在 src/extention.ts 里实现下命令的注册:

javascript 复制代码
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
    const transformCommand = vscode.commands.registerCommand('transformToOptionalChain', () => {
            vscode.window.showInformationMessage('转换成功!');
    });

    context.subscriptions.push(transformCommand);
}

然后在 package.json 里也要声明:

json 复制代码
"contributes": {
    "commands": [
      {
        "command": "transformToOptionalChain",
        "title": "xxx"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "command": "transformToOptionalChain"
        }
      ]
    }
},

commands 里声明这个 command,指定 title。

menus 声明 editor/context 也就是编辑器的上下文菜单,添加一个。

测试下:

确实多了一个菜单项,点击之后会执行 command 的逻辑:

回过头来看下这段配置:

这里的 editor/context 是注册编辑器的右键菜单,当然,还有很多别的地方的菜单可以注册:

这个菜单项还可以指定出现的时机,显示的分组:

比如 1_modification 就是这里:

而 navigation 就是这里:

这个分组在文档里也有写:

然后指定菜单项出现的时机:

当语言类型 为 js 或者 ts,并且选中文本的时候才出现:

这样在非 js、ts 文件里是没这个菜单的:

在 js、ts 里不选中也是没有的:

只有在 js、ts 文件,并且选中文本,才会出现这个菜单项:

然后我们就可以写具体的逻辑了:

javascript 复制代码
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {

	const transformCommand = vscode.commands.registerCommand('transformToOptionalChain', () => {
		const editor = vscode.window.activeTextEditor;

		if(editor) {
			const selectedText = editor.document.getText(editor.selection);
		
			editor.edit(builder => {
				builder.replace(editor.selection, selectedText.toUpperCase());
			});
			vscode.window.showInformationMessage('转换成功!');
		}
	});

	context.subscriptions.push(transformCommand);
}

通过 vscode.window.activeTextEditor 拿到当前的 editor,然后拿到选中区域的文本,执行替换。

这里只是替换为了大写。

打个断点试试:

代码执行到这里会断住:

可以看到,拿到的文本就是选中的。

那我们把之前用 babel 插件做代码转换的逻辑拿过来就好了。

安装用到的 @babel/core 包和它的 ts 类型包:

scss 复制代码
npm install --save @babel/core

npm i --save-dev @types/babel__core

然后用它来做下选中代码的转换:

javascript 复制代码
import * as vscode from 'vscode';
import * as babel from '@babel/core';
import type { NodePath, types} from '@babel/core';

function transform(code: string): string{
	function autoOptionalPlugin() {
		return {
			visitor: {
				MemberExpression(path: NodePath<types.MemberExpression>) {
					const text = path.toString();

					path.replaceWithSourceString(text.replace(/\./g, '?.'));
				}
			}
		}
	}

	const res = babel.transformSync(code, {
		plugins: [autoOptionalPlugin]
	});

	return res?.code || '';
}

export function activate(context: vscode.ExtensionContext) {

	const transformCommand = vscode.commands.registerCommand('transformToOptionalChain', () => {
		const editor = vscode.window.activeTextEditor;

		if(editor) {
			const selectedText = editor.document.getText(editor.selection);
		
			if(!selectedText) {
				return;
			}

			editor.edit(builder => {
				builder.replace(editor.selection, transform(selectedText));
			});
			vscode.window.showInformationMessage('转换成功!');
		}
	});

	context.subscriptions.push(transformCommand);
}

这里的 transform 方法就是前面讲过的用 babel 做代码转换的实现。

测试下:

只有选中的代码才会做转换,没选中的不会:

还可以把这个功能注册成快捷键:

json 复制代码
"keybindings": [
  {
    "command": "transformToOptionalChain",
    "key": "ctrl+y",
    "mac": "cmd+y",
    "when": "(resourceLangId == javascript || resourceLangId == typescript) && editorHasSelection"
  }
]

在 windows 下是 ctrl + y,在 mac 下是 command + y

是不是用起来超级方便?

总结

我们想自动把代码里的 data.xxx 转成可选链的形式 data?.xxx。

于是写了 eslint 插件、babel 插件来做这件事。

eslint 插件的 fix 是通过字符串替换的方式修改源码。

babel 插件是通过 ast 的方式修改代码,而且只是改了编译后的代码。

但这俩都是全局替换的,还是自己选择替换哪部分更好,所以我们又写了一个 vscode 插件。

vscode 插件可以在 package.json 的 contributes 里配置 commands、menus、keybindings。

我们注册了一个命令,配置了编辑器右键菜单,并且绑定了快捷键。

当执行这个命令的时候,拿到选中的文本内容,通过 babel 插件来做转换,之后替换回去。

写完这个 vscode 插件以后,再遇到这种情况,只要选中文本,按个快捷键就可以搞定。

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax