You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
12 KiB
262 lines
12 KiB
"use strict"; |
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
const fs = require("fs"); |
|
const path = require("path"); |
|
const FilesRegister_1 = require("./FilesRegister"); |
|
const FilesWatcher_1 = require("./FilesWatcher"); |
|
const linterConfigHelpers_1 = require("./linterConfigHelpers"); |
|
const WorkSet_1 = require("./WorkSet"); |
|
const NormalizedMessage_1 = require("./NormalizedMessage"); |
|
const resolution_1 = require("./resolution"); |
|
const minimatch = require("minimatch"); |
|
const VueProgram_1 = require("./VueProgram"); |
|
const FsHelper_1 = require("./FsHelper"); |
|
class IncrementalChecker { |
|
constructor({ typescript, context, programConfigFile, compilerOptions, createNormalizedMessageFromDiagnostic, linterConfigFile, linterAutoFix, createNormalizedMessageFromRuleFailure, eslinter, watchPaths, workNumber = 0, workDivision = 1, vue = false, checkSyntacticErrors = false, resolveModuleName, resolveTypeReferenceDirective }) { |
|
// it's shared between compilations |
|
this.linterConfigs = {}; |
|
this.files = new FilesRegister_1.FilesRegister(() => ({ |
|
// data shape |
|
source: undefined, |
|
linted: false, |
|
tslints: [], |
|
eslints: [] |
|
})); |
|
// Use empty array of exclusions in general to avoid having |
|
// to check of its existence later on. |
|
this.linterExclusions = []; |
|
this.getLinterConfig = linterConfigHelpers_1.makeGetLinterConfig(this.linterConfigs, this.linterExclusions, this.context); |
|
this.typescript = typescript; |
|
this.context = context; |
|
this.programConfigFile = programConfigFile; |
|
this.compilerOptions = compilerOptions; |
|
this.createNormalizedMessageFromDiagnostic = createNormalizedMessageFromDiagnostic; |
|
this.linterConfigFile = linterConfigFile; |
|
this.linterAutoFix = linterAutoFix; |
|
this.createNormalizedMessageFromRuleFailure = createNormalizedMessageFromRuleFailure; |
|
this.eslinter = eslinter; |
|
this.watchPaths = watchPaths; |
|
this.workNumber = workNumber; |
|
this.workDivision = workDivision; |
|
this.vue = vue; |
|
this.checkSyntacticErrors = checkSyntacticErrors; |
|
this.resolveModuleName = resolveModuleName; |
|
this.resolveTypeReferenceDirective = resolveTypeReferenceDirective; |
|
this.hasFixedConfig = typeof this.linterConfigFile === 'string'; |
|
} |
|
static loadProgramConfig(typescript, configFile, compilerOptions) { |
|
const tsconfig = typescript.readConfigFile(configFile, typescript.sys.readFile).config; |
|
tsconfig.compilerOptions = tsconfig.compilerOptions || {}; |
|
tsconfig.compilerOptions = Object.assign({}, tsconfig.compilerOptions, compilerOptions); |
|
const parsed = typescript.parseJsonConfigFileContent(tsconfig, typescript.sys, path.dirname(configFile)); |
|
return parsed; |
|
} |
|
static createProgram(typescript, programConfig, files, watcher, oldProgram, userResolveModuleName, userResolveTypeReferenceDirective) { |
|
const host = typescript.createCompilerHost(programConfig.options); |
|
const realGetSourceFile = host.getSourceFile; |
|
const { resolveModuleName, resolveTypeReferenceDirective } = resolution_1.makeResolutionFunctions(userResolveModuleName, userResolveTypeReferenceDirective); |
|
host.resolveModuleNames = (moduleNames, containingFile) => { |
|
return moduleNames.map(moduleName => { |
|
return resolveModuleName(typescript, moduleName, containingFile, programConfig.options, host).resolvedModule; |
|
}); |
|
}; |
|
host.resolveTypeReferenceDirectives = (typeDirectiveNames, containingFile) => { |
|
return typeDirectiveNames.map(typeDirectiveName => { |
|
return resolveTypeReferenceDirective(typescript, typeDirectiveName, containingFile, programConfig.options, host).resolvedTypeReferenceDirective; |
|
}); |
|
}; |
|
host.getSourceFile = (filePath, languageVersion, onError) => { |
|
// first check if watcher is watching file - if not - check it's mtime |
|
if (!watcher.isWatchingFile(filePath)) { |
|
try { |
|
const stats = fs.statSync(filePath); |
|
files.setMtime(filePath, stats.mtime.valueOf()); |
|
} |
|
catch (e) { |
|
// probably file does not exists |
|
files.remove(filePath); |
|
} |
|
} |
|
// get source file only if there is no source in files register |
|
if (!files.has(filePath) || !files.getData(filePath).source) { |
|
files.mutateData(filePath, data => { |
|
data.source = realGetSourceFile(filePath, languageVersion, onError); |
|
}); |
|
} |
|
return files.getData(filePath).source; |
|
}; |
|
return typescript.createProgram(programConfig.fileNames, programConfig.options, host, oldProgram // re-use old program |
|
); |
|
} |
|
createLinter(program) { |
|
// tslint:disable-next-line:no-implicit-dependencies |
|
const tslint = require('tslint'); |
|
return new tslint.Linter({ fix: this.linterAutoFix }, program); |
|
} |
|
hasLinter() { |
|
return !!this.linter; |
|
} |
|
hasEsLinter() { |
|
return this.eslinter !== undefined; |
|
} |
|
static isFileExcluded(filePath, linterExclusions) { |
|
return (filePath.endsWith('.d.ts') || |
|
linterExclusions.some(matcher => matcher.match(filePath))); |
|
} |
|
nextIteration() { |
|
if (!this.watcher) { |
|
const watchExtensions = this.vue |
|
? ['.ts', '.tsx', '.vue'] |
|
: ['.ts', '.tsx']; |
|
this.watcher = new FilesWatcher_1.FilesWatcher(this.watchPaths, watchExtensions); |
|
// connect watcher with register |
|
this.watcher.on('change', (filePath, stats) => { |
|
this.files.setMtime(filePath, stats.mtime.valueOf()); |
|
}); |
|
this.watcher.on('unlink', (filePath) => { |
|
this.files.remove(filePath); |
|
}); |
|
this.watcher.watch(); |
|
} |
|
if (!this.linterConfig && this.hasFixedConfig) { |
|
this.linterConfig = linterConfigHelpers_1.loadLinterConfig(this.linterConfigFile); |
|
if (this.linterConfig.linterOptions && |
|
this.linterConfig.linterOptions.exclude) { |
|
// Pre-build minimatch patterns to avoid additional overhead later on. |
|
// Note: Resolving the path is required to properly match against the full file paths, |
|
// and also deals with potential cross-platform problems regarding path separators. |
|
this.linterExclusions = this.linterConfig.linterOptions.exclude.map(pattern => new minimatch.Minimatch(path.resolve(pattern))); |
|
} |
|
} |
|
this.program = this.vue ? this.loadVueProgram() : this.loadDefaultProgram(); |
|
if (this.linterConfigFile) { |
|
this.linter = this.createLinter(this.program); |
|
} |
|
} |
|
loadVueProgram() { |
|
this.programConfig = |
|
this.programConfig || |
|
VueProgram_1.VueProgram.loadProgramConfig(this.typescript, this.programConfigFile, this.compilerOptions); |
|
return VueProgram_1.VueProgram.createProgram(this.typescript, this.programConfig, path.dirname(this.programConfigFile), this.files, this.watcher, this.program, this.resolveModuleName, this.resolveTypeReferenceDirective); |
|
} |
|
loadDefaultProgram() { |
|
this.programConfig = |
|
this.programConfig || |
|
IncrementalChecker.loadProgramConfig(this.typescript, this.programConfigFile, this.compilerOptions); |
|
return IncrementalChecker.createProgram(this.typescript, this.programConfig, this.files, this.watcher, this.program, this.resolveModuleName, this.resolveTypeReferenceDirective); |
|
} |
|
getDiagnostics(cancellationToken) { |
|
const { program } = this; |
|
if (!program) { |
|
throw new Error('Invoked called before program initialized'); |
|
} |
|
const diagnostics = []; |
|
// select files to check (it's semantic check - we have to include all files :/) |
|
const filesToCheck = program.getSourceFiles(); |
|
// calculate subset of work to do |
|
const workSet = new WorkSet_1.WorkSet(filesToCheck, this.workNumber, this.workDivision); |
|
// check given work set |
|
workSet.forEach(sourceFile => { |
|
if (cancellationToken) { |
|
cancellationToken.throwIfCancellationRequested(); |
|
} |
|
const diagnosticsToRegister = this |
|
.checkSyntacticErrors |
|
? program |
|
.getSemanticDiagnostics(sourceFile, cancellationToken) |
|
.concat(program.getSyntacticDiagnostics(sourceFile, cancellationToken)) |
|
: program.getSemanticDiagnostics(sourceFile, cancellationToken); |
|
diagnostics.push(...diagnosticsToRegister); |
|
}); |
|
// normalize and deduplicate diagnostics |
|
return Promise.resolve(NormalizedMessage_1.NormalizedMessage.deduplicate(diagnostics.map(this.createNormalizedMessageFromDiagnostic))); |
|
} |
|
getLints(cancellationToken) { |
|
const { linter } = this; |
|
if (!linter) { |
|
throw new Error('Cannot get lints - checker has no linter.'); |
|
} |
|
// select files to lint |
|
const filesToLint = this.files |
|
.keys() |
|
.filter(filePath => !this.files.getData(filePath).linted && |
|
!IncrementalChecker.isFileExcluded(filePath, this.linterExclusions)); |
|
// calculate subset of work to do |
|
const workSet = new WorkSet_1.WorkSet(filesToLint, this.workNumber, this.workDivision); |
|
// lint given work set |
|
workSet.forEach(fileName => { |
|
cancellationToken.throwIfCancellationRequested(); |
|
const config = this.hasFixedConfig |
|
? this.linterConfig |
|
: this.getLinterConfig(fileName); |
|
if (!config) { |
|
return; |
|
} |
|
try { |
|
// Assertion: `.lint` second parameter can be undefined |
|
linter.lint(fileName, undefined, config); |
|
} |
|
catch (e) { |
|
FsHelper_1.throwIfIsInvalidSourceFileError(fileName, e); |
|
} |
|
}); |
|
// set lints in files register |
|
linter.getResult().failures.forEach(lint => { |
|
const filePath = lint.getFileName(); |
|
this.files.mutateData(filePath, data => { |
|
data.linted = true; |
|
data.tslints.push(lint); |
|
}); |
|
}); |
|
// set all files as linted |
|
this.files.keys().forEach(filePath => { |
|
this.files.mutateData(filePath, data => { |
|
data.linted = true; |
|
}); |
|
}); |
|
// get all lints |
|
const lints = this.files |
|
.keys() |
|
.reduce((innerLints, filePath) => innerLints.concat(this.files.getData(filePath).tslints), []); |
|
// normalize and deduplicate lints |
|
return NormalizedMessage_1.NormalizedMessage.deduplicate(lints.map(this.createNormalizedMessageFromRuleFailure)); |
|
} |
|
getEsLints(cancellationToken) { |
|
// select files to lint |
|
const filesToLint = this.files |
|
.keys() |
|
.filter(filePath => !this.files.getData(filePath).linted && |
|
!IncrementalChecker.isFileExcluded(filePath, this.linterExclusions)); |
|
// calculate subset of work to do |
|
const workSet = new WorkSet_1.WorkSet(filesToLint, this.workNumber, this.workDivision); |
|
// lint given work set |
|
const currentEsLintErrors = new Map(); |
|
workSet.forEach(fileName => { |
|
cancellationToken.throwIfCancellationRequested(); |
|
const lints = this.eslinter.getLints(fileName); |
|
if (lints !== undefined) { |
|
currentEsLintErrors.set(fileName, lints); |
|
} |
|
}); |
|
// set lints in files register |
|
for (const [filePath, lint] of currentEsLintErrors) { |
|
this.files.mutateData(filePath, data => { |
|
data.linted = true; |
|
data.eslints.push(lint); |
|
}); |
|
} |
|
// set all files as linted |
|
this.files.keys().forEach(filePath => { |
|
this.files.mutateData(filePath, data => { |
|
data.linted = true; |
|
}); |
|
}); |
|
// get all lints |
|
const allEsLintReports = this.files |
|
.keys() |
|
.reduce((innerLints, filePath) => innerLints.concat(this.files.getData(filePath).eslints), []); |
|
return this.eslinter.getFormattedLints(allEsLintReports); |
|
} |
|
} |
|
exports.IncrementalChecker = IncrementalChecker; |
|
//# sourceMappingURL=IncrementalChecker.js.map
|