alm
Version:
The best IDE for TypeScript
323 lines (285 loc) • 11.2 kB
text/typescript
/**
* The heart of the linter
*/
/**
* Load up TypeScript
*/
import * as byots from "byots";
const ensureImport = byots;
import * as sw from "../../utils/simpleWorker";
import * as contract from "./lintContract";
import { resolve, timer } from "../../../common/utils";
import * as utils from "../../../common/utils";
import * as types from "../../../common/types";
import { LanguageServiceHost } from "../../../languageServiceHost/languageServiceHostNode";
import { isFileInTypeScriptDir } from "../lang/core/typeScriptDir";
import { ErrorsCache } from "../../utils/errorsCache";
/** Bring in tslint */
import { Configuration, RuleFailure, Linter as LinterLocal } from "tslint";
/** The linter currently in use */
let Linter = LinterLocal;
/** TSLint types only used in options */
import { IConfigurationFile } from "../../../../node_modules/tslint/lib/configuration";
/**
* Horrible file access :)
*/
import * as fsu from "../../utils/fsu";
const linterMessagePrefix = `[LINT]`
namespace Worker {
export const setProjectData: typeof contract.worker.setProjectData = (data) => {
/** Load a local linter if any */
const basedir = utils.getDirectory(data.configFile.projectFilePath);
return getLocalLinter(basedir).then((linter) => {
Linter = linter;
LinterImplementation.setProjectData(data);
return {};
})
}
export const fileSaved: typeof contract.worker.fileSaved = (data) => {
LinterImplementation.fileSaved(data);
return resolve({});
}
}
// Ensure that the namespace follows the contract
const _checkTypes: typeof contract.worker = Worker;
// run worker
export const {master} = sw.runWorker({
workerImplementation: Worker,
masterContract: contract.master
});
/**
* The actual linter stuff lives in this namespace
*/
namespace LinterImplementation {
/** The tslint linter takes a few configuration options before it can lint a file */
interface LinterConfig {
projectData: types.ProjectDataLoaded;
ls: ts.LanguageService;
lsh: LanguageServiceHost;
/** Only if there is a valid linter config found */
linterConfig?: {
configuration: IConfigurationFile,
rulesDirectory: string | string[]
}
}
let linterConfig: LinterConfig | null = null;
/** We only do this once per project change */
let informedUserAboutMissingConfig: boolean = false;
/** Our error cache */
const errorCache = new ErrorsCache();
errorCache.errorsDelta.on(master.receiveErrorCacheDelta);
/**
* This is the entry point for the linter to start its work
*/
export function setProjectData(projectData: types.ProjectDataLoaded) {
/** Reinit */
errorCache.clearErrors();
informedUserAboutMissingConfig = false;
linterConfig = null;
/**
* Create the program
*/
const languageServiceHost = new LanguageServiceHost(undefined, projectData.configFile.project.compilerOptions);
// Add all the files
projectData.filePathWithContents.forEach(({filePath, contents}) => {
languageServiceHost.addScript(filePath, contents);
});
// And for incremental ones lint again
languageServiceHost.incrementallyAddedFile.on((data) => {
// console.log(data); // DEBUG
loadLintConfigAndLint();
});
const languageService = ts.createLanguageService(languageServiceHost, ts.createDocumentRegistry());
/**
* We must call get program before making any changes to the files otherwise TypeScript throws up
* we don't actually use the program just yet :)
*/
const program = languageService.getProgram();
/**
* Now create the tslint config
*/
linterConfig = {
projectData,
ls: languageService,
lsh: languageServiceHost,
};
loadLintConfigAndLint();
}
/**
* Called whenever
* - a file is edited
* - added to the compilation context
*/
function loadLintConfigAndLint() {
linterConfig.linterConfig = undefined;
errorCache.clearErrors();
/** Look for tslint.json by findup from the project dir */
const projectDir = linterConfig.projectData.configFile.projectFileDirectory;
const configurationPath = Linter.findConfigurationPath(null, projectDir);
// console.log({configurationPath}); // DEBUG
/** lint abort if the config is not ready present yet */
if (!configurationPath) {
if (!informedUserAboutMissingConfig) {
informedUserAboutMissingConfig = true;
console.log(linterMessagePrefix, 'No tslint configuration found.');
}
return;
}
/** We have our configuration file. Now lets convert it to configuration :) */
let configuration: IConfigurationFile;
try {
configuration = Linter.loadConfigurationFromPath(configurationPath);
}
catch (err) {
console.log(linterMessagePrefix, 'Invalid config:', configurationPath);
errorCache.setErrorsByFilePaths([configurationPath], [types.makeBlandError(configurationPath, err.message, 'linter')]);
return;
}
/** Also need to setup the rules directory */
const possiblyRelativeRulesDirectory = configuration.rulesDirectory;
const rulesDirectory = Linter.getRulesDirectories(possiblyRelativeRulesDirectory, configurationPath);
/**
* The linter config is now also good to go
*/
linterConfig.linterConfig = {
configuration, rulesDirectory
}
/** Now start the lazy lint */
lintWithCancellationToken();
}
/** lint support cancellation token */
let cancellationToken = utils.cancellationToken();
function lintWithCancellationToken() {
/** Cancel any previous */
if (cancellationToken) {
cancellationToken.cancel();
cancellationToken = utils.cancellationToken();
}
const program = linterConfig.ls.getProgram();
const sourceFiles =
program.getSourceFiles()
.filter(x => !x.isDeclarationFile);
console.log(linterMessagePrefix, 'About to start linting files: ', sourceFiles.length); // DEBUG
// Note: tslint is a big stingy with its definitions so we use `any` to make our ts def compat with its ts defs.
const lintprogram = program as any;
/** Used to push to the errorCache */
const filePaths: string[] = [];
let errors: types.CodeError[] = [];
const time = timer();
/** create the Linter for each file and get its output */
utils
.cancellableForEach({
cancellationToken,
items: sourceFiles,
cb: (sf => {
const filePath = sf.fileName;
const contents = sf.getFullText();
const linter = new Linter({
rulesDirectory: linterConfig.linterConfig.rulesDirectory,
fix: false,
}, lintprogram);
linter.lint(filePath, contents, linterConfig.linterConfig.configuration);
const lintResult = linter.getResult();
filePaths.push(filePath);
if (lintResult.errorCount || lintResult.warningCount) {
// console.log(linterMessagePrefix, filePath, lintResult.failureCount); // DEBUG
errors = errors.concat(
lintResult.failures.map(
le => lintErrorToCodeError(le, contents)
)
);
}
})
})
.then((res) => {
/** Push to errorCache */
errorCache.setErrorsByFilePaths(filePaths, errors);
console.log(linterMessagePrefix, 'Lint complete', time.seconds);
})
.catch((e) => {
if (e === utils.cancelled) {
console.log(linterMessagePrefix, 'Lint cancelled');
}
else {
console.log(linterMessagePrefix, 'Linter crashed', e);
}
});
}
export function fileSaved({filePath}: { filePath: string }) {
if (!linterConfig) {
return;
}
/** tslint : do the whole thing */
if (filePath.endsWith('tslint.json')) {
loadLintConfigAndLint();
return;
}
/**
* Now only proceed further if we have a linter config
* and the file is a ts file
* and in the current project
*/
if (!linterConfig.linterConfig) {
return;
}
if (!filePath.endsWith('.ts')) {
return;
}
const sf = linterConfig.ls.getProgram().getSourceFiles().find(sf => sf.fileName === filePath);
if (!sf) {
return;
}
/** Update the file contents (so that when we get the program it just works) */
linterConfig.lsh.setContents(filePath, fsu.readFile(sf.fileName));
/**
* Since we use program and types flow we would still need to lint the whole thing
*/
lintWithCancellationToken();
}
/** Utility */
function lintErrorToCodeError(lintError: RuleFailure, contents: string): types.CodeError {
const start = lintError.getStartPosition().getLineAndCharacter();
const end = lintError.getEndPosition().getLineAndCharacter();
const preview = contents.substring(
lintError.getStartPosition().getPosition(),
lintError.getEndPosition().getPosition()
);
const result: types.CodeError = {
source: 'linter',
filePath: lintError.getFileName(),
message: lintError.getFailure(),
from: {
line: start.line,
ch: start.character
},
to: {
line: end.line,
ch: end.character
},
preview: preview,
level: 'warning',
}
return result;
}
}
/**
* From
* https://github.com/AtomLinter/linter-tslint/blob/2c8e99da92f9f2392adf26f5dd49d78a1a4ef753/lib/main.js
*/
const TSLINT_MODULE_NAME = 'tslint';
import * as requireResolve from 'resolve';
function getLocalLinter(basedir: string) {
return new Promise<typeof LinterLocal>(resolve =>
requireResolve(TSLINT_MODULE_NAME, { basedir },
(err, linterPath, pkg) => {
let linter;
if (!err && pkg && /^4\./.test(pkg.version)) {
linter = (require(linterPath) as any).Linter;
} else {
linter = LinterLocal;
}
return resolve(linter);
},
),
);
}