Skip to content

Commit

Permalink
fix: πŸ› prevent extraction of non-deterministic message ids
Browse files Browse the repository at this point in the history
βœ… Closes: #89
  • Loading branch information
kaisermann committed Nov 6, 2020
1 parent 89ffb6d commit 9b6adb6
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 63 deletions.
50 changes: 24 additions & 26 deletions src/cli/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,13 @@ import { Message } from './types';
const LIB_NAME = 'svelte-i18n';
const DEFINE_MESSAGES_METHOD_NAME = 'defineMessages';
const FORMAT_METHOD_NAMES = new Set(['format', '_', 't']);
const IGNORED_UTILITIES = new Set(['number', 'date', 'time']);

function isFormatCall(node: Node, imports: Set<string>) {
if (node.type !== 'CallExpression') return false;

let identifier: Identifier;

if (
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') {
if (node.callee.type === 'Identifier') {
identifier = node.callee;
}

Expand Down Expand Up @@ -150,23 +143,28 @@ export function collectMessages(markup: string): Message[] {
...definitions.map((definition) => getObjFromExpression(definition)),
...calls.map((call) => {
const [pathNode, options] = call.arguments;
let messageObj;

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;
const id = node.value as string;
if (messageObj?.id == null) return null;

if (options && options.type === 'ObjectExpression') {
const messageObj = getObjFromExpression(options);

messageObj.meta.id = id;

return messageObj;
}

return { node, meta: { id } };
return messageObj;
}),
].filter(Boolean);
}
Expand All @@ -175,28 +173,28 @@ export function extractMessages(
markup: string,
{ accumulator = {}, shallow = false, overwrite = false } = {} as any,
) {
collectMessages(markup).forEach((message) => {
let defaultValue = message.meta.default;
collectMessages(markup).forEach((messageObj) => {
let defaultValue = messageObj.default;

if (typeof defaultValue === 'undefined') {
defaultValue = '';
}

if (shallow) {
if (overwrite === false && message.meta.id in accumulator) {
if (overwrite === false && messageObj.id in accumulator) {
return;
}

accumulator[message.meta.id] = defaultValue;
accumulator[messageObj.id] = defaultValue;
} else {
if (
overwrite === false &&
typeof dlv(accumulator, message.meta.id) !== 'undefined'
typeof dlv(accumulator, messageObj.id) !== 'undefined'
) {
return;
}

deepSet(accumulator, message.meta.id, defaultValue);
deepSet(accumulator, messageObj.id, defaultValue);
}
});

Expand Down
25 changes: 11 additions & 14 deletions src/cli/includes/getObjFromExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,17 @@ import { ObjectExpression, Property, Identifier } from 'estree';
import { Message } from '../types';

export function getObjFromExpression(exprNode: ObjectExpression) {
return exprNode.properties.reduce<Message>(
(acc, prop: Property) => {
// we only want primitives
if (
prop.value.type === 'Literal' &&
prop.value.value !== Object(prop.value.value)
) {
const key = (prop.key as Identifier).name as string;
return exprNode.properties.reduce<Message>((acc, prop: Property) => {
// we only want primitives
if (
prop.value.type === 'Literal' &&
prop.value.value !== Object(prop.value.value)
) {
const key = (prop.key as Identifier).name as string;

acc.meta[key] = prop.value.value;
}
acc[key] = prop.value.value;
}

return acc;
},
{ node: exprNode, meta: {} },
);
return acc;
}, {});
}
11 changes: 3 additions & 8 deletions src/cli/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { Node } from 'estree';

export interface Message {
node: Node;
meta: {
id?: string;
default?: string;
[key: string]: any;
};
id?: string;
default?: string;
[key: string]: any;
}
44 changes: 29 additions & 15 deletions test/cli/extract.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// TODO: better tests. these are way too generic.

import { parse } from 'svelte/compiler';

import {
Expand Down Expand Up @@ -124,7 +122,7 @@ describe('collecting messages', () => {
import { _, defineMessages } from 'svelte-i18n';
console.log($_({ id: 'foo' }))
console.log($_.title({ id: 'page.title' }))
console.log($_({ id: 'page.title' }))
const messages = defineMessages({
enabled: { id: 'enabled' , default: 'Enabled' },
Expand All @@ -141,20 +139,36 @@ describe('collecting messages', () => {
expect(messages).toHaveLength(7);
expect(messages).toEqual(
expect.arrayContaining([
expect.objectContaining({ meta: { id: 'foo' } }),
expect.objectContaining({ meta: { id: 'msg_1' } }),
expect.objectContaining({ meta: { id: 'msg_2' } }),
expect.objectContaining({ meta: { id: 'msg_3', default: 'Message' } }),
expect.objectContaining({ meta: { id: 'page.title' } }),
expect.objectContaining({ id: 'foo' }),
expect.objectContaining({ id: 'msg_1' }),
expect.objectContaining({ id: 'msg_2' }),
expect.objectContaining({ id: 'msg_3', default: 'Message' }),
expect.objectContaining({ id: 'page.title' }),
expect.objectContaining({
meta: { id: 'disabled', default: 'Disabled' },
id: 'disabled',
default: 'Disabled',
}),
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', () => {
Expand All @@ -163,7 +177,7 @@ describe('messages extraction', () => {
import { _ } from 'svelte-i18n';
</script>
<h1>{$_.title('title')}</h1>
<h1>{$_('title')}</h1>
<h2>{$_({ id: 'subtitle'})}</h2>
`;

Expand All @@ -176,7 +190,7 @@ describe('messages extraction', () => {
const markup = `
<script>import { _ } from 'svelte-i18n';</script>
<h1>{$_.title('home.page.title')}</h1>
<h1>{$_('home.page.title')}</h1>
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
<ul>
<li>{$_('list.0')}</li>
Expand All @@ -197,7 +211,7 @@ describe('messages extraction', () => {
const markup = `
<script>import { _ } from 'svelte-i18n';</script>
<h1>{$_.title('home.page.title')}</h1>
<h1>{$_('home.page.title')}</h1>
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
<ul>
<li>{$_('list.0')}</li>
Expand All @@ -221,7 +235,7 @@ describe('messages extraction', () => {
const markup = `
<script>import { _ } from 'svelte-i18n';</script>
<h1>{$_.title('home.page.title')}</h1>
<h1>{$_('home.page.title')}</h1>
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
<ul>
<li>{$_('list.0')}</li>
Expand Down Expand Up @@ -256,7 +270,7 @@ describe('messages extraction', () => {
const markup = `
<script>import { _ } from 'svelte-i18n';</script>
<h1>{$_.title('home.page.title')}</h1>
<h1>{$_('home.page.title')}</h1>
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
`;

Expand Down

0 comments on commit 9b6adb6

Please sign in to comment.