blob: e474ec72297e4a39799903ac130739bb402cfff4 [file] [log] [blame]
import 'jasmine';
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as ts from 'typescript';
import {Checker} from '../../checker';
import {Failure, fixToString} from '../../failure';
import {AbstractRule} from '../../rule';
/**
* Turns the provided source (as strings) into a ts.Program. The source files
* will be named `.../file_${n}.ts`, with n the index of the source file in
* the `sourceCode` array.
*/
export function compile(...sourceCode: string[]): ts.Program {
const temporaryFolder = os.tmpdir() + path.sep +
`tslint_test_input_${crypto.randomBytes(16).toString('hex')}`;
const fullPaths: string[] = [];
sourceCode.forEach((s, i) => {
fullPaths.push(`${temporaryFolder}${path.sep}file_${i}.ts`);
});
let error: Error|undefined = undefined;
let program: ts.Program|undefined = undefined;
try { // Wrap it all in a try/finally to clean up the temp files afterwards
fs.mkdirSync(temporaryFolder);
sourceCode.forEach((s, i) => {
fs.writeFileSync(fullPaths[i], s);
});
program = ts.createProgram(fullPaths, {});
if (ts.getPreEmitDiagnostics(program).length !== 0) {
throw new Error(
'Your program does not compile cleanly. Diagnostics:\n' +
ts.formatDiagnostics(
ts.getPreEmitDiagnostics(program), ts.createCompilerHost({})));
}
} catch (e) {
error = e;
} finally {
fullPaths.forEach(p => fs.unlinkSync(p));
fs.rmdirSync(temporaryFolder);
}
if (program && !error) {
return program;
} else {
throw error;
}
}
function check(rule: AbstractRule, program: ts.Program): Failure[] {
const checker = new Checker(program);
rule.register(checker);
return program.getSourceFiles()
.map(s => checker.execute(s))
.reduce((prev, cur) => prev.concat(cur));
}
/** Builds and run the given Rule upon the source files that were provided. */
export function compileAndCheck(
rule: AbstractRule, ...sourceCode: string[]): Failure[] {
const program = compile(...sourceCode);
return check(rule, program);
}
/** Turns a Failure to a fileName. */
export function toFileName(f: Failure) {
const file = f.toDiagnostic().file;
return file ? file.fileName : 'unknown';
}
/**
* Returns the location the temp directory for that platform (with forward
* slashes).
*/
export function getTempDirForWhitelist() {
// TS uses forward slashes on Windows ¯\_(ツ)_/¯
return os.platform() === 'win32' ? os.tmpdir().replace(/\\/g, '/') :
os.tmpdir();
}
interface FailureExpectations {
fileName?: string;
start?: number;
end?: number;
matchedCode?: string;
messageText?: string;
fix?: FixExpectations;
}
type FixExpectations = [{
fileName?: string;
start?: number;
end?: number;
replacement?: string;
}];
function failureMatchesExpectation(
f1: Failure, exp: FailureExpectations): {pass: boolean, message: string} {
const diagnostic = f1.toDiagnostic();
let regrets = '';
if (exp === undefined) {
regrets += 'The matcher requires two arguments. ';
}
if (exp.fileName) {
if (!diagnostic.file) {
regrets += `Expected diagnostic to have a source file, but it had ${
diagnostic.file}. `;
} else if (!diagnostic.file.fileName.endsWith(exp.fileName)) {
regrets +=
`Expected ${diagnostic.file.fileName} to end with ${exp.fileName}. `;
}
}
if (exp.messageText !== undefined &&
exp.messageText !== diagnostic.messageText) {
regrets +=
expectation('errorMessage', exp.messageText, diagnostic.messageText);
}
if (exp.start !== undefined && diagnostic.start !== exp.start) {
regrets += expectation('start', exp.start, diagnostic.start);
}
if (exp.end !== undefined && diagnostic.end !== exp.end) {
regrets += expectation('end', exp.end, diagnostic.end);
}
if (exp.matchedCode) {
if (!diagnostic.file) {
regrets += `Expected diagnostic to have a source file, but it had ${
diagnostic.file}. `;
} else if (diagnostic.start === undefined) {
// I don't know how this could happen, but typings say so.
regrets += `Expected diagnostic to have a starting position. `;
} else {
const foundMatchedCode = diagnostic.file.getFullText().slice(
Number(diagnostic.start), diagnostic.end);
if (foundMatchedCode !== exp.matchedCode) {
regrets += `Expected diagnostic to match ${exp.matchedCode}, but was ${
foundMatchedCode} (from ${Number(diagnostic.start)} to ${
diagnostic.end}). `;
}
}
}
if (exp.fix) {
const {pass, message: fixMessage} = fixMatchesExpectation(f1, exp.fix);
if (!pass) {
regrets += fixMessage;
}
}
return {pass: regrets === '', message: regrets};
}
function fixMatchesExpectation(failure: Failure, expected: FixExpectations):
{pass: boolean, message: string} {
let regrets = '';
const actualFix = failure.toDiagnostic().fix;
if (!actualFix) {
regrets += `Expected ${failure.toString()} to have fix ${
JSON.stringify(expected)}. `;
} else if (actualFix.changes.length !== expected.length) {
regrets += `Expected ${expected.length} individual changes, got ${
actualFix.changes.length}. `;
if (actualFix.changes.length) {
regrets += '\n' + fixToString(actualFix);
}
} else {
for (let i = 0; i < expected.length; i++) {
const e = expected[i];
const a = actualFix.changes[i];
if (e.start !== undefined && e.start !== a.start) {
regrets +=
expectation(`${i}th individualChange's start`, e.start, a.start);
}
if (e.end !== undefined && e.end !== a.end) {
regrets += expectation(`${i}th individualChange's end`, e.end, a.end);
}
if (e.replacement !== undefined && e.replacement !== a.replacement) {
regrets += expectation(
`${i}th individualChange's replacement`, e.replacement,
a.replacement);
}
if (e.fileName !== undefined && e.fileName !== a.sourceFile.fileName) {
regrets += expectation(
`${i}th individualChange's fileName`, e.fileName,
a.sourceFile.fileName);
}
// TODO: Consider adding matchedCode as for the failure matcher.
}
}
return {pass: regrets === '', message: regrets};
}
/**
* Custom matcher for Jasmine, for a better experience matching failures and
* fixes.
*/
export const customMatchers: jasmine.CustomMatcherFactories = {
toBeFailureMatching(): jasmine.CustomMatcher {
return {compare: failureMatchesExpectation};
},
/** Checks that a Failure has the expected Fix field. */
toHaveFixMatching(): jasmine.CustomMatcher {
return {compare: fixMatchesExpectation};
},
toHaveNFailures(): jasmine.CustomMatcher {
return {
compare: (actual: Failure[], expected: number) => {
if (actual.length === expected) {
return {pass: true};
} else {
let message =
`Expected ${expected} Failures, but found ${actual.length}.`;
if (actual.length) {
message += '\n' + actual.map(f => f.toString()).join('\n');
}
return {pass: false, message};
}
}
};
},
toHaveFailuresMatching(): jasmine.CustomMatcher {
return {
compare: (actual: Failure[], ...expected: FailureExpectations[]) => {
if (actual.length !== expected.length) {
let message =
`Expected ${expected} Failures, but found ${actual.length}.`;
if (actual.length) {
message += '\n' + actual.map(f => f.toString()).join('\n');
}
return {pass: false, message};
}
let pass = true, message = '';
for (let i = 0; i < actual.length; i++) {
const comparison = failureMatchesExpectation(actual[i], expected[i]);
pass = pass && comparison.pass;
if (comparison.message) {
message += `For failure ${i}: ${comparison.message}\n`;
}
message += comparison.message;
}
return {pass, message};
}
};
},
/**
* Asserts that a Failure has no fix.
*/
toHaveNoFix(): jasmine.CustomMatcher {
return {
compare: (actualFailure: Failure) => {
return {
pass: actualFailure.toDiagnostic().fix === undefined,
message: 'This failure should not have a fix.'
};
}
};
},
toHaveNoFailures(): jasmine.CustomMatcher {
return {
compare: (actual: Failure[]) => {
if (actual.length !== 0) {
let message = `Expected no Failures, but found ${actual.length}.`;
message += '\n' + actual.map(f => f.toString()).join('\n');
return {pass: false, message};
}
return {pass: true, message: ''};
}
};
}
};
function expectation<T>(fieldname: string, expectation: T, actual: T) {
return `Expected .${fieldname} to be ${expectation}, was ${actual}. `;
}
// And the matching type
declare global {
namespace jasmine {
interface Matchers<T> {
toBeFailureMatching(expected: FailureExpectations): void;
/** Checks that a Failure has the expected Fix field. */
toHaveFixMatching(expected: FixExpectations): void;
/** Asserts that a Failure has no fix. */
toHaveNoFix(): void;
toHaveNFailures(expected: number): void;
toHaveFailuresMatching(...expected: FailureExpectations[]): void;
/** Asserts that the results are empty. */
toHaveNoFailures(): void;
}
}
}