eslint-plugin-formatjs 4.2.0 → 4.3.1

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 (88) hide show
  1. package/index.d.ts +22 -0
  2. package/index.d.ts.map +1 -0
  3. package/index.js +38 -0
  4. package/package.json +4 -4
  5. package/rules/blocklist-elements.d.ts +4 -0
  6. package/rules/blocklist-elements.d.ts.map +1 -0
  7. package/rules/blocklist-elements.js +127 -0
  8. package/rules/enforce-default-message.d.ts +4 -0
  9. package/rules/enforce-default-message.d.ts.map +1 -0
  10. package/rules/enforce-default-message.js +57 -0
  11. package/rules/enforce-description.d.ts +4 -0
  12. package/rules/enforce-description.d.ts.map +1 -0
  13. package/rules/enforce-description.js +54 -0
  14. package/rules/enforce-id.d.ts +4 -0
  15. package/rules/enforce-id.d.ts.map +1 -0
  16. package/rules/enforce-id.js +125 -0
  17. package/rules/enforce-placeholders.d.ts +4 -0
  18. package/rules/enforce-placeholders.d.ts.map +1 -0
  19. package/rules/enforce-placeholders.js +118 -0
  20. package/rules/enforce-plural-rules.d.ts +4 -0
  21. package/rules/enforce-plural-rules.d.ts.map +1 -0
  22. package/rules/enforce-plural-rules.js +104 -0
  23. package/rules/no-camel-case.d.ts +4 -0
  24. package/rules/no-camel-case.d.ts.map +1 -0
  25. package/rules/no-camel-case.js +77 -0
  26. package/rules/no-complex-selectors.d.ts +4 -0
  27. package/rules/no-complex-selectors.d.ts.map +1 -0
  28. package/rules/no-complex-selectors.js +99 -0
  29. package/rules/no-emoji.d.ts +4 -0
  30. package/rules/no-emoji.d.ts.map +1 -0
  31. package/rules/no-emoji.js +47 -0
  32. package/rules/no-id.d.ts +4 -0
  33. package/rules/no-id.d.ts.map +1 -0
  34. package/rules/no-id.js +52 -0
  35. package/rules/no-invalid-icu.d.ts +4 -0
  36. package/rules/no-invalid-icu.d.ts.map +1 -0
  37. package/rules/no-invalid-icu.js +54 -0
  38. package/rules/no-literal-string-in-jsx.d.ts +4 -0
  39. package/rules/no-literal-string-in-jsx.d.ts.map +1 -0
  40. package/rules/no-literal-string-in-jsx.js +166 -0
  41. package/rules/no-multiple-plurals.d.ts +4 -0
  42. package/rules/no-multiple-plurals.d.ts.map +1 -0
  43. package/rules/no-multiple-plurals.js +71 -0
  44. package/rules/no-multiple-whitespaces.d.ts +4 -0
  45. package/rules/no-multiple-whitespaces.d.ts.map +1 -0
  46. package/rules/no-multiple-whitespaces.js +146 -0
  47. package/rules/no-offset.d.ts +4 -0
  48. package/rules/no-offset.d.ts.map +1 -0
  49. package/rules/no-offset.js +70 -0
  50. package/util.d.ts +24 -0
  51. package/util.d.ts.map +1 -0
  52. package/util.js +240 -0
  53. package/BUILD +0 -89
  54. package/CHANGELOG.md +0 -892
  55. package/index.ts +0 -38
  56. package/rules/blocklist-elements.ts +0 -159
  57. package/rules/enforce-default-message.ts +0 -71
  58. package/rules/enforce-description.ts +0 -68
  59. package/rules/enforce-id.ts +0 -171
  60. package/rules/enforce-placeholders.ts +0 -161
  61. package/rules/enforce-plural-rules.ts +0 -134
  62. package/rules/no-camel-case.ts +0 -97
  63. package/rules/no-complex-selectors.ts +0 -125
  64. package/rules/no-emoji.ts +0 -60
  65. package/rules/no-id.ts +0 -63
  66. package/rules/no-invalid-icu.ts +0 -69
  67. package/rules/no-literal-string-in-jsx.ts +0 -213
  68. package/rules/no-multiple-plurals.ts +0 -89
  69. package/rules/no-multiple-whitespaces.ts +0 -194
  70. package/rules/no-offset.ts +0 -88
  71. package/tests/blocklist-elements.test.ts +0 -120
  72. package/tests/enforce-default-message.test.ts +0 -260
  73. package/tests/enforce-description.test.ts +0 -117
  74. package/tests/enforce-id.test.ts +0 -209
  75. package/tests/enforce-placeholders.test.ts +0 -170
  76. package/tests/enforce-plural-rules.test.ts +0 -86
  77. package/tests/fixtures.ts +0 -15
  78. package/tests/no-camel-case.test.ts +0 -31
  79. package/tests/no-complex-selectors.test.ts +0 -125
  80. package/tests/no-id.test.ts +0 -151
  81. package/tests/no-invalid-icu.test.ts +0 -106
  82. package/tests/no-literal-string-in-jsx.test.ts +0 -213
  83. package/tests/no-multiple-plurals.test.ts +0 -42
  84. package/tests/no-multiple-whitespaces.test.ts +0 -100
  85. package/tests/no-offset.test.ts +0 -41
  86. package/tests/util.ts +0 -26
  87. package/tsconfig.json +0 -5
  88. package/util.ts +0 -307
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const util_1 = require("../util");
4
+ const icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser");
5
+ class PluralRulesEnforcement extends Error {
6
+ constructor(message) {
7
+ super();
8
+ this.message = message;
9
+ }
10
+ }
11
+ var LDML;
12
+ (function (LDML) {
13
+ LDML["zero"] = "zero";
14
+ LDML["one"] = "one";
15
+ LDML["two"] = "two";
16
+ LDML["few"] = "few";
17
+ LDML["many"] = "many";
18
+ LDML["other"] = "other";
19
+ })(LDML || (LDML = {}));
20
+ function verifyAst(plConfig, ast) {
21
+ for (const el of ast) {
22
+ if ((0, icu_messageformat_parser_1.isPluralElement)(el)) {
23
+ const rules = Object.keys(plConfig);
24
+ for (const rule of rules) {
25
+ if (plConfig[rule] && !el.options[rule]) {
26
+ throw new PluralRulesEnforcement(`Missing plural rule "${rule}"`);
27
+ }
28
+ if (!plConfig[rule] && el.options[rule]) {
29
+ throw new PluralRulesEnforcement(`Plural rule "${rule}" is forbidden`);
30
+ }
31
+ }
32
+ const { options } = el;
33
+ for (const selector of Object.keys(options)) {
34
+ verifyAst(plConfig, options[selector].value);
35
+ }
36
+ }
37
+ }
38
+ }
39
+ function checkNode(context, node) {
40
+ const settings = (0, util_1.getSettings)(context);
41
+ const msgs = (0, util_1.extractMessages)(node, settings);
42
+ if (!msgs.length) {
43
+ return;
44
+ }
45
+ const plConfig = context.options[0];
46
+ if (!plConfig) {
47
+ return;
48
+ }
49
+ for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
50
+ if (!defaultMessage || !messageNode) {
51
+ continue;
52
+ }
53
+ try {
54
+ verifyAst(context.options[0], (0, icu_messageformat_parser_1.parse)(defaultMessage, {
55
+ ignoreTag: settings.ignoreTag,
56
+ }));
57
+ }
58
+ catch (e) {
59
+ context.report({
60
+ node: messageNode,
61
+ message: e instanceof Error ? e.message : String(e),
62
+ });
63
+ }
64
+ }
65
+ }
66
+ const rule = {
67
+ meta: {
68
+ type: 'problem',
69
+ docs: {
70
+ description: 'Enforce plural rules to always specify certain categories like `one`/`other`',
71
+ category: 'Errors',
72
+ recommended: false,
73
+ url: 'https://formatjs.io/docs/tooling/linter#enforce-plural-rules',
74
+ },
75
+ fixable: 'code',
76
+ schema: [
77
+ {
78
+ type: 'object',
79
+ properties: Object.keys(LDML).reduce((schema, k) => {
80
+ schema[k] = {
81
+ type: 'boolean',
82
+ };
83
+ return schema;
84
+ }, {}),
85
+ additionalProperties: false,
86
+ },
87
+ ],
88
+ },
89
+ create(context) {
90
+ const callExpressionVisitor = (node) => checkNode(context, node);
91
+ if (context.parserServices.defineTemplateBodyVisitor) {
92
+ return context.parserServices.defineTemplateBodyVisitor({
93
+ CallExpression: callExpressionVisitor,
94
+ }, {
95
+ CallExpression: callExpressionVisitor,
96
+ });
97
+ }
98
+ return {
99
+ JSXOpeningElement: (node) => checkNode(context, node),
100
+ CallExpression: callExpressionVisitor,
101
+ };
102
+ },
103
+ };
104
+ exports.default = rule;
@@ -0,0 +1,4 @@
1
+ import { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=no-camel-case.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-camel-case.d.ts","sourceRoot":"","sources":["no-camel-case.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AAgE3B,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA8BhB,CAAA;AAED,eAAe,IAAI,CAAA"}
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser");
4
+ const util_1 = require("../util");
5
+ const CAMEL_CASE_REGEX = /[A-Z]/;
6
+ class CamelCase extends Error {
7
+ constructor() {
8
+ super(...arguments);
9
+ this.message = 'Camel case arguments are not allowed';
10
+ }
11
+ }
12
+ function verifyAst(ast) {
13
+ for (const el of ast) {
14
+ if ((0, icu_messageformat_parser_1.isArgumentElement)(el)) {
15
+ if (CAMEL_CASE_REGEX.test(el.value)) {
16
+ throw new CamelCase();
17
+ }
18
+ continue;
19
+ }
20
+ if ((0, icu_messageformat_parser_1.isPluralElement)(el)) {
21
+ if (CAMEL_CASE_REGEX.test(el.value)) {
22
+ throw new CamelCase();
23
+ }
24
+ const { options } = el;
25
+ for (const selector of Object.keys(options)) {
26
+ verifyAst(options[selector].value);
27
+ }
28
+ }
29
+ }
30
+ }
31
+ function checkNode(context, node) {
32
+ const settings = (0, util_1.getSettings)(context);
33
+ const msgs = (0, util_1.extractMessages)(node, settings);
34
+ for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
35
+ if (!defaultMessage || !messageNode) {
36
+ continue;
37
+ }
38
+ try {
39
+ verifyAst((0, icu_messageformat_parser_1.parse)(defaultMessage, {
40
+ ignoreTag: settings.ignoreTag,
41
+ }));
42
+ }
43
+ catch (e) {
44
+ context.report({
45
+ node: messageNode,
46
+ message: e instanceof Error ? e.message : String(e),
47
+ });
48
+ }
49
+ }
50
+ }
51
+ const rule = {
52
+ meta: {
53
+ type: 'problem',
54
+ docs: {
55
+ description: 'Disallow camel case placeholders in message',
56
+ category: 'Errors',
57
+ recommended: false,
58
+ url: 'https://formatjs.io/docs/tooling/linter#no-camel-case',
59
+ },
60
+ fixable: 'code',
61
+ },
62
+ create(context) {
63
+ const callExpressionVisitor = (node) => checkNode(context, node);
64
+ if (context.parserServices.defineTemplateBodyVisitor) {
65
+ return context.parserServices.defineTemplateBodyVisitor({
66
+ CallExpression: callExpressionVisitor,
67
+ }, {
68
+ CallExpression: callExpressionVisitor,
69
+ });
70
+ }
71
+ return {
72
+ JSXOpeningElement: (node) => checkNode(context, node),
73
+ CallExpression: callExpressionVisitor,
74
+ };
75
+ },
76
+ };
77
+ exports.default = rule;
@@ -0,0 +1,4 @@
1
+ import { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=no-complex-selectors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-complex-selectors.d.ts","sourceRoot":"","sources":["no-complex-selectors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AAyE3B,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAiDhB,CAAA;AAED,eAAe,IAAI,CAAA"}
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const util_1 = require("../util");
4
+ const icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser");
5
+ const manipulator_1 = require("@formatjs/icu-messageformat-parser/manipulator");
6
+ function calculateComplexity(ast) {
7
+ if (ast.length === 1) {
8
+ const el = ast[0];
9
+ if ((0, icu_messageformat_parser_1.isPluralElement)(el) || (0, icu_messageformat_parser_1.isSelectElement)(el)) {
10
+ return Object.keys(el.options).reduce((complexity, k) => {
11
+ return complexity + calculateComplexity(el.options[k].value);
12
+ }, 0);
13
+ }
14
+ }
15
+ return 1;
16
+ }
17
+ function checkNode(context, node) {
18
+ const settings = (0, util_1.getSettings)(context);
19
+ const msgs = (0, util_1.extractMessages)(node, settings);
20
+ if (!msgs.length) {
21
+ return;
22
+ }
23
+ const config = {
24
+ limit: 20,
25
+ ...(context.options[0] || {}),
26
+ };
27
+ for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
28
+ if (!defaultMessage || !messageNode) {
29
+ continue;
30
+ }
31
+ let ast;
32
+ try {
33
+ ast = (0, icu_messageformat_parser_1.parse)(defaultMessage, {
34
+ ignoreTag: settings.ignoreTag,
35
+ });
36
+ }
37
+ catch (e) {
38
+ context.report({
39
+ node: messageNode,
40
+ message: e instanceof Error ? e.message : String(e),
41
+ });
42
+ return;
43
+ }
44
+ const hoistedAst = (0, manipulator_1.hoistSelectors)(ast);
45
+ const complexity = calculateComplexity(hoistedAst);
46
+ if (complexity > config.limit) {
47
+ context.report({
48
+ node: messageNode,
49
+ message: `Message complexity is too high (${complexity} vs limit at ${config.limit})`,
50
+ });
51
+ }
52
+ }
53
+ }
54
+ const rule = {
55
+ meta: {
56
+ type: 'problem',
57
+ docs: {
58
+ description: `Make sure a sentence is not too complex.
59
+ Complexity is determined by how many strings are produced when we try to
60
+ flatten the sentence given its selectors. For example:
61
+ "I have {count, plural, one{a dog} other{many dogs}}"
62
+ has the complexity of 2 because flattening the plural selector
63
+ results in 2 sentences: "I have a dog" & "I have many dogs".
64
+ Default complexity limit is 20
65
+ (using Smartling as a reference: https://help.smartling.com/hc/en-us/articles/360008030994-ICU-MessageFormat)
66
+ `,
67
+ category: 'Errors',
68
+ recommended: false,
69
+ url: 'https://formatjs.io/docs/tooling/linter#no-complex-selectors',
70
+ },
71
+ schema: [
72
+ {
73
+ type: 'object',
74
+ properties: {
75
+ limit: {
76
+ type: 'number',
77
+ },
78
+ },
79
+ additionalProperties: false,
80
+ },
81
+ ],
82
+ fixable: 'code',
83
+ },
84
+ create(context) {
85
+ const callExpressionVisitor = (node) => checkNode(context, node);
86
+ if (context.parserServices.defineTemplateBodyVisitor) {
87
+ return context.parserServices.defineTemplateBodyVisitor({
88
+ CallExpression: callExpressionVisitor,
89
+ }, {
90
+ CallExpression: callExpressionVisitor,
91
+ });
92
+ }
93
+ return {
94
+ JSXOpeningElement: (node) => checkNode(context, node),
95
+ CallExpression: callExpressionVisitor,
96
+ };
97
+ },
98
+ };
99
+ exports.default = rule;
@@ -0,0 +1,4 @@
1
+ import { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=no-emoji.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const util_1 = require("../util");
5
+ const emoji_regex_1 = tslib_1.__importDefault(require("emoji-regex"));
6
+ const EMOJI_REGEX = emoji_regex_1.default();
7
+ function checkNode(context, node) {
8
+ const msgs = (0, util_1.extractMessages)(node, (0, util_1.getSettings)(context));
9
+ for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
10
+ if (!defaultMessage || !messageNode) {
11
+ continue;
12
+ }
13
+ if (EMOJI_REGEX.test(defaultMessage)) {
14
+ context.report({
15
+ node: messageNode,
16
+ message: 'Emojis are not allowed',
17
+ });
18
+ }
19
+ }
20
+ }
21
+ const rule = {
22
+ meta: {
23
+ type: 'problem',
24
+ docs: {
25
+ description: 'Disallow emojis in message',
26
+ category: 'Errors',
27
+ recommended: false,
28
+ url: 'https://formatjs.io/docs/tooling/linter#no-emoji',
29
+ },
30
+ fixable: 'code',
31
+ },
32
+ create(context) {
33
+ const callExpressionVisitor = (node) => checkNode(context, node);
34
+ if (context.parserServices.defineTemplateBodyVisitor) {
35
+ return context.parserServices.defineTemplateBodyVisitor({
36
+ CallExpression: callExpressionVisitor,
37
+ }, {
38
+ CallExpression: callExpressionVisitor,
39
+ });
40
+ }
41
+ return {
42
+ JSXOpeningElement: (node) => checkNode(context, node),
43
+ CallExpression: callExpressionVisitor,
44
+ };
45
+ },
46
+ };
47
+ exports.default = rule;
@@ -0,0 +1,4 @@
1
+ import { Rule } from 'eslint';
2
+ declare const _default: Rule.RuleModule;
3
+ export default _default;
4
+ //# sourceMappingURL=no-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-id.d.ts","sourceRoot":"","sources":["no-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAa,MAAM,QAAQ,CAAA;;AAgCvC,wBA8BoB"}
package/rules/no-id.js ADDED
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const util_1 = require("../util");
4
+ function isComment(token) {
5
+ return !!token && (token.type === 'Block' || token.type === 'Line');
6
+ }
7
+ function checkNode(context, node) {
8
+ const msgs = (0, util_1.extractMessages)(node, (0, util_1.getSettings)(context));
9
+ for (const [{ idPropNode }] of msgs) {
10
+ if (idPropNode) {
11
+ context.report({
12
+ node: idPropNode,
13
+ message: 'Manual `id` are not allowed in message descriptor',
14
+ fix(fixer) {
15
+ const src = context.getSourceCode();
16
+ const token = src.getTokenAfter(idPropNode);
17
+ const fixes = [fixer.remove(idPropNode)];
18
+ if (token && !isComment(token) && token?.value === ',') {
19
+ fixes.push(fixer.remove(token));
20
+ }
21
+ return fixes;
22
+ },
23
+ });
24
+ }
25
+ }
26
+ }
27
+ exports.default = {
28
+ meta: {
29
+ type: 'problem',
30
+ docs: {
31
+ description: 'Ban explicit ID from MessageDescriptor',
32
+ category: 'Errors',
33
+ recommended: false,
34
+ url: 'https://formatjs.io/docs/tooling/linter#no-id',
35
+ },
36
+ fixable: 'code',
37
+ },
38
+ create(context) {
39
+ const callExpressionVisitor = (node) => checkNode(context, node);
40
+ if (context.parserServices.defineTemplateBodyVisitor) {
41
+ return context.parserServices.defineTemplateBodyVisitor({
42
+ CallExpression: callExpressionVisitor,
43
+ }, {
44
+ CallExpression: callExpressionVisitor,
45
+ });
46
+ }
47
+ return {
48
+ JSXOpeningElement: (node) => checkNode(context, node),
49
+ CallExpression: callExpressionVisitor,
50
+ };
51
+ },
52
+ };
@@ -0,0 +1,4 @@
1
+ import { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=no-invalid-icu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-invalid-icu.d.ts","sourceRoot":"","sources":["no-invalid-icu.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AAqC3B,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA6BhB,CAAA;AAED,eAAe,IAAI,CAAA"}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser");
4
+ const util_1 = require("../util");
5
+ function checkNode(context, node) {
6
+ const settings = (0, util_1.getSettings)(context);
7
+ const msgs = (0, util_1.extractMessages)(node, settings);
8
+ if (!msgs.length) {
9
+ return;
10
+ }
11
+ for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
12
+ if (!defaultMessage || !messageNode) {
13
+ continue;
14
+ }
15
+ try {
16
+ (0, icu_messageformat_parser_1.parse)(defaultMessage, {
17
+ ignoreTag: settings.ignoreTag,
18
+ });
19
+ }
20
+ catch (e) {
21
+ const msg = e instanceof Error ? e.message : e;
22
+ context.report({
23
+ node: messageNode,
24
+ message: `Error parsing ICU string: ${msg}`,
25
+ });
26
+ }
27
+ }
28
+ }
29
+ const rule = {
30
+ meta: {
31
+ type: 'problem',
32
+ docs: {
33
+ description: `Make sure ICU messages are formatted correctly with no bad select statements, plurals, etc.`,
34
+ category: 'Errors',
35
+ recommended: true,
36
+ },
37
+ fixable: 'code',
38
+ },
39
+ create(context) {
40
+ const callExpressionVisitor = (node) => checkNode(context, node);
41
+ if (context.parserServices.defineTemplateBodyVisitor) {
42
+ return context.parserServices.defineTemplateBodyVisitor({
43
+ CallExpression: callExpressionVisitor,
44
+ }, {
45
+ CallExpression: callExpressionVisitor,
46
+ });
47
+ }
48
+ return {
49
+ JSXOpeningElement: (node) => checkNode(context, node),
50
+ CallExpression: callExpressionVisitor,
51
+ };
52
+ },
53
+ };
54
+ 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=no-literal-string-in-jsx.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-literal-string-in-jsx.d.ts","sourceRoot":"","sources":["no-literal-string-in-jsx.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AAqDhC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA4JhB,CAAA;AAED,eAAe,IAAI,CAAA"}
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const picomatch_1 = tslib_1.__importDefault(require("picomatch"));
5
+ const typescript_estree_1 = require("@typescript-eslint/typescript-estree");
6
+ const propMatcherSchema = {
7
+ type: 'array',
8
+ items: {
9
+ type: 'array',
10
+ items: [{ type: 'string' }, { type: 'string' }],
11
+ },
12
+ };
13
+ const defaultPropIncludePattern = [
14
+ ['*', 'aria-{label,description,details,errormessage}'],
15
+ ['[a-z]*([a-z0-9])', '(placeholder|title)'],
16
+ ['img', 'alt'],
17
+ ];
18
+ const defaultPropExcludePattern = [];
19
+ function stringifyJsxTagName(tagName) {
20
+ switch (tagName.type) {
21
+ case typescript_estree_1.TSESTree.AST_NODE_TYPES.JSXIdentifier:
22
+ return tagName.name;
23
+ case typescript_estree_1.TSESTree.AST_NODE_TYPES.JSXMemberExpression:
24
+ return `${stringifyJsxTagName(tagName.object)}.${tagName.property.name}`;
25
+ case typescript_estree_1.TSESTree.AST_NODE_TYPES.JSXNamespacedName:
26
+ return `${tagName.namespace.name}:${tagName.name.name}`;
27
+ }
28
+ }
29
+ function compilePropMatcher(propMatcher) {
30
+ return propMatcher.map(([tagNamePattern, propNamePattern]) => {
31
+ return [
32
+ picomatch_1.default.makeRe(tagNamePattern, { contains: false }),
33
+ picomatch_1.default.makeRe(propNamePattern, { contains: false }),
34
+ ];
35
+ });
36
+ }
37
+ const rule = {
38
+ meta: {
39
+ type: 'problem',
40
+ docs: {
41
+ description: 'Disallow untranslated literal strings without translation.',
42
+ category: 'Errors',
43
+ recommended: false,
44
+ url: 'https://formatjs.io/docs/tooling/linter#no-literal-string-in-jsx',
45
+ },
46
+ schema: [
47
+ {
48
+ type: 'object',
49
+ properties: {
50
+ props: {
51
+ type: 'object',
52
+ properties: {
53
+ include: {
54
+ ...propMatcherSchema,
55
+ },
56
+ exclude: {
57
+ ...propMatcherSchema,
58
+ },
59
+ },
60
+ },
61
+ },
62
+ },
63
+ ],
64
+ },
65
+ // TODO: Vue support
66
+ create(context) {
67
+ const userConfig = context.options[0] || {};
68
+ const propIncludePattern = compilePropMatcher([
69
+ ...defaultPropIncludePattern,
70
+ ...(userConfig.props?.include ?? []),
71
+ ]);
72
+ const propExcludePattern = compilePropMatcher([
73
+ ...defaultPropExcludePattern,
74
+ ...(userConfig.props?.exclude ?? []),
75
+ ]);
76
+ const lexicalJsxStack = [];
77
+ const shouldSkipCurrentJsxAttribute = (node) => {
78
+ const currentJsxNode = lexicalJsxStack[lexicalJsxStack.length - 1];
79
+ if (currentJsxNode.type === 'JSXFragment') {
80
+ return false;
81
+ }
82
+ const nameString = stringifyJsxTagName(currentJsxNode.openingElement.name);
83
+ const attributeName = typeof node.name.name === 'string'
84
+ ? node.name.name
85
+ : node.name.name.name;
86
+ // match exclude
87
+ for (const [tagNamePattern, propNamePattern] of propExcludePattern) {
88
+ if (tagNamePattern.test(nameString) &&
89
+ propNamePattern.test(attributeName)) {
90
+ return true;
91
+ }
92
+ }
93
+ // match include
94
+ for (const [tagNamePattern, propNamePattern] of propIncludePattern) {
95
+ if (tagNamePattern.test(nameString) &&
96
+ propNamePattern.test(attributeName)) {
97
+ return false;
98
+ }
99
+ }
100
+ return true;
101
+ };
102
+ const checkJSXExpression = (node) => {
103
+ // Check if this is either a string literal / template literal, or the concat of them.
104
+ if ((node.type === 'Literal' && typeof node.value === 'string') ||
105
+ node.type === 'TemplateLiteral') {
106
+ context.report({
107
+ node: node,
108
+ message: 'Cannot have untranslated text in JSX',
109
+ });
110
+ }
111
+ else if (node.type === 'BinaryExpression' && node.operator === '+') {
112
+ checkJSXExpression(node.left);
113
+ checkJSXExpression(node.right);
114
+ }
115
+ };
116
+ return {
117
+ JSXElement: (node) => {
118
+ lexicalJsxStack.push(node);
119
+ },
120
+ 'JSXElement:exit': () => {
121
+ lexicalJsxStack.pop();
122
+ },
123
+ JSXFragment: (node) => {
124
+ lexicalJsxStack.push(node);
125
+ },
126
+ 'JSXFragment:exit': () => {
127
+ lexicalJsxStack.pop();
128
+ },
129
+ JSXAttribute: (node) => {
130
+ if (shouldSkipCurrentJsxAttribute(node)) {
131
+ return;
132
+ }
133
+ if (!node.value) {
134
+ return;
135
+ }
136
+ if (node.value.type === 'Literal') {
137
+ context.report({
138
+ node: node,
139
+ message: 'Cannot have untranslated text in JSX',
140
+ });
141
+ }
142
+ else if (node.value.type === 'JSXExpressionContainer' &&
143
+ node.value.expression.type !== 'JSXEmptyExpression') {
144
+ checkJSXExpression(node.value.expression);
145
+ }
146
+ },
147
+ JSXText: (node) => {
148
+ // Ignore purely spacing fragments
149
+ if (!node.value.replace(/\s*/gm, '')) {
150
+ return;
151
+ }
152
+ context.report({
153
+ node: node,
154
+ message: 'Cannot have untranslated text in JSX',
155
+ });
156
+ },
157
+ // Children expression container
158
+ 'JSXElement > JSXExpressionContainer': (node) => {
159
+ if (node.expression.type !== 'JSXEmptyExpression') {
160
+ checkJSXExpression(node.expression);
161
+ }
162
+ },
163
+ };
164
+ },
165
+ };
166
+ exports.default = rule;
@@ -0,0 +1,4 @@
1
+ import { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=no-multiple-plurals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-multiple-plurals.d.ts","sourceRoot":"","sources":["no-multiple-plurals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AAwD3B,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA8BhB,CAAA;AAED,eAAe,IAAI,CAAA"}