Skip to content
This repository has been archived by the owner on Oct 12, 2022. It is now read-only.

working proposal for autofix based on eslint example #47

Merged
merged 13 commits into from
May 24, 2016
2 changes: 1 addition & 1 deletion tslint-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"semver": "^5.1.0"
},
"devDependencies": {
"typescript": "^1.6.2"
"typescript": "^1.8.9"
},
"scripts": {
"compile": "installServerIntoExtension ../tslint ./package.json ./src/tsconfig.json && tsc -p ./src",
Expand Down
213 changes: 202 additions & 11 deletions tslint-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as minimatch from 'minimatch';
import * as server from 'vscode-languageserver';
import * as fs from 'fs';
import * as autofix from './tslintAutoFix';

// Settings as defined in VS Code
interface Settings {
Expand All @@ -19,6 +20,63 @@ interface Settings {
};
}

interface Map<V> {
[key: string]: V;
}

class ID {
private static base: string = `${Date.now().toString()}-`;
private static counter: number = 0;
public static next(): string {
return `${ID.base}${ID.counter++}`;
}
}

function computeKey(diagnostic: server.Diagnostic): string {
let range = diagnostic.range;
return `[${range.start.line},${range.start.character},${range.end.line},${range.end.character}]-${diagnostic.code}`;
}

export interface TSLintPosition {
line: number;
character: number;
position: number;
}

export interface TSLintAutofixEdit {
range: [server.Position, server.Position];
text: string;
}

export interface AutoFix {
label: string;
documentVersion: number;
ruleId: string;
edit: TSLintAutofixEdit;
}

export interface TSLintProblem {
fix?: TSLintAutofixEdit;
failure: string;
startPosition: TSLintPosition;
endPosition: TSLintPosition;
ruleName: string;
}

export interface TSLintDocumentReport {
filePath: string;
errorCount: number;
warningCount: number;
messages: TSLintProblem[];
output?: string;
}

export interface TSLintReport {
errorCount: number;
warningCount: number;
results: TSLintDocumentReport[];
}

let settings: Settings = null;

let linter: typeof Lint.Linter = null;
Expand All @@ -43,8 +101,8 @@ let configCache = {
isDefaultConfig: false
};

function makeDiagnostic(problem: any): server.Diagnostic {
return {
function makeDiagnostic(problem: TSLintProblem): server.Diagnostic {
let diagnostic: server.Diagnostic = {
severity: server.DiagnosticSeverity.Warning,
message: problem.failure,
range: {
Expand All @@ -60,6 +118,41 @@ function makeDiagnostic(problem: any): server.Diagnostic {
code: problem.ruleName,
source: 'tslint'
};

return diagnostic;
}

let codeActions: Map<Map<AutoFix>> = Object.create(null);
function recordCodeAction(document: server.TextDocument, diagnostic: server.Diagnostic, problem: TSLintProblem): void {

let afix = autofix.tsLintAutoFixes.filter(autoFix => autoFix.tsLintMessage === problem.failure);
if (afix.length > 0) {

// createAnAutoFixEntryInTheCodeActions
let uri = document.uri;
let edits: Map<AutoFix> = codeActions[uri];
if (!edits) {
edits = Object.create(null);
codeActions[uri] = edits;
}

/** temporary variable for debugging purpose
* it's not possible to use console.log to trace the autofx rules.
* so uncomment the following variable put a break point on the line and check in/out of autofix rules
*/
// let debugCodeBefore = document.getText().slice(problem.startPosition.position, problem.endPosition.position);
// let debugCodeAfter = afix[0].autoFix(document.getText().slice(problem.startPosition.position, problem.endPosition.position));

edits[computeKey(diagnostic)] = {
label: `Fix this "${problem.failure}" tslint failure?`,
documentVersion: document.version,
ruleId: problem.failure,
edit: {
range: [problem.startPosition, problem.endPosition],
text: afix[0].autoFix(document.getText().slice(problem.startPosition.position, problem.endPosition.position))
}
};
}
}

function getConfiguration(filePath: string, configFileName: string): any {
Expand All @@ -69,7 +162,7 @@ function getConfiguration(filePath: string, configFileName: string): any {

let isDefaultConfig = false;
if (linter.findConfigurationPath) {
isDefaultConfig = linter.findConfigurationPath(configFileName, filePath) === undefined
isDefaultConfig = linter.findConfigurationPath(configFileName, filePath) === undefined;
}
configCache = {
filePath: filePath,
Expand Down Expand Up @@ -138,7 +231,7 @@ connection.onInitialize((params): Thenable<server.InitializeResult | server.Resp
return server.Files.resolveModule(rootFolder, 'tslint').
then((value): server.InitializeResult | server.ResponseError<server.InitializeError> => {
linter = value;
let result: server.InitializeResult = { capabilities: { textDocumentSync: documents.syncKind } };
let result: server.InitializeResult = { capabilities: { textDocumentSync: documents.syncKind, codeActionProvider: true } };
return result;
}, (error) => {
return Promise.reject(
Expand All @@ -151,6 +244,8 @@ connection.onInitialize((params): Thenable<server.InitializeResult | server.Resp
function doValidate(conn: server.IConnection, document: server.TextDocument): server.Diagnostic[] {
let uri = document.uri;
let diagnostics: server.Diagnostic[] = [];
// Clean previously computed code actions.
delete codeActions[uri];

let fsPath = server.Files.uriToFilePath(uri);
if (!fsPath) {
Expand Down Expand Up @@ -190,17 +285,19 @@ function doValidate(conn: server.IConnection, document: server.TextDocument): se
}

if (result.failureCount > 0) {
let problems: any[] = JSON.parse(result.output);
problems.forEach(each => {
diagnostics.push(makeDiagnostic(each));
let lintProblems: any[] = JSON.parse(result.output);
lintProblems.forEach(problem => {
let diagnostic = makeDiagnostic(problem);
diagnostics.push(diagnostic);
recordCodeAction(document, diagnostic, problem);
});
}
return diagnostics;
}

function fileIsExcluded(path: string): boolean {
function testForExclusionPattern(path: string, pattern: string): boolean {
return minimatch(path, pattern)
return minimatch(path, pattern);
}

if (settings && settings.tslint) {
Expand All @@ -212,13 +309,12 @@ function fileIsExcluded(path: string): boolean {

if (settings.tslint.exclude) {
if (Array.isArray(settings.tslint.exclude)) {
for (var pattern of settings.tslint.exclude) {
for (let pattern of settings.tslint.exclude) {
if (testForExclusionPattern(path, pattern)) {
return true;
}
}
}
else if (testForExclusionPattern(path, <string>settings.tslint.exclude)) {
} else if (testForExclusionPattern(path, <string>settings.tslint.exclude)) {
return true;
}
}
Expand Down Expand Up @@ -293,4 +389,99 @@ connection.onDidChangeWatchedFiles((params) => {
}
});

connection.onCodeAction((params) => {
let result: server.Command[] = [];
let uri = params.textDocument.uri;
let edits = codeActions[uri];
let documentVersion: number = -1;
let ruleId: string;
function createTextEdit(editInfo: AutoFix): server.TextEdit {
return server.TextEdit.replace(
server.Range.create(
editInfo.edit.range[0],
editInfo.edit.range[1]),
editInfo.edit.text || '');
}
if (edits) {
for (let diagnostic of params.context.diagnostics) {
let key = computeKey(diagnostic);
let editInfo = edits[key];
if (editInfo) {
documentVersion = editInfo.documentVersion;
ruleId = editInfo.ruleId;
result.push(server.Command.create(editInfo.label, 'tslint.applySingleFix', uri, documentVersion, [
createTextEdit(editInfo)
]));
}
}
if (result.length > 0) {
let same: AutoFix[] = [];
let all: AutoFix[] = [];
let fixes: AutoFix[] = Object.keys(edits).map(key => edits[key]);

// TODO from eslint: why? order the fixes for? overlap?
// fixes = fixes.sort((a, b) => {
// let d = a.edit.range[0] - b.edit.range[0];
// if (d !== 0) {
// return d;
// }
// if (a.edit.range[1] === 0) {
// return -1;
// }
// if (b.edit.range[1] === 0) {
// return 1;
// }
// return a.edit.range[1] - b.edit.range[1];
// });

// TODO check if there are fixes overlaps
function overlaps(lastEdit: AutoFix, newEdit: AutoFix): boolean {
return !!lastEdit && lastEdit.edit.range[1] > newEdit.edit.range[0];
}
function getLastEdit(array: AutoFix[]): AutoFix {
let length = array.length;
if (length === 0) {
return undefined;
}
return array[length - 1];
}

// TODO group the edit?
for (let editInfo of fixes) {
if (documentVersion === -1) {
documentVersion = editInfo.documentVersion;
}
if (editInfo.ruleId === ruleId && !overlaps(getLastEdit(same), editInfo)) {
same.push(editInfo);
}
if (!overlaps(getLastEdit(all), editInfo)) {
all.push(editInfo);
}
}

// if there several time the same rule identified => propose to fix all
if (same.length > 1) {
result.push(
server.Command.create(
`Fix all "${same[0].ruleId}" tslint failures?`,
'tslint.applySameFixes',
uri,
documentVersion, same.map(createTextEdit)));
}

// TODO from eslint: why?
// if several type of same auto fixable problem => propose to fix all
// if (all.length > 1) {
// result.push(
// server.Command.create(
// `Fix all auto-fixable problems`,
// 'tslint.applyAllFixes',
// uri,
// documentVersion,
// all.map(createTextEdit)));
// }
}
}
return result;
});
connection.listen();
91 changes: 91 additions & 0 deletions tslint-server/src/tslintAutoFix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
export interface TsLintAutoFix {
tsLintCode: string;
tsLintMessage: string;
autoFixMessage: string;
autoFix(codeBefore: string): string;
}
export let tsLintAutoFixes: TsLintAutoFix[] = [];

/**
* AutoFix rules are all in this file
* each autoFix should support the interface TsLintAutoFix and added in this.tsLintAutoFixes
*
* the key to map tsLint problem and autofix rules is => tsLintMessage
*/
let autoFix: TsLintAutoFix;
autoFix = {
tsLintCode: "one-line",
tsLintMessage: "missing whitespace",
autoFixMessage: "Add a whitespace",
autoFix: (codeBefore: string): string => {
let codeAfter = " " + codeBefore;
return codeAfter;
}
};
this.tsLintAutoFixes.push(autoFix);

autoFix = {
tsLintCode: "one-line",
tsLintMessage: "missing semicolon",
autoFixMessage: "Add semicolon",
autoFix: (codeBefore: string): string => {
let codeAfter = codeBefore + ";";
return codeAfter;
}
};
this.tsLintAutoFixes.push(autoFix);

autoFix = {
tsLintCode: "quotemark",
tsLintMessage: "' should be \"",
autoFixMessage: "Replace ' by \" ",
autoFix: (codeBefore: string): string => {
let codeAfter = "\"" + codeBefore.slice(1, codeBefore.length - 1) + "\"";
return codeAfter;
}
};
this.tsLintAutoFixes.push(autoFix);

autoFix = {
tsLintCode: "no-trailing-whitespace",
tsLintMessage: "trailing whitespace",
autoFixMessage: "Trim whitespace",
autoFix: (codeBefore: string): string => {
let codeAfter = "";
return codeAfter;
}
};
this.tsLintAutoFixes.push(autoFix);

autoFix = {
tsLintCode: "indent",
tsLintMessage: "tab indentation expected",
autoFixMessage: "Replace 4 spaces by 1 tab",
autoFix: (codeBefore: string): string => {
let codeAfter = " ";
return codeAfter;
}
};
this.tsLintAutoFixes.push(autoFix);

autoFix = {
tsLintCode: "indent",
tsLintMessage: "space indentation expected",
autoFixMessage: "Replace 1 tab by 4 spaces",
autoFix: (codeBefore: string): string => {
let codeAfter = " ";
return codeAfter;
}
};
this.tsLintAutoFixes.push(autoFix);

autoFix = {
tsLintCode: "eofline",
tsLintMessage: "file should end with a newline",
autoFixMessage: "add new line",
autoFix: (codeBefore: string): string => {
let codeAfter = "\n";
return codeAfter;
}
};
this.tsLintAutoFixes.push(autoFix);
Loading