fix: 🐛 prevent extraction of non-deterministic message ids

 Closes: #89
This commit is contained in:
Christian Kaisermann 2020-11-06 09:57:22 -03:00
parent 89ffb6dd28
commit 9b6adb6538
4 changed files with 67 additions and 63 deletions

View File

@ -19,20 +19,13 @@ import { Message } from './types';
const LIB_NAME = 'svelte-i18n'; const LIB_NAME = 'svelte-i18n';
const DEFINE_MESSAGES_METHOD_NAME = 'defineMessages'; const DEFINE_MESSAGES_METHOD_NAME = 'defineMessages';
const FORMAT_METHOD_NAMES = new Set(['format', '_', 't']); const FORMAT_METHOD_NAMES = new Set(['format', '_', 't']);
const IGNORED_UTILITIES = new Set(['number', 'date', 'time']);
function isFormatCall(node: Node, imports: Set<string>) { function isFormatCall(node: Node, imports: Set<string>) {
if (node.type !== 'CallExpression') return false; if (node.type !== 'CallExpression') return false;
let identifier: Identifier; let identifier: Identifier;
if ( if (node.callee.type === 'Identifier') {
node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier' &&
!IGNORED_UTILITIES.has(node.callee.property.name)
) {
identifier = node.callee.object as Identifier;
} else if (node.callee.type === 'Identifier') {
identifier = node.callee; identifier = node.callee;
} }
@ -150,23 +143,28 @@ export function collectMessages(markup: string): Message[] {
...definitions.map((definition) => getObjFromExpression(definition)), ...definitions.map((definition) => getObjFromExpression(definition)),
...calls.map((call) => { ...calls.map((call) => {
const [pathNode, options] = call.arguments; const [pathNode, options] = call.arguments;
let messageObj;
if (pathNode.type === 'ObjectExpression') { if (pathNode.type === 'ObjectExpression') {
return getObjFromExpression(pathNode); // _({ ...opts })
messageObj = getObjFromExpression(pathNode);
} else {
const node = pathNode as Literal;
const id = node.value as string;
if (options && options.type === 'ObjectExpression') {
// _(id, { ...opts })
messageObj = getObjFromExpression(options);
messageObj.id = id;
} else {
// _(id)
messageObj = { id };
}
} }
const node = pathNode as Literal; if (messageObj?.id == null) return null;
const id = node.value as string;
if (options && options.type === 'ObjectExpression') { return messageObj;
const messageObj = getObjFromExpression(options);
messageObj.meta.id = id;
return messageObj;
}
return { node, meta: { id } };
}), }),
].filter(Boolean); ].filter(Boolean);
} }
@ -175,28 +173,28 @@ export function extractMessages(
markup: string, markup: string,
{ accumulator = {}, shallow = false, overwrite = false } = {} as any, { accumulator = {}, shallow = false, overwrite = false } = {} as any,
) { ) {
collectMessages(markup).forEach((message) => { collectMessages(markup).forEach((messageObj) => {
let defaultValue = message.meta.default; let defaultValue = messageObj.default;
if (typeof defaultValue === 'undefined') { if (typeof defaultValue === 'undefined') {
defaultValue = ''; defaultValue = '';
} }
if (shallow) { if (shallow) {
if (overwrite === false && message.meta.id in accumulator) { if (overwrite === false && messageObj.id in accumulator) {
return; return;
} }
accumulator[message.meta.id] = defaultValue; accumulator[messageObj.id] = defaultValue;
} else { } else {
if ( if (
overwrite === false && overwrite === false &&
typeof dlv(accumulator, message.meta.id) !== 'undefined' typeof dlv(accumulator, messageObj.id) !== 'undefined'
) { ) {
return; return;
} }
deepSet(accumulator, message.meta.id, defaultValue); deepSet(accumulator, messageObj.id, defaultValue);
} }
}); });

View File

@ -3,20 +3,17 @@ import { ObjectExpression, Property, Identifier } from 'estree';
import { Message } from '../types'; import { Message } from '../types';
export function getObjFromExpression(exprNode: ObjectExpression) { export function getObjFromExpression(exprNode: ObjectExpression) {
return exprNode.properties.reduce<Message>( return exprNode.properties.reduce<Message>((acc, prop: Property) => {
(acc, prop: Property) => { // we only want primitives
// we only want primitives if (
if ( prop.value.type === 'Literal' &&
prop.value.type === 'Literal' && prop.value.value !== Object(prop.value.value)
prop.value.value !== Object(prop.value.value) ) {
) { const key = (prop.key as Identifier).name as string;
const key = (prop.key as Identifier).name as string;
acc.meta[key] = prop.value.value; acc[key] = prop.value.value;
} }
return acc; return acc;
}, }, {});
{ node: exprNode, meta: {} },
);
} }

View File

@ -1,10 +1,5 @@
import { Node } from 'estree';
export interface Message { export interface Message {
node: Node; id?: string;
meta: { default?: string;
id?: string; [key: string]: any;
default?: string;
[key: string]: any;
};
} }

View File

@ -1,5 +1,3 @@
// TODO: better tests. these are way too generic.
import { parse } from 'svelte/compiler'; import { parse } from 'svelte/compiler';
import { import {
@ -124,7 +122,7 @@ describe('collecting messages', () => {
import { _, defineMessages } from 'svelte-i18n'; import { _, defineMessages } from 'svelte-i18n';
console.log($_({ id: 'foo' })) console.log($_({ id: 'foo' }))
console.log($_.title({ id: 'page.title' })) console.log($_({ id: 'page.title' }))
const messages = defineMessages({ const messages = defineMessages({
enabled: { id: 'enabled' , default: 'Enabled' }, enabled: { id: 'enabled' , default: 'Enabled' },
@ -141,20 +139,36 @@ describe('collecting messages', () => {
expect(messages).toHaveLength(7); expect(messages).toHaveLength(7);
expect(messages).toEqual( expect(messages).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ meta: { id: 'foo' } }), expect.objectContaining({ id: 'foo' }),
expect.objectContaining({ meta: { id: 'msg_1' } }), expect.objectContaining({ id: 'msg_1' }),
expect.objectContaining({ meta: { id: 'msg_2' } }), expect.objectContaining({ id: 'msg_2' }),
expect.objectContaining({ meta: { id: 'msg_3', default: 'Message' } }), expect.objectContaining({ id: 'msg_3', default: 'Message' }),
expect.objectContaining({ meta: { id: 'page.title' } }), expect.objectContaining({ id: 'page.title' }),
expect.objectContaining({ expect.objectContaining({
meta: { id: 'disabled', default: 'Disabled' }, id: 'disabled',
default: 'Disabled',
}), }),
expect.objectContaining({ expect.objectContaining({
meta: { id: 'enabled', default: 'Enabled' }, id: 'enabled',
default: 'Enabled',
}), }),
]), ]),
); );
}); });
it('ignores non-static message ids', () => {
const markup = `
<script>
import { _, defineMessages } from 'svelte-i18n';
$_({ id: 'foo' + i })
$_('bar' + i)
</script>`;
const messages = collectMessages(markup);
expect(messages).toHaveLength(0);
});
}); });
describe('messages extraction', () => { describe('messages extraction', () => {
@ -163,7 +177,7 @@ describe('messages extraction', () => {
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
</script> </script>
<h1>{$_.title('title')}</h1> <h1>{$_('title')}</h1>
<h2>{$_({ id: 'subtitle'})}</h2> <h2>{$_({ id: 'subtitle'})}</h2>
`; `;
@ -176,7 +190,7 @@ describe('messages extraction', () => {
const markup = ` const markup = `
<script>import { _ } from 'svelte-i18n';</script> <script>import { _ } from 'svelte-i18n';</script>
<h1>{$_.title('home.page.title')}</h1> <h1>{$_('home.page.title')}</h1>
<h2>{$_({ id: 'home.page.subtitle'})}</h2> <h2>{$_({ id: 'home.page.subtitle'})}</h2>
<ul> <ul>
<li>{$_('list.0')}</li> <li>{$_('list.0')}</li>
@ -197,7 +211,7 @@ describe('messages extraction', () => {
const markup = ` const markup = `
<script>import { _ } from 'svelte-i18n';</script> <script>import { _ } from 'svelte-i18n';</script>
<h1>{$_.title('home.page.title')}</h1> <h1>{$_('home.page.title')}</h1>
<h2>{$_({ id: 'home.page.subtitle'})}</h2> <h2>{$_({ id: 'home.page.subtitle'})}</h2>
<ul> <ul>
<li>{$_('list.0')}</li> <li>{$_('list.0')}</li>
@ -221,7 +235,7 @@ describe('messages extraction', () => {
const markup = ` const markup = `
<script>import { _ } from 'svelte-i18n';</script> <script>import { _ } from 'svelte-i18n';</script>
<h1>{$_.title('home.page.title')}</h1> <h1>{$_('home.page.title')}</h1>
<h2>{$_({ id: 'home.page.subtitle'})}</h2> <h2>{$_({ id: 'home.page.subtitle'})}</h2>
<ul> <ul>
<li>{$_('list.0')}</li> <li>{$_('list.0')}</li>
@ -256,7 +270,7 @@ describe('messages extraction', () => {
const markup = ` const markup = `
<script>import { _ } from 'svelte-i18n';</script> <script>import { _ } from 'svelte-i18n';</script>
<h1>{$_.title('home.page.title')}</h1> <h1>{$_('home.page.title')}</h1>
<h2>{$_({ id: 'home.page.subtitle'})}</h2> <h2>{$_({ id: 'home.page.subtitle'})}</h2>
`; `;