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.
Files changed (74) hide show
  1. package/context-compat.js +1 -5
  2. package/index.d.ts +15 -1
  3. package/index.js +46 -45
  4. package/package.json +6 -4
  5. package/rules/blocklist-elements.d.ts +1 -2
  6. package/rules/blocklist-elements.js +23 -26
  7. package/rules/enforce-default-message.js +8 -11
  8. package/rules/enforce-description.js +8 -11
  9. package/rules/enforce-id.d.ts +1 -0
  10. package/rules/enforce-id.js +21 -14
  11. package/rules/enforce-placeholders.js +14 -17
  12. package/rules/enforce-plural-rules.js +10 -13
  13. package/rules/no-camel-case.js +11 -14
  14. package/rules/no-complex-selectors.js +13 -16
  15. package/rules/no-emoji.js +11 -14
  16. package/rules/no-id.js +6 -9
  17. package/rules/no-invalid-icu.js +9 -12
  18. package/rules/no-literal-string-in-jsx.js +33 -13
  19. package/rules/no-literal-string-in-object.js +8 -11
  20. package/rules/no-missing-icu-plural-one-placeholders.js +15 -19
  21. package/rules/no-multiple-plurals.js +10 -13
  22. package/rules/no-multiple-whitespaces.js +27 -32
  23. package/rules/no-offset.js +10 -13
  24. package/rules/no-useless-message.js +13 -16
  25. package/rules/prefer-formatted-message.js +4 -7
  26. package/rules/prefer-pound-in-plural.js +25 -29
  27. package/util.js +5 -12
  28. package/lib_esnext/context-compat.d.ts +0 -3
  29. package/lib_esnext/context-compat.js +0 -6
  30. package/lib_esnext/index.d.ts +0 -1
  31. package/lib_esnext/index.js +0 -146
  32. package/lib_esnext/package.json +0 -31
  33. package/lib_esnext/rules/blocklist-elements.d.ts +0 -15
  34. package/lib_esnext/rules/blocklist-elements.js +0 -132
  35. package/lib_esnext/rules/enforce-default-message.d.ts +0 -10
  36. package/lib_esnext/rules/enforce-default-message.js +0 -68
  37. package/lib_esnext/rules/enforce-description.d.ts +0 -10
  38. package/lib_esnext/rules/enforce-description.js +0 -66
  39. package/lib_esnext/rules/enforce-id.d.ts +0 -10
  40. package/lib_esnext/rules/enforce-id.js +0 -153
  41. package/lib_esnext/rules/enforce-placeholders.d.ts +0 -8
  42. package/lib_esnext/rules/enforce-placeholders.js +0 -147
  43. package/lib_esnext/rules/enforce-plural-rules.d.ts +0 -17
  44. package/lib_esnext/rules/enforce-plural-rules.js +0 -103
  45. package/lib_esnext/rules/no-camel-case.d.ts +0 -5
  46. package/lib_esnext/rules/no-camel-case.js +0 -76
  47. package/lib_esnext/rules/no-complex-selectors.d.ts +0 -9
  48. package/lib_esnext/rules/no-complex-selectors.js +0 -136
  49. package/lib_esnext/rules/no-emoji.d.ts +0 -9
  50. package/lib_esnext/rules/no-emoji.js +0 -99
  51. package/lib_esnext/rules/no-id.d.ts +0 -5
  52. package/lib_esnext/rules/no-id.js +0 -58
  53. package/lib_esnext/rules/no-invalid-icu.d.ts +0 -5
  54. package/lib_esnext/rules/no-invalid-icu.js +0 -60
  55. package/lib_esnext/rules/no-literal-string-in-jsx.d.ts +0 -13
  56. package/lib_esnext/rules/no-literal-string-in-jsx.js +0 -179
  57. package/lib_esnext/rules/no-literal-string-in-object.d.ts +0 -9
  58. package/lib_esnext/rules/no-literal-string-in-object.js +0 -90
  59. package/lib_esnext/rules/no-missing-icu-plural-one-placeholders.d.ts +0 -6
  60. package/lib_esnext/rules/no-missing-icu-plural-one-placeholders.js +0 -99
  61. package/lib_esnext/rules/no-multiple-plurals.d.ts +0 -5
  62. package/lib_esnext/rules/no-multiple-plurals.js +0 -70
  63. package/lib_esnext/rules/no-multiple-whitespaces.d.ts +0 -5
  64. package/lib_esnext/rules/no-multiple-whitespaces.js +0 -141
  65. package/lib_esnext/rules/no-offset.d.ts +0 -5
  66. package/lib_esnext/rules/no-offset.js +0 -69
  67. package/lib_esnext/rules/no-useless-message.d.ts +0 -5
  68. package/lib_esnext/rules/no-useless-message.js +0 -71
  69. package/lib_esnext/rules/prefer-formatted-message.d.ts +0 -5
  70. package/lib_esnext/rules/prefer-formatted-message.js +0 -33
  71. package/lib_esnext/rules/prefer-pound-in-plural.d.ts +0 -5
  72. package/lib_esnext/rules/prefer-pound-in-plural.js +0 -191
  73. package/lib_esnext/util.d.ts +0 -32
  74. 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,5 +0,0 @@
1
- import { RuleModule } from '@typescript-eslint/utils/ts-eslint';
2
- type MessageIds = 'jsxChildren';
3
- export declare const name = "prefer-formatted-message";
4
- export declare const rule: RuleModule<MessageIds>;
5
- export {};
@@ -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,5 +0,0 @@
1
- import { RuleModule } from '@typescript-eslint/utils/ts-eslint';
2
- type MessageIds = 'preferPoundInPlurals' | 'parseError';
3
- export declare const name = "prefer-pound-in-plural";
4
- export declare const rule: RuleModule<MessageIds>;
5
- export {};
@@ -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
- };
@@ -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;
@@ -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
- }