eslint-plugin-formatjs 4.2.0 → 4.2.2
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 +22 -0
- package/index.d.ts.map +1 -0
- package/index.js +38 -0
- package/package.json +3 -3
- package/rules/blocklist-elements.d.ts +4 -0
- package/rules/blocklist-elements.d.ts.map +1 -0
- package/rules/blocklist-elements.js +127 -0
- package/rules/enforce-default-message.d.ts +4 -0
- package/rules/enforce-default-message.d.ts.map +1 -0
- package/rules/enforce-default-message.js +57 -0
- package/rules/enforce-description.d.ts +4 -0
- package/rules/enforce-description.d.ts.map +1 -0
- package/rules/enforce-description.js +54 -0
- package/rules/enforce-id.d.ts +4 -0
- package/rules/enforce-id.d.ts.map +1 -0
- package/rules/enforce-id.js +125 -0
- package/rules/enforce-placeholders.d.ts +4 -0
- package/rules/enforce-placeholders.d.ts.map +1 -0
- package/rules/enforce-placeholders.js +118 -0
- package/rules/enforce-plural-rules.d.ts +4 -0
- package/rules/enforce-plural-rules.d.ts.map +1 -0
- package/rules/enforce-plural-rules.js +104 -0
- package/rules/no-camel-case.d.ts +4 -0
- package/rules/no-camel-case.d.ts.map +1 -0
- package/rules/no-camel-case.js +77 -0
- package/rules/no-complex-selectors.d.ts +4 -0
- package/rules/no-complex-selectors.d.ts.map +1 -0
- package/rules/no-complex-selectors.js +99 -0
- package/rules/no-emoji.d.ts +4 -0
- package/rules/no-emoji.d.ts.map +1 -0
- package/rules/no-emoji.js +47 -0
- package/rules/no-id.d.ts +4 -0
- package/rules/no-id.d.ts.map +1 -0
- package/rules/no-id.js +52 -0
- package/rules/no-invalid-icu.d.ts +4 -0
- package/rules/no-invalid-icu.d.ts.map +1 -0
- package/rules/no-invalid-icu.js +54 -0
- package/rules/no-literal-string-in-jsx.d.ts +4 -0
- package/rules/no-literal-string-in-jsx.d.ts.map +1 -0
- package/rules/no-literal-string-in-jsx.js +166 -0
- package/rules/no-multiple-plurals.d.ts +4 -0
- package/rules/no-multiple-plurals.d.ts.map +1 -0
- package/rules/no-multiple-plurals.js +71 -0
- package/rules/no-multiple-whitespaces.d.ts +4 -0
- package/rules/no-multiple-whitespaces.d.ts.map +1 -0
- package/rules/no-multiple-whitespaces.js +146 -0
- package/rules/no-offset.d.ts +4 -0
- package/rules/no-offset.d.ts.map +1 -0
- package/rules/no-offset.js +70 -0
- package/util.d.ts +24 -0
- package/util.d.ts.map +1 -0
- package/util.js +240 -0
- package/BUILD +0 -89
- package/CHANGELOG.md +0 -892
- package/index.ts +0 -38
- package/rules/blocklist-elements.ts +0 -159
- package/rules/enforce-default-message.ts +0 -71
- package/rules/enforce-description.ts +0 -68
- package/rules/enforce-id.ts +0 -171
- package/rules/enforce-placeholders.ts +0 -161
- package/rules/enforce-plural-rules.ts +0 -134
- package/rules/no-camel-case.ts +0 -97
- package/rules/no-complex-selectors.ts +0 -125
- package/rules/no-emoji.ts +0 -60
- package/rules/no-id.ts +0 -63
- package/rules/no-invalid-icu.ts +0 -69
- package/rules/no-literal-string-in-jsx.ts +0 -213
- package/rules/no-multiple-plurals.ts +0 -89
- package/rules/no-multiple-whitespaces.ts +0 -194
- package/rules/no-offset.ts +0 -88
- package/tests/blocklist-elements.test.ts +0 -120
- package/tests/enforce-default-message.test.ts +0 -260
- package/tests/enforce-description.test.ts +0 -117
- package/tests/enforce-id.test.ts +0 -209
- package/tests/enforce-placeholders.test.ts +0 -170
- package/tests/enforce-plural-rules.test.ts +0 -86
- package/tests/fixtures.ts +0 -15
- package/tests/no-camel-case.test.ts +0 -31
- package/tests/no-complex-selectors.test.ts +0 -125
- package/tests/no-id.test.ts +0 -151
- package/tests/no-invalid-icu.test.ts +0 -106
- package/tests/no-literal-string-in-jsx.test.ts +0 -213
- package/tests/no-multiple-plurals.test.ts +0 -42
- package/tests/no-multiple-whitespaces.test.ts +0 -100
- package/tests/no-offset.test.ts +0 -41
- package/tests/util.ts +0 -26
- package/tsconfig.json +0 -5
- package/util.ts +0 -307
|
@@ -0,0 +1,71 @@
|
|
|
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 MultiplePlurals extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super(...arguments);
|
|
8
|
+
this.message = 'Cannot specify more than 1 plural rules';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function verifyAst(ast, pluralCount = { count: 0 }) {
|
|
12
|
+
for (const el of ast) {
|
|
13
|
+
if ((0, icu_messageformat_parser_1.isPluralElement)(el)) {
|
|
14
|
+
pluralCount.count++;
|
|
15
|
+
if (pluralCount.count > 1) {
|
|
16
|
+
throw new MultiplePlurals();
|
|
17
|
+
}
|
|
18
|
+
const { options } = el;
|
|
19
|
+
for (const selector of Object.keys(options)) {
|
|
20
|
+
verifyAst(options[selector].value, pluralCount);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function checkNode(context, node) {
|
|
26
|
+
const settings = (0, util_1.getSettings)(context);
|
|
27
|
+
const msgs = (0, util_1.extractMessages)(node, settings);
|
|
28
|
+
for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
|
|
29
|
+
if (!defaultMessage || !messageNode) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
verifyAst((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
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const rule = {
|
|
46
|
+
meta: {
|
|
47
|
+
type: 'problem',
|
|
48
|
+
docs: {
|
|
49
|
+
description: 'Disallow multiple plural rules in the same message',
|
|
50
|
+
category: 'Errors',
|
|
51
|
+
recommended: false,
|
|
52
|
+
url: 'https://formatjs.io/docs/tooling/linter#no-multiple-plurals',
|
|
53
|
+
},
|
|
54
|
+
fixable: 'code',
|
|
55
|
+
},
|
|
56
|
+
create(context) {
|
|
57
|
+
const callExpressionVisitor = (node) => checkNode(context, node);
|
|
58
|
+
if (context.parserServices.defineTemplateBodyVisitor) {
|
|
59
|
+
return context.parserServices.defineTemplateBodyVisitor({
|
|
60
|
+
CallExpression: callExpressionVisitor,
|
|
61
|
+
}, {
|
|
62
|
+
CallExpression: callExpressionVisitor,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
JSXOpeningElement: (node) => checkNode(context, node),
|
|
67
|
+
CallExpression: callExpressionVisitor,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
exports.default = rule;
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
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 isAstValid(ast) {
|
|
6
|
+
for (const element of ast) {
|
|
7
|
+
switch (element.type) {
|
|
8
|
+
case icu_messageformat_parser_1.TYPE.literal:
|
|
9
|
+
if (/\s{2,}/gm.test(element.value)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
break;
|
|
13
|
+
case icu_messageformat_parser_1.TYPE.argument:
|
|
14
|
+
case icu_messageformat_parser_1.TYPE.date:
|
|
15
|
+
case icu_messageformat_parser_1.TYPE.literal:
|
|
16
|
+
case icu_messageformat_parser_1.TYPE.number:
|
|
17
|
+
case icu_messageformat_parser_1.TYPE.pound:
|
|
18
|
+
case icu_messageformat_parser_1.TYPE.tag:
|
|
19
|
+
case icu_messageformat_parser_1.TYPE.time:
|
|
20
|
+
break;
|
|
21
|
+
case icu_messageformat_parser_1.TYPE.plural:
|
|
22
|
+
case icu_messageformat_parser_1.TYPE.select: {
|
|
23
|
+
for (const option of Object.values(element.options)) {
|
|
24
|
+
if (!isAstValid(option.value)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function trimMultiWhitespaces(message, ast) {
|
|
35
|
+
const literalElements = [];
|
|
36
|
+
const collectLiteralElements = (elements) => {
|
|
37
|
+
for (const element of elements) {
|
|
38
|
+
switch (element.type) {
|
|
39
|
+
case icu_messageformat_parser_1.TYPE.literal:
|
|
40
|
+
literalElements.push(element);
|
|
41
|
+
break;
|
|
42
|
+
case icu_messageformat_parser_1.TYPE.argument:
|
|
43
|
+
case icu_messageformat_parser_1.TYPE.date:
|
|
44
|
+
case icu_messageformat_parser_1.TYPE.literal:
|
|
45
|
+
case icu_messageformat_parser_1.TYPE.number:
|
|
46
|
+
case icu_messageformat_parser_1.TYPE.pound:
|
|
47
|
+
case icu_messageformat_parser_1.TYPE.tag:
|
|
48
|
+
case icu_messageformat_parser_1.TYPE.time:
|
|
49
|
+
break;
|
|
50
|
+
case icu_messageformat_parser_1.TYPE.plural:
|
|
51
|
+
case icu_messageformat_parser_1.TYPE.select: {
|
|
52
|
+
for (const option of Object.values(element.options)) {
|
|
53
|
+
collectLiteralElements(option.value);
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
collectLiteralElements(ast);
|
|
61
|
+
// Surgically trim whitespaces in the literal element ranges.
|
|
62
|
+
// This is to preserve the original whitespaces and newlines info that are lost to parsing.
|
|
63
|
+
let trimmedFragments = [];
|
|
64
|
+
let currentOffset = 0;
|
|
65
|
+
for (const literal of literalElements) {
|
|
66
|
+
const { start, end } = literal.location;
|
|
67
|
+
const startOffset = start.offset;
|
|
68
|
+
const endOffset = end.offset;
|
|
69
|
+
trimmedFragments.push(message.slice(currentOffset, startOffset));
|
|
70
|
+
trimmedFragments.push(message.slice(startOffset, endOffset).replace(/\s{2,}/gm, ' '));
|
|
71
|
+
currentOffset = endOffset;
|
|
72
|
+
}
|
|
73
|
+
trimmedFragments.push(message.slice(currentOffset));
|
|
74
|
+
return trimmedFragments.join('');
|
|
75
|
+
}
|
|
76
|
+
function checkNode(context, node) {
|
|
77
|
+
const msgs = (0, util_1.extractMessages)(node, (0, util_1.getSettings)(context));
|
|
78
|
+
for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
|
|
79
|
+
if (!defaultMessage || !messageNode) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
let ast;
|
|
83
|
+
try {
|
|
84
|
+
ast = (0, icu_messageformat_parser_1.parse)(defaultMessage, { captureLocation: true });
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
context.report({
|
|
88
|
+
node: messageNode,
|
|
89
|
+
message: e instanceof Error ? e.message : String(e),
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!isAstValid(ast)) {
|
|
94
|
+
const reportObject = {
|
|
95
|
+
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);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const rule = {
|
|
121
|
+
meta: {
|
|
122
|
+
type: 'problem',
|
|
123
|
+
docs: {
|
|
124
|
+
description: 'Prevents usage of multiple consecutive whitespaces in message',
|
|
125
|
+
category: 'Errors',
|
|
126
|
+
recommended: false,
|
|
127
|
+
url: 'https://formatjs.io/docs/tooling/linter#no-multiple-whitespaces',
|
|
128
|
+
},
|
|
129
|
+
fixable: 'code',
|
|
130
|
+
},
|
|
131
|
+
create(context) {
|
|
132
|
+
const callExpressionVisitor = (node) => checkNode(context, node);
|
|
133
|
+
if (context.parserServices.defineTemplateBodyVisitor) {
|
|
134
|
+
return context.parserServices.defineTemplateBodyVisitor({
|
|
135
|
+
CallExpression: callExpressionVisitor,
|
|
136
|
+
}, {
|
|
137
|
+
CallExpression: callExpressionVisitor,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
JSXOpeningElement: (node) => checkNode(context, node),
|
|
142
|
+
CallExpression: callExpressionVisitor,
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
exports.default = rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-offset.d.ts","sourceRoot":"","sources":["no-offset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,QAAQ,CAAA;AAuD3B,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UA8BhB,CAAA;AAED,eAAe,IAAI,CAAA"}
|
|
@@ -0,0 +1,70 @@
|
|
|
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 NoOffsetError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super(...arguments);
|
|
8
|
+
this.message = 'offset are not allowed in plural rules';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function verifyAst(ast) {
|
|
12
|
+
for (const el of ast) {
|
|
13
|
+
if ((0, icu_messageformat_parser_1.isPluralElement)(el)) {
|
|
14
|
+
if (el.offset) {
|
|
15
|
+
throw new NoOffsetError();
|
|
16
|
+
}
|
|
17
|
+
const { options } = el;
|
|
18
|
+
for (const selector of Object.keys(options)) {
|
|
19
|
+
verifyAst(options[selector].value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function checkNode(context, node) {
|
|
25
|
+
const settings = (0, util_1.getSettings)(context);
|
|
26
|
+
const msgs = (0, util_1.extractMessages)(node, settings);
|
|
27
|
+
for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
|
|
28
|
+
if (!defaultMessage || !messageNode) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
verifyAst((0, icu_messageformat_parser_1.parse)(defaultMessage, {
|
|
33
|
+
ignoreTag: settings.ignoreTag,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
context.report({
|
|
38
|
+
node: messageNode,
|
|
39
|
+
message: e.message,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const rule = {
|
|
45
|
+
meta: {
|
|
46
|
+
type: 'problem',
|
|
47
|
+
docs: {
|
|
48
|
+
description: 'Disallow offset in plural rules',
|
|
49
|
+
category: 'Errors',
|
|
50
|
+
recommended: false,
|
|
51
|
+
url: 'https://formatjs.io/docs/tooling/linter#no-offset',
|
|
52
|
+
},
|
|
53
|
+
fixable: 'code',
|
|
54
|
+
},
|
|
55
|
+
create(context) {
|
|
56
|
+
const callExpressionVisitor = (node) => checkNode(context, node);
|
|
57
|
+
if (context.parserServices.defineTemplateBodyVisitor) {
|
|
58
|
+
return context.parserServices.defineTemplateBodyVisitor({
|
|
59
|
+
CallExpression: callExpressionVisitor,
|
|
60
|
+
}, {
|
|
61
|
+
CallExpression: callExpressionVisitor,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
JSXOpeningElement: (node) => checkNode(context, node),
|
|
66
|
+
CallExpression: callExpressionVisitor,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
exports.default = rule;
|
package/util.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Rule } from 'eslint';
|
|
2
|
+
import { TSESTree } from '@typescript-eslint/typescript-estree';
|
|
3
|
+
export interface MessageDescriptor {
|
|
4
|
+
id?: string;
|
|
5
|
+
defaultMessage?: string;
|
|
6
|
+
description?: string | object;
|
|
7
|
+
}
|
|
8
|
+
export interface Settings {
|
|
9
|
+
excludeMessageDeclCalls?: boolean;
|
|
10
|
+
additionalFunctionNames?: string[];
|
|
11
|
+
additionalComponentNames?: string[];
|
|
12
|
+
ignoreTag?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface MessageDescriptorNodeInfo {
|
|
15
|
+
message: MessageDescriptor;
|
|
16
|
+
messageNode?: TSESTree.Property['value'] | TSESTree.JSXAttribute['value'];
|
|
17
|
+
messagePropNode?: TSESTree.Property | TSESTree.JSXAttribute;
|
|
18
|
+
descriptionNode?: TSESTree.Property['value'] | TSESTree.JSXAttribute['value'];
|
|
19
|
+
idValueNode?: TSESTree.Property['value'] | TSESTree.JSXAttribute['value'];
|
|
20
|
+
idPropNode?: TSESTree.Property | TSESTree.JSXAttribute;
|
|
21
|
+
}
|
|
22
|
+
export declare function getSettings({ settings }: Rule.RuleContext): Settings;
|
|
23
|
+
export declare function extractMessages(node: TSESTree.Node, { additionalComponentNames, additionalFunctionNames, excludeMessageDeclCalls, }?: Settings): Array<[MessageDescriptorNodeInfo, TSESTree.Expression | undefined]>;
|
|
24
|
+
//# sourceMappingURL=util.d.ts.map
|
package/util.d.ts.map
ADDED
|
@@ -0,0 +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"}
|
package/util.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractMessages = exports.getSettings = void 0;
|
|
4
|
+
const FORMAT_FUNCTION_NAMES = new Set(['$formatMessage', 'formatMessage']);
|
|
5
|
+
const COMPONENT_NAMES = new Set(['FormattedMessage']);
|
|
6
|
+
const DECLARATION_FUNCTION_NAMES = new Set(['defineMessage']);
|
|
7
|
+
function getSettings({ settings }) {
|
|
8
|
+
return settings.formatjs ?? settings;
|
|
9
|
+
}
|
|
10
|
+
exports.getSettings = getSettings;
|
|
11
|
+
function isStringLiteral(node) {
|
|
12
|
+
return node.type === 'Literal' && typeof node.value === 'string';
|
|
13
|
+
}
|
|
14
|
+
function isTemplateLiteralWithoutVar(node) {
|
|
15
|
+
return node.type === 'TemplateLiteral' && node.quasis.length === 1;
|
|
16
|
+
}
|
|
17
|
+
function staticallyEvaluateStringConcat(node) {
|
|
18
|
+
if (!isStringLiteral(node.right)) {
|
|
19
|
+
return ['', false];
|
|
20
|
+
}
|
|
21
|
+
if (isStringLiteral(node.left)) {
|
|
22
|
+
return [String(node.left.value) + node.right.value, true];
|
|
23
|
+
}
|
|
24
|
+
if (node.left.type === 'BinaryExpression') {
|
|
25
|
+
const [result, isStaticallyEvaluatable] = staticallyEvaluateStringConcat(node.left);
|
|
26
|
+
return [result + node.right.value, isStaticallyEvaluatable];
|
|
27
|
+
}
|
|
28
|
+
return ['', false];
|
|
29
|
+
}
|
|
30
|
+
function isIntlFormatMessageCall(node) {
|
|
31
|
+
return (node.type === 'CallExpression' &&
|
|
32
|
+
node.callee.type === 'MemberExpression' &&
|
|
33
|
+
node.callee.object.type === 'Identifier' &&
|
|
34
|
+
node.callee.object.name === 'intl' &&
|
|
35
|
+
node.callee.property.type === 'Identifier' &&
|
|
36
|
+
node.callee.property.name === 'formatMessage' &&
|
|
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
|
+
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
|
+
else if (isTemplateLiteralWithoutVar(valueNode)) {
|
|
71
|
+
value = valueNode.quasis[0].value.cooked;
|
|
72
|
+
}
|
|
73
|
+
else if (valueNode.type === 'BinaryExpression') {
|
|
74
|
+
const [result, isStatic] = staticallyEvaluateStringConcat(valueNode);
|
|
75
|
+
if (isStatic) {
|
|
76
|
+
value = result;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
switch (prop.key.name) {
|
|
80
|
+
case 'defaultMessage':
|
|
81
|
+
result.messagePropNode = prop;
|
|
82
|
+
result.messageNode = valueNode;
|
|
83
|
+
result.message.defaultMessage = value;
|
|
84
|
+
break;
|
|
85
|
+
case 'description':
|
|
86
|
+
result.descriptionNode = valueNode;
|
|
87
|
+
result.message.description = value;
|
|
88
|
+
break;
|
|
89
|
+
case 'id':
|
|
90
|
+
result.message.id = value;
|
|
91
|
+
result.idValueNode = valueNode;
|
|
92
|
+
result.idPropNode = prop;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
function extractMessageDescriptorFromJSXElement(node) {
|
|
99
|
+
if (!node || !node.attributes) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let values;
|
|
103
|
+
const result = {
|
|
104
|
+
message: {},
|
|
105
|
+
messageNode: undefined,
|
|
106
|
+
messagePropNode: undefined,
|
|
107
|
+
descriptionNode: undefined,
|
|
108
|
+
idValueNode: undefined,
|
|
109
|
+
idPropNode: undefined,
|
|
110
|
+
};
|
|
111
|
+
let hasSpreadAttribute = false;
|
|
112
|
+
for (const prop of node.attributes) {
|
|
113
|
+
// We can't analyze spread attr
|
|
114
|
+
if (prop.type === 'JSXSpreadAttribute') {
|
|
115
|
+
hasSpreadAttribute = true;
|
|
116
|
+
}
|
|
117
|
+
if (prop.type !== 'JSXAttribute' || prop.name.type !== 'JSXIdentifier') {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const key = prop.name;
|
|
121
|
+
let valueNode = prop.value;
|
|
122
|
+
let value = undefined;
|
|
123
|
+
if (valueNode) {
|
|
124
|
+
if (isStringLiteral(valueNode)) {
|
|
125
|
+
value = valueNode.value;
|
|
126
|
+
}
|
|
127
|
+
else if (valueNode?.type === 'JSXExpressionContainer') {
|
|
128
|
+
const { expression } = valueNode;
|
|
129
|
+
if (expression.type === 'BinaryExpression') {
|
|
130
|
+
const [result, isStatic] = staticallyEvaluateStringConcat(expression);
|
|
131
|
+
if (isStatic) {
|
|
132
|
+
value = result;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (isTemplateLiteralWithoutVar(expression)) {
|
|
136
|
+
value = expression.quasis[0].value.cooked;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
switch (key.name) {
|
|
141
|
+
case 'defaultMessage':
|
|
142
|
+
result.messagePropNode = prop;
|
|
143
|
+
result.messageNode = valueNode;
|
|
144
|
+
if (value) {
|
|
145
|
+
result.message.defaultMessage = value;
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
case 'description':
|
|
149
|
+
result.descriptionNode = valueNode;
|
|
150
|
+
if (value) {
|
|
151
|
+
result.message.description = value;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
case 'id':
|
|
155
|
+
result.idValueNode = valueNode;
|
|
156
|
+
result.idPropNode = prop;
|
|
157
|
+
if (value) {
|
|
158
|
+
result.message.id = value;
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
case 'values':
|
|
162
|
+
if (valueNode?.type === 'JSXExpressionContainer' &&
|
|
163
|
+
valueNode.expression.type === 'ObjectExpression') {
|
|
164
|
+
values = valueNode.expression;
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!result.messagePropNode &&
|
|
170
|
+
!result.descriptionNode &&
|
|
171
|
+
!result.idPropNode &&
|
|
172
|
+
hasSpreadAttribute) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
return [result, values];
|
|
176
|
+
}
|
|
177
|
+
function extractMessageDescriptors(node) {
|
|
178
|
+
if (!node || node.type !== 'ObjectExpression' || !node.properties.length) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
const msgs = [];
|
|
182
|
+
for (const prop of node.properties) {
|
|
183
|
+
if (prop.type !== 'Property') {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const msg = prop.value;
|
|
187
|
+
if (msg.type !== 'ObjectExpression') {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const nodeInfo = extractMessageDescriptor(msg);
|
|
191
|
+
if (nodeInfo) {
|
|
192
|
+
msgs.push(nodeInfo);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return msgs;
|
|
196
|
+
}
|
|
197
|
+
function extractMessages(node, { additionalComponentNames, additionalFunctionNames, excludeMessageDeclCalls, } = {}) {
|
|
198
|
+
const allFormatFunctionNames = Array.isArray(additionalFunctionNames)
|
|
199
|
+
? new Set([
|
|
200
|
+
...Array.from(FORMAT_FUNCTION_NAMES),
|
|
201
|
+
...additionalFunctionNames,
|
|
202
|
+
])
|
|
203
|
+
: FORMAT_FUNCTION_NAMES;
|
|
204
|
+
const allComponentNames = Array.isArray(additionalComponentNames)
|
|
205
|
+
? new Set([...Array.from(COMPONENT_NAMES), ...additionalComponentNames])
|
|
206
|
+
: COMPONENT_NAMES;
|
|
207
|
+
if (node.type === 'CallExpression') {
|
|
208
|
+
const expr = node;
|
|
209
|
+
const args0 = expr.arguments[0];
|
|
210
|
+
const args1 = expr.arguments[1];
|
|
211
|
+
// We can't really analyze spread element
|
|
212
|
+
if (!args0 || args0.type === 'SpreadElement') {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
if ((!excludeMessageDeclCalls &&
|
|
216
|
+
isSingleMessageDescriptorDeclaration(node, DECLARATION_FUNCTION_NAMES)) ||
|
|
217
|
+
isIntlFormatMessageCall(node) ||
|
|
218
|
+
isSingleMessageDescriptorDeclaration(node, allFormatFunctionNames)) {
|
|
219
|
+
const msgDescriptorNodeInfo = extractMessageDescriptor(args0);
|
|
220
|
+
if (msgDescriptorNodeInfo && (!args1 || args1.type !== 'SpreadElement')) {
|
|
221
|
+
return [[msgDescriptorNodeInfo, args1]];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (!excludeMessageDeclCalls &&
|
|
225
|
+
isMultipleMessageDescriptorDeclaration(node)) {
|
|
226
|
+
return extractMessageDescriptors(args0).map(msg => [msg, undefined]);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (node.type === 'JSXOpeningElement' &&
|
|
230
|
+
node.name &&
|
|
231
|
+
node.name.type === 'JSXIdentifier' &&
|
|
232
|
+
allComponentNames.has(node.name.name)) {
|
|
233
|
+
const msgDescriptorNodeInfo = extractMessageDescriptorFromJSXElement(node);
|
|
234
|
+
if (msgDescriptorNodeInfo) {
|
|
235
|
+
return [msgDescriptorNodeInfo];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
exports.extractMessages = extractMessages;
|
package/BUILD
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_files")
|
|
2
|
-
load("@aspect_rules_js//npm/private:npm_package.bzl", "npm_package")
|
|
3
|
-
load("@npm//:defs.bzl", "npm_link_all_packages")
|
|
4
|
-
load("//tools:index.bzl", "check_format", "package_json_test", "ts_compile_node")
|
|
5
|
-
load("//tools:jest.bzl", "jest_test")
|
|
6
|
-
|
|
7
|
-
npm_link_all_packages(name = "node_modules")
|
|
8
|
-
|
|
9
|
-
PACKAGE_NAME = "eslint-plugin-formatjs"
|
|
10
|
-
|
|
11
|
-
npm_package(
|
|
12
|
-
name = PACKAGE_NAME,
|
|
13
|
-
srcs = [
|
|
14
|
-
"LICENSE.md",
|
|
15
|
-
"README.md",
|
|
16
|
-
"package.json",
|
|
17
|
-
":dist",
|
|
18
|
-
],
|
|
19
|
-
package = PACKAGE_NAME,
|
|
20
|
-
visibility = ["//visibility:public"],
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
SRCS = glob(["rules/*.ts"]) + [
|
|
24
|
-
"index.ts",
|
|
25
|
-
"util.ts",
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
SRC_DEPS = [
|
|
29
|
-
"//:node_modules/@types/eslint",
|
|
30
|
-
"//:node_modules/@types/estree",
|
|
31
|
-
"//:node_modules/@types/node",
|
|
32
|
-
"//:node_modules/@types/picomatch",
|
|
33
|
-
"//:node_modules/@typescript-eslint/typescript-estree",
|
|
34
|
-
"//:node_modules/emoji-regex",
|
|
35
|
-
"//:node_modules/eslint",
|
|
36
|
-
"//:node_modules/picomatch",
|
|
37
|
-
"//:node_modules/typescript",
|
|
38
|
-
":node_modules/@formatjs/icu-messageformat-parser",
|
|
39
|
-
":node_modules/@formatjs/ts-transformer",
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
ts_compile_node(
|
|
43
|
-
name = "dist",
|
|
44
|
-
srcs = SRCS,
|
|
45
|
-
package = PACKAGE_NAME,
|
|
46
|
-
deps = SRC_DEPS,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
TESTS_BASE_SRCS = SRCS + glob(
|
|
50
|
-
[
|
|
51
|
-
"tests/*.ts",
|
|
52
|
-
],
|
|
53
|
-
exclude = ["tests/*.test.ts"],
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
TEST_FILES = glob([
|
|
57
|
-
"tests/*.test.ts",
|
|
58
|
-
])
|
|
59
|
-
|
|
60
|
-
[jest_test(
|
|
61
|
-
name = "unit-%s" % f[6:f.index(".test.ts")],
|
|
62
|
-
srcs = TESTS_BASE_SRCS + [f],
|
|
63
|
-
deps = [
|
|
64
|
-
"//:node_modules/@typescript-eslint/parser",
|
|
65
|
-
"//:node_modules/vue-eslint-parser",
|
|
66
|
-
] + SRC_DEPS,
|
|
67
|
-
) for f in TEST_FILES]
|
|
68
|
-
|
|
69
|
-
write_source_files(
|
|
70
|
-
name = "tsconfig_json",
|
|
71
|
-
files = {"tsconfig.json": "//tools:tsconfig.golden.json"},
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
check_format(
|
|
75
|
-
name = "prettier",
|
|
76
|
-
srcs = glob(
|
|
77
|
-
[
|
|
78
|
-
"**/*",
|
|
79
|
-
],
|
|
80
|
-
exclude = [
|
|
81
|
-
"CHANGELOG.md",
|
|
82
|
-
],
|
|
83
|
-
),
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
package_json_test(
|
|
87
|
-
name = "package_json_test",
|
|
88
|
-
deps = SRC_DEPS,
|
|
89
|
-
)
|