Port linter from vscode-elm

This commit is contained in:
Kolja Lampe
2018-07-09 21:34:07 +02:00
parent 27626b9b4d
commit d992e39e16
4 changed files with 269 additions and 5 deletions

View File

@@ -18,7 +18,7 @@
"vscode": "^1.25.0"
},
"activationEvents": [
"onLanguage:plaintext"
"onLanguage:elm"
],
"main": "./client/out/extension",
"contributes": {

182
server/src/elmLinter.ts Normal file
View File

@@ -0,0 +1,182 @@
import * as cp from 'child_process';
import * as readline from 'readline';
import * as utils from './elmUtils';
import * as vscode from 'vscode-languageserver/lib/main';
import Uri from 'vscode-uri/lib/umd';
import { Diagnostic } from 'vscode-languageserver/lib/main';
export interface IElmIssueRegion {
start: { line: number; column: number };
end: { line: number; column: number };
}
export interface IElmIssue {
tag: string;
overview: string;
subregion: string;
details: string;
region: IElmIssueRegion;
type: string;
file: string;
}
function severityStringToDiagnosticSeverity(
severity: string,
): vscode.DiagnosticSeverity {
switch (severity) {
case 'error':
return vscode.DiagnosticSeverity.Error;
case 'warning':
return vscode.DiagnosticSeverity.Warning;
default:
return vscode.DiagnosticSeverity.Error;
}
}
function elmMakeIssueToDiagnostic(issue: IElmIssue): vscode.Diagnostic {
let lineRange: vscode.Range = vscode.Range.create(
issue.region.start.line - 1,
issue.region.start.column - 1,
issue.region.end.line - 1,
issue.region.end.column - 1,
);
return vscode.Diagnostic.create(
lineRange,
issue.overview + ' - ' + issue.details.replace(/\[\d+m/g, ''),
severityStringToDiagnosticSeverity(issue.type),
);
}
function checkForErrors(connection: vscode.Connection, rootPath: string, filename: string): Promise<IElmIssue[]> {
return new Promise((resolve, reject) => {
const makeCommand: string = 'elm-make';
const cwd: string =
utils.detectProjectRoot(filename) || rootPath;
let make: cp.ChildProcess;
if (utils.isWindows) {
filename = "\"" + filename + "\""
}
const args = [filename, '--report', 'json', '--output', '/dev/null'];
if (utils.isWindows) {
make = cp.exec(makeCommand + ' ' + args.join(' '), { cwd: cwd });
} else {
make = cp.spawn(makeCommand, args, { cwd: cwd });
}
// output is actually optional
// (fixed in https://github.com/Microsoft/vscode/commit/b4917afe9bdee0e9e67f4094e764f6a72a997c70,
// but unreleased at this time)
const stdoutlines: readline.ReadLine = readline.createInterface({
input: make.stdout,
output: undefined,
});
const lines: IElmIssue[] = [];
stdoutlines.on('line', (line: string) => {
// Ignore compiler success.
if (line.startsWith('Successfully generated')) {
return;
}
// Elm writes out JSON arrays of diagnostics, with one array per line.
// Multiple lines may be received.
lines.push(...(<IElmIssue[]>JSON.parse(line)));
});
const stderr: Buffer[] = [];
make.stderr.on('data', (data: Buffer) => {
if (data) {
stderr.push(data);
}
});
make.on('error', (err: Error) => {
stdoutlines.close();
if (err && (<any>err).code === 'ENOENT') {
connection.console.log(
"The 'elm-make' compiler is not available. Install Elm from http://elm-lang.org/.",
);
resolve([]);
} else {
reject(err);
}
});
make.on('close', (code: number, signal: string) => {
stdoutlines.close();
if (stderr.length) {
let errorResult: IElmIssue = {
tag: 'error',
overview: '',
subregion: '',
details: stderr.join(''),
region: {
start: {
line: 1,
column: 1,
},
end: {
line: 1,
column: 1,
},
},
type: 'error',
file: filename,
};
resolve([errorResult]);
} else {
resolve(lines);
}
});
});
}
export function runLinter(
connection: vscode.Connection,
rootPath: string,
document: vscode.TextDocument,
// elmAnalyse: ElmAnalyse,
): void {
let compileErrors: vscode.Diagnostic[] = [];
let uri: Uri = Uri.parse(document.uri);
checkForErrors(connection, rootPath, uri.fsPath)
.then((compilerErrors: IElmIssue[]) => {
const cwd: string =
utils.detectProjectRoot(uri.fsPath) || rootPath;
let splitCompilerErrors: Map<string, IElmIssue[]> = new Map();
compilerErrors.forEach((issue: IElmIssue) => {
// If provided path is relative, make it absolute
if (issue.file.startsWith('.')) {
issue.file = cwd + issue.file.slice(1);
}
if (splitCompilerErrors.has(issue.file)) {
splitCompilerErrors.get(issue.file).push(issue);
} else {
splitCompilerErrors.set(issue.file, [issue]);
}
});
// Turn split arrays into diagnostics and associate them with correct files in VS
splitCompilerErrors.forEach((issue: IElmIssue[], path: string) => {
connection.sendDiagnostics({
uri: document.uri,
diagnostics: issue.map(error => elmMakeIssueToDiagnostic(error)),
});
});
})
.catch(error => {
});
// if (elmAnalyse.elmAnalyseIssues.length > 0) {
// let splitCompilerErrors: Map<string, IElmIssue[]> = new Map();
// elmAnalyse.elmAnalyseIssues.forEach((issue: IElmIssue) => {
// if (splitCompilerErrors.has(issue.file)) {
// splitCompilerErrors.get(issue.file).push(issue);
// } else {
// splitCompilerErrors.set(issue.file, [issue]);
// }
// splitCompilerErrors.forEach(
// (analyserIssue: IElmIssue[], path: string) => {
// compileErrors.concat(
// analyserIssue.map(error => elmMakeIssueToDiagnostic(error)),
// );
// },
// );
// });
// }
}

67
server/src/elmUtils.ts Normal file
View File

@@ -0,0 +1,67 @@
import * as cp from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode-languageserver';
export const isWindows = process.platform === 'win32';
/** Options for execCmd */
export interface ExecCmdOptions {
/** The project root folder for this file is used as the cwd of the process */
fileName?: string;
/** Any arguments */
cmdArguments?: string[];
/** Shows a message if an error occurs (in particular the command not being */
/* found), instead of rejecting. If this happens, the promise never resolves */
showMessageOnError?: boolean;
/** Called after the process successfully starts */
onStart?: () => void;
/** Called when data is sent to stdout */
onStdout?: (data: string) => void;
/** Called when data is sent to stderr */
onStderr?: (data: string) => void;
/** Called after the command (successfully or unsuccessfully) exits */
onExit?: () => void;
/** Text to add when command is not found (maybe helping how to install) */
notFoundText?: string;
}
export function findProj(dir: string): string {
if (fs.lstatSync(dir).isDirectory()) {
const files = fs.readdirSync(dir);
const file = files.find((v, i) => v === 'elm-package.json');
if (file !== undefined) {
return dir + path.sep + file;
}
let parent = '';
if (dir.lastIndexOf(path.sep) > 0) {
parent = dir.substr(0, dir.lastIndexOf(path.sep));
}
if (parent === '') {
return '';
} else {
return findProj(parent);
}
}
}
export function detectProjectRoot(fileName: string): string {
const proj = findProj(path.dirname(fileName));
if (proj !== '') {
return path.dirname(proj);
}
return undefined;
}
export function getIndicesOf(searchStr: string, str: string): number[] {
let startIndex = 0,
searchStrLen = searchStr.length;
let index,
indices = [];
while ((index = str.indexOf(searchStr, startIndex)) > -1) {
indices.push(index);
startIndex = index + searchStrLen;
}
return indices;
}

View File

@@ -1,6 +1,6 @@
'use strict';
import * as cp from 'child_process';
import Uri from 'vscode-uri'
import Uri from 'vscode-uri/lib/umd'
const Compiler = require('node-elm-compiler');
import {
createConnection,
@@ -18,6 +18,8 @@ import {
Position,
TextEdit
} from 'vscode-languageserver';
import { runLinter, IElmIssue } from './elmLinter';
// import { ElmAnalyse } from './elmAnalyse';
// Create a connection for the server. The connection uses Node's IPC as a transport.
// Also include all preview / proposed LSP features.
@@ -29,9 +31,11 @@ let documents: TextDocuments = new TextDocuments();
let hasConfigurationCapability: boolean = false;
let hasWorkspaceFolderCapability: boolean = false;
let rootPath: string = undefined;
connection.onInitialize((params: InitializeParams) => {
let capabilities = params.capabilities;
this.rootPath = params.rootPath;
// Does the client support the `workspace/configuration` request?
// If not, we will fall back using global settings
@@ -66,12 +70,20 @@ connection.onInitialized(() => {
}
});
documents.onDidOpen(params => {
validateTextDocument(params.document);
const elmAnalyseIssues: IElmIssue[] = [];
// const elmAnalyse = new ElmAnalyse(elmAnalyseIssues);
runLinter(connection, this.rootPath, params.document);
// validateTextDocument(params.document);
});
documents.onDidSave(params => {
validateTextDocument(params.document);
// const elmAnalyseIssues: IElmIssue[] = [];
// const elmAnalyse = new ElmAnalyse(elmAnalyseIssues);
runLinter(connection, this.rootPath, params.document);
// validateTextDocument(params.document);
});
async function validateTextDocument(textDocument: TextDocument): Promise<void> {
@@ -81,7 +93,10 @@ async function validateTextDocument(textDocument: TextDocument): Promise<void> {
let diagnostics: Diagnostic[] = []
try {
await Compiler.compileToString(uri.fsPath, { report: 'json' })
var x = await Compiler.findAllDependencies(uri.fsPath);
connection.console.log(x);
} catch (err) {
const issues = JSON.parse(err.message.split('\n')[1]);
const byFile = issues.reduce((acc: any, issue: any) => {