Skip to content

Commit

Permalink
[Tools] Throw FailError with code context on babel/parser exception (#…
Browse files Browse the repository at this point in the history
…22810) (#22992)

* [Tools] Throw FailError with code context on babel/parser exception

* Add description for createParserErrorMessage function
  • Loading branch information
maryia-lapata committed Sep 13, 2018
1 parent 5a65d1e commit 0a44c3f
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 27 deletions.
9 changes: 9 additions & 0 deletions src/dev/i18n/__snapshots__/utils.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`i18n utils should create verbose parser error message 1`] = `
"Unexpected token, expected \\",\\" (4:19):
const object = {
object: 'with',
semicolon: '->';
};
"
`;
exports[`i18n utils should not escape linebreaks 1`] = `
"Text
with
Expand Down
40 changes: 23 additions & 17 deletions src/dev/i18n/extractors/code.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ import {
} from '@babel/types';

import { extractI18nCallMessages } from './i18n_call';
import { isI18nTranslateFunction, traverseNodes } from '../utils';
import { createParserErrorMessage, isI18nTranslateFunction, traverseNodes } from '../utils';
import { extractIntlMessages, extractFormattedMessages } from './react';
import { createFailError } from '../../run';

/**
* Detect Intl.formatMessage() function call (React).
Expand All @@ -43,17 +44,11 @@ import { extractIntlMessages, extractFormattedMessages } from './react';
export function isIntlFormatMessageFunction(node) {
return (
isCallExpression(node) &&
(
isIdentifier(node.callee, { name: 'formatMessage' }) ||
(
isMemberExpression(node.callee) &&
(
isIdentifier(node.callee.object, { name: 'intl' }) ||
isIdentifier(node.callee.object.property, { name: 'intl' })
) &&
isIdentifier(node.callee.property, { name: 'formatMessage' })
)
)
(isIdentifier(node.callee, { name: 'formatMessage' }) ||
(isMemberExpression(node.callee) &&
(isIdentifier(node.callee.object, { name: 'intl' }) ||
isIdentifier(node.callee.object.property, { name: 'intl' })) &&
isIdentifier(node.callee.property, { name: 'formatMessage' })))
);
}

Expand All @@ -67,12 +62,23 @@ export function isFormattedMessageElement(node) {
}

export function* extractCodeMessages(buffer) {
const content = parse(buffer.toString(), {
sourceType: 'module',
plugins: ['jsx', 'typescript', 'objectRestSpread', 'classProperties', 'asyncGenerators'],
});
let ast;

for (const node of traverseNodes(content.program.body)) {
try {
ast = parse(buffer.toString(), {
sourceType: 'module',
plugins: ['jsx', 'typescript', 'objectRestSpread', 'classProperties', 'asyncGenerators'],
});
} catch (error) {
if (error instanceof SyntaxError) {
const errorWithContext = createParserErrorMessage(buffer.toString(), error);
throw createFailError(errorWithContext);
}

throw error;
}

for (const node of traverseNodes(ast.program.body)) {
if (isI18nTranslateFunction(node)) {
yield extractI18nCallMessages(node);
} else if (isIntlFormatMessageFunction(node)) {
Expand Down
44 changes: 39 additions & 5 deletions src/dev/i18n/extractors/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ import { jsdom } from 'jsdom';
import { parse } from '@babel/parser';
import { isDirectiveLiteral, isObjectExpression, isStringLiteral } from '@babel/types';

import { isPropertyWithKey, formatHTMLString, formatJSString, traverseNodes } from '../utils';
import {
isPropertyWithKey,
formatHTMLString,
formatJSString,
traverseNodes,
createParserErrorMessage,
} from '../utils';
import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants';
import { createFailError } from '../../run';

Expand All @@ -38,10 +44,23 @@ const I18N_FILTER_MARKER = '| i18n: ';
* @returns {string} Default message
*/
function parseFilterObjectExpression(expression) {
// parse an object expression instead of block statement
const nodes = parse(`+${expression}`).program.body;
let ast;

try {
// parse an object expression instead of block statement
ast = parse(`+${expression}`);
} catch (error) {
if (error instanceof SyntaxError) {
const errorWithContext = createParserErrorMessage(` ${expression}`, error);
throw createFailError(
`Couldn't parse angular expression with i18n filter:\n${errorWithContext}`
);
}

for (const node of traverseNodes(nodes)) {
throw error;
}

for (const node of traverseNodes(ast.program.body)) {
if (!isObjectExpression(node)) {
continue;
}
Expand Down Expand Up @@ -72,7 +91,22 @@ function parseFilterObjectExpression(expression) {
}

function parseIdExpression(expression) {
for (const node of traverseNodes(parse(expression).program.directives)) {
let ast;

try {
ast = parse(expression);
} catch (error) {
if (error instanceof SyntaxError) {
const errorWithContext = createParserErrorMessage(expression, error);
throw createFailError(
`Couldn't parse angular expression with i18n filter:\n${errorWithContext}`
);
}

throw error;
}

for (const node of traverseNodes(ast.program.directives)) {
if (isDirectiveLiteral(node)) {
return formatJSString(node.value);
}
Expand Down
2 changes: 0 additions & 2 deletions src/dev/i18n/extractors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,4 @@
export { extractCodeMessages } from './code';
export { extractHandlebarsMessages } from './handlebars';
export { extractHtmlMessages } from './html';
export { extractI18nCallMessages } from './i18n_call';
export { extractPugMessages } from './pug';
export { extractFormattedMessages, extractIntlMessages } from './react';
20 changes: 18 additions & 2 deletions src/dev/i18n/extractors/pug.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
import { parse } from '@babel/parser';

import { extractI18nCallMessages } from './i18n_call';
import { isI18nTranslateFunction, traverseNodes } from '../utils';
import { isI18nTranslateFunction, traverseNodes, createParserErrorMessage } from '../utils';
import { createFailError } from '../../run';

/**
* Matches `i18n(...)` in `#{i18n('id', { defaultMessage: 'Message text' })}`
Expand All @@ -34,7 +35,22 @@ export function* extractPugMessages(buffer) {
const expressions = buffer.toString().match(PUG_I18N_REGEX) || [];

for (const expression of expressions) {
for (const node of traverseNodes(parse(expression).program.body)) {
let ast;

try {
ast = parse(expression);
} catch (error) {
if (error instanceof SyntaxError) {
const errorWithContext = createParserErrorMessage(expression, error);
throw createFailError(
`Couldn't parse Pug expression with i18n(...) call:\n${errorWithContext}`
);
}

throw error;
}

for (const node of traverseNodes(ast.program.body)) {
if (isI18nTranslateFunction(node)) {
yield extractI18nCallMessages(node);
break;
Expand Down
30 changes: 30 additions & 0 deletions src/dev/i18n/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import fs from 'fs';
import glob from 'glob';
import { promisify } from 'util';
import chalk from 'chalk';

const ESCAPE_LINE_BREAK_REGEX = /(?<!\\)\\\n/g;
const HTML_LINE_BREAK_REGEX = /[\s]*\n[\s]*/g;
Expand Down Expand Up @@ -84,3 +85,32 @@ export function* traverseNodes(nodes) {
}
}
}

/**
* Forms an formatted error message for parser errors.
*
* This function returns a string which represents an error message and a place in the code where the error happened.
* In total five lines of the code are displayed: the line where the error occured, two lines before and two lines after.
*
* @param {string} content a code string where parsed error happened
* @param {{ loc: { line: number, column: number }, message: string }} error an object that contains an error message and
* the line number and the column number in the file that raised this error
* @returns {string} a formatted string representing parser error message
*/
export function createParserErrorMessage(content, error) {
const line = error.loc.line - 1;
const column = error.loc.column;

const contentLines = content.split(/\n/);
const firstLine = Math.max(line - 2, 0);
const lastLine = Math.min(line + 2, contentLines.length - 1);

contentLines[line] =
contentLines[line].substring(0, column) +
chalk.white.bgRed(contentLines[line][column] || ' ') +
contentLines[line].substring(column + 1);

const context = contentLines.slice(firstLine, lastLine + 1).join('\n');

return `${error.message}:\n${context}`;
}
30 changes: 29 additions & 1 deletion src/dev/i18n/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@
import { parse } from '@babel/parser';
import { isExpressionStatement, isObjectExpression } from '@babel/types';

import { isI18nTranslateFunction, isPropertyWithKey, traverseNodes, formatJSString } from './utils';
import {
isI18nTranslateFunction,
isPropertyWithKey,
traverseNodes,
formatJSString,
createParserErrorMessage,
} from './utils';

const i18nTranslateSources = ['i18n', 'i18n.translate'].map(
callee => `
Expand All @@ -44,13 +50,15 @@ describe('i18n utils', () => {
test('should remove escaped linebreak', () => {
expect(formatJSString('Test\\\n str\\\ning')).toEqual('Test string');
});

test('should not escape linebreaks', () => {
expect(
formatJSString(`Text \n with
line-breaks
`)
).toMatchSnapshot();
});

test('should detect i18n translate function call', () => {
let source = i18nTranslateSources[0];
let expressionStatementNode = [...traverseNodes(parse(source).program.body)].find(node =>
Expand All @@ -76,4 +84,24 @@ describe('i18n utils', () => {
expect(isPropertyWithKey(objectExpresssionProperty, 'id')).toBe(true);
expect(isPropertyWithKey(objectExpresssionProperty, 'not_id')).toBe(false);
});

test('should create verbose parser error message', () => {
expect.assertions(1);

const content = `function testFunction() {
const object = {
object: 'with',
semicolon: '->';
};
return object;
}
`;

try {
parse(content);
} catch (error) {
expect(createParserErrorMessage(content, error)).toMatchSnapshot();
}
});
});

0 comments on commit 0a44c3f

Please sign in to comment.