UNPKG

tslint-etc

Version:
261 lines (260 loc) 10.3 kB
"use strict"; 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; }