您好,欢迎访问一九零五行业门户网

VSCode插件开发实战:实现一个代码诊断插件

本篇文章给大家分享一个vscode插件开发实战,开发一个代码诊断插件,分析一下基本原理,并一步步实现,希望对大家有所帮助!
最近,我们内部出了一份 code review 指南,但是 code review 过程非常占时间,大家不会太仔细去 review 代码,因此想通过一个插件让开发者在开发阶段就能感知到写法的错误,做出的效果如下图
接下来将介绍如何从 0 实现这么一个功能。
基本原理visual studio code 的编程语言功能扩展是有 language server 来实现的,这很好理解,毕竟检查语言功能是耗费性能的,需要另起一个进程来作为语言服务,这就是 language server 语言服务器。【推荐学习:《vscode入门教程》】
language server 是一种特殊的 visual studio code 扩展,可为许多编程语言提供编辑体验。使用语言服务器,您可以实现自动完成、错误检查(诊断)、跳转到定义以及vs code 支持的许多其他语言功能。
既然有了服务器提供的语法检查功能,就需要客户端去连接语言服务器,然后和服务器进行交互,比如用户在客户端进行代码编辑时,进行语言检查。具体交互如下:
当打开 vue 文件时会激活插件,此时就会启动 language server,当文档发生变化时,语言服务器就会重新诊断代码,并把诊断结果发送给客户端。
代码诊断的效果是出现波浪线,鼠标移上显示提示消息,如果有快速修复,会在弹出提示的窗口下出现快速修复的按钮
动手实现了解了代码诊断的基本原理之后,开始动手实现,从上面的基本原理可知,我们需要实现两大部分的功能:
客户端与语言服务器交互
语言服务器的诊断和快速修复功能
客户端与语言服务器交互官方文档 提供了一个示例 - 用于纯文本文件的简单语言服务器,我们可以在这个示例的基础上去修改。
> git clone https://github.com/microsoft/vscode-extension-samples.git> cd vscode-extension-samples/lsp-sample> npm install> npm run compile> code .
首先在 client 建立服务器
// client/src/extension.tsexport function activate(context: extensioncontext) { ... const clientoptions: languageclientoptions = { documentselector: [{ scheme: 'file', language: 'vue' }], // 打开 vue 文件时才激活 ... }; client = new languageclient(...); client.start();}
接着在 server/src/server.ts 中,编写于客户端的交互逻辑,比如在客户端文档发生变化的时候,校验代码:
// server/src/server.tsimport { createconnection textdocuments, proposedfeatures, ...} from 'vscode-languageserver/node';const connection = createconnection(proposedfeatures.all);const documents: textdocuments<textdocument> = new textdocuments(textdocument);documents.ondidchangecontent(change => { // 文档发生变化时,校验文档 validatetextdocument(change.document);});async function validatetextdocument(textdocument: textdocument): promise<void> { ... // 拿到诊断结果 const diagnostics = getdiagnostics(textdocument, settings); // 发给客户端 connection.senddiagnostics({ uri: textdocument.uri, diagnostics });}// 提供快速修复的操作connection.oncodeaction(providecodeactions);async function providecodeactions(params: codeactionparams): promise<codeaction[]> { ... return quickfix(textdocument, params);}
在完成上面客户端与服务端交互之后,可以注意到这两个方法 getdiagnostics(textdocument, settings) 和 quickfix(textdocument, params)。 这两个方法分别是为文档提供诊断数据和快速修复的操作。
代码诊断整体流程
1. 将代码文档转成 ast 语法树在处理客户端传递过来的 vue 代码文本的,需要通过 vue/compiler-dom 解析成三部分 ast 格式的数据结构,分别是 template、js、css, 由于现在前端代码使用的都是 typescript,js 部分没有解析成 ast,因此需要使用 babel/parser 去解析 typescript 代码生成最终的 js 的 ast 数据结构。
const vueparser = require('@vue/compiler-dom');// 该函数返回诊断结果客户端function getdiagnostics(textdocument: textdocument, settings: any): diagnostic[] { const text = textdocument.gettext(); const res = vueparser.parse(text); const [template, script] = res.children; return [ ...analyzetemplate(template), // 解析 template 得到诊断结果 ...analyzescript(script, textdocument), // 解析 js 得到诊断结果 ];}// 分析 js 语法function analyzescript(script: any, textdocument: textdocument) { const scriptast = parser.parse(script.children[0]?.content, { sourcetype: 'module', plugins: [ 'typescript', // typescript ['decorators', { decoratorsbeforeexport: true }], // 装饰器 'classproperties', // es6 class 写法 'classprivateproperties', ], });
得到的 ast 语法树结构如下:
template ast
js ast
2. 遍历语法树对代码校验在得到代码的语法树之后,我们需要对每一个代码节点进行检查,来判断是否符合 code review 的要求,因此需要遍历语法树来对每个节点处理。
使用深度优先搜索对 template 的 ast 进行遍历:
function deeploopdata( data: asttemplateinterface[], handler: function, diagnostics: diagnostic[],) { function dfs(data: asttemplateinterface[]) { for (let i = 0; i < data.length; i++) { handler(data[i], diagnostics); // 在这一步对代码进行处理 if (data[i]?.children?.length) { dfs(data[i].children); } else { continue; } } } dfs(data);}function analyzetemplate(template: any) { const diagnostics: diagnostic[] = []; deeploopdata(template.children, templatehandler, diagnostics); return diagnostics;}function templatehandler(currdata: asttemplateinterface, diagnostics: diagnostic[]){ // ...对代码节点检查}
而对于 js ast 遍历,可以使用 babel/traverse 遍历:
traverse(scriptast, { enter(path: any) { ... } }
3. 发现不合规代码,生成诊断根据 ast 语法节点去判断语法是否合规,如果不符合要求,需要在代码处生成诊断,一个基础的诊断对象(diagnostics)包括下面几个属性:
range: 诊断有问题的范围,也就是画波浪线的地方
severity: 严重性,分别有四个等级,不同等级标记的颜色不同,分别是:
error: 1warning: 2information:3hint:4message: 诊断的提示信息
source: 来源,比如说来源是 eslint
data:携带数据,可以将修复好的数据放在这里,用于后面的快速修复功能
比如实现一个提示函数过长的诊断:
function islongfunction(node: record<string, any>) { return ( // 如果结束位置的行 - 开始位置的行 > 80 的话,我们认为这个函数写得太长了 node.type === 'classmethod' && node.loc.end.line - node.loc.start.line > 80 );}
在遍历 ast 时如果遇到某个节点是出现函数过长的时候,就往诊断数据中添加此诊断
traverse(scriptast, { enter(path: any) { const { node } = path; if (islongfunction(node)) { const diagnostic: diagnostic ={ severity: diagnosticseverity.warning, range: getpositionrange(node, scriptstart), message: '尽可能保持一个函数的单一职责原则,单个函数不宜超过 80 行', source: 'code review 指南', } diagnostics.push(diagnostic); } ... }});
文档中所有的诊断结果会保存在 diagnostics 数组中,最后通过交互返回给客户端。
4. 提供快速修复上面那个函数过长的诊断没办法快速修复,如果能快速修复的话,可以将修正后的结果放在 diagnostics.data 。换个例子写一个快速修复, 比如 vue template 属性排序不正确,我们需要把代码自动修复
// attributeordervalidator 得到判断结果 和 修复后的代码const {isgoodsort, newtext} = attributeordervalidator(props, currdata.loc.source); if (!isgoodsort) { const range = { start: { line: props[0].loc.start.line - 1, character: props[0].loc.start.column - 1, }, end: { line: props[props.length - 1].loc.end.line - 1, character: props[props.length - 1].loc.end.column - 1, }, } let diagnostic: diagnostic = gendiagnostics( 'vue template 上的属性顺序', range ); if (newtext) { // 如果有修复后的代码 // 将快速修复数据保存在 diagnostic.data diagnostic.data = { title: '按照 code review 指南的顺序修复', newtext, } } diagnostics.push(diagnostic); }
quickfix(textdocument, params)
export function quickfix( textdocument: textdocument, params: codeactionparams): codeaction[] { const diagnostics = params.context.diagnostics; if (isnullorundefined(diagnostics) || diagnostics.length === 0) { return []; } const codeactions: codeaction[] = []; diagnostics.foreach((diag) => { if (diag.severity === diagnosticseverity.warning) { if (diag.data) { // 如果有快速修复数据 // 添加快速修复 codeactions.push({ title: (diag.data as any)?.title, kind: codeactionkind.quickfix, // 快速修复 diagnostics: [diag], // 属于哪个诊断的操作 edit: { changes: { [params.textdocument.uri]: [ { range: diag.range, newtext: (diag.data as any)?.newtext, // 修复后的内容 }, ], }, }, }); } }});
有快速修复的诊断会保存在 codeactions 中,并且返回给客户端, 重新回看交互的代码,在 documents.ondidchangecontent 事件中,通过 connection.senddiagnostics({ uri: textdocument.uri, diagnostics }) 把诊断发送给客户端。quickfix 结果通过 connection.oncodeaction 发给客户端。
import { createconnection textdocuments, proposedfeatures, ...} from 'vscode-languageserver/node';const connection = createconnection(proposedfeatures.all);const documents: textdocuments<textdocument> = new textdocuments(textdocument);documents.ondidchangecontent(change => { ... // 拿到诊断结果 const diagnostics = getdiagnostics(textdocument, settings); // 发给客户端 connection.senddiagnostics({ uri: textdocument.uri, diagnostics });});// 提供快速修复的操作connection.oncodeaction(providecodeactions);async function providecodeactions(params: codeactionparams): promise<codeaction[]> { ... return quickfix(textdocument, params);}
总结实现一个代码诊断的插件功能,需要两个步骤,首先建立语言服务器,并且建立客户端与语言服务器的交互。接着需要 服务器根据客户端的代码进行校验,把诊断结果放入 diagnostics,快速修复结果放在 codeactions,通过与客户端的通信,把两个结果返回给客户端,客户端即可出现黄色波浪线的问题提示。
更多关于vscode的相关知识,请访问:vscode教程!!
以上就是vscode插件开发实战:实现一个代码诊断插件的详细内容。
其它类似信息

推荐信息