Build a linter extension for Visual Studio Code

Tyler Liu
RingCentral Developers
5 min readJul 26, 2022

--

At the time that I was writing this article, I had already published a working extension to Visual Studio Code marketplace with the name OpenAPI Linter. I wanted to share my experience building this.

What is a linter?

Lint, or a linter, is a static code analysis tool used to flag programming errors, bugs, stylistic errors and suspicious constructs.

I pulled the above quote from wikipedia here. I would like to add that, linter is not only used to “flag” problems, but sometimes to “fix” them too. I will go over this more in the ESLint section below.

One of the most popular linter is ESLint. I bet 99% of JavaScript programmers have used it either directly or indirectly:

Take this screenshot for example, the linter found a couple of issues in the code snippet. The ones with red underlines are errors while the ones with yellow underlines are warnings. We could put the cursor over to read more details:

You can see that, there is also an option named “Quick Fix…”, which means there are proposed fixes to the issue provided by the linter. We could simply apply the fix by clicking our mouse, or we could setup Visual Studio Code to auto fix upon saving.

Spectral, a linter for OpenAPI

Spectral is a linter for OpenAPI.

Spectral is an open source JSON/YAML linter, which allows you to create style guides for your structured data; things like OpenAPI/AsyncAPI/RAML descriptions, Kubernetes config, GitHub Actions, you name it, Spectral can help you lint it. Go beyond making sure they are “Technically Correct”, make sure they are useful.

Here is a screenshot from the Spectral official site:

RingCentral takes advantages of Spectral to improve the quality of our API specs.

Spectral also provides JavaScript APIs. So you that you could be invoking the linter API programmatically.

Bootstrap a new Visual Studio Code extension

The best resource to reference is the Your First Extension guide. The easiest way to get started is to install Yeoman and the VS Code Extension Generator.

The generated code is ready to run. To run/debug it, justpress F5.

Language Server Extension

There are many kinds of Visual Studio Code extensions that one could build. But to build a linter extension for Visual Studio Code, your best bet is to build a Language Server Extension. The official guide/tutorial is pretty comprehensive and it is highly recommended to go through it first before continuing reading here.

It can be overwhelming to start from scratch. So I suggest to just copy the code from the LSP example.

Client code snippet

Our client code is based on the code template from the LSP example. We only made some notable changes regarding the clientOptions:

const clientOptions: LanguageClientOptions = {
// Register the server for plain text documents
documentSelector: [
{ scheme: 'file', language: 'yaml' },
],
synchronize: {
// Notify the server about file changes to '.spectral.yml files contained in the workspace
fileEvents: workspace.createFileSystemWatcher('**/.spectral.yml')
}
};

So the extension will lint documents with language yaml. And it will also notice server if we make change to the .spectral.yml file.

Server code snippets

const fakeFS: any = {
promises: {
async readFile(filepath: string) {
if (filepath === '/.spectral-default.yaml') {
return `extends: ["spectral:oas", "spectral:asyncapi"]`;
}
return fs.promises.readFile(filepath);
},
},
};
const spectral = new Spectral();
...const customRules = await bundleAndLoadRuleset(settings.spectralRulesetsFile, {
fs: fakeFS,
fetch: globalThis.fetch,
});
spectral.setRuleset(customRules);

Spectral supports custom rulesets. We could define custom rules in a file and load the rules. Please pay attention that we defined a default customer rule file named /.spectral-default.yaml and its content is extends: [“spectral:oas”, “spectral:asyncapi”] . Which means, if user doesn’t specify any custom rules, we will use the rules from spectral:oas and spectral:asyncapi which are provided by Spectral project as the default rules.

const loadConfig = async () => {
let settings: LinterSettings;
if(initialized) {
const workspacePath = (await connection.workspace.getWorkspaceFolders())![0].uri;
let spectralRulesetsFile = join(workspacePath, '.spectral.yml');
if(spectralRulesetsFile.startsWith('file:')) {
spectralRulesetsFile = spectralRulesetsFile.substring(5);
}
if(fs.existsSync(spectralRulesetsFile)){
settings = {spectralRulesetsFile};
} else {
settings = await connection.workspace.getConfiguration('openApiLinter') as LinterSettings;
if(!fs.existsSync(settings.spectralRulesetsFile ?? '')) {
settings.spectralRulesetsFile = '/.spectral-default.yaml';
}
}
} else {
settings = {spectralRulesetsFile: '/.spectral-default.yaml'};
}
...

First of all, we try to find the config file at the workspace root. If it doesn’t exist, we will try to find the config file specified in the extension configuration. If it doesn’t exists either, we will use the default configuration file instead. This one, users will have the flexibility to specify configuration files at different levels.

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
const text = textDocument.getText();
let diagnostics: Diagnostic[] = [];
if(text.startsWith('openapi:')) {
const issues = await spectral.run(new Document(text, Yaml, 'spec.yaml'));
diagnostics = issues.map(issue => ({
severity: issue.severity + 1,
code: issue.code,
range: issue.range,
message: issue.message,
source: 'OpenAPI Linter'
})) as Diagnostic[];
}
// Send the computed diagnostics to VSCode.
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

Now it’s time to do the actual linting work. We just invoke Spectral API to get the list of issues, then we translate the issues to diagnostics which is consumable by Visual Studio code. With all the preparation work, this part is short and simple.

And here is how it looks when it is working:

Full Source Code

Source code for OpenAPI Linter is available on GitHub.

Summary

In this article we’ve covered a lot, from what is a linter to how to actually code a linter. Now you should have all the necessary information to code your own linter extension for Visual Studio Code.

To learn more about RingCentral features we have make sure to visit our developer site and if you’re ever stuck make sure to go to our developer forum.

Want to stay up to date and in the know about new APIs and features? Join our Game Changer Program and earn great rewards for building your skills and learning more about RingCentral!

--

--