UNPKG

dtslint

Version:

Runs tests on TypeScript definition files

340 lines 14.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getProgram = exports.Rule = void 0; const fs_1 = require("fs"); const os = require("os"); const path_1 = require("path"); const Lint = __importStar(require("tslint")); const TsType = __importStar(require("typescript")); const util_1 = require("../util"); // Based on https://github.com/danvk/typings-checker const cacheDir = (0, path_1.join)(os.homedir(), ".dts"); const perfDir = (0, path_1.join)(os.homedir(), ".dts", "perf"); class Rule extends Lint.Rules.TypedRule { static FAILURE_STRING(expectedVersion, expectedType, actualType) { return `TypeScript@${expectedVersion} expected type to be:\n ${expectedType}\ngot:\n ${actualType}`; } applyWithProgram(sourceFile, lintProgram) { const options = this.ruleArguments[0]; if (!options) { return this.applyWithFunction(sourceFile, ctx => walk(ctx, lintProgram, TsType, "next", /*nextHigherVersion*/ undefined)); } const { tsconfigPath, versionsToTest } = options; const getFailures = ({ versionName, path }, nextHigherVersion, writeOutput) => { const ts = require(path); const program = getProgram(tsconfigPath, ts, versionName, lintProgram); const failures = this.applyWithFunction(sourceFile, ctx => walk(ctx, program, ts, versionName, nextHigherVersion)); if (writeOutput) { const packageName = (0, path_1.basename)((0, path_1.dirname)(tsconfigPath)); if (!packageName.match(/v\d+/) && !packageName.match(/ts\d\.\d/)) { const d = { [packageName]: { typeCount: program.getTypeCount(), memory: ts.sys.getMemoryUsage ? ts.sys.getMemoryUsage() : 0, }, }; if (!(0, fs_1.existsSync)(cacheDir)) { (0, fs_1.mkdirSync)(cacheDir); } if (!(0, fs_1.existsSync)(perfDir)) { (0, fs_1.mkdirSync)(perfDir); } (0, fs_1.writeFileSync)((0, path_1.join)(perfDir, `${packageName}.json`), JSON.stringify(d)); } } return failures; }; const maxFailures = getFailures((0, util_1.last)(versionsToTest), undefined, /*writeOutput*/ true); if (maxFailures.length) { return maxFailures; } // As an optimization, check the earliest version for errors; // assume that if it works on min and max, it works for everything in between. const minFailures = getFailures(versionsToTest[0], undefined, /*writeOutput*/ false); if (!minFailures.length) { return []; } // There are no failures in the max version, but there are failures in the min version. // Work backward to find the newest version with failures. for (let i = versionsToTest.length - 2; i >= 0; i--) { const failures = getFailures(versionsToTest[i], options.versionsToTest[i + 1].versionName, /*writeOutput*/ false); if (failures.length) { return failures; } } throw new Error(); // unreachable -- at least the min version should have failures. } } exports.Rule = Rule; /* tslint:disable:object-literal-sort-keys */ Rule.metadata = { ruleName: "expect", description: "Asserts types with $ExpectType and presence of errors with $ExpectError.", optionsDescription: "Not configurable.", options: null, type: "functionality", typescriptOnly: true, requiresTypeInfo: true, }; /* tslint:enable:object-literal-sort-keys */ Rule.FAILURE_STRING_DUPLICATE_ASSERTION = "This line has 2 $ExpectType assertions."; Rule.FAILURE_STRING_ASSERTION_MISSING_NODE = "Can not match a node to this assertion."; Rule.FAILURE_STRING_EXPECTED_ERROR = "Expected an error on this line, but found none."; const programCache = new WeakMap(); /** Maps a tslint Program to one created with the version specified in `options`. */ function getProgram(configFile, ts, versionName, lintProgram) { let versionToProgram = programCache.get(lintProgram); if (versionToProgram === undefined) { versionToProgram = new Map(); programCache.set(lintProgram, versionToProgram); } let newProgram = versionToProgram.get(versionName); if (newProgram === undefined) { newProgram = createProgram(configFile, ts); versionToProgram.set(versionName, newProgram); } return newProgram; } exports.getProgram = getProgram; function createProgram(configFile, ts) { const projectDirectory = (0, path_1.dirname)(configFile); const { config } = ts.readConfigFile(configFile, ts.sys.readFile); const parseConfigHost = { fileExists: fs_1.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => (0, fs_1.readFileSync)(file, "utf8"), useCaseSensitiveFileNames: true, }; const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, (0, path_1.resolve)(projectDirectory), { noEmit: true }); const host = ts.createCompilerHost(parsed.options, true); return ts.createProgram(parsed.fileNames, parsed.options, host); } function walk(ctx, program, ts, versionName, nextHigherVersion) { const { fileName } = ctx.sourceFile; const sourceFile = program.getSourceFile(fileName); if (!sourceFile) { ctx.addFailure(0, 0, `Program source files differ between TypeScript versions. This may be a dtslint bug.\n` + `Expected to find a file '${fileName}' present in ${TsType.version}, but did not find it in ts@${versionName}.`); return; } const checker = program.getTypeChecker(); // Don't care about emit errors. const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile); if (sourceFile.isDeclarationFile || !/\$Expect(Type|Error)/.test(sourceFile.text)) { // Normal file. for (const diagnostic of diagnostics) { addDiagnosticFailure(diagnostic); } return; } const { errorLines, typeAssertions, duplicates } = parseAssertions(sourceFile); for (const line of duplicates) { addFailureAtLine(line, Rule.FAILURE_STRING_DUPLICATE_ASSERTION); } const seenDiagnosticsOnLine = new Set(); for (const diagnostic of diagnostics) { const line = lineOfPosition(diagnostic.start, sourceFile); seenDiagnosticsOnLine.add(line); if (!errorLines.has(line)) { addDiagnosticFailure(diagnostic); } } for (const line of errorLines) { if (!seenDiagnosticsOnLine.has(line)) { addFailureAtLine(line, Rule.FAILURE_STRING_EXPECTED_ERROR); } } const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, typeAssertions, checker, ts); for (const { node, expected, actual } of unmetExpectations) { ctx.addFailureAtNode(node, Rule.FAILURE_STRING(versionName, expected, actual)); } for (const line of unusedAssertions) { addFailureAtLine(line, Rule.FAILURE_STRING_ASSERTION_MISSING_NODE); } function addDiagnosticFailure(diagnostic) { const intro = getIntro(); if (diagnostic.file === sourceFile) { const msg = `${intro}\n${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`; ctx.addFailureAt(diagnostic.start, diagnostic.length, msg); } else { ctx.addFailureAt(0, 0, `${intro}\n${fileName}${diagnostic.messageText}`); } } function getIntro() { if (nextHigherVersion === undefined) { return `TypeScript@${versionName} compile error: `; } else { const msg = `Compile error in typescript@${versionName} but not in typescript@${nextHigherVersion}.\n`; const explain = nextHigherVersion === "next" ? "TypeScript@next features not yet supported." : `Fix with a comment '// Minimum TypeScript Version: ${nextHigherVersion}' just under the header.`; return msg + explain; } } function addFailureAtLine(line, failure) { const start = sourceFile.getPositionOfLineAndCharacter(line, 0); let end = start + sourceFile.text.split("\n")[line].length; if (sourceFile.text[end - 1] === "\r") { end--; } ctx.addFailure(start, end, `TypeScript@${versionName}: ${failure}`); } } function parseAssertions(sourceFile) { const errorLines = new Set(); const typeAssertions = new Map(); const duplicates = []; const { text } = sourceFile; const commentRegexp = /\/\/(.*)/g; const lineStarts = sourceFile.getLineStarts(); let curLine = 0; // eslint-disable-next-line no-constant-condition while (true) { const commentMatch = commentRegexp.exec(text); if (commentMatch === null) { break; } // Match on the contents of that comment so we do nothing in a commented-out assertion, // i.e. `// foo; // $ExpectType number` const match = /^ \$Expect((Type (.*))|Error)$/.exec(commentMatch[1]); if (match === null) { continue; } const line = getLine(commentMatch.index); if (match[1] === "Error") { if (errorLines.has(line)) { duplicates.push(line); } errorLines.add(line); } else { const expectedType = match[3]; // Don't bother with the assertion if there are 2 assertions on 1 line. Just fail for the duplicate. if (typeAssertions.delete(line)) { duplicates.push(line); } else { typeAssertions.set(line, expectedType); } } } return { errorLines, typeAssertions, duplicates }; function getLine(pos) { // advance curLine to be the line preceding 'pos' while (lineStarts[curLine + 1] <= pos) { curLine++; } // If this is the first token on the line, it applies to the next line. // Otherwise, it applies to the text to the left of it. return isFirstOnLine(text, lineStarts[curLine], pos) ? curLine + 1 : curLine; } } function isFirstOnLine(text, lineStart, pos) { for (let i = lineStart; i < pos; i++) { if (text[i] !== " ") { return false; } } return true; } function matchReadonlyArray(actual, expected) { if (!(/\breadonly\b/.test(actual) && /\bReadonlyArray\b/.test(expected))) return false; const readonlyArrayRegExp = /\bReadonlyArray</y; const readonlyModifierRegExp = /\breadonly /y; // A<ReadonlyArray<B<ReadonlyArray<C>>>> // A<readonly B<readonly C[]>[]> let expectedPos = 0; let actualPos = 0; let depth = 0; while (expectedPos < expected.length && actualPos < actual.length) { const expectedChar = expected.charAt(expectedPos); const actualChar = actual.charAt(actualPos); if (expectedChar === actualChar) { expectedPos++; actualPos++; continue; } // check for end of readonly array if (depth > 0 && expectedChar === ">" && actualChar === "[" && actualPos < actual.length - 1 && actual.charAt(actualPos + 1) === "]") { depth--; expectedPos++; actualPos += 2; continue; } // check for start of readonly array readonlyArrayRegExp.lastIndex = expectedPos; readonlyModifierRegExp.lastIndex = actualPos; if (readonlyArrayRegExp.test(expected) && readonlyModifierRegExp.test(actual)) { depth++; expectedPos += 14; // "ReadonlyArray<".length; actualPos += 9; // "readonly ".length; continue; } return false; } return true; } function getExpectTypeFailures(sourceFile, typeAssertions, checker, ts) { const unmetExpectations = []; // Match assertions to the first node that appears on the line they apply to. // `forEachChild` isn't available as a method in older TypeScript versions, so must use `ts.forEachChild` instead. ts.forEachChild(sourceFile, function iterate(node) { const line = lineOfPosition(node.getStart(sourceFile), sourceFile); const expected = typeAssertions.get(line); if (expected !== undefined) { // https://github.com/Microsoft/TypeScript/issues/14077 if (node.kind === ts.SyntaxKind.ExpressionStatement) { node = node.expression; } const type = checker.getTypeAtLocation(getNodeForExpectType(node, ts)); const actual = type ? checker.typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.NoTruncation) : ""; if (!expected.split(/\s*\|\|\s*/).some(s => actual === s || matchReadonlyArray(actual, s))) { unmetExpectations.push({ node, expected, actual }); } typeAssertions.delete(line); } ts.forEachChild(node, iterate); }); return { unmetExpectations, unusedAssertions: typeAssertions.keys() }; } function getNodeForExpectType(node, ts) { if (node.kind === ts.SyntaxKind.VariableStatement) { // ts2.0 doesn't have `isVariableStatement` const { declarationList: { declarations } } = node; if (declarations.length === 1) { const { initializer } = declarations[0]; if (initializer) { return initializer; } } } return node; } function lineOfPosition(pos, sourceFile) { return sourceFile.getLineAndCharacterOfPosition(pos).line; } //# sourceMappingURL=expectRule.js.map