eslint-plugin-dtslint
Version:
ESLint rules for dtslint tests
227 lines (226 loc) • 8.15 kB
JavaScript
"use strict";
const tslib_1 = require("tslib");
const eslint_etc_1 = require("eslint-etc");
const ts = tslib_1.__importStar(require("typescript"));
const utils_1 = require("../utils");
const rule = (0, utils_1.ruleCreator)({
defaultOptions: [],
meta: {
docs: {
description: "Asserts types with `$ExpectType` and presence of errors with `$ExpectError`.",
recommended: false,
},
fixable: undefined,
hasSuggestions: false,
messages: {
duplicateAssertion: "This line has two `$ExpectType` assertions.",
expectedError: "Expected an error on this line, but found none.",
expectedType: "Expected type to be: {{expected}}; got: {{actual}}.",
missingNode: "Can not match a node to this assertion.",
},
schema: [],
type: "problem",
},
name: "expect-type",
create: (context) => {
const { esTreeNodeToTSNodeMap, program } = (0, eslint_etc_1.getParserServices)(context);
return {
Program: (node) => {
const sourceFile = esTreeNodeToTSNodeMap.get(node);
walk(context.report, sourceFile.fileName, program, ts.version, undefined);
},
};
},
});
function walk(report, fileName, program, versionName, nextHigherVersion) {
const sourceFile = program.getSourceFile(fileName);
if (!sourceFile) {
throw new Error(`Source file ${fileName} not in program.`);
}
const checker = program.getTypeChecker();
const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
const { errorLines, typeAssertions, duplicates } = parseAssertions(sourceFile);
for (const line of duplicates) {
addFailureAtLine(line, "duplicateAssertion");
}
const seenDiagnosticsOnLine = new Set();
for (const diagnostic of diagnostics) {
if (diagnostic.start != null) {
const line = lineOfPosition(diagnostic.start, sourceFile);
seenDiagnosticsOnLine.add(line);
}
}
for (const line of errorLines) {
if (!seenDiagnosticsOnLine.has(line)) {
addFailureAtLine(line, "expectedError");
}
}
const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, typeAssertions, checker);
for (const { node, expected, actual } of unmetExpectations) {
report({
data: { expected, actual },
loc: (0, eslint_etc_1.getLoc)(node),
messageId: "expectedType",
});
}
for (const line of unusedAssertions) {
addFailureAtLine(line, "missingNode");
}
function addFailureAtLine(line, messageId) {
if (!sourceFile) {
return;
}
const startPosition = sourceFile.getPositionOfLineAndCharacter(line, 0);
let endPosition = startPosition + sourceFile.text.split("\n")[line].length;
if (sourceFile.text[endPosition - 1] === "\r") {
endPosition--;
}
const start = ts.getLineAndCharacterOfPosition(sourceFile, startPosition);
const end = ts.getLineAndCharacterOfPosition(sourceFile, endPosition);
report({
data: {},
loc: {
start: {
line: start.line + 1,
column: start.character,
},
end: {
line: end.line + 1,
column: end.character,
},
},
messageId,
});
}
}
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) {
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));
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) {
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;
}
module.exports = rule;