eslint-plugin-formatjs 5.2.5 → 5.2.6

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 (68) hide show
  1. package/lib_esnext/context-compat.d.ts +2 -0
  2. package/lib_esnext/context-compat.js +6 -0
  3. package/lib_esnext/index.d.ts +1 -0
  4. package/lib_esnext/index.js +143 -0
  5. package/lib_esnext/package.json +37 -0
  6. package/lib_esnext/rules/blocklist-elements.d.ts +14 -0
  7. package/lib_esnext/rules/blocklist-elements.js +132 -0
  8. package/lib_esnext/rules/enforce-default-message.d.ts +10 -0
  9. package/lib_esnext/rules/enforce-default-message.js +68 -0
  10. package/lib_esnext/rules/enforce-description.d.ts +10 -0
  11. package/lib_esnext/rules/enforce-description.js +66 -0
  12. package/lib_esnext/rules/enforce-id.d.ts +10 -0
  13. package/lib_esnext/rules/enforce-id.js +153 -0
  14. package/lib_esnext/rules/enforce-placeholders.d.ts +8 -0
  15. package/lib_esnext/rules/enforce-placeholders.js +147 -0
  16. package/lib_esnext/rules/enforce-plural-rules.d.ts +17 -0
  17. package/lib_esnext/rules/enforce-plural-rules.js +103 -0
  18. package/lib_esnext/rules/no-camel-case.d.ts +5 -0
  19. package/lib_esnext/rules/no-camel-case.js +76 -0
  20. package/lib_esnext/rules/no-complex-selectors.d.ts +9 -0
  21. package/lib_esnext/rules/no-complex-selectors.js +136 -0
  22. package/lib_esnext/rules/no-emoji.d.ts +9 -0
  23. package/lib_esnext/rules/no-emoji.js +99 -0
  24. package/lib_esnext/rules/no-id.d.ts +5 -0
  25. package/lib_esnext/rules/no-id.js +58 -0
  26. package/lib_esnext/rules/no-invalid-icu.d.ts +5 -0
  27. package/lib_esnext/rules/no-invalid-icu.js +60 -0
  28. package/lib_esnext/rules/no-literal-string-in-jsx.d.ts +13 -0
  29. package/lib_esnext/rules/no-literal-string-in-jsx.js +179 -0
  30. package/lib_esnext/rules/no-missing-icu-plural-one-placeholders.d.ts +6 -0
  31. package/lib_esnext/rules/no-missing-icu-plural-one-placeholders.js +99 -0
  32. package/lib_esnext/rules/no-multiple-plurals.d.ts +5 -0
  33. package/lib_esnext/rules/no-multiple-plurals.js +70 -0
  34. package/lib_esnext/rules/no-multiple-whitespaces.d.ts +5 -0
  35. package/lib_esnext/rules/no-multiple-whitespaces.js +141 -0
  36. package/lib_esnext/rules/no-offset.d.ts +5 -0
  37. package/lib_esnext/rules/no-offset.js +69 -0
  38. package/lib_esnext/rules/no-useless-message.d.ts +5 -0
  39. package/lib_esnext/rules/no-useless-message.js +71 -0
  40. package/lib_esnext/rules/prefer-formatted-message.d.ts +5 -0
  41. package/lib_esnext/rules/prefer-formatted-message.js +33 -0
  42. package/lib_esnext/rules/prefer-pound-in-plural.d.ts +5 -0
  43. package/lib_esnext/rules/prefer-pound-in-plural.js +191 -0
  44. package/lib_esnext/util.d.ts +32 -0
  45. package/lib_esnext/util.js +261 -0
  46. package/package.json +4 -4
  47. package/context-compat.js.map +0 -1
  48. package/index.js.map +0 -1
  49. package/rules/blocklist-elements.js.map +0 -1
  50. package/rules/enforce-default-message.js.map +0 -1
  51. package/rules/enforce-description.js.map +0 -1
  52. package/rules/enforce-id.js.map +0 -1
  53. package/rules/enforce-placeholders.js.map +0 -1
  54. package/rules/enforce-plural-rules.js.map +0 -1
  55. package/rules/no-camel-case.js.map +0 -1
  56. package/rules/no-complex-selectors.js.map +0 -1
  57. package/rules/no-emoji.js.map +0 -1
  58. package/rules/no-id.js.map +0 -1
  59. package/rules/no-invalid-icu.js.map +0 -1
  60. package/rules/no-literal-string-in-jsx.js.map +0 -1
  61. package/rules/no-missing-icu-plural-one-placeholders.js.map +0 -1
  62. package/rules/no-multiple-plurals.js.map +0 -1
  63. package/rules/no-multiple-whitespaces.js.map +0 -1
  64. package/rules/no-offset.js.map +0 -1
  65. package/rules/no-useless-message.js.map +0 -1
  66. package/rules/prefer-formatted-message.js.map +0 -1
  67. package/rules/prefer-pound-in-plural.js.map +0 -1
  68. package/util.js.map +0 -1
@@ -0,0 +1,2 @@
1
+ import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
2
+ export declare const getParserServices: <TRuleContext extends RuleContext<string, unknown[]>>(context: TRuleContext) => Partial<import("@typescript-eslint/utils").ParserServicesWithTypeInformation> | Partial<import("@typescript-eslint/utils").ParserServicesWithoutTypeInformation> | undefined;
@@ -0,0 +1,6 @@
1
+ export const getParserServices = (context) => {
2
+ if (context.parserServices) {
3
+ return context.parserServices;
4
+ }
5
+ return context.sourceCode.parserServices;
6
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,143 @@
1
+ import { name as blocklistElementRuleName, rule as blocklistElements, } from './rules/blocklist-elements';
2
+ import { rule as enforceDefaultMessage, name as enforceDefaultMessageName, } from './rules/enforce-default-message';
3
+ import { rule as enforceDescription, name as enforceDescriptionName, } from './rules/enforce-description';
4
+ import { rule as enforceId, name as enforceIdName } from './rules/enforce-id';
5
+ import { rule as enforcePlaceholders, name as enforcePlaceholdersName, } from './rules/enforce-placeholders';
6
+ import { rule as enforcePluralRules, name as enforcePluralRulesName, } from './rules/enforce-plural-rules';
7
+ import { rule as noCamelCase, name as noCamelCaseName, } from './rules/no-camel-case';
8
+ import { rule as noComplexSelectors, name as noComplexSelectorsName, } from './rules/no-complex-selectors';
9
+ import { rule as noEmoji, name as noEmojiName } from './rules/no-emoji';
10
+ import { rule as noId, name as noIdName } from './rules/no-id';
11
+ import { rule as noInvalidICU, name as noInvalidICUName, } from './rules/no-invalid-icu';
12
+ import { rule as noLiteralStringInJsx, name as noLiteralStringInJsxName, } from './rules/no-literal-string-in-jsx';
13
+ import { rule as noMissingIcuPluralOnePlaceholders, name as noMissingIcuPluralOnePlaceholdersName, } from './rules/no-missing-icu-plural-one-placeholders';
14
+ import { rule as noMultiplePlurals, name as noMultiplePluralsName, } from './rules/no-multiple-plurals';
15
+ import { rule as noMultipleWhitespaces, name as noMultipleWhitespacesName, } from './rules/no-multiple-whitespaces';
16
+ import { rule as noOffset, name as noOffsetName } from './rules/no-offset';
17
+ import { rule as noUselessMessage, name as noUselessMessageName, } from './rules/no-useless-message';
18
+ import { rule as preferFormattedMessage, name as preferFormattedMessageName, } from './rules/prefer-formatted-message';
19
+ import { rule as preferPoundInPlural, name as preferPoundInPluralName, } from './rules/prefer-pound-in-plural';
20
+ import { name, version } from './package.json';
21
+ // All rules
22
+ const rules = {
23
+ // @ts-expect-error
24
+ [blocklistElementRuleName]: blocklistElements,
25
+ // @ts-expect-error
26
+ [enforceDefaultMessageName]: enforceDefaultMessage,
27
+ // @ts-expect-error
28
+ [enforceDescriptionName]: enforceDescription,
29
+ // @ts-expect-error
30
+ [enforceIdName]: enforceId,
31
+ // @ts-expect-error
32
+ [enforcePlaceholdersName]: enforcePlaceholders,
33
+ // @ts-expect-error
34
+ [enforcePluralRulesName]: enforcePluralRules,
35
+ // @ts-expect-error
36
+ [noCamelCaseName]: noCamelCase,
37
+ // @ts-expect-error
38
+ [noComplexSelectorsName]: noComplexSelectors,
39
+ // @ts-expect-error
40
+ [noEmojiName]: noEmoji,
41
+ // @ts-expect-error
42
+ [noIdName]: noId,
43
+ // @ts-expect-error
44
+ [noInvalidICUName]: noInvalidICU,
45
+ // @ts-expect-error
46
+ [noLiteralStringInJsxName]: noLiteralStringInJsx,
47
+ // @ts-expect-error
48
+ [noMultiplePluralsName]: noMultiplePlurals,
49
+ // @ts-expect-error
50
+ [noMultipleWhitespacesName]: noMultipleWhitespaces,
51
+ // @ts-expect-error
52
+ [noOffsetName]: noOffset,
53
+ // @ts-expect-error
54
+ [noUselessMessageName]: noUselessMessage,
55
+ // @ts-expect-error
56
+ [preferFormattedMessageName]: preferFormattedMessage,
57
+ // @ts-expect-error
58
+ [preferPoundInPluralName]: preferPoundInPlural,
59
+ // @ts-expect-error
60
+ [noMissingIcuPluralOnePlaceholdersName]: noMissingIcuPluralOnePlaceholders,
61
+ };
62
+ // Base plugin
63
+ const plugin = {
64
+ meta: { name, version },
65
+ rules,
66
+ };
67
+ // Configs
68
+ const configs = {
69
+ strict: {
70
+ name: 'formatjs/strict',
71
+ plugins: { formatjs: plugin },
72
+ rules: {
73
+ 'formatjs/no-offset': 'error',
74
+ 'formatjs/enforce-default-message': ['error', 'literal'],
75
+ 'formatjs/enforce-description': ['error', 'literal'],
76
+ 'formatjs/enforce-placeholders': 'error',
77
+ 'formatjs/no-emoji': 'error',
78
+ 'formatjs/no-multiple-whitespaces': 'error',
79
+ 'formatjs/no-multiple-plurals': 'error',
80
+ 'formatjs/no-complex-selectors': ['error', { limit: 20 }],
81
+ 'formatjs/no-useless-message': 'error',
82
+ 'formatjs/prefer-pound-in-plural': 'error',
83
+ 'formatjs/no-missing-icu-plural-one-placeholders': 'error',
84
+ 'formatjs/enforce-id': [
85
+ 'error',
86
+ {
87
+ idInterpolationPattern: '[sha512:contenthash:base64:10]',
88
+ },
89
+ ],
90
+ 'formatjs/enforce-plural-rules': [
91
+ 'error',
92
+ {
93
+ one: true,
94
+ other: true,
95
+ },
96
+ ],
97
+ 'formatjs/no-literal-string-in-jsx': [
98
+ 'error',
99
+ {
100
+ props: {
101
+ include: [['*', '{label,placeholder,title}']],
102
+ },
103
+ },
104
+ ],
105
+ 'formatjs/blocklist-elements': ['error', ['selectordinal']],
106
+ },
107
+ },
108
+ recommended: {
109
+ name: 'formatjs/recommended',
110
+ plugins: { formatjs: plugin },
111
+ rules: {
112
+ 'formatjs/no-offset': 'error',
113
+ 'formatjs/enforce-default-message': ['error', 'literal'],
114
+ 'formatjs/enforce-description': ['error', 'literal'],
115
+ 'formatjs/enforce-placeholders': 'error',
116
+ 'formatjs/no-emoji': 'error',
117
+ 'formatjs/no-multiple-whitespaces': 'error',
118
+ 'formatjs/no-multiple-plurals': 'error',
119
+ 'formatjs/no-complex-selectors': ['error', { limit: 20 }],
120
+ 'formatjs/no-useless-message': 'error',
121
+ 'formatjs/prefer-pound-in-plural': 'error',
122
+ 'formatjs/no-missing-icu-plural-one-placeholders': 'error',
123
+ 'formatjs/enforce-plural-rules': [
124
+ 'error',
125
+ {
126
+ one: true,
127
+ other: true,
128
+ },
129
+ ],
130
+ 'formatjs/no-literal-string-in-jsx': [
131
+ 'warn',
132
+ {
133
+ props: {
134
+ include: [['*', '{label,placeholder,title}']],
135
+ },
136
+ },
137
+ ],
138
+ 'formatjs/blocklist-elements': ['error', ['selectordinal']],
139
+ },
140
+ },
141
+ };
142
+ plugin.configs = configs;
143
+ module.exports = plugin;
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "eslint-plugin-formatjs",
3
+ "version": "5.2.6",
4
+ "description": "ESLint plugin for formatjs",
5
+ "main": "index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+ssh://git@github.com/formatjs/formatjs.git"
9
+ },
10
+ "keywords": [
11
+ "eslint",
12
+ "eslintplugin",
13
+ "i18n",
14
+ "formatjs"
15
+ ],
16
+ "author": "Long Ho <holevietlong@gmail.com>",
17
+ "license": "MIT",
18
+ "bugs": {
19
+ "url": "https://github.com/formatjs/formatjs/issues"
20
+ },
21
+ "homepage": "https://github.com/formatjs/formatjs#readme",
22
+ "dependencies": {
23
+ "@formatjs/icu-messageformat-parser": "workspace:*",
24
+ "@formatjs/ts-transformer": "workspace:*",
25
+ "@types/eslint": "9",
26
+ "@types/picomatch": "3",
27
+ "@typescript-eslint/utils": "8.17.0",
28
+ "emoji-regex": "10",
29
+ "magic-string": "^0.30.0",
30
+ "picomatch": "2 || 3 || 4",
31
+ "tslib": "2",
32
+ "unicode-emoji-utils": "^1.2.0"
33
+ },
34
+ "peerDependencies": {
35
+ "eslint": "9"
36
+ }
37
+ }
@@ -0,0 +1,14 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const name = "blocklist-elements";
3
+ export declare enum Element {
4
+ literal = "literal",
5
+ argument = "argument",
6
+ number = "number",
7
+ date = "date",
8
+ time = "time",
9
+ select = "select",
10
+ selectordinal = "selectordinal",
11
+ plural = "plural",
12
+ tag = "tag"
13
+ }
14
+ export declare const rule: ESLintUtils.RuleModule<"blocklist", [], unknown, ESLintUtils.RuleListener>;
@@ -0,0 +1,132 @@
1
+ import { isArgumentElement, isDateElement, isLiteralElement, isNumberElement, isPluralElement, isSelectElement, isTagElement, isTimeElement, parse, } from '@formatjs/icu-messageformat-parser';
2
+ import { ESLintUtils } from '@typescript-eslint/utils';
3
+ import { getParserServices } from '../context-compat';
4
+ import { extractMessages, getSettings } from '../util';
5
+ export const name = 'blocklist-elements';
6
+ function getMessage(type) {
7
+ return {
8
+ messageId: 'blocklist',
9
+ data: { type },
10
+ };
11
+ }
12
+ export var Element;
13
+ (function (Element) {
14
+ Element["literal"] = "literal";
15
+ Element["argument"] = "argument";
16
+ Element["number"] = "number";
17
+ Element["date"] = "date";
18
+ Element["time"] = "time";
19
+ Element["select"] = "select";
20
+ Element["selectordinal"] = "selectordinal";
21
+ Element["plural"] = "plural";
22
+ Element["tag"] = "tag";
23
+ })(Element || (Element = {}));
24
+ function verifyAst(blocklist, ast) {
25
+ const errors = [];
26
+ for (const el of ast) {
27
+ if (isLiteralElement(el) && blocklist.includes(Element.literal)) {
28
+ errors.push(getMessage(Element.literal));
29
+ }
30
+ if (isArgumentElement(el) && blocklist.includes(Element.argument)) {
31
+ errors.push(getMessage(Element.argument));
32
+ }
33
+ if (isNumberElement(el) && blocklist.includes(Element.number)) {
34
+ errors.push(getMessage(Element.number));
35
+ }
36
+ if (isDateElement(el) && blocklist.includes(Element.date)) {
37
+ errors.push(getMessage(Element.date));
38
+ }
39
+ if (isTimeElement(el) && blocklist.includes(Element.time)) {
40
+ errors.push(getMessage(Element.time));
41
+ }
42
+ if (isSelectElement(el) && blocklist.includes(Element.select)) {
43
+ errors.push(getMessage(Element.select));
44
+ }
45
+ if (isTagElement(el) && blocklist.includes(Element.tag)) {
46
+ errors.push(getMessage(Element.tag));
47
+ }
48
+ if (isPluralElement(el)) {
49
+ if (blocklist.includes(Element.plural)) {
50
+ errors.push(getMessage(Element.argument));
51
+ }
52
+ if (el.pluralType === 'ordinal' &&
53
+ blocklist.includes(Element.selectordinal)) {
54
+ errors.push(getMessage(Element.selectordinal));
55
+ }
56
+ }
57
+ if (isSelectElement(el) || isPluralElement(el)) {
58
+ const { options } = el;
59
+ for (const selector of Object.keys(options)) {
60
+ verifyAst(blocklist, options[selector].value);
61
+ }
62
+ }
63
+ }
64
+ return errors;
65
+ }
66
+ function checkNode(context, node) {
67
+ const settings = getSettings(context);
68
+ const msgs = extractMessages(node, settings);
69
+ if (!msgs.length) {
70
+ return;
71
+ }
72
+ const blocklist = context.options[0];
73
+ if (!Array.isArray(blocklist) || !blocklist.length) {
74
+ return;
75
+ }
76
+ for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
77
+ if (!defaultMessage || !messageNode) {
78
+ continue;
79
+ }
80
+ const errors = verifyAst(blocklist, parse(defaultMessage, {
81
+ ignoreTag: settings.ignoreTag,
82
+ }));
83
+ for (const error of errors) {
84
+ context.report({
85
+ node,
86
+ ...error,
87
+ });
88
+ }
89
+ }
90
+ }
91
+ const createRule = ESLintUtils.RuleCreator(_ => 'https://formatjs.github.io/docs/tooling/linter#blocklist-elements');
92
+ export const rule = createRule({
93
+ name,
94
+ meta: {
95
+ type: 'problem',
96
+ docs: {
97
+ description: 'Disallow specific elements in ICU message format',
98
+ url: 'https://formatjs.github.io/docs/tooling/linter#blocklist-elements',
99
+ },
100
+ fixable: 'code',
101
+ schema: [
102
+ {
103
+ type: 'array',
104
+ items: {
105
+ type: 'string',
106
+ enum: Object.keys(Element),
107
+ },
108
+ },
109
+ ],
110
+ messages: {
111
+ blocklist: `{{type}} element is blocklisted`,
112
+ },
113
+ },
114
+ defaultOptions: [],
115
+ create(context) {
116
+ const callExpressionVisitor = node => checkNode(context, node);
117
+ const parserServices = getParserServices(context);
118
+ //@ts-expect-error defineTemplateBodyVisitor exists in Vue parser
119
+ if (parserServices?.defineTemplateBodyVisitor) {
120
+ //@ts-expect-error
121
+ return parserServices.defineTemplateBodyVisitor({
122
+ CallExpression: callExpressionVisitor,
123
+ }, {
124
+ CallExpression: callExpressionVisitor,
125
+ });
126
+ }
127
+ return {
128
+ JSXOpeningElement: node => checkNode(context, node),
129
+ CallExpression: callExpressionVisitor,
130
+ };
131
+ },
132
+ });
@@ -0,0 +1,10 @@
1
+ import { RuleModule } from '@typescript-eslint/utils/ts-eslint';
2
+ export declare enum Option {
3
+ literal = "literal",
4
+ anything = "anything"
5
+ }
6
+ type MessageIds = 'defaultMessage' | 'defaultMessageLiteral';
7
+ type Options = [`${Option}`?];
8
+ export declare const name = "enforce-default-message";
9
+ export declare const rule: RuleModule<MessageIds, Options>;
10
+ export {};
@@ -0,0 +1,68 @@
1
+ import { getParserServices } from '../context-compat';
2
+ import { extractMessages, getSettings } from '../util';
3
+ export var Option;
4
+ (function (Option) {
5
+ Option["literal"] = "literal";
6
+ Option["anything"] = "anything";
7
+ })(Option || (Option = {}));
8
+ export const name = 'enforce-default-message';
9
+ function checkNode(context, node) {
10
+ const msgs = extractMessages(node, getSettings(context));
11
+ const { options: [type], } = context;
12
+ for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
13
+ if (!defaultMessage) {
14
+ if (type === 'literal' && messageNode) {
15
+ context.report({
16
+ node: messageNode,
17
+ messageId: 'defaultMessageLiteral',
18
+ });
19
+ }
20
+ else if (!messageNode) {
21
+ context.report({
22
+ node: node,
23
+ messageId: 'defaultMessage',
24
+ });
25
+ }
26
+ }
27
+ }
28
+ }
29
+ export const rule = {
30
+ meta: {
31
+ type: 'problem',
32
+ docs: {
33
+ description: 'Enforce defaultMessage in message descriptor',
34
+ url: 'https://formatjs.github.io/docs/tooling/linter#enforce-default-message',
35
+ },
36
+ fixable: 'code',
37
+ schema: [
38
+ {
39
+ type: 'string',
40
+ enum: Object.keys(Option),
41
+ },
42
+ ],
43
+ messages: {
44
+ defaultMessageLiteral: `"defaultMessage" must be:
45
+ - a string literal or
46
+ - template literal without variable`,
47
+ defaultMessage: '`defaultMessage` has to be specified in message descriptor',
48
+ },
49
+ },
50
+ defaultOptions: [],
51
+ create(context) {
52
+ const callExpressionVisitor = (node) => checkNode(context, node);
53
+ const parserServices = getParserServices(context);
54
+ //@ts-expect-error defineTemplateBodyVisitor exists in Vue parser
55
+ if (parserServices?.defineTemplateBodyVisitor) {
56
+ //@ts-expect-error
57
+ return parserServices.defineTemplateBodyVisitor({
58
+ CallExpression: callExpressionVisitor,
59
+ }, {
60
+ CallExpression: callExpressionVisitor,
61
+ });
62
+ }
63
+ return {
64
+ JSXOpeningElement: (node) => checkNode(context, node),
65
+ CallExpression: callExpressionVisitor,
66
+ };
67
+ },
68
+ };
@@ -0,0 +1,10 @@
1
+ import { RuleModule } from '@typescript-eslint/utils/ts-eslint';
2
+ export declare enum Option {
3
+ literal = "literal",
4
+ anything = "anything"
5
+ }
6
+ type MessageIds = 'enforceDescription' | 'enforceDescriptionLiteral';
7
+ type Options = [`${Option}`?];
8
+ export declare const name = "enforce-description";
9
+ export declare const rule: RuleModule<MessageIds, Options>;
10
+ export {};
@@ -0,0 +1,66 @@
1
+ import { getParserServices } from '../context-compat';
2
+ import { extractMessages, getSettings } from '../util';
3
+ export var Option;
4
+ (function (Option) {
5
+ Option["literal"] = "literal";
6
+ Option["anything"] = "anything";
7
+ })(Option || (Option = {}));
8
+ function checkNode(context, node) {
9
+ const msgs = extractMessages(node, getSettings(context));
10
+ const { options: [type], } = context;
11
+ for (const [{ message: { description }, descriptionNode, },] of msgs) {
12
+ if (!description) {
13
+ if (type === 'literal' && descriptionNode) {
14
+ context.report({
15
+ node: descriptionNode,
16
+ messageId: 'enforceDescriptionLiteral',
17
+ });
18
+ }
19
+ else if (!descriptionNode) {
20
+ context.report({
21
+ node: node,
22
+ messageId: 'enforceDescription',
23
+ });
24
+ }
25
+ }
26
+ }
27
+ }
28
+ export const name = 'enforce-description';
29
+ export const rule = {
30
+ meta: {
31
+ type: 'problem',
32
+ docs: {
33
+ description: 'Enforce description in message descriptor',
34
+ url: 'https://formatjs.github.io/docs/tooling/linter#enforce-description',
35
+ },
36
+ fixable: 'code',
37
+ schema: [
38
+ {
39
+ type: 'string',
40
+ enum: Object.keys(Option),
41
+ },
42
+ ],
43
+ messages: {
44
+ enforceDescription: '`description` has to be specified in message descriptor',
45
+ enforceDescriptionLiteral: '`description` has to be a string literal (not function call or variable)',
46
+ },
47
+ },
48
+ defaultOptions: [],
49
+ create(context) {
50
+ const callExpressionVisitor = (node) => checkNode(context, node);
51
+ const parserServices = getParserServices(context);
52
+ //@ts-expect-error defineTemplateBodyVisitor exists in Vue parser
53
+ if (parserServices?.defineTemplateBodyVisitor) {
54
+ //@ts-expect-error
55
+ return parserServices.defineTemplateBodyVisitor({
56
+ CallExpression: callExpressionVisitor,
57
+ }, {
58
+ CallExpression: callExpressionVisitor,
59
+ });
60
+ }
61
+ return {
62
+ JSXOpeningElement: (node) => checkNode(context, node),
63
+ CallExpression: callExpressionVisitor,
64
+ };
65
+ },
66
+ };
@@ -0,0 +1,10 @@
1
+ import { RuleModule } from '@typescript-eslint/utils/ts-eslint';
2
+ export type Option = {
3
+ idInterpolationPattern: string;
4
+ idWhitelist?: string[];
5
+ };
6
+ type MessageIds = 'enforceId' | 'enforceIdDefaultMessage' | 'enforceIdDescription' | 'enforceIdMatching' | 'enforceIdMatchingAllowlisted';
7
+ type Options = [Option];
8
+ export declare const name = "enforce-id";
9
+ export declare const rule: RuleModule<MessageIds, Options>;
10
+ export {};
@@ -0,0 +1,153 @@
1
+ import { interpolateName } from '@formatjs/ts-transformer';
2
+ import { getParserServices } from '../context-compat';
3
+ import { extractMessages, getSettings } from '../util';
4
+ function checkNode(context, node, { idInterpolationPattern, idWhitelistRegexps, }) {
5
+ const msgs = extractMessages(node, getSettings(context));
6
+ for (const [{ message: { defaultMessage, description, id }, idPropNode, descriptionNode, messagePropNode, },] of msgs) {
7
+ if (!idInterpolationPattern && !idPropNode) {
8
+ context.report({
9
+ node,
10
+ messageId: 'enforceId',
11
+ });
12
+ }
13
+ else if (idInterpolationPattern) {
14
+ if (!defaultMessage) {
15
+ context.report({
16
+ node,
17
+ messageId: 'enforceIdDefaultMessage',
18
+ });
19
+ }
20
+ else if (!description && descriptionNode) {
21
+ context.report({
22
+ node,
23
+ messageId: 'enforceIdDescription',
24
+ });
25
+ }
26
+ else {
27
+ if (idWhitelistRegexps &&
28
+ id &&
29
+ idWhitelistRegexps.some((r) => r.test(id))) {
30
+ // messageId is allowlisted so skip interpolation id check
31
+ continue;
32
+ }
33
+ const correctId = interpolateName({
34
+ resourcePath: context.getFilename(),
35
+ }, idInterpolationPattern, {
36
+ content: description
37
+ ? `${defaultMessage}#${description}`
38
+ : defaultMessage,
39
+ });
40
+ if (id !== correctId) {
41
+ let messageId = 'enforceIdMatching';
42
+ let messageData = {
43
+ idInterpolationPattern,
44
+ expected: correctId,
45
+ actual: id,
46
+ };
47
+ if (idWhitelistRegexps) {
48
+ messageId = 'enforceIdMatchingAllowlisted';
49
+ messageData = {
50
+ ...messageData,
51
+ idWhitelist: idWhitelistRegexps
52
+ .map(r => `"${r.toString()}"`)
53
+ .join(', '),
54
+ };
55
+ }
56
+ context.report({
57
+ node,
58
+ messageId,
59
+ data: messageData,
60
+ fix(fixer) {
61
+ if (idPropNode) {
62
+ if (idPropNode.type === 'JSXAttribute') {
63
+ return fixer.replaceText(idPropNode, `id="${correctId}"`);
64
+ }
65
+ return fixer.replaceText(idPropNode, `id: '${correctId}'`);
66
+ }
67
+ if (messagePropNode) {
68
+ // Insert after default message node
69
+ if (messagePropNode.type === 'JSXAttribute') {
70
+ return fixer.insertTextAfter(messagePropNode, ` id="${correctId}"`);
71
+ }
72
+ return fixer.insertTextAfter(messagePropNode, `, id: '${correctId}'`);
73
+ }
74
+ return null;
75
+ },
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ export const name = 'enforce-id';
83
+ export const rule = {
84
+ meta: {
85
+ type: 'problem',
86
+ docs: {
87
+ description: 'Enforce (generated) ID in message descriptor',
88
+ url: 'https://formatjs.github.io/docs/tooling/linter#enforce-id',
89
+ },
90
+ fixable: 'code',
91
+ schema: [
92
+ {
93
+ type: 'object',
94
+ properties: {
95
+ idInterpolationPattern: {
96
+ type: 'string',
97
+ description: 'Pattern to verify ID against. Recommended value: [sha512:contenthash:base64:6]',
98
+ },
99
+ idWhitelist: {
100
+ type: 'array',
101
+ description: "An array of strings with regular expressions. This array allows allowlist custom ids for messages. For example '`\\\\.`' allows any id which has dot; `'^payment_.*'` - allows any custom id which has prefix `payment_`. Be aware that any backslash \\ provided via string must be escaped with an additional backslash.",
102
+ items: {
103
+ type: 'string',
104
+ },
105
+ },
106
+ },
107
+ required: ['idInterpolationPattern'],
108
+ additionalProperties: false,
109
+ },
110
+ ],
111
+ messages: {
112
+ enforceId: `id must be specified`,
113
+ enforceIdDefaultMessage: `defaultMessage must be a string literal to calculate generated IDs`,
114
+ enforceIdDescription: `description must be a string literal to calculate generated IDs`,
115
+ enforceIdMatching: `"id" does not match with hash pattern {{idInterpolationPattern}}.
116
+ Expected: {{expected}}
117
+ Actual: {{actual}}`,
118
+ enforceIdMatchingAllowlisted: `"id" does not match with hash pattern {{idInterpolationPattern}} or allowlisted patterns {{idWhitelist}}.
119
+ Expected: {{expected}}
120
+ Actual: {{actual}}`,
121
+ },
122
+ },
123
+ defaultOptions: [
124
+ {
125
+ idInterpolationPattern: '[sha512:contenthash:base64:6]',
126
+ },
127
+ ],
128
+ create(context) {
129
+ const tmp = context.options[0];
130
+ let opts = {
131
+ idInterpolationPattern: tmp?.idInterpolationPattern,
132
+ };
133
+ if (Array.isArray(tmp?.idWhitelist)) {
134
+ const { idWhitelist } = tmp;
135
+ opts.idWhitelistRegexps = idWhitelist.map((str) => new RegExp(str, 'i'));
136
+ }
137
+ const callExpressionVisitor = (node) => checkNode(context, node, opts);
138
+ const parserServices = getParserServices(context);
139
+ //@ts-expect-error defineTemplateBodyVisitor exists in Vue parser
140
+ if (parserServices?.defineTemplateBodyVisitor) {
141
+ //@ts-expect-error
142
+ return parserServices.defineTemplateBodyVisitor({
143
+ CallExpression: callExpressionVisitor,
144
+ }, {
145
+ CallExpression: callExpressionVisitor,
146
+ });
147
+ }
148
+ return {
149
+ JSXOpeningElement: (node) => checkNode(context, node, opts),
150
+ CallExpression: callExpressionVisitor,
151
+ };
152
+ },
153
+ };
@@ -0,0 +1,8 @@
1
+ import { RuleModule } from '@typescript-eslint/utils/ts-eslint';
2
+ type MessageIds = 'parserError' | 'missingValue' | 'unusedValue';
3
+ type Options = [{
4
+ ignoreList: string[];
5
+ }?];
6
+ export declare const name = "enforce-placeholders";
7
+ export declare const rule: RuleModule<MessageIds, Options>;
8
+ export {};