Skip to content
Permalink
Browse files

New: Add parameter to output formatter output to a file

Fix #2056
  • Loading branch information...
sarvaje authored and molant committed Mar 13, 2019
1 parent 3f94789 commit 640bd86d6db80d039c87b273ba5785c25a725aa2
@@ -15,21 +15,22 @@
"devDependencies": {
"@types/log-symbols": "^2.0.0",
"@types/sinon": "^7.0.10",
"@typescript-eslint/eslint-plugin": "^1.4.2",
"@typescript-eslint/parser": "1.4.2",
"ava": "^1.3.1",
"cpx": "^1.5.0",
"eslint": "^5.15.1",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-markdown": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^1.4.2",
"hint": "^4.5.0",
"npm-link-check": "^3.0.0",
"npm-run-all": "^4.1.5",
"nyc": "^13.3.0",
"proxyquire": "2.0.0",
"rimraf": "^2.6.3",
"sinon": "^7.3.0",
"typescript": "^3.3.4000",
"@typescript-eslint/parser": "1.4.2"
"strip-ansi": "^5.1.0",
"typescript": "^3.3.4000"
},
"engines": {
"node": ">=8.0.0"
@@ -13,20 +13,22 @@

import chalk from 'chalk';
import {
forEach,
groupBy,
reduce,
sortBy
} from 'lodash';
import * as logSymbols from 'log-symbols';
const stripAnsi = require('strip-ansi');

import cutString from 'hint/dist/src/lib/utils/misc/cut-string';
import { debug as d } from 'hint/dist/src/lib/utils/debug';
import { IFormatter, Problem, ProblemLocation, Severity } from 'hint/dist/src/lib/types';
import { IFormatter, Problem, ProblemLocation, Severity, FormatterOptions } from 'hint/dist/src/lib/types';
import * as logger from 'hint/dist/src/lib/utils/logging';
import writeFileAsync from 'hint/dist/src/lib/utils/fs/write-file-async';

const _ = {
forEach,
groupBy,
reduce,
sortBy
};
const debug = d(__filename);
@@ -46,7 +48,7 @@ const safeTrim = (txt: string, charsToRemove: number): boolean => {
return (/^\s+$/).test(txt.substr(0, charsToRemove));
};

const codeFrame = (code: string, location: ProblemLocation) => {
const codeFrame = (code: string, location: ProblemLocation): string => {
/* istanbul ignore next */
const line: number = typeof location.elementLine === 'number' ? location.elementLine : -1;
/* istanbul ignore next */
@@ -57,21 +59,22 @@ const codeFrame = (code: string, location: ProblemLocation) => {
const extraLinesToShow: number = 2;
const firstLine: number = line - extraLinesToShow > 0 ? line - extraLinesToShow : 0;
const lastLine: number = line + (extraLinesToShow + 1) < codeInLines.length ? line + (extraLinesToShow + 1) : codeInLines.length;
let result = '';

if (firstLine !== 0) {
logger.log('…');
result += '…\n';
}

for (let i: number = firstLine; i < lastLine; i++) {
let result = '';
let partialResult = '';
let mark = '';
const canTrim: boolean = safeTrim(codeInLines[i], whiteSpacesToRemove);

if (i === 1 || !canTrim) {
result = codeInLines[i];
partialResult = codeInLines[i];
} else {
// The first line doesn't have spaces but the other elements keep the original format
result = codeInLines[i].substr(whiteSpacesToRemove);
partialResult = codeInLines[i].substr(whiteSpacesToRemove);
}

if (i === line) {
@@ -94,26 +97,28 @@ const codeFrame = (code: string, location: ProblemLocation) => {

if (cutPosition > 50) {
markPosition = 50 + 3;
result = `… ${result.substr(column - 50)}`;
partialResult = `… ${partialResult.substr(column - 50)}`;
}

if (result.length > cutPosition + 50) {
result = `${result.substr(0, cutPosition + 50)} …`;
if (partialResult.length > cutPosition + 50) {
partialResult = `${partialResult.substr(0, cutPosition + 50)} …`;
}

mark = `${new Array(markPosition).join(' ')}^`;
}

logger.log(result);
result += `${partialResult}\n`;

if (mark) {
logger.log(mark);
result += `${mark}\n`;
}
}

if (lastLine !== codeInLines.length) {
logger.log('…');
result += '…\n';
}

return result;
};

/*
@@ -127,7 +132,7 @@ export default class CodeframeFormatter implements IFormatter {
* Format the problems grouped by `resource` name and sorted by line and column number,
* indicating where in the element there is an error.
*/
public format(messages: Problem[]) {
public async format(messages: Problem[], target: string | undefined, options: FormatterOptions = {}) {
debug('Formatting results');

if (messages.length === 0) {
@@ -138,11 +143,12 @@ export default class CodeframeFormatter implements IFormatter {
let totalErrors: number = 0;
let totalWarnings: number = 0;

_.forEach(resources, (msgs: Problem[], resource: string) => {
let result = _.reduce(resources, (total: string, msgs: Problem[], resource: string) => {
const sortedMessages: Problem[] = _.sortBy(msgs, ['location.line', 'location.column']);
const resourceString = chalk.cyan(`${cutString(resource, 80)}`);

_.forEach(sortedMessages, (msg: Problem) => {
const partialResult = _.reduce(sortedMessages, (subtotal: string, msg: Problem) => {
let partial: string;
const severity = Severity.error === msg.severity ? chalk.red('Error') : chalk.yellow('Warning');
const location = msg.location;

@@ -152,18 +158,30 @@ export default class CodeframeFormatter implements IFormatter {
totalWarnings++;
}

logger.log(`${severity}: ${msg.message} (${msg.hintId}) at ${resourceString}${msg.sourceCode ? `:${location.line}:${location.column}` : ''}`);
partial = `${severity}: ${msg.message} (${msg.hintId}) at ${resourceString}${msg.sourceCode ? `:${location.line}:${location.column}` : ''}\n`;

if (msg.sourceCode) {
codeFrame(msg.sourceCode, location);
partial += codeFrame(msg.sourceCode, location);
}

logger.log('');
});
});
partial += '\n';

return subtotal + partial;
}, '');

return total + partialResult;
}, '');

const color: typeof chalk = totalErrors > 0 ? chalk.red : chalk.yellow;

logger.log(color.bold(`${logSymbols.error} Found a total of ${totalErrors} ${totalErrors === 1 ? 'error' : 'errors'} and ${totalWarnings} ${totalWarnings === 1 ? 'warning' : 'warnings'}`));
result += color.bold(`${logSymbols.error} Found a total of ${totalErrors} ${totalErrors === 1 ? 'error' : 'errors'} and ${totalWarnings} ${totalWarnings === 1 ? 'warning' : 'warnings'}`);

if (!options.output) {
logger.log(result);

return;
}

await writeFileAsync(options.output, stripAnsi(result));
}
}
@@ -2,27 +2,40 @@ import anyTest, { TestInterface, ExecutionContext } from 'ava';
import chalk from 'chalk';
import * as sinon from 'sinon';
import * as proxyquire from 'proxyquire';
import * as logSymbols from 'log-symbols';
const stripAnsi = require('strip-ansi');

import * as problems from './fixtures/list-of-problems';

type Logging = {
log: () => void;
};

type WriteFileAsync = {
default: () => void;
};

type CodeframeContext = {
logging: Logging;
loggingLogSpy: sinon.SinonSpy<any, void>;
writeFileAsync: WriteFileAsync;
writeFileAsyncDefaultStub: sinon.SinonStub<any, void>;
};

const test = anyTest as TestInterface<CodeframeContext>;

const initContext = (t: ExecutionContext<CodeframeContext>) => {
t.context.logging = { log() { } };
t.context.loggingLogSpy = sinon.spy(t.context.logging, 'log');
t.context.writeFileAsync = { default() { } };
t.context.writeFileAsyncDefaultStub = sinon.stub(t.context.writeFileAsync, 'default').returns();
};

const loadScript = (context: CodeframeContext) => {
const script = proxyquire('../src/formatter', { 'hint/dist/src/lib/utils/logging': context.logging });
const script = proxyquire('../src/formatter', {
'hint/dist/src/lib/utils/fs/write-file-async': context.writeFileAsync,
'hint/dist/src/lib/utils/logging': context.logging
});

return script.default;
};
@@ -42,38 +55,157 @@ test(`Codeframe formatter doesn't print anything if no values`, (t) => {
t.is(t.context.loggingLogSpy.callCount, 0);
});

test(`Codeframe formatter prints a table and a summary for each resource`, (t) => {
const CodeframeFormatter = loadScript(t.context);
const formatter = new CodeframeFormatter();
const generateExpectedLogResult = () => {
let problem = problems.codeframeproblems[0];

formatter.format(problems.codeframeproblems);
let expectedLogResult = `${chalk.yellow('Warning')}: ${problem.message} (${problem.hintId}) at ${chalk.cyan(problem.resource)}`;

const log = t.context.loggingLogSpy;
problem = problems.codeframeproblems[1];
let sourceCode = problem.sourceCode.split('\n');

expectedLogResult += `
${chalk.yellow('Warning')}: ${problem.message} (${problem.hintId}) at ${chalk.cyan(problem.resource)}:${problem.location.line}:${problem.location.column}
${sourceCode[0]}
^
${sourceCode[1]}
${sourceCode[2]}
…`;

problem = problems.codeframeproblems[2];
sourceCode = problem.sourceCode.split('\n');

expectedLogResult += `
${chalk.yellow('Warning')}: ${problem.message} (${problem.hintId}) at ${chalk.cyan(problem.resource)}:${problem.location.line}:${problem.location.column}
${sourceCode[0]}
^
${sourceCode[1]}
${sourceCode[2]}
…`;

problem = problems.codeframeproblems[3];
sourceCode = problem.sourceCode.split('\n');

expectedLogResult += `
${chalk.yellow('Warning')}: ${problem.message} (${problem.hintId}) at ${chalk.cyan(problem.resource)}:${problem.location.line}:${problem.location.column}
${sourceCode[0]}
${sourceCode[1].substr(8)}
^
${sourceCode[2].substr(8)}`;

problem = problems.codeframeproblems[4];
sourceCode = problem.sourceCode.split('\n');

expectedLogResult += `
${chalk.red('Error')}: ${problem.message} (${problem.hintId}) at ${chalk.cyan(problem.resource)}:${problem.location.line}:${problem.location.column}
${sourceCode[0]}
${sourceCode[1]}
^
${sourceCode[2].substr(8)}
${sourceCode[3]}
…
${chalk.red.bold(`${logSymbols.error} Found a total of 1 error and 4 warnings`)}`;

return expectedLogResult;
};

const generateExpectedOutputResult = () => {
let problem = problems.codeframeproblems[0];

t.is(log.args[0][0], `${chalk.yellow('Warning')}: ${problem.message} (${problem.hintId}) at ${chalk.cyan(problem.resource)}`);
let expectedLogResult = `Warning: ${problem.message} (${problem.hintId}) at ${problem.resource}`;

problem = problems.codeframeproblems[1];
let sourceCode = problem.sourceCode.split('\n');

t.is(log.args[2][0], `${chalk.yellow('Warning')}: ${problem.message} (${problem.hintId}) at ${chalk.cyan(problem.resource)}:${problem.location.line}:${problem.location.column}`);
t.is(log.args[4][0], sourceCode[0]);
t.is(log.args[5][0], '^');
t.is(log.args[8][0], '…');
expectedLogResult += `
Warning: ${problem.message} (${problem.hintId}) at ${problem.resource}:${problem.location.line}:${problem.location.column}
${sourceCode[0]}
^
${sourceCode[1]}
${sourceCode[2]}
…`;

problem = problems.codeframeproblems[2];
sourceCode = problem.sourceCode.split('\n');

t.is(log.args[14][0], sourceCode[1]);
expectedLogResult += `
Warning: ${problem.message} (${problem.hintId}) at ${problem.resource}:${problem.location.line}:${problem.location.column}
${sourceCode[0]}
^
${sourceCode[1]}
${sourceCode[2]}
…`;

problem = problems.codeframeproblems[3];
sourceCode = problem.sourceCode.split('\n');
t.is(log.args[22][0], ` ^`);

expectedLogResult += `
Warning: ${problem.message} (${problem.hintId}) at ${problem.resource}:${problem.location.line}:${problem.location.column}
${sourceCode[0]}
${sourceCode[1].substr(8)}
^
${sourceCode[2].substr(8)}`;

problem = problems.codeframeproblems[4];
sourceCode = problem.sourceCode.split('\n');
t.is(log.args[29][0], ` ^`);
t.is(log.args[30][0], sourceCode[2].trim());
t.is(log.args[31][0], sourceCode[3]);
t.is(log.args[32][0], '…');

expectedLogResult += `
Error: ${problem.message} (${problem.hintId}) at ${problem.resource}:${problem.location.line}:${problem.location.column}
${sourceCode[0]}
${sourceCode[1]}
^
${sourceCode[2].substr(8)}
${sourceCode[3]}
…
${stripAnsi(logSymbols.error)} Found a total of 1 error and 4 warnings`;

return expectedLogResult;
};

test(`Codeframe formatter prints a table and a summary for each resource`, (t) => {
const CodeframeFormatter = loadScript(t.context);
const formatter = new CodeframeFormatter();

formatter.format(problems.codeframeproblems);

const log = t.context.loggingLogSpy;
const writeFileStub = t.context.writeFileAsyncDefaultStub;
const expectedLogResult = generateExpectedLogResult();

t.is(log.args[0][0], expectedLogResult);
t.false(writeFileStub.called);
});

test(`Codeframe formatter called with the output option should write the result in the output file`, (t) => {
const CodeframeFormatter = loadScript(t.context);
const formatter = new CodeframeFormatter();
const outputFile = 'output.json';

formatter.format(problems.codeframeproblems, null, { output: outputFile });

const log = t.context.loggingLogSpy;
const writeFileStub = t.context.writeFileAsyncDefaultStub;
const expectedOutputResult = generateExpectedOutputResult();

t.false(log.called);
t.true(writeFileStub.calledOnce);
t.is(writeFileStub.args[0][0], outputFile);
t.is(writeFileStub.args[0][1], expectedOutputResult);
});
Oops, something went wrong.

0 comments on commit 640bd86

Please sign in to comment.
You can’t perform that action at this time.