UNPKG

@fimbul/wotan

Version:

Pluggable TypeScript and JavaScript linter

241 lines 10.4 kB
"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