@fimbul/wotan
Version:
Pluggable TypeScript and JavaScript linter
241 lines • 10.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.module = void 0;
const tslib_1 = require("tslib");
const inversify_1 = require("inversify");
const ymir_1 = require("@fimbul/ymir");
const base_1 = require("./base");
const cached_file_system_1 = require("../services/cached-file-system");
const baseline_1 = require("../baseline");
const path = require("path");
const chalk = require("chalk");
const utils_1 = require("../utils");
const glob = require("glob");
const semver_1 = require("semver");
const runner_1 = require("../runner");
const ts = require("typescript");
const diff = require("diff");
const argparse_1 = require("../argparse");
const optparse_1 = require("../optparse");
const TEST_OPTION_SPEC = {
...argparse_1.GLOBAL_OPTIONS_SPEC,
fix: optparse_1.OptionParser.Transform.noDefault(argparse_1.GLOBAL_OPTIONS_SPEC.fix),
typescriptVersion: optparse_1.OptionParser.Factory.parsePrimitive('string'),
};
let FakeDirectoryService = class FakeDirectoryService {
constructor(realDirectorySerivce) {
this.realDirectorySerivce = realDirectorySerivce;
}
getCurrentDirectory() {
return this.cwd;
}
getRealCurrentDirectory() {
return this.realDirectorySerivce.getCurrentDirectory();
}
};
FakeDirectoryService = tslib_1.__decorate([
inversify_1.injectable(),
tslib_1.__metadata("design:paramtypes", [ymir_1.DirectoryService])
], FakeDirectoryService);
let TestCommandRunner = class TestCommandRunner extends base_1.AbstractCommandRunner {
constructor(runner, fs, logger, directoryService) {
super();
this.runner = runner;
this.fs = fs;
this.logger = logger;
this.directoryService = directoryService;
}
run(options) {
const currentTypescriptVersion = getNormalizedTypescriptVersion();
const basedir = this.directoryService.getRealCurrentDirectory();
let baselineDir;
let root;
let baselinesSeen;
let success = true;
const host = {
checkResult: (file, kind, summary) => {
const relative = path.relative(root, file);
if (relative.startsWith('..' + path.sep))
throw new ymir_1.ConfigurationError(`Testing file '${file}' outside of '${root}'.`);
const actual = kind === "fix" /* Fix */ ? summary.content : baseline_1.createBaseline(summary);
const baselineFile = `${path.resolve(baselineDir, relative)}.${kind}`;
const end = (pass, text, baselineDiff) => {
this.logger.log(` ${chalk.grey.dim(path.relative(basedir, baselineFile))} ${chalk[pass ? 'green' : 'red'](text)}`);
if (pass)
return true;
if (baselineDiff !== undefined)
this.logger.log(baselineDiff);
success = false;
return !options.bail;
};
if (kind === "fix" /* Fix */ && summary.fixes === 0) {
if (!this.fs.isFile(baselineFile))
return true;
if (options.updateBaselines) {
this.fs.remove(baselineFile);
return end(true, 'REMOVED');
}
baselinesSeen.push(utils_1.unixifyPath(baselineFile));
return end(false, 'EXISTS');
}
baselinesSeen.push(utils_1.unixifyPath(baselineFile));
let expected;
try {
expected = this.fs.readFile(baselineFile);
}
catch {
if (!options.updateBaselines)
return end(false, 'MISSING');
this.fs.createDirectory(path.dirname(baselineFile));
this.fs.writeFile(baselineFile, actual);
return end(true, 'CREATED');
}
if (expected === actual)
return end(true, 'PASSED');
if (options.updateBaselines) {
this.fs.writeFile(baselineFile, actual);
return end(true, 'UPDATED');
}
return end(false, 'FAILED', createBaselineDiff(actual, expected));
},
};
const globOptions = {
absolute: true,
cache: {},
nodir: true,
realpathCache: {},
statCache: {},
symlinks: {},
cwd: basedir,
};
for (const pattern of options.files) {
for (const testcase of glob.sync(pattern, globOptions)) {
const { typescriptVersion, ...testConfig } = optparse_1.OptionParser.parse(require(testcase), TEST_OPTION_SPEC, { validate: true, context: testcase, exhaustive: true });
if (typescriptVersion !== undefined && !semver_1.satisfies(currentTypescriptVersion, typescriptVersion)) {
this.logger.log(`${path.relative(basedir, testcase)} ${chalk.yellow(`SKIPPED, requires TypeScript ${typescriptVersion}`)}`);
continue;
}
root = path.dirname(testcase);
baselineDir = buildBaselineDirectoryName(basedir, 'baselines', testcase);
this.logger.log(path.relative(basedir, testcase));
this.directoryService.cwd = root;
baselinesSeen = [];
if (!this.test(testConfig, host))
return false;
if (options.exact) {
const remainingGlobOptions = { ...globOptions, cwd: baselineDir, ignore: baselinesSeen };
for (const unchecked of glob.sync('**', remainingGlobOptions)) {
if (options.updateBaselines) {
this.fs.remove(unchecked);
this.logger.log(` ${chalk.grey.dim(path.relative(basedir, unchecked))} ${chalk.green('REMOVED')}`);
}
else {
this.logger.log(` ${chalk.grey.dim(path.relative(basedir, unchecked))} ${chalk.red('UNCHECKED')}`);
if (options.bail)
return false;
success = false;
}
}
}
}
}
return success;
}
test(config, host) {
const lintOptions = { ...config, fix: false };
const lintResult = Array.from(this.runner.lintCollection(lintOptions));
let containsFixes = false;
for (const [fileName, summary] of lintResult) {
if (!host.checkResult(fileName, "lint" /* Lint */, summary))
return false;
containsFixes = containsFixes || summary.findings.some(isFixable);
}
if (config.fix || config.fix === undefined) {
lintOptions.fix = config.fix || true; // fix defaults to true if not specified
const fixResult = containsFixes ? this.runner.lintCollection(lintOptions) : lintResult;
for (const [fileName, summary] of fixResult)
if (!host.checkResult(fileName, "fix" /* Fix */, summary))
return false;
}
return true;
}
};
TestCommandRunner = tslib_1.__decorate([
inversify_1.injectable(),
tslib_1.__metadata("design:paramtypes", [runner_1.Runner,
cached_file_system_1.CachedFileSystem,
ymir_1.MessageHandler,
FakeDirectoryService])
], TestCommandRunner);
function buildBaselineDirectoryName(basedir, baselineDir, testcase) {
const parts = path.relative(basedir, path.dirname(testcase)).split(path.sep);
if (/^(__)?tests?(__)?$/.test(parts[0])) {
parts[0] = baselineDir;
}
else {
parts.unshift(baselineDir);
}
return path.resolve(basedir, parts.join(path.sep), getTestName(path.basename(testcase)));
}
function getTestName(basename) {
let ext = path.extname(basename);
basename = basename.slice(0, -ext.length);
ext = path.extname(basename);
if (ext === '')
return 'default';
return basename.slice(0, -ext.length);
}
/** Removes everything related to prereleases and just returns MAJOR.MINOR.PATCH, thus treating prereleases like the stable release. */
function getNormalizedTypescriptVersion() {
const v = new semver_1.SemVer(ts.version);
return new semver_1.SemVer(`${v.major}.${v.minor}.${v.patch}`);
}
function isFixable(finding) {
return finding.fix !== undefined;
}
function createBaselineDiff(actual, expected) {
const result = [
chalk.red('Expected'),
chalk.green('Actual'),
];
const lines = diff.createPatch('', expected, actual, '', '').split(/\n(?!\\)/g).slice(4);
for (let line of lines) {
switch (line[0]) {
case '@':
line = chalk.blueBright(line);
break;
case '+':
line = chalk.green('+' + prettyLine(line.substr(1)));
break;
case '-':
line = chalk.red('-' + prettyLine(line.substr(1)));
}
result.push(line);
}
return result.join('\n');
}
function prettyLine(line) {
return line
.replace(/\t/g, '\u2409') // ␉
.replace(/\r$/, '\u240d') // ␍
.replace(/^\uFEFF/, '<BOM>');
}
exports.module = new inversify_1.ContainerModule((bind) => {
bind(FakeDirectoryService).toSelf().inSingletonScope();
bind(ymir_1.DirectoryService).toDynamicValue((context) => {
return context.container.get(FakeDirectoryService);
}).inSingletonScope().when((request) => {
return request.parentRequest == undefined || request.parentRequest.target.serviceIdentifier !== FakeDirectoryService;
});
bind(ymir_1.DirectoryService).toDynamicValue(({ container }) => {
if (container.parent && container.parent.isBound(ymir_1.DirectoryService))
return container.parent.get(ymir_1.DirectoryService);
return {
getCurrentDirectory() {
return process.cwd();
},
};
}).inSingletonScope().whenInjectedInto(FakeDirectoryService);
bind(base_1.AbstractCommandRunner).to(TestCommandRunner);
});
//# sourceMappingURL=test.js.map