eslint-plugin-formatjs 5.4.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/context-compat.js +1 -5
- package/index.d.ts +15 -1
- package/index.js +46 -45
- package/package.json +6 -4
- package/rules/blocklist-elements.d.ts +1 -2
- package/rules/blocklist-elements.js +23 -26
- package/rules/enforce-default-message.js +8 -11
- package/rules/enforce-description.js +8 -11
- package/rules/enforce-id.d.ts +1 -0
- package/rules/enforce-id.js +21 -14
- package/rules/enforce-placeholders.js +14 -17
- package/rules/enforce-plural-rules.js +10 -13
- package/rules/no-camel-case.js +11 -14
- package/rules/no-complex-selectors.js +13 -16
- package/rules/no-emoji.js +11 -14
- package/rules/no-id.js +6 -9
- package/rules/no-invalid-icu.js +9 -12
- package/rules/no-literal-string-in-jsx.js +33 -13
- package/rules/no-literal-string-in-object.js +8 -11
- package/rules/no-missing-icu-plural-one-placeholders.js +15 -19
- package/rules/no-multiple-plurals.js +10 -13
- package/rules/no-multiple-whitespaces.js +27 -32
- package/rules/no-offset.js +10 -13
- package/rules/no-useless-message.js +13 -16
- package/rules/prefer-formatted-message.js +4 -7
- package/rules/prefer-pound-in-plural.js +25 -29
- package/util.js +5 -12
- package/lib_esnext/context-compat.d.ts +0 -3
- package/lib_esnext/context-compat.js +0 -6
- package/lib_esnext/index.d.ts +0 -1
- package/lib_esnext/index.js +0 -146
- package/lib_esnext/package.json +0 -31
- package/lib_esnext/rules/blocklist-elements.d.ts +0 -15
- package/lib_esnext/rules/blocklist-elements.js +0 -132
- package/lib_esnext/rules/enforce-default-message.d.ts +0 -10
- package/lib_esnext/rules/enforce-default-message.js +0 -68
- package/lib_esnext/rules/enforce-description.d.ts +0 -10
- package/lib_esnext/rules/enforce-description.js +0 -66
- package/lib_esnext/rules/enforce-id.d.ts +0 -10
- package/lib_esnext/rules/enforce-id.js +0 -153
- package/lib_esnext/rules/enforce-placeholders.d.ts +0 -8
- package/lib_esnext/rules/enforce-placeholders.js +0 -147
- package/lib_esnext/rules/enforce-plural-rules.d.ts +0 -17
- package/lib_esnext/rules/enforce-plural-rules.js +0 -103
- package/lib_esnext/rules/no-camel-case.d.ts +0 -5
- package/lib_esnext/rules/no-camel-case.js +0 -76
- package/lib_esnext/rules/no-complex-selectors.d.ts +0 -9
- package/lib_esnext/rules/no-complex-selectors.js +0 -136
- package/lib_esnext/rules/no-emoji.d.ts +0 -9
- package/lib_esnext/rules/no-emoji.js +0 -99
- package/lib_esnext/rules/no-id.d.ts +0 -5
- package/lib_esnext/rules/no-id.js +0 -58
- package/lib_esnext/rules/no-invalid-icu.d.ts +0 -5
- package/lib_esnext/rules/no-invalid-icu.js +0 -60
- package/lib_esnext/rules/no-literal-string-in-jsx.d.ts +0 -13
- package/lib_esnext/rules/no-literal-string-in-jsx.js +0 -179
- package/lib_esnext/rules/no-literal-string-in-object.d.ts +0 -9
- package/lib_esnext/rules/no-literal-string-in-object.js +0 -90
- package/lib_esnext/rules/no-missing-icu-plural-one-placeholders.d.ts +0 -6
- package/lib_esnext/rules/no-missing-icu-plural-one-placeholders.js +0 -99
- package/lib_esnext/rules/no-multiple-plurals.d.ts +0 -5
- package/lib_esnext/rules/no-multiple-plurals.js +0 -70
- package/lib_esnext/rules/no-multiple-whitespaces.d.ts +0 -5
- package/lib_esnext/rules/no-multiple-whitespaces.js +0 -141
- package/lib_esnext/rules/no-offset.d.ts +0 -5
- package/lib_esnext/rules/no-offset.js +0 -69
- package/lib_esnext/rules/no-useless-message.d.ts +0 -5
- package/lib_esnext/rules/no-useless-message.js +0 -71
- package/lib_esnext/rules/prefer-formatted-message.d.ts +0 -5
- package/lib_esnext/rules/prefer-formatted-message.js +0 -33
- package/lib_esnext/rules/prefer-pound-in-plural.d.ts +0 -5
- package/lib_esnext/rules/prefer-pound-in-plural.js +0 -191
- package/lib_esnext/util.d.ts +0 -32
- package/lib_esnext/util.js +0 -280
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { RuleModule } from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
-
type MessageIds = 'unnecessaryFormat' | 'unnecessaryFormatNumber' | 'unnecessaryFormatDate' | 'unnecessaryFormatTime';
|
|
3
|
-
export declare const name = "no-useless-message";
|
|
4
|
-
export declare const rule: RuleModule<MessageIds>;
|
|
5
|
-
export {};
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { parse, TYPE, } from '@formatjs/icu-messageformat-parser';
|
|
2
|
-
import { getParserServices } from '../context-compat';
|
|
3
|
-
import { extractMessages, getSettings } from '../util';
|
|
4
|
-
function verifyAst(ast) {
|
|
5
|
-
if (ast.length !== 1) {
|
|
6
|
-
return;
|
|
7
|
-
}
|
|
8
|
-
switch (ast[0].type) {
|
|
9
|
-
case TYPE.argument:
|
|
10
|
-
return 'unnecessaryFormat';
|
|
11
|
-
case TYPE.number:
|
|
12
|
-
return 'unnecessaryFormatNumber';
|
|
13
|
-
case TYPE.date:
|
|
14
|
-
return 'unnecessaryFormatDate';
|
|
15
|
-
case TYPE.time:
|
|
16
|
-
return 'unnecessaryFormatTime';
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function checkNode(context, node) {
|
|
20
|
-
const settings = getSettings(context);
|
|
21
|
-
const msgs = extractMessages(node, settings);
|
|
22
|
-
for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
|
|
23
|
-
if (!defaultMessage || !messageNode) {
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
const messageId = verifyAst(parse(defaultMessage, {
|
|
27
|
-
ignoreTag: settings.ignoreTag,
|
|
28
|
-
}));
|
|
29
|
-
if (messageId)
|
|
30
|
-
context.report({
|
|
31
|
-
node: messageNode,
|
|
32
|
-
messageId,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export const name = 'no-useless-message';
|
|
37
|
-
export const rule = {
|
|
38
|
-
meta: {
|
|
39
|
-
type: 'problem',
|
|
40
|
-
docs: {
|
|
41
|
-
description: 'Disallow unnecessary formatted message',
|
|
42
|
-
url: 'https://formatjs.github.io/docs/tooling/linter#no-useless-message',
|
|
43
|
-
},
|
|
44
|
-
fixable: 'code',
|
|
45
|
-
schema: [],
|
|
46
|
-
messages: {
|
|
47
|
-
unnecessaryFormat: 'Unnecessary formatted message.',
|
|
48
|
-
unnecessaryFormatNumber: 'Unnecessary formatted message: just use FormattedNumber or intl.formatNumber.',
|
|
49
|
-
unnecessaryFormatDate: 'Unnecessary formatted message: just use FormattedDate or intl.formatDate.',
|
|
50
|
-
unnecessaryFormatTime: 'Unnecessary formatted message: just use FormattedTime or intl.formatTime.',
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
defaultOptions: [],
|
|
54
|
-
create(context) {
|
|
55
|
-
const callExpressionVisitor = (node) => checkNode(context, node);
|
|
56
|
-
const parserServices = getParserServices(context);
|
|
57
|
-
//@ts-expect-error defineTemplateBodyVisitor exists in Vue parser
|
|
58
|
-
if (parserServices?.defineTemplateBodyVisitor) {
|
|
59
|
-
//@ts-expect-error
|
|
60
|
-
return parserServices.defineTemplateBodyVisitor({
|
|
61
|
-
CallExpression: callExpressionVisitor,
|
|
62
|
-
}, {
|
|
63
|
-
CallExpression: callExpressionVisitor,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
JSXOpeningElement: (node) => checkNode(context, node),
|
|
68
|
-
CallExpression: callExpressionVisitor,
|
|
69
|
-
};
|
|
70
|
-
},
|
|
71
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { isIntlFormatMessageCall } from '../util';
|
|
2
|
-
export const name = 'prefer-formatted-message';
|
|
3
|
-
export const rule = {
|
|
4
|
-
meta: {
|
|
5
|
-
type: 'suggestion',
|
|
6
|
-
docs: {
|
|
7
|
-
description: 'Prefer `FormattedMessage` component over `intl.formatMessage` if applicable.',
|
|
8
|
-
url: 'https://formatjs.github.io/docs/tooling/linter#prefer-formatted-message',
|
|
9
|
-
},
|
|
10
|
-
messages: {
|
|
11
|
-
jsxChildren: 'Prefer `FormattedMessage` over `intl.formatMessage` in the JSX children expression.',
|
|
12
|
-
},
|
|
13
|
-
schema: [],
|
|
14
|
-
},
|
|
15
|
-
defaultOptions: [],
|
|
16
|
-
// TODO: Vue support
|
|
17
|
-
create(context) {
|
|
18
|
-
return {
|
|
19
|
-
JSXElement: (node) => {
|
|
20
|
-
node.children.forEach(child => {
|
|
21
|
-
if (child.type !== 'JSXExpressionContainer' ||
|
|
22
|
-
!isIntlFormatMessageCall(child.expression)) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
context.report({
|
|
26
|
-
node: child,
|
|
27
|
-
messageId: 'jsxChildren',
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
},
|
|
33
|
-
};
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { parse, TYPE, } from '@formatjs/icu-messageformat-parser';
|
|
2
|
-
import MagicString from 'magic-string';
|
|
3
|
-
import { getParserServices } from '../context-compat';
|
|
4
|
-
import { extractMessages, getSettings, patchMessage } from '../util';
|
|
5
|
-
function verifyAst(context, messageNode, ast) {
|
|
6
|
-
const patches = [];
|
|
7
|
-
_verifyAst(ast);
|
|
8
|
-
if (patches.length > 0) {
|
|
9
|
-
const patchedMessage = patchMessage(messageNode, ast, content => {
|
|
10
|
-
return patches
|
|
11
|
-
.reduce((magicString, patch) => {
|
|
12
|
-
switch (patch.type) {
|
|
13
|
-
case 'prependLeft':
|
|
14
|
-
return magicString.prependLeft(patch.index, patch.content);
|
|
15
|
-
case 'remove':
|
|
16
|
-
return magicString.remove(patch.start, patch.end);
|
|
17
|
-
case 'update':
|
|
18
|
-
return magicString.update(patch.start, patch.end, patch.content);
|
|
19
|
-
}
|
|
20
|
-
}, new MagicString(content))
|
|
21
|
-
.toString();
|
|
22
|
-
});
|
|
23
|
-
context.report({
|
|
24
|
-
node: messageNode,
|
|
25
|
-
messageId: 'preferPoundInPlurals',
|
|
26
|
-
fix: patchedMessage !== null
|
|
27
|
-
? fixer => fixer.replaceText(messageNode, patchedMessage)
|
|
28
|
-
: null,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
function _verifyAst(ast) {
|
|
32
|
-
for (let i = 0; i < ast.length; i++) {
|
|
33
|
-
const current = ast[i];
|
|
34
|
-
switch (current.type) {
|
|
35
|
-
case TYPE.argument:
|
|
36
|
-
case TYPE.number: {
|
|
37
|
-
// Applicable to only plain argument or number argument without any style
|
|
38
|
-
if (current.type === TYPE.number && current.style) {
|
|
39
|
-
break;
|
|
40
|
-
}
|
|
41
|
-
const next = ast[i + 1];
|
|
42
|
-
const nextNext = ast[i + 2];
|
|
43
|
-
if (next &&
|
|
44
|
-
nextNext &&
|
|
45
|
-
next.type === TYPE.literal &&
|
|
46
|
-
next.value === ' ' &&
|
|
47
|
-
nextNext.type === TYPE.plural &&
|
|
48
|
-
nextNext.value === current.value) {
|
|
49
|
-
// `{A} {A, plural, one {B} other {Bs}}` => `{A, plural, one {# B} other {# Bs}}`
|
|
50
|
-
_removeRangeAndPrependPluralClauses(current.location.start.offset, next.location.end.offset, nextNext, '# ');
|
|
51
|
-
}
|
|
52
|
-
else if (next &&
|
|
53
|
-
next.type === TYPE.plural &&
|
|
54
|
-
next.value === current.value) {
|
|
55
|
-
// `{A}{A, plural, one {B} other {Bs}}` => `{A, plural, one {#B} other {#Bs}}`
|
|
56
|
-
_removeRangeAndPrependPluralClauses(current.location.start.offset, current.location.end.offset, next, '#');
|
|
57
|
-
}
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
case TYPE.plural: {
|
|
61
|
-
// `{A, plural, one {{A} B} other {{A} Bs}}` => `{A, plural, one {# B} other {# Bs}}`
|
|
62
|
-
const name = current.value;
|
|
63
|
-
for (const { value } of Object.values(current.options)) {
|
|
64
|
-
_replacementArgumentWithPound(name, value);
|
|
65
|
-
}
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
case TYPE.select: {
|
|
69
|
-
for (const { value } of Object.values(current.options)) {
|
|
70
|
-
_verifyAst(value);
|
|
71
|
-
}
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
case TYPE.tag:
|
|
75
|
-
_verifyAst(current.children);
|
|
76
|
-
break;
|
|
77
|
-
default:
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// Replace plain argument of number argument w/o style option that matches
|
|
83
|
-
// the name with a pound sign.
|
|
84
|
-
function _replacementArgumentWithPound(name, ast) {
|
|
85
|
-
for (const element of ast) {
|
|
86
|
-
switch (element.type) {
|
|
87
|
-
case TYPE.argument:
|
|
88
|
-
case TYPE.number: {
|
|
89
|
-
if (element.value === name &&
|
|
90
|
-
// Either plain argument or number argument without any style
|
|
91
|
-
(element.type !== TYPE.number || !element.style)) {
|
|
92
|
-
patches.push({
|
|
93
|
-
type: 'update',
|
|
94
|
-
start: element.location.start.offset,
|
|
95
|
-
end: element.location.end.offset,
|
|
96
|
-
content: '#',
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
case TYPE.tag: {
|
|
102
|
-
_replacementArgumentWithPound(name, element.children);
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
case TYPE.select: {
|
|
106
|
-
for (const { value } of Object.values(element.options)) {
|
|
107
|
-
_replacementArgumentWithPound(name, value);
|
|
108
|
-
}
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
default:
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
// Helper to remove a certain text range and then prepend the specified text to
|
|
117
|
-
// each plural clause.
|
|
118
|
-
function _removeRangeAndPrependPluralClauses(rangeToRemoveStart, rangeToRemoveEnd, pluralElement, prependText) {
|
|
119
|
-
// Delete both the `{A}` and ` `
|
|
120
|
-
patches.push({
|
|
121
|
-
type: 'remove',
|
|
122
|
-
start: rangeToRemoveStart,
|
|
123
|
-
end: rangeToRemoveEnd,
|
|
124
|
-
});
|
|
125
|
-
// Insert `# ` to the beginning of every option clause
|
|
126
|
-
for (const { location } of Object.values(pluralElement.options)) {
|
|
127
|
-
// location marks the entire clause with the surrounding braces
|
|
128
|
-
patches.push({
|
|
129
|
-
type: 'prependLeft',
|
|
130
|
-
index: location.start.offset + 1,
|
|
131
|
-
content: prependText,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function checkNode(context, node) {
|
|
137
|
-
const msgs = extractMessages(node, getSettings(context));
|
|
138
|
-
for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
|
|
139
|
-
if (!defaultMessage || !messageNode) {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
let ast;
|
|
143
|
-
try {
|
|
144
|
-
ast = parse(defaultMessage, { captureLocation: true });
|
|
145
|
-
}
|
|
146
|
-
catch (e) {
|
|
147
|
-
context.report({
|
|
148
|
-
node: messageNode,
|
|
149
|
-
messageId: 'parseError',
|
|
150
|
-
data: { message: e instanceof Error ? e.message : String(e) },
|
|
151
|
-
});
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
verifyAst(context, messageNode, ast);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
export const name = 'prefer-pound-in-plural';
|
|
158
|
-
export const rule = {
|
|
159
|
-
meta: {
|
|
160
|
-
type: 'suggestion',
|
|
161
|
-
docs: {
|
|
162
|
-
description: 'Prefer using # to reference the count in the plural argument.',
|
|
163
|
-
url: 'https://formatjs.github.io/docs/tooling/linter#prefer-pound-in-plurals',
|
|
164
|
-
},
|
|
165
|
-
messages: {
|
|
166
|
-
preferPoundInPlurals: 'Prefer using # to reference the count in the plural argument instead of repeating the argument.',
|
|
167
|
-
parseError: '{{message}}',
|
|
168
|
-
},
|
|
169
|
-
fixable: 'code',
|
|
170
|
-
schema: [],
|
|
171
|
-
},
|
|
172
|
-
defaultOptions: [],
|
|
173
|
-
// TODO: Vue support
|
|
174
|
-
create(context) {
|
|
175
|
-
const callExpressionVisitor = (node) => checkNode(context, node);
|
|
176
|
-
const parserServices = getParserServices(context);
|
|
177
|
-
//@ts-expect-error defineTemplateBodyVisitor exists in Vue parser
|
|
178
|
-
if (parserServices?.defineTemplateBodyVisitor) {
|
|
179
|
-
//@ts-expect-error
|
|
180
|
-
return parserServices.defineTemplateBodyVisitor({
|
|
181
|
-
CallExpression: callExpressionVisitor,
|
|
182
|
-
}, {
|
|
183
|
-
CallExpression: callExpressionVisitor,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
return {
|
|
187
|
-
JSXOpeningElement: (node) => checkNode(context, node),
|
|
188
|
-
CallExpression: callExpressionVisitor,
|
|
189
|
-
};
|
|
190
|
-
},
|
|
191
|
-
};
|
package/lib_esnext/util.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { MessageFormatElement } from '@formatjs/icu-messageformat-parser';
|
|
2
|
-
import { TSESTree } from '@typescript-eslint/utils';
|
|
3
|
-
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
|
4
|
-
export interface MessageDescriptor {
|
|
5
|
-
id?: string;
|
|
6
|
-
defaultMessage?: string;
|
|
7
|
-
description?: string | object;
|
|
8
|
-
}
|
|
9
|
-
export interface Settings {
|
|
10
|
-
excludeMessageDeclCalls?: boolean;
|
|
11
|
-
additionalFunctionNames?: string[];
|
|
12
|
-
additionalComponentNames?: string[];
|
|
13
|
-
ignoreTag?: boolean;
|
|
14
|
-
}
|
|
15
|
-
export interface MessageDescriptorNodeInfo {
|
|
16
|
-
message: MessageDescriptor;
|
|
17
|
-
messageNode?: TSESTree.Property['value'] | TSESTree.JSXAttribute['value'];
|
|
18
|
-
messagePropNode?: TSESTree.Property | TSESTree.JSXAttribute;
|
|
19
|
-
descriptionNode?: TSESTree.Property['value'] | TSESTree.JSXAttribute['value'];
|
|
20
|
-
idValueNode?: TSESTree.Property['value'] | TSESTree.JSXAttribute['value'];
|
|
21
|
-
idPropNode?: TSESTree.Property | TSESTree.JSXAttribute;
|
|
22
|
-
}
|
|
23
|
-
export declare function getSettings<TMessageIds extends string, TOptions extends readonly unknown[]>({ settings }: RuleContext<TMessageIds, TOptions>): Settings;
|
|
24
|
-
export declare function isIntlFormatMessageCall(node: TSESTree.Node): node is TSESTree.CallExpression;
|
|
25
|
-
export declare function extractMessageDescriptor(node?: TSESTree.Expression): MessageDescriptorNodeInfo | undefined;
|
|
26
|
-
export declare function extractMessages(node: TSESTree.Node, { additionalComponentNames, additionalFunctionNames, excludeMessageDeclCalls, }?: Settings): Array<[MessageDescriptorNodeInfo, TSESTree.Expression | undefined]>;
|
|
27
|
-
/**
|
|
28
|
-
* Apply changes to the ICU message in code. The return value can be used in
|
|
29
|
-
* `fixer.replaceText(messageNode, <return value>)`. If the return value is null,
|
|
30
|
-
* it means that the patch cannot be applied.
|
|
31
|
-
*/
|
|
32
|
-
export declare function patchMessage(messageNode: TSESTree.Node, ast: MessageFormatElement[], patcher: (messageContent: string, ast: MessageFormatElement[]) => string): string | null;
|
package/lib_esnext/util.js
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
const FORMAT_FUNCTION_NAMES = new Set(['$formatMessage', 'formatMessage', '$t']);
|
|
2
|
-
const COMPONENT_NAMES = new Set(['FormattedMessage']);
|
|
3
|
-
const DECLARATION_FUNCTION_NAMES = new Set(['defineMessage']);
|
|
4
|
-
export function getSettings({ settings }) {
|
|
5
|
-
return settings.formatjs ?? settings;
|
|
6
|
-
}
|
|
7
|
-
function isStringLiteral(node) {
|
|
8
|
-
return node.type === 'Literal' && typeof node.value === 'string';
|
|
9
|
-
}
|
|
10
|
-
function isTemplateLiteralWithoutVar(node) {
|
|
11
|
-
return node.type === 'TemplateLiteral' && node.quasis.length === 1;
|
|
12
|
-
}
|
|
13
|
-
function staticallyEvaluateStringConcat(node) {
|
|
14
|
-
if (!isStringLiteral(node.right)) {
|
|
15
|
-
return ['', false];
|
|
16
|
-
}
|
|
17
|
-
if (isStringLiteral(node.left)) {
|
|
18
|
-
return [String(node.left.value) + node.right.value, true];
|
|
19
|
-
}
|
|
20
|
-
if (node.left.type === 'BinaryExpression') {
|
|
21
|
-
const [result, isStaticallyEvaluatable] = staticallyEvaluateStringConcat(node.left);
|
|
22
|
-
return [result + node.right.value, isStaticallyEvaluatable];
|
|
23
|
-
}
|
|
24
|
-
return ['', false];
|
|
25
|
-
}
|
|
26
|
-
export function isIntlFormatMessageCall(node) {
|
|
27
|
-
return (node.type === 'CallExpression' &&
|
|
28
|
-
node.callee.type === 'MemberExpression' &&
|
|
29
|
-
((node.callee.object.type === 'Identifier' &&
|
|
30
|
-
node.callee.object.name === 'intl') ||
|
|
31
|
-
(node.callee.object.type === 'MemberExpression' &&
|
|
32
|
-
node.callee.object.property.type === 'Identifier' &&
|
|
33
|
-
node.callee.object.property.name === 'intl')) &&
|
|
34
|
-
node.callee.property.type === 'Identifier' &&
|
|
35
|
-
(node.callee.property.name === 'formatMessage' ||
|
|
36
|
-
node.callee.property.name === '$t') &&
|
|
37
|
-
node.arguments.length >= 1 &&
|
|
38
|
-
node.arguments[0].type === 'ObjectExpression');
|
|
39
|
-
}
|
|
40
|
-
function isSingleMessageDescriptorDeclaration(node, functionNames) {
|
|
41
|
-
return (node.type === 'CallExpression' &&
|
|
42
|
-
node.callee.type === 'Identifier' &&
|
|
43
|
-
functionNames.has(node.callee.name));
|
|
44
|
-
}
|
|
45
|
-
function isMultipleMessageDescriptorDeclaration(node) {
|
|
46
|
-
return (node.type === 'CallExpression' &&
|
|
47
|
-
node.callee.type === 'Identifier' &&
|
|
48
|
-
node.callee.name === 'defineMessages');
|
|
49
|
-
}
|
|
50
|
-
export function extractMessageDescriptor(node) {
|
|
51
|
-
if (!node || node.type !== 'ObjectExpression') {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const result = {
|
|
55
|
-
message: {},
|
|
56
|
-
messageNode: undefined,
|
|
57
|
-
messagePropNode: undefined,
|
|
58
|
-
descriptionNode: undefined,
|
|
59
|
-
idValueNode: undefined,
|
|
60
|
-
};
|
|
61
|
-
for (const prop of node.properties) {
|
|
62
|
-
if (prop.type !== 'Property' || prop.key.type !== 'Identifier') {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const valueNode = prop.value;
|
|
66
|
-
let value = undefined;
|
|
67
|
-
if (isStringLiteral(valueNode)) {
|
|
68
|
-
value = valueNode.value;
|
|
69
|
-
}
|
|
70
|
-
// like "`asd`"
|
|
71
|
-
else if (isTemplateLiteralWithoutVar(valueNode)) {
|
|
72
|
-
value = valueNode.quasis[0].value.cooked;
|
|
73
|
-
}
|
|
74
|
-
// like "dedent`asd`"
|
|
75
|
-
else if (valueNode.type === 'TaggedTemplateExpression') {
|
|
76
|
-
const { quasi } = valueNode;
|
|
77
|
-
if (!isTemplateLiteralWithoutVar(quasi)) {
|
|
78
|
-
throw new Error('Tagged template expression must be no substitution');
|
|
79
|
-
}
|
|
80
|
-
value = quasi.quasis[0].value.cooked;
|
|
81
|
-
}
|
|
82
|
-
// like "`asd` + `asd`"
|
|
83
|
-
else if (valueNode.type === 'BinaryExpression') {
|
|
84
|
-
const [result, isStatic] = staticallyEvaluateStringConcat(valueNode);
|
|
85
|
-
if (isStatic) {
|
|
86
|
-
value = result;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
switch (prop.key.name) {
|
|
90
|
-
case 'defaultMessage':
|
|
91
|
-
result.messagePropNode = prop;
|
|
92
|
-
result.messageNode = valueNode;
|
|
93
|
-
result.message.defaultMessage = value;
|
|
94
|
-
break;
|
|
95
|
-
case 'description':
|
|
96
|
-
result.descriptionNode = valueNode;
|
|
97
|
-
result.message.description = value;
|
|
98
|
-
break;
|
|
99
|
-
case 'id':
|
|
100
|
-
result.message.id = value;
|
|
101
|
-
result.idValueNode = valueNode;
|
|
102
|
-
result.idPropNode = prop;
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
function extractMessageDescriptorFromJSXElement(node) {
|
|
109
|
-
if (!node || !node.attributes) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
let values;
|
|
113
|
-
const result = {
|
|
114
|
-
message: {},
|
|
115
|
-
messageNode: undefined,
|
|
116
|
-
messagePropNode: undefined,
|
|
117
|
-
descriptionNode: undefined,
|
|
118
|
-
idValueNode: undefined,
|
|
119
|
-
idPropNode: undefined,
|
|
120
|
-
};
|
|
121
|
-
let hasSpreadAttribute = false;
|
|
122
|
-
for (const prop of node.attributes) {
|
|
123
|
-
// We can't analyze spread attr
|
|
124
|
-
if (prop.type === 'JSXSpreadAttribute') {
|
|
125
|
-
hasSpreadAttribute = true;
|
|
126
|
-
}
|
|
127
|
-
if (prop.type !== 'JSXAttribute' || prop.name.type !== 'JSXIdentifier') {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
const key = prop.name;
|
|
131
|
-
let valueNode = prop.value;
|
|
132
|
-
let value = undefined;
|
|
133
|
-
if (valueNode) {
|
|
134
|
-
if (isStringLiteral(valueNode)) {
|
|
135
|
-
value = valueNode.value;
|
|
136
|
-
}
|
|
137
|
-
else if (valueNode?.type === 'JSXExpressionContainer') {
|
|
138
|
-
const { expression } = valueNode;
|
|
139
|
-
if (expression.type === 'BinaryExpression') {
|
|
140
|
-
const [result, isStatic] = staticallyEvaluateStringConcat(expression);
|
|
141
|
-
if (isStatic) {
|
|
142
|
-
value = result;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// like "`asd`"
|
|
146
|
-
else if (isTemplateLiteralWithoutVar(expression)) {
|
|
147
|
-
value = expression.quasis[0].value.cooked;
|
|
148
|
-
}
|
|
149
|
-
// like "dedent`asd`"
|
|
150
|
-
else if (expression.type === 'TaggedTemplateExpression') {
|
|
151
|
-
const { quasi } = expression;
|
|
152
|
-
if (!isTemplateLiteralWithoutVar(quasi)) {
|
|
153
|
-
throw new Error('Tagged template expression must be no substitution');
|
|
154
|
-
}
|
|
155
|
-
value = quasi.quasis[0].value.cooked;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
switch (key.name) {
|
|
160
|
-
case 'defaultMessage':
|
|
161
|
-
result.messagePropNode = prop;
|
|
162
|
-
result.messageNode = valueNode;
|
|
163
|
-
if (value) {
|
|
164
|
-
result.message.defaultMessage = value;
|
|
165
|
-
}
|
|
166
|
-
break;
|
|
167
|
-
case 'description':
|
|
168
|
-
result.descriptionNode = valueNode;
|
|
169
|
-
if (value) {
|
|
170
|
-
result.message.description = value;
|
|
171
|
-
}
|
|
172
|
-
break;
|
|
173
|
-
case 'id':
|
|
174
|
-
result.idValueNode = valueNode;
|
|
175
|
-
result.idPropNode = prop;
|
|
176
|
-
if (value) {
|
|
177
|
-
result.message.id = value;
|
|
178
|
-
}
|
|
179
|
-
break;
|
|
180
|
-
case 'values':
|
|
181
|
-
if (valueNode?.type === 'JSXExpressionContainer' &&
|
|
182
|
-
valueNode.expression.type === 'ObjectExpression') {
|
|
183
|
-
values = valueNode.expression;
|
|
184
|
-
}
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (!result.messagePropNode &&
|
|
189
|
-
!result.descriptionNode &&
|
|
190
|
-
!result.idPropNode &&
|
|
191
|
-
hasSpreadAttribute) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
return [result, values];
|
|
195
|
-
}
|
|
196
|
-
function extractMessageDescriptors(node) {
|
|
197
|
-
if (!node || node.type !== 'ObjectExpression' || !node.properties.length) {
|
|
198
|
-
return [];
|
|
199
|
-
}
|
|
200
|
-
const msgs = [];
|
|
201
|
-
for (const prop of node.properties) {
|
|
202
|
-
if (prop.type !== 'Property') {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
const msg = prop.value;
|
|
206
|
-
if (msg.type !== 'ObjectExpression') {
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
const nodeInfo = extractMessageDescriptor(msg);
|
|
210
|
-
if (nodeInfo) {
|
|
211
|
-
msgs.push(nodeInfo);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return msgs;
|
|
215
|
-
}
|
|
216
|
-
export function extractMessages(node, { additionalComponentNames, additionalFunctionNames, excludeMessageDeclCalls, } = {}) {
|
|
217
|
-
const allFormatFunctionNames = Array.isArray(additionalFunctionNames)
|
|
218
|
-
? new Set([
|
|
219
|
-
...Array.from(FORMAT_FUNCTION_NAMES),
|
|
220
|
-
...additionalFunctionNames,
|
|
221
|
-
])
|
|
222
|
-
: FORMAT_FUNCTION_NAMES;
|
|
223
|
-
const allComponentNames = Array.isArray(additionalComponentNames)
|
|
224
|
-
? new Set([...Array.from(COMPONENT_NAMES), ...additionalComponentNames])
|
|
225
|
-
: COMPONENT_NAMES;
|
|
226
|
-
if (node.type === 'CallExpression') {
|
|
227
|
-
const expr = node;
|
|
228
|
-
const args0 = expr.arguments[0];
|
|
229
|
-
const args1 = expr.arguments[1];
|
|
230
|
-
// We can't really analyze spread element
|
|
231
|
-
if (!args0 || args0.type === 'SpreadElement') {
|
|
232
|
-
return [];
|
|
233
|
-
}
|
|
234
|
-
if ((!excludeMessageDeclCalls &&
|
|
235
|
-
isSingleMessageDescriptorDeclaration(node, DECLARATION_FUNCTION_NAMES)) ||
|
|
236
|
-
isIntlFormatMessageCall(node) ||
|
|
237
|
-
isSingleMessageDescriptorDeclaration(node, allFormatFunctionNames)) {
|
|
238
|
-
const msgDescriptorNodeInfo = extractMessageDescriptor(args0);
|
|
239
|
-
if (msgDescriptorNodeInfo && (!args1 || args1.type !== 'SpreadElement')) {
|
|
240
|
-
return [[msgDescriptorNodeInfo, args1]];
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
else if (!excludeMessageDeclCalls &&
|
|
244
|
-
isMultipleMessageDescriptorDeclaration(node)) {
|
|
245
|
-
return extractMessageDescriptors(args0).map(msg => [msg, undefined]);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
else if (node.type === 'JSXOpeningElement' &&
|
|
249
|
-
node.name &&
|
|
250
|
-
node.name.type === 'JSXIdentifier' &&
|
|
251
|
-
allComponentNames.has(node.name.name)) {
|
|
252
|
-
const msgDescriptorNodeInfo = extractMessageDescriptorFromJSXElement(node);
|
|
253
|
-
if (msgDescriptorNodeInfo) {
|
|
254
|
-
return [msgDescriptorNodeInfo];
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return [];
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Apply changes to the ICU message in code. The return value can be used in
|
|
261
|
-
* `fixer.replaceText(messageNode, <return value>)`. If the return value is null,
|
|
262
|
-
* it means that the patch cannot be applied.
|
|
263
|
-
*/
|
|
264
|
-
export function patchMessage(messageNode, ast, patcher) {
|
|
265
|
-
if (messageNode.type === 'Literal' &&
|
|
266
|
-
messageNode.value &&
|
|
267
|
-
typeof messageNode.value === 'string') {
|
|
268
|
-
return ('"' + patcher(messageNode.value, ast).replace('"', '\\"') + '"');
|
|
269
|
-
}
|
|
270
|
-
else if (messageNode.type === 'TemplateLiteral' &&
|
|
271
|
-
messageNode.quasis.length === 1 &&
|
|
272
|
-
messageNode.expressions.length === 0) {
|
|
273
|
-
return ('`' +
|
|
274
|
-
patcher(messageNode.quasis[0].value.cooked, ast)
|
|
275
|
-
.replace(/\\/g, '\\\\')
|
|
276
|
-
.replace(/`/g, '\\`') +
|
|
277
|
-
'`');
|
|
278
|
-
}
|
|
279
|
-
return null;
|
|
280
|
-
}
|