dtslint
Version:
Runs tests on TypeScript definition files
340 lines • 14.8 kB
JavaScript
;
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