eslint-plugin-formatjs 4.5.0 → 4.7.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/index.d.ts CHANGED
@@ -16,6 +16,8 @@ declare const plugin: {
16
16
  'no-multiple-whitespaces': import("eslint").Rule.RuleModule;
17
17
  'no-offset': import("eslint").Rule.RuleModule;
18
18
  'no-useless-message': import("eslint").Rule.RuleModule;
19
+ 'prefer-formatted-message': import("eslint").Rule.RuleModule;
20
+ 'prefer-pound-in-plural': import("eslint").Rule.RuleModule;
19
21
  };
20
22
  };
21
23
  export type Plugin = typeof plugin;
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAiBA,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;CAmBX,CAAA;AAED,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAmBA,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;CAqBX,CAAA;AAED,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAA"}
package/index.js CHANGED
@@ -17,6 +17,8 @@ const no_multiple_whitespaces_1 = tslib_1.__importDefault(require("./rules/no-mu
17
17
  const no_offset_1 = tslib_1.__importDefault(require("./rules/no-offset"));
18
18
  const no_literal_string_in_jsx_1 = tslib_1.__importDefault(require("./rules/no-literal-string-in-jsx"));
19
19
  const no_useless_message_1 = tslib_1.__importDefault(require("./rules/no-useless-message"));
20
+ const prefer_formatted_message_1 = tslib_1.__importDefault(require("./rules/prefer-formatted-message"));
21
+ const prefer_pound_in_plural_1 = tslib_1.__importDefault(require("./rules/prefer-pound-in-plural"));
20
22
  const plugin = {
21
23
  rules: {
22
24
  'blocklist-elements': blocklist_elements_1.default,
@@ -34,7 +36,9 @@ const plugin = {
34
36
  'no-multiple-plurals': no_multiple_plurals_1.default,
35
37
  'no-multiple-whitespaces': no_multiple_whitespaces_1.default,
36
38
  'no-offset': no_offset_1.default,
37
- 'no-useless-message': no_useless_message_1.default
38
- }
39
+ 'no-useless-message': no_useless_message_1.default,
40
+ 'prefer-formatted-message': prefer_formatted_message_1.default,
41
+ 'prefer-pound-in-plural': prefer_pound_in_plural_1.default,
42
+ },
39
43
  };
40
44
  module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-formatjs",
3
- "version": "4.5.0",
3
+ "version": "4.7.0",
4
4
  "description": "ESLint plugin for formatjs",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -20,15 +20,17 @@
20
20
  },
21
21
  "homepage": "https://github.com/formatjs/formatjs#readme",
22
22
  "dependencies": {
23
- "@formatjs/icu-messageformat-parser": "2.1.14",
24
- "@formatjs/ts-transformer": "3.11.5",
23
+ "@formatjs/icu-messageformat-parser": "2.2.0",
24
+ "@formatjs/ts-transformer": "3.11.6",
25
25
  "@types/eslint": "7 || 8",
26
26
  "@types/picomatch": "^2.3.0",
27
- "@typescript-eslint/typescript-estree": "^5.9.1",
28
- "emoji-regex": "^10.0.0",
27
+ "@typescript-eslint/typescript-estree": "5.45.0",
28
+ "emoji-regex": "^10.2.1",
29
+ "magic-string": "^0.29.0",
29
30
  "picomatch": "^2.3.1",
30
- "tslib": "^2.4.0",
31
- "typescript": "^4.7"
31
+ "tslib": "2.4.0",
32
+ "typescript": "^4.7",
33
+ "unicode-emoji-utils": "^1.1.1"
32
34
  },
33
35
  "peerDependencies": {
34
36
  "eslint": "7 || 8"
@@ -1 +1 @@
1
- {"version":3,"file":"no-emoji.d.ts","sourceRoot":"","sources":["no-emoji.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AA2B3B,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA8BhB,CAAA;AAED,eAAe,IAAI,CAAA"}
1
+ {"version":3,"file":"no-emoji.d.ts","sourceRoot":"","sources":["no-emoji.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AA+E3B,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA0ChB,CAAA;AAED,eAAe,IAAI,CAAA"}
package/rules/no-emoji.js CHANGED
@@ -1,23 +1,63 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const tslib_1 = require("tslib");
4
3
  const util_1 = require("../util");
5
- const emoji_regex_1 = tslib_1.__importDefault(require("emoji-regex"));
6
- const EMOJI_REGEX = emoji_regex_1.default();
4
+ const unicode_emoji_utils_1 = require("unicode-emoji-utils");
7
5
  function checkNode(context, node) {
8
6
  const msgs = (0, util_1.extractMessages)(node, (0, util_1.getSettings)(context));
7
+ let allowedEmojis = [];
8
+ let versionAbove;
9
+ const [emojiConfig] = context.options;
10
+ if (emojiConfig?.versionAbove &&
11
+ (0, unicode_emoji_utils_1.isValidEmojiVersion)(emojiConfig.versionAbove) &&
12
+ !versionAbove &&
13
+ allowedEmojis.length === 0) {
14
+ versionAbove = emojiConfig.versionAbove;
15
+ allowedEmojis = (0, unicode_emoji_utils_1.getAllEmojis)((0, unicode_emoji_utils_1.filterEmojis)(versionAbove));
16
+ }
9
17
  for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
10
18
  if (!defaultMessage || !messageNode) {
11
19
  continue;
12
20
  }
13
- if (EMOJI_REGEX.test(defaultMessage)) {
14
- context.report({
15
- node: messageNode,
16
- message: 'Emojis are not allowed',
17
- });
21
+ if ((0, unicode_emoji_utils_1.hasEmoji)(defaultMessage)) {
22
+ if (versionAbove) {
23
+ for (const emoji of (0, unicode_emoji_utils_1.extractEmojis)(defaultMessage)) {
24
+ if (!allowedEmojis.includes(emoji)) {
25
+ context.report({
26
+ node: messageNode,
27
+ messageId: 'notAllowedAboveVersion',
28
+ data: {
29
+ versionAbove,
30
+ emoji,
31
+ },
32
+ });
33
+ }
34
+ }
35
+ }
36
+ else {
37
+ context.report({
38
+ node: messageNode,
39
+ messageId: 'notAllowed',
40
+ });
41
+ }
18
42
  }
19
43
  }
20
44
  }
45
+ const versionAboveEnums = [
46
+ '0.6',
47
+ '0.7',
48
+ '1.0',
49
+ '2.0',
50
+ '3.0',
51
+ '4.0',
52
+ '5.0',
53
+ '11.0',
54
+ '12.0',
55
+ '12.1',
56
+ '13.0',
57
+ '13.1',
58
+ '14.0',
59
+ '15.0',
60
+ ];
21
61
  const rule = {
22
62
  meta: {
23
63
  type: 'problem',
@@ -28,6 +68,17 @@ const rule = {
28
68
  url: 'https://formatjs.io/docs/tooling/linter#no-emoji',
29
69
  },
30
70
  fixable: 'code',
71
+ schema: [
72
+ {
73
+ type: 'object',
74
+ properties: { versionAbove: { type: 'string', enum: versionAboveEnums } },
75
+ additionalProperties: false,
76
+ },
77
+ ],
78
+ messages: {
79
+ notAllowed: 'Emojis are not allowed',
80
+ notAllowedAboveVersion: 'Emojis above version {{versionAbove}} are not allowed - Emoji: {{emoji}}',
81
+ },
31
82
  },
32
83
  create(context) {
33
84
  const callExpressionVisitor = (node) => checkNode(context, node);
@@ -1 +1 @@
1
- {"version":3,"file":"no-multiple-whitespaces.d.ts","sourceRoot":"","sources":["no-multiple-whitespaces.ts"],"names":[],"mappings":"AAOA,OAAO,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AAyJ3B,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA+BhB,CAAA;AAED,eAAe,IAAI,CAAA"}
1
+ {"version":3,"file":"no-multiple-whitespaces.d.ts","sourceRoot":"","sources":["no-multiple-whitespaces.ts"],"names":[],"mappings":"AAOA,OAAO,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AAiI3B,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAkChB,CAAA;AAED,eAAe,IAAI,CAAA"}
@@ -15,7 +15,6 @@ function isAstValid(ast) {
15
15
  case icu_messageformat_parser_1.TYPE.literal:
16
16
  case icu_messageformat_parser_1.TYPE.number:
17
17
  case icu_messageformat_parser_1.TYPE.pound:
18
- case icu_messageformat_parser_1.TYPE.tag:
19
18
  case icu_messageformat_parser_1.TYPE.time:
20
19
  break;
21
20
  case icu_messageformat_parser_1.TYPE.plural:
@@ -27,6 +26,8 @@ function isAstValid(ast) {
27
26
  }
28
27
  break;
29
28
  }
29
+ case icu_messageformat_parser_1.TYPE.tag:
30
+ return isAstValid(element.children);
30
31
  }
31
32
  }
32
33
  return true;
@@ -44,7 +45,6 @@ function trimMultiWhitespaces(message, ast) {
44
45
  case icu_messageformat_parser_1.TYPE.literal:
45
46
  case icu_messageformat_parser_1.TYPE.number:
46
47
  case icu_messageformat_parser_1.TYPE.pound:
47
- case icu_messageformat_parser_1.TYPE.tag:
48
48
  case icu_messageformat_parser_1.TYPE.time:
49
49
  break;
50
50
  case icu_messageformat_parser_1.TYPE.plural:
@@ -54,6 +54,9 @@ function trimMultiWhitespaces(message, ast) {
54
54
  }
55
55
  break;
56
56
  }
57
+ case icu_messageformat_parser_1.TYPE.tag:
58
+ collectLiteralElements(element.children);
59
+ break;
57
60
  }
58
61
  }
59
62
  };
@@ -91,29 +94,14 @@ function checkNode(context, node) {
91
94
  return;
92
95
  }
93
96
  if (!isAstValid(ast)) {
94
- const reportObject = {
97
+ const newMessage = (0, util_1.patchMessage)(messageNode, ast, trimMultiWhitespaces);
98
+ context.report({
95
99
  node: messageNode,
96
- message: 'Multiple consecutive whitespaces are not allowed',
97
- };
98
- if (messageNode.type === 'Literal' &&
99
- messageNode.value &&
100
- typeof messageNode.value === 'string') {
101
- reportObject.fix = function (fixer) {
102
- return fixer.replaceText(messageNode, JSON.stringify(trimMultiWhitespaces(messageNode.value, ast)));
103
- };
104
- }
105
- else if (messageNode.type === 'TemplateLiteral' &&
106
- messageNode.quasis.length === 1 &&
107
- messageNode.expressions.length === 0) {
108
- reportObject.fix = function (fixer) {
109
- return fixer.replaceText(messageNode, '`' +
110
- trimMultiWhitespaces(messageNode.quasis[0].value.cooked, ast)
111
- .replace(/\\/g, '\\\\')
112
- .replace(/`/g, '\\`') +
113
- '`');
114
- };
115
- }
116
- context.report(reportObject);
100
+ messageId: 'noMultipleWhitespaces',
101
+ fix: newMessage !== null
102
+ ? fixer => fixer.replaceText(messageNode, newMessage)
103
+ : null,
104
+ });
117
105
  }
118
106
  }
119
107
  }
@@ -126,6 +114,9 @@ const rule = {
126
114
  recommended: false,
127
115
  url: 'https://formatjs.io/docs/tooling/linter#no-multiple-whitespaces',
128
116
  },
117
+ messages: {
118
+ noMultipleWhitespaces: 'Multiple consecutive whitespaces are not allowed',
119
+ },
129
120
  fixable: 'code',
130
121
  },
131
122
  create(context) {
@@ -50,13 +50,13 @@ function checkNode(context, node) {
50
50
  }
51
51
  try {
52
52
  verifyAst((0, icu_messageformat_parser_1.parse)(defaultMessage, {
53
- ignoreTag: settings.ignoreTag
53
+ ignoreTag: settings.ignoreTag,
54
54
  }));
55
55
  }
56
56
  catch (e) {
57
57
  context.report({
58
58
  node: messageNode,
59
- message: e instanceof Error ? e.message : String(e)
59
+ message: e instanceof Error ? e.message : String(e),
60
60
  });
61
61
  }
62
62
  }
@@ -67,23 +67,23 @@ const rule = {
67
67
  docs: {
68
68
  description: 'Disallow unnecessary formatted message',
69
69
  recommended: true,
70
- url: 'https://formatjs.io/docs/tooling/linter#no-useless-message'
70
+ url: 'https://formatjs.io/docs/tooling/linter#no-useless-message',
71
71
  },
72
- fixable: 'code'
72
+ fixable: 'code',
73
73
  },
74
74
  create(context) {
75
75
  const callExpressionVisitor = (node) => checkNode(context, node);
76
76
  if (context.parserServices.defineTemplateBodyVisitor) {
77
77
  return context.parserServices.defineTemplateBodyVisitor({
78
- CallExpression: callExpressionVisitor
78
+ CallExpression: callExpressionVisitor,
79
79
  }, {
80
- CallExpression: callExpressionVisitor
80
+ CallExpression: callExpressionVisitor,
81
81
  });
82
82
  }
83
83
  return {
84
84
  JSXOpeningElement: (node) => checkNode(context, node),
85
- CallExpression: callExpressionVisitor
85
+ CallExpression: callExpressionVisitor,
86
86
  };
87
- }
87
+ },
88
88
  };
89
89
  exports.default = rule;
@@ -0,0 +1,4 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=prefer-formatted-message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-formatted-message.d.ts","sourceRoot":"","sources":["prefer-formatted-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AAIhC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAiChB,CAAA;AAED,eAAe,IAAI,CAAA"}
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const util_1 = require("../util");
4
+ const rule = {
5
+ meta: {
6
+ type: 'suggestion',
7
+ docs: {
8
+ description: 'Prefer `FormattedMessage` component over `intl.formatMessage` if applicable.',
9
+ recommended: false,
10
+ url: 'https://formatjs.io/docs/tooling/linter#prefer-formatted-message',
11
+ },
12
+ messages: {
13
+ jsxChildren: 'Prefer `FormattedMessage` over `intl.formatMessage` in the JSX children expression.',
14
+ },
15
+ },
16
+ // TODO: Vue support
17
+ create(context) {
18
+ return {
19
+ JSXElement: (node) => {
20
+ node.children.forEach(child => {
21
+ if (child.type !== 'JSXExpressionContainer' ||
22
+ !(0, util_1.isIntlFormatMessageCall)(child.expression)) {
23
+ return;
24
+ }
25
+ context.report({
26
+ node: node,
27
+ messageId: 'jsxChildren',
28
+ });
29
+ });
30
+ },
31
+ };
32
+ },
33
+ };
34
+ exports.default = rule;
@@ -0,0 +1,4 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=prefer-pound-in-plural.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-pound-in-plural.d.ts","sourceRoot":"","sources":["prefer-pound-in-plural.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AA6MhC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAmChB,CAAA;AAED,eAAe,IAAI,CAAA"}
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser");
5
+ const util_1 = require("../util");
6
+ const magic_string_1 = tslib_1.__importDefault(require("magic-string"));
7
+ function verifyAst(context, messageNode, ast) {
8
+ const patches = [];
9
+ _verifyAst(ast);
10
+ if (patches.length > 0) {
11
+ const patchedMessage = (0, util_1.patchMessage)(messageNode, ast, content => {
12
+ return patches
13
+ .reduce((magicString, patch) => {
14
+ switch (patch.type) {
15
+ case 'prependLeft':
16
+ return magicString.prependLeft(patch.index, patch.content);
17
+ case 'remove':
18
+ return magicString.remove(patch.start, patch.end);
19
+ case 'update':
20
+ return magicString.update(patch.start, patch.end, patch.content);
21
+ }
22
+ }, new magic_string_1.default(content))
23
+ .toString();
24
+ });
25
+ context.report({
26
+ node: messageNode,
27
+ messageId: 'preferPoundInPlurals',
28
+ fix: patchedMessage !== null
29
+ ? fixer => fixer.replaceText(messageNode, patchedMessage)
30
+ : null,
31
+ });
32
+ }
33
+ function _verifyAst(ast) {
34
+ for (let i = 0; i < ast.length; i++) {
35
+ const current = ast[i];
36
+ switch (current.type) {
37
+ case icu_messageformat_parser_1.TYPE.argument:
38
+ case icu_messageformat_parser_1.TYPE.number: {
39
+ // Applicable to only plain argument or number argument without any style
40
+ if (current.type === icu_messageformat_parser_1.TYPE.number && current.style) {
41
+ break;
42
+ }
43
+ const next = ast[i + 1];
44
+ const nextNext = ast[i + 2];
45
+ if (next &&
46
+ nextNext &&
47
+ next.type === icu_messageformat_parser_1.TYPE.literal &&
48
+ next.value === ' ' &&
49
+ nextNext.type === icu_messageformat_parser_1.TYPE.plural &&
50
+ nextNext.value === current.value) {
51
+ // `{A} {A, plural, one {B} other {Bs}}` => `{A, plural, one {# B} other {# Bs}}`
52
+ _removeRangeAndPrependPluralClauses(current.location.start.offset, next.location.end.offset, nextNext, '# ');
53
+ }
54
+ else if (next &&
55
+ next.type === icu_messageformat_parser_1.TYPE.plural &&
56
+ next.value === current.value) {
57
+ // `{A}{A, plural, one {B} other {Bs}}` => `{A, plural, one {#B} other {#Bs}}`
58
+ _removeRangeAndPrependPluralClauses(current.location.start.offset, current.location.end.offset, next, '#');
59
+ }
60
+ break;
61
+ }
62
+ case icu_messageformat_parser_1.TYPE.plural: {
63
+ // `{A, plural, one {{A} B} other {{A} Bs}}` => `{A, plural, one {# B} other {# Bs}}`
64
+ const name = current.value;
65
+ for (const { value } of Object.values(current.options)) {
66
+ _replacementArgumentWithPound(name, value);
67
+ }
68
+ break;
69
+ }
70
+ case icu_messageformat_parser_1.TYPE.select: {
71
+ for (const { value } of Object.values(current.options)) {
72
+ _verifyAst(value);
73
+ }
74
+ break;
75
+ }
76
+ case icu_messageformat_parser_1.TYPE.tag:
77
+ _verifyAst(current.children);
78
+ break;
79
+ default:
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ // Replace plain argument of number argument w/o style option that matches
85
+ // the name with a pound sign.
86
+ function _replacementArgumentWithPound(name, ast) {
87
+ for (const element of ast) {
88
+ switch (element.type) {
89
+ case icu_messageformat_parser_1.TYPE.argument:
90
+ case icu_messageformat_parser_1.TYPE.number: {
91
+ if (element.value === name &&
92
+ // Either plain argument or number argument without any style
93
+ (element.type !== icu_messageformat_parser_1.TYPE.number || !element.style)) {
94
+ patches.push({
95
+ type: 'update',
96
+ start: element.location.start.offset,
97
+ end: element.location.end.offset,
98
+ content: '#',
99
+ });
100
+ }
101
+ break;
102
+ }
103
+ case icu_messageformat_parser_1.TYPE.tag: {
104
+ _replacementArgumentWithPound(name, element.children);
105
+ break;
106
+ }
107
+ case icu_messageformat_parser_1.TYPE.select: {
108
+ for (const { value } of Object.values(element.options)) {
109
+ _replacementArgumentWithPound(name, value);
110
+ }
111
+ break;
112
+ }
113
+ default:
114
+ break;
115
+ }
116
+ }
117
+ }
118
+ // Helper to remove a certain text range and then prepend the specified text to
119
+ // each plural clause.
120
+ function _removeRangeAndPrependPluralClauses(rangeToRemoveStart, rangeToRemoveEnd, pluralElement, prependText) {
121
+ // Delete both the `{A}` and ` `
122
+ patches.push({
123
+ type: 'remove',
124
+ start: rangeToRemoveStart,
125
+ end: rangeToRemoveEnd,
126
+ });
127
+ // Insert `# ` to the beginning of every option clause
128
+ for (const { location } of Object.values(pluralElement.options)) {
129
+ // location marks the entire clause with the surrounding braces
130
+ patches.push({
131
+ type: 'prependLeft',
132
+ index: location.start.offset + 1,
133
+ content: prependText,
134
+ });
135
+ }
136
+ }
137
+ }
138
+ function checkNode(context, node) {
139
+ const msgs = (0, util_1.extractMessages)(node, (0, util_1.getSettings)(context));
140
+ for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
141
+ if (!defaultMessage || !messageNode) {
142
+ continue;
143
+ }
144
+ let ast;
145
+ try {
146
+ ast = (0, icu_messageformat_parser_1.parse)(defaultMessage, { captureLocation: true });
147
+ }
148
+ catch (e) {
149
+ context.report({
150
+ node: messageNode,
151
+ message: e instanceof Error ? e.message : String(e),
152
+ });
153
+ return;
154
+ }
155
+ verifyAst(context, messageNode, ast);
156
+ }
157
+ }
158
+ const rule = {
159
+ meta: {
160
+ type: 'suggestion',
161
+ docs: {
162
+ description: 'Prefer using # to reference the count in the plural argument.',
163
+ recommended: false,
164
+ url: 'https://formatjs.io/docs/tooling/linter#prefer-pound-in-plurals',
165
+ },
166
+ messages: {
167
+ preferPoundInPlurals: 'Prefer using # to reference the count in the plural argument instead of repeating the argument.',
168
+ },
169
+ fixable: 'code',
170
+ },
171
+ // TODO: Vue support
172
+ create(context) {
173
+ const callExpressionVisitor = (node) => checkNode(context, node);
174
+ if (context.parserServices.defineTemplateBodyVisitor) {
175
+ return context.parserServices.defineTemplateBodyVisitor({
176
+ CallExpression: callExpressionVisitor,
177
+ }, {
178
+ CallExpression: callExpressionVisitor,
179
+ });
180
+ }
181
+ return {
182
+ JSXOpeningElement: (node) => checkNode(context, node),
183
+ CallExpression: callExpressionVisitor,
184
+ };
185
+ },
186
+ };
187
+ exports.default = rule;
package/util.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Rule } from 'eslint';
2
2
  import { TSESTree } from '@typescript-eslint/typescript-estree';
3
+ import { MessageFormatElement } from '@formatjs/icu-messageformat-parser';
3
4
  export interface MessageDescriptor {
4
5
  id?: string;
5
6
  defaultMessage?: string;
@@ -20,5 +21,13 @@ export interface MessageDescriptorNodeInfo {
20
21
  idPropNode?: TSESTree.Property | TSESTree.JSXAttribute;
21
22
  }
22
23
  export declare function getSettings({ settings }: Rule.RuleContext): Settings;
24
+ export declare function isIntlFormatMessageCall(node: TSESTree.Node): node is TSESTree.CallExpression;
25
+ export declare function extractMessageDescriptor(node?: TSESTree.Expression): MessageDescriptorNodeInfo | undefined;
23
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;
24
33
  //# sourceMappingURL=util.d.ts.map
package/util.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAC,QAAQ,EAAC,MAAM,sCAAsC,CAAA;AAE7D,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAC9B;AAMD,MAAM,WAAW,QAAQ;IACvB,uBAAuB,CAAC,EAAE,OAAO,CAAA;IACjC,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAA;IAClC,wBAAwB,CAAC,EAAE,MAAM,EAAE,CAAA;IACnC,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AACD,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,iBAAiB,CAAA;IAC1B,WAAW,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;IACzE,eAAe,CAAC,EAAE,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAA;IAC3D,eAAe,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;IAC7E,WAAW,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;IACzE,UAAU,CAAC,EAAE,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAA;CACvD;AAED,wBAAgB,WAAW,CAAC,EAAC,QAAQ,EAAC,EAAE,IAAI,CAAC,WAAW,GAAG,QAAQ,CAElE;AA4ND,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,CAAC,IAAI,EACnB,EACE,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,GACxB,GAAE,QAAa,GACf,KAAK,CAAC,CAAC,yBAAyB,EAAE,QAAQ,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC,CAiDrE"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAC,QAAQ,EAAC,MAAM,sCAAsC,CAAA;AAC7D,OAAO,EAAC,oBAAoB,EAAC,MAAM,oCAAoC,CAAA;AAEvE,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAC9B;AAMD,MAAM,WAAW,QAAQ;IACvB,uBAAuB,CAAC,EAAE,OAAO,CAAA;IACjC,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAA;IAClC,wBAAwB,CAAC,EAAE,MAAM,EAAE,CAAA;IACnC,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AACD,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,iBAAiB,CAAA;IAC1B,WAAW,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;IACzE,eAAe,CAAC,EAAE,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAA;IAC3D,eAAe,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;IAC7E,WAAW,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;IACzE,UAAU,CAAC,EAAE,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAA;CACvD;AAED,wBAAgB,WAAW,CAAC,EAAC,QAAQ,EAAC,EAAE,IAAI,CAAC,WAAW,GAAG,QAAQ,CAElE;AA8BD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,CAAC,IAAI,GAClB,IAAI,IAAI,QAAQ,CAAC,cAAc,CAWjC;AAqBD,wBAAgB,wBAAwB,CACtC,IAAI,CAAC,EAAE,QAAQ,CAAC,UAAU,GACzB,yBAAyB,GAAG,SAAS,CA8CvC;AA8GD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,CAAC,IAAI,EACnB,EACE,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,GACxB,GAAE,QAAa,GACf,KAAK,CAAC,CAAC,yBAAyB,EAAE,QAAQ,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC,CAiDrE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,QAAQ,CAAC,IAAI,EAC1B,GAAG,EAAE,oBAAoB,EAAE,EAC3B,OAAO,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,EAAE,KAAK,MAAM,GACvE,MAAM,GAAG,IAAI,CAsBf"}
package/util.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractMessages = exports.getSettings = void 0;
3
+ exports.patchMessage = exports.extractMessages = exports.extractMessageDescriptor = exports.isIntlFormatMessageCall = exports.getSettings = void 0;
4
4
  const FORMAT_FUNCTION_NAMES = new Set(['$formatMessage', 'formatMessage', '$t']);
5
5
  const COMPONENT_NAMES = new Set(['FormattedMessage']);
6
6
  const DECLARATION_FUNCTION_NAMES = new Set(['defineMessage']);
@@ -37,6 +37,7 @@ function isIntlFormatMessageCall(node) {
37
37
  node.arguments.length >= 1 &&
38
38
  node.arguments[0].type === 'ObjectExpression');
39
39
  }
40
+ exports.isIntlFormatMessageCall = isIntlFormatMessageCall;
40
41
  function isSingleMessageDescriptorDeclaration(node, functionNames) {
41
42
  return (node.type === 'CallExpression' &&
42
43
  node.callee.type === 'Identifier' &&
@@ -95,6 +96,7 @@ function extractMessageDescriptor(node) {
95
96
  }
96
97
  return result;
97
98
  }
99
+ exports.extractMessageDescriptor = extractMessageDescriptor;
98
100
  function extractMessageDescriptorFromJSXElement(node) {
99
101
  if (!node || !node.attributes) {
100
102
  return;
@@ -238,3 +240,26 @@ function extractMessages(node, { additionalComponentNames, additionalFunctionNam
238
240
  return [];
239
241
  }
240
242
  exports.extractMessages = extractMessages;
243
+ /**
244
+ * Apply changes to the ICU message in code. The return value can be used in
245
+ * `fixer.replaceText(messageNode, <return value>)`. If the return value is null,
246
+ * it means that the patch cannot be applied.
247
+ */
248
+ function patchMessage(messageNode, ast, patcher) {
249
+ if (messageNode.type === 'Literal' &&
250
+ messageNode.value &&
251
+ typeof messageNode.value === 'string') {
252
+ return JSON.stringify(patcher(messageNode.value, ast));
253
+ }
254
+ else if (messageNode.type === 'TemplateLiteral' &&
255
+ messageNode.quasis.length === 1 &&
256
+ messageNode.expressions.length === 0) {
257
+ return ('`' +
258
+ patcher(messageNode.quasis[0].value.cooked, ast)
259
+ .replace(/\\/g, '\\\\')
260
+ .replace(/`/g, '\\`') +
261
+ '`');
262
+ }
263
+ return null;
264
+ }
265
+ exports.patchMessage = patchMessage;