UNPKG

eslint-plugin-dtslint

Version:
227 lines (226 loc) 8.15 kB
"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;