Skip to content

Commit dd2f327

Browse files
authored
Add protolint.lint command (#21)
1 parent e6dc9f5 commit dd2f327

File tree

5 files changed

+109
-88
lines changed

5 files changed

+109
-88
lines changed

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131
"onLanguage:proto"
3232
],
3333
"main": "./out/src/extension",
34+
"contributes": {
35+
"commands": [
36+
{
37+
"command": "protolint.lint",
38+
"title": "Protolint: Lint protobuf file"
39+
}
40+
]
41+
},
3442
"scripts": {
3543
"vscode:prepublish": "npm run compile",
3644
"compile": "tsc",

src/extension.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,48 @@ import * as vscode from 'vscode';
22
import * as cp from 'child_process';
33
import Linter, { LinterError } from './linter';
44

5+
const diagnosticCollection = vscode.languages.createDiagnosticCollection("protolint");
6+
57
export function activate(context: vscode.ExtensionContext) {
8+
9+
// Verify that protolint can be successfully executed on the host machine by running the version command.
10+
// In the event the binary cannot be executed, tell the user where to download protolint from.
611
const result = cp.spawnSync('protolint', ['version']);
712
if (result.status !== 0) {
813
vscode.window.showErrorMessage("protolint was not detected. Download from: https://github.com/yoheimuta/protolint");
914
return;
1015
}
1116

12-
const commandId = 'extension.protobuflint';
13-
const diagnosticCollection = vscode.languages.createDiagnosticCollection(commandId);
14-
let events = vscode.commands.registerCommand(commandId, () => {
15-
vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => {
16-
doLint(document, diagnosticCollection);
17-
});
17+
vscode.commands.registerCommand('protolint.lint', runLint);
1818

19-
vscode.workspace.onDidOpenTextDocument((document: vscode.TextDocument) => {
20-
doLint(document, diagnosticCollection);
21-
});
19+
vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => {
20+
vscode.commands.executeCommand('protolint.lint');
2221
});
2322

24-
vscode.commands.executeCommand(commandId);
25-
context.subscriptions.push(events);
23+
// Run the linter when the user changes the file that they are currently viewing
24+
// so that the lint results show up immediately.
25+
vscode.window.onDidChangeActiveTextEditor((e: vscode.TextEditor | undefined) => {
26+
vscode.commands.executeCommand('protolint.lint');
27+
});
2628
}
2729

28-
async function doLint(codeDocument: vscode.TextDocument, collection: vscode.DiagnosticCollection): Promise<void> {
29-
if(codeDocument.languageId === 'proto3' || codeDocument.languageId === 'proto') {
30+
function runLint() {
31+
let editor = vscode.window.activeTextEditor;
32+
if (!editor) {
33+
return;
34+
}
35+
36+
// We only want to run protolint on documents that are known to be
37+
// protocol buffer files.
38+
const doc = editor.document;
39+
if(doc.languageId !== 'proto3' && doc.languageId !== 'proto') {
3040
return;
3141
}
3242

43+
doLint(doc, diagnosticCollection);
44+
}
45+
46+
async function doLint(codeDocument: vscode.TextDocument, collection: vscode.DiagnosticCollection): Promise<void> {
3347
const linter = new Linter(codeDocument);
3448
const errors: LinterError[] = await linter.lint();
3549
const diagnostics = errors.map(error => {

src/linter.ts

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,65 +16,61 @@ export default class Linter {
1616
}
1717

1818
public async lint(): Promise<LinterError[]> {
19-
const errors = await this.runProtoLint();
20-
if (!errors) {
19+
const result = await this.runProtoLint();
20+
if (!result) {
2121
return [];
2222
}
2323

24-
// protolint returns "not found config file by searching" when it is
25-
// executed and a configuration file cannot be found.
26-
if (errors.includes("not found config")) {
27-
vscode.window.showErrorMessage(errors);
24+
const lintingErrors: LinterError[] = this.parseErrors(result);
25+
26+
// When errors exist, but no linting errors were returned show the error window
27+
// in VSCode as it is most likely an issue with the binary itself such as not being
28+
// able to find a configuration or a file to lint.
29+
if (lintingErrors.length === 0) {
30+
vscode.window.showErrorMessage("protolint: " + result);
2831
return [];
2932
}
3033

31-
const lintingErrors: LinterError[] = this.parseErrors(errors);
32-
return lintingErrors;
33-
}
3434

35-
private parseErrors(errorStr: string): LinterError[] {
36-
let errors = errorStr.split('\n') || [];
37-
38-
var result = errors.reduce((errors: LinterError[], currentError: string) => {
39-
const parsedError = parseProtoError(currentError);
40-
if (!parsedError.reason) {
41-
return errors;
42-
}
43-
44-
const linterError: LinterError = this.createLinterError(parsedError);
45-
return errors.concat(linterError);
46-
}, []);
47-
48-
return result;
35+
return lintingErrors;
4936
}
5037

5138
private async runProtoLint(): Promise<string> {
5239
if (!vscode.workspace.workspaceFolders) {
5340
return "";
5441
}
5542

56-
const currentFile = this.codeDocument.uri.fsPath;
5743
let workspaceFolder: vscode.WorkspaceFolder = vscode.workspace.getWorkspaceFolder(this.codeDocument.uri) || vscode.workspace.workspaceFolders[0];
58-
const cmd = `protolint lint -config_dir_path="${workspaceFolder.uri.fsPath}" "${currentFile}"`;
59-
60-
const exec = util.promisify(cp.exec);
44+
const cmd = `protolint lint -config_dir_path="${workspaceFolder.uri.fsPath}" "${this.codeDocument.uri.fsPath}"`;
6145

6246
let lintResults: string = "";
47+
48+
// Execute the protolint binary and store the output from standard error.
49+
// The output could either be an error from using the binary improperly, such as unable to find
50+
// a configuration, or linting errors.
51+
const exec = util.promisify(cp.exec);
6352
await exec(cmd).catch((error: any) => lintResults = error.stderr);
6453

6554
return lintResults;
6655
}
6756

68-
private createLinterError(error: ProtoError): LinterError {
69-
const linterError: LinterError = {
70-
proto: error,
71-
range: this.getErrorRange(error)
72-
};
57+
private parseErrors(errorStr: string): LinterError[] {
58+
let errors = errorStr.split('\n') || [];
7359

74-
return linterError;
75-
}
60+
var result = errors.reduce((errors: LinterError[], currentError: string) => {
61+
const parsedError = parseProtoError(currentError);
62+
if (!parsedError.reason) {
63+
return errors;
64+
}
65+
66+
const linterError: LinterError = {
67+
proto: parsedError,
68+
range: this.codeDocument.lineAt(parsedError.line - 1).range
69+
};
7670

77-
private getErrorRange(error: ProtoError): vscode.Range {
78-
return this.codeDocument.lineAt(error.line - 1).range;
71+
return errors.concat(linterError);
72+
}, []);
73+
74+
return result;
7975
}
8076
}

src/protoError.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ export interface ProtoError {
33
reason: string;
44
}
55

6+
// parseProtoError takes the an error message from protolint
7+
// and attempts to parse it as a linting error.
8+
//
9+
// Linting errors are in the format:
10+
// [path/to/file.proto:line:column] an error message is here
611
export function parseProtoError(error: string): ProtoError {
712
if (!error) {
813
return getEmptyProtoError();

test/protoError.test.ts

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,37 @@
1-
import * as assert from 'assert';
2-
import { ProtoError, parseProtoError, getEmptyProtoError } from '../src/protoError';
3-
4-
// Proto errors are in the format:
5-
// [path/to/file.proto:line:column] an error message is here
6-
describe('parseProtoError', () => {
7-
it('should return empty protoerror when there is no error', () => {
8-
const expected: ProtoError = getEmptyProtoError();
9-
const actual = parseProtoError("");
10-
11-
assert.deepEqual(actual, expected);
12-
});
13-
14-
describe('should return the correct values', () => {
15-
it('when parsing a valid error', () => {
16-
const expected: ProtoError = {
17-
line: 1,
18-
reason: "test error"
19-
};
20-
21-
const error: string = "[path/to/file.proto:1:5] test error";
22-
const actual = parseProtoError(error);
23-
24-
assert.deepEqual(actual, expected);
25-
});
26-
27-
it('when file directory contains a space', () => {
28-
const expected: ProtoError = {
29-
line: 1,
30-
reason: "test error"
31-
};
32-
33-
const error: string = "[path/to /file.proto:1:5] test error";
34-
const actual = parseProtoError(error);
35-
36-
assert.deepEqual(actual, expected);
37-
});
38-
});
39-
});
1+
import * as assert from 'assert';
2+
import { ProtoError, parseProtoError, getEmptyProtoError } from '../src/protoError';
3+
4+
describe('parseProtoError', () => {
5+
it('should return empty protoerror when there is no error', () => {
6+
const expected: ProtoError = getEmptyProtoError();
7+
const actual = parseProtoError("");
8+
9+
assert.deepStrictEqual(actual, expected);
10+
});
11+
12+
describe('should return the correct values', () => {
13+
it('when parsing a valid error', () => {
14+
const expected: ProtoError = {
15+
line: 1,
16+
reason: "test error"
17+
};
18+
19+
const error: string = "[path/to/file.proto:1:5] test error";
20+
const actual = parseProtoError(error);
21+
22+
assert.deepStrictEqual(actual, expected);
23+
});
24+
25+
it('when file directory contains a space', () => {
26+
const expected: ProtoError = {
27+
line: 1,
28+
reason: "test error"
29+
};
30+
31+
const error: string = "[path/to /file.proto:1:5] test error";
32+
const actual = parseProtoError(error);
33+
34+
assert.deepStrictEqual(actual, expected);
35+
});
36+
});
37+
});

0 commit comments

Comments
 (0)