tslint-etc
Version:
More rules for TSLint
261 lines (260 loc) • 10.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getProgram = exports.Rule = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const Lint = require("tslint");
const TsType = require("typescript");
class Rule extends Lint.Rules.TypedRule {
static FAILURE_STRING(expectedVersion, expectedType, actualType) {
return `Expected type to be: ${expectedType}; got: ${actualType}`;
}
applyWithProgram(sourceFile, lintProgram) {
return this.applyWithFunction(sourceFile, (ctx) => walk(ctx, lintProgram, TsType, TsType.version, undefined));
}
}
exports.Rule = Rule;
Rule.metadata = {
ruleName: "expect-type",
description: "Asserts types with $ExpectType and presence of errors with $ExpectError.",
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
requiresTypeInfo: true,
};
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();
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 = path_1.dirname(configFile);
const { config } = ts.readConfigFile(configFile, ts.sys.readFile);
const parseConfigHost = {
fileExists: fs_1.existsSync,
readDirectory: ts.sys.readDirectory,
readFile: (file) => fs_1.readFileSync(file, "utf8"),
useCaseSensitiveFileNames: true,
};
const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, 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();
const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
if (sourceFile.isDeclarationFile ||
!/\$Expect(Type|Error)/.test(sourceFile.text)) {
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) {
if (diagnostic.start != null) {
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 '// 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, 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;
while (true) {
const commentMatch = commentRegexp.exec(text);
if (commentMatch === null) {
break;
}
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];
if (typeAssertions.delete(line)) {
duplicates.push(line);
}
else {
typeAssertions.set(line, expectedType);
}
}
}
return { errorLines, typeAssertions, duplicates };
function getLine(pos) {
while (lineStarts[curLine + 1] <= pos) {
curLine++;
}
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;
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;
}
if (depth > 0 &&
expectedChar === ">" &&
actualChar === "[" &&
actualPos < actual.length - 1 &&
actual.charAt(actualPos + 1) === "]") {
depth--;
expectedPos++;
actualPos += 2;
continue;
}
readonlyArrayRegExp.lastIndex = expectedPos;
readonlyModifierRegExp.lastIndex = actualPos;
if (readonlyArrayRegExp.test(expected) &&
readonlyModifierRegExp.test(actual)) {
depth++;
expectedPos += 14;
actualPos += 9;
continue;
}
return false;
}
return true;
}
function getExpectTypeFailures(sourceFile, typeAssertions, checker, ts) {
const unmetExpectations = [];
ts.forEachChild(sourceFile, function iterate(node) {
const line = lineOfPosition(node.getStart(sourceFile), sourceFile);
const expected = typeAssertions.get(line);
if (expected !== undefined) {
if (node.kind === ts.SyntaxKind.ExpressionStatement) {
node = node.expression;
}
const type = checker.getTypeAtLocation(getNodeForExpectType(node, ts));
const actual = type
? checker.typeToString(type, 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) {
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;
}