eslint-plugin-formatjs 5.4.2 → 6.0.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.
- package/context-compat.js +1 -5
- package/index.d.ts +15 -1
- package/index.js +46 -45
- package/package.json +5 -3
- package/rules/blocklist-elements.d.ts +1 -2
- package/rules/blocklist-elements.js +23 -26
- package/rules/enforce-default-message.js +8 -11
- package/rules/enforce-description.js +8 -11
- package/rules/enforce-id.d.ts +1 -0
- package/rules/enforce-id.js +21 -14
- package/rules/enforce-placeholders.js +14 -17
- package/rules/enforce-plural-rules.js +10 -13
- package/rules/no-camel-case.js +11 -14
- package/rules/no-complex-selectors.js +13 -16
- package/rules/no-emoji.js +11 -14
- package/rules/no-id.js +6 -9
- package/rules/no-invalid-icu.js +9 -12
- package/rules/no-literal-string-in-jsx.js +33 -13
- package/rules/no-literal-string-in-object.js +8 -11
- package/rules/no-missing-icu-plural-one-placeholders.js +15 -19
- package/rules/no-multiple-plurals.js +10 -13
- package/rules/no-multiple-whitespaces.js +27 -32
- package/rules/no-offset.js +10 -13
- package/rules/no-useless-message.js +13 -16
- package/rules/prefer-formatted-message.js +4 -7
- package/rules/prefer-pound-in-plural.js +25 -29
- package/util.js +5 -12
- package/lib_esnext/context-compat.d.ts +0 -3
- package/lib_esnext/context-compat.js +0 -6
- package/lib_esnext/index.d.ts +0 -1
- package/lib_esnext/index.js +0 -146
- package/lib_esnext/package.json +0 -31
- package/lib_esnext/rules/blocklist-elements.d.ts +0 -15
- package/lib_esnext/rules/blocklist-elements.js +0 -132
- package/lib_esnext/rules/enforce-default-message.d.ts +0 -10
- package/lib_esnext/rules/enforce-default-message.js +0 -68
- package/lib_esnext/rules/enforce-description.d.ts +0 -10
- package/lib_esnext/rules/enforce-description.js +0 -66
- package/lib_esnext/rules/enforce-id.d.ts +0 -10
- package/lib_esnext/rules/enforce-id.js +0 -153
- package/lib_esnext/rules/enforce-placeholders.d.ts +0 -8
- package/lib_esnext/rules/enforce-placeholders.js +0 -147
- package/lib_esnext/rules/enforce-plural-rules.d.ts +0 -17
- package/lib_esnext/rules/enforce-plural-rules.js +0 -103
- package/lib_esnext/rules/no-camel-case.d.ts +0 -5
- package/lib_esnext/rules/no-camel-case.js +0 -76
- package/lib_esnext/rules/no-complex-selectors.d.ts +0 -9
- package/lib_esnext/rules/no-complex-selectors.js +0 -136
- package/lib_esnext/rules/no-emoji.d.ts +0 -9
- package/lib_esnext/rules/no-emoji.js +0 -99
- package/lib_esnext/rules/no-id.d.ts +0 -5
- package/lib_esnext/rules/no-id.js +0 -58
- package/lib_esnext/rules/no-invalid-icu.d.ts +0 -5
- package/lib_esnext/rules/no-invalid-icu.js +0 -60
- package/lib_esnext/rules/no-literal-string-in-jsx.d.ts +0 -13
- package/lib_esnext/rules/no-literal-string-in-jsx.js +0 -179
- package/lib_esnext/rules/no-literal-string-in-object.d.ts +0 -9
- package/lib_esnext/rules/no-literal-string-in-object.js +0 -90
- package/lib_esnext/rules/no-missing-icu-plural-one-placeholders.d.ts +0 -6
- package/lib_esnext/rules/no-missing-icu-plural-one-placeholders.js +0 -99
- package/lib_esnext/rules/no-multiple-plurals.d.ts +0 -5
- package/lib_esnext/rules/no-multiple-plurals.js +0 -70
- package/lib_esnext/rules/no-multiple-whitespaces.d.ts +0 -5
- package/lib_esnext/rules/no-multiple-whitespaces.js +0 -141
- package/lib_esnext/rules/no-offset.d.ts +0 -5
- package/lib_esnext/rules/no-offset.js +0 -69
- package/lib_esnext/rules/no-useless-message.d.ts +0 -5
- package/lib_esnext/rules/no-useless-message.js +0 -71
- package/lib_esnext/rules/prefer-formatted-message.d.ts +0 -5
- package/lib_esnext/rules/prefer-formatted-message.js +0 -33
- package/lib_esnext/rules/prefer-pound-in-plural.d.ts +0 -5
- package/lib_esnext/rules/prefer-pound-in-plural.js +0 -191
- package/lib_esnext/util.d.ts +0 -32
- package/lib_esnext/util.js +0 -280
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import picomatch from 'picomatch';
|
|
3
|
-
const propMatcherSchema = {
|
|
4
|
-
type: 'array',
|
|
5
|
-
items: {
|
|
6
|
-
type: 'array',
|
|
7
|
-
items: [{ type: 'string' }, { type: 'string' }],
|
|
8
|
-
},
|
|
9
|
-
};
|
|
10
|
-
const defaultPropIncludePattern = [
|
|
11
|
-
['*', 'aria-{label,description,details,errormessage}'],
|
|
12
|
-
['[a-z]*([a-z0-9])', '(placeholder|title)'],
|
|
13
|
-
['img', 'alt'],
|
|
14
|
-
];
|
|
15
|
-
const defaultPropExcludePattern = [];
|
|
16
|
-
function stringifyJsxTagName(tagName) {
|
|
17
|
-
switch (tagName.type) {
|
|
18
|
-
case TSESTree.AST_NODE_TYPES.JSXIdentifier:
|
|
19
|
-
return tagName.name;
|
|
20
|
-
case TSESTree.AST_NODE_TYPES.JSXMemberExpression:
|
|
21
|
-
return `${stringifyJsxTagName(tagName.object)}.${tagName.property.name}`;
|
|
22
|
-
case TSESTree.AST_NODE_TYPES.JSXNamespacedName:
|
|
23
|
-
return `${tagName.namespace.name}:${tagName.name.name}`;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function compilePropMatcher(propMatcher) {
|
|
27
|
-
return propMatcher.map(([tagNamePattern, propNamePattern]) => {
|
|
28
|
-
return [
|
|
29
|
-
picomatch.makeRe(tagNamePattern, { contains: false }),
|
|
30
|
-
picomatch.makeRe(propNamePattern, { contains: false }),
|
|
31
|
-
];
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
export const name = 'no-literal-string-in-jsx';
|
|
35
|
-
export const rule = {
|
|
36
|
-
meta: {
|
|
37
|
-
type: 'problem',
|
|
38
|
-
docs: {
|
|
39
|
-
description: 'Disallow untranslated literal strings without translation.',
|
|
40
|
-
url: 'https://formatjs.github.io/docs/tooling/linter#no-literal-string-in-jsx',
|
|
41
|
-
},
|
|
42
|
-
schema: [
|
|
43
|
-
{
|
|
44
|
-
type: 'object',
|
|
45
|
-
properties: {
|
|
46
|
-
props: {
|
|
47
|
-
type: 'object',
|
|
48
|
-
properties: {
|
|
49
|
-
include: {
|
|
50
|
-
...propMatcherSchema,
|
|
51
|
-
},
|
|
52
|
-
exclude: {
|
|
53
|
-
...propMatcherSchema,
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
messages: {
|
|
61
|
-
noLiteralStringInJsx: 'Cannot have untranslated text in JSX',
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
defaultOptions: [],
|
|
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
|
-
// It also ignores the empty string.
|
|
105
|
-
if ((node.type === 'Literal' &&
|
|
106
|
-
typeof node.value === 'string' &&
|
|
107
|
-
node.value.length > 0) ||
|
|
108
|
-
(node.type === 'TemplateLiteral' &&
|
|
109
|
-
(node.quasis.length > 1 || node.quasis[0].value.raw.length > 0))) {
|
|
110
|
-
context.report({
|
|
111
|
-
node: node,
|
|
112
|
-
messageId: 'noLiteralStringInJsx',
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
else if (node.type === 'BinaryExpression' && node.operator === '+') {
|
|
116
|
-
checkJSXExpression(node.left);
|
|
117
|
-
checkJSXExpression(node.right);
|
|
118
|
-
}
|
|
119
|
-
else if (node.type === 'ConditionalExpression') {
|
|
120
|
-
checkJSXExpression(node.consequent);
|
|
121
|
-
checkJSXExpression(node.alternate);
|
|
122
|
-
}
|
|
123
|
-
else if (node.type === 'LogicalExpression') {
|
|
124
|
-
checkJSXExpression(node.left);
|
|
125
|
-
checkJSXExpression(node.right);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
return {
|
|
129
|
-
JSXElement: (node) => {
|
|
130
|
-
lexicalJsxStack.push(node);
|
|
131
|
-
},
|
|
132
|
-
'JSXElement:exit': () => {
|
|
133
|
-
lexicalJsxStack.pop();
|
|
134
|
-
},
|
|
135
|
-
JSXFragment: (node) => {
|
|
136
|
-
lexicalJsxStack.push(node);
|
|
137
|
-
},
|
|
138
|
-
'JSXFragment:exit': () => {
|
|
139
|
-
lexicalJsxStack.pop();
|
|
140
|
-
},
|
|
141
|
-
JSXAttribute: (node) => {
|
|
142
|
-
if (shouldSkipCurrentJsxAttribute(node)) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (!node.value) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (node.value.type === 'Literal' &&
|
|
149
|
-
typeof node.value.value === 'string' &&
|
|
150
|
-
node.value.value.length > 0) {
|
|
151
|
-
context.report({
|
|
152
|
-
node: node,
|
|
153
|
-
messageId: 'noLiteralStringInJsx',
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
else if (node.value.type === 'JSXExpressionContainer' &&
|
|
157
|
-
node.value.expression.type !== 'JSXEmptyExpression') {
|
|
158
|
-
checkJSXExpression(node.value.expression);
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
JSXText: (node) => {
|
|
162
|
-
// Ignore purely spacing fragments
|
|
163
|
-
if (!node.value.replace(/\s*/gm, '')) {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
context.report({
|
|
167
|
-
node: node,
|
|
168
|
-
messageId: 'noLiteralStringInJsx',
|
|
169
|
-
});
|
|
170
|
-
},
|
|
171
|
-
// Children expression container
|
|
172
|
-
'JSXElement > JSXExpressionContainer': (node) => {
|
|
173
|
-
if (node.expression.type !== 'JSXEmptyExpression') {
|
|
174
|
-
checkJSXExpression(node.expression);
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
},
|
|
179
|
-
};
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { RuleModule } from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
-
type MessageIds = 'untranslatedProperty';
|
|
3
|
-
type PropertyConfig = {
|
|
4
|
-
include: string[];
|
|
5
|
-
};
|
|
6
|
-
type Options = [PropertyConfig?];
|
|
7
|
-
export declare const name = "no-literal-string-in-object";
|
|
8
|
-
export declare const rule: RuleModule<MessageIds, Options>;
|
|
9
|
-
export {};
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import { getParserServices } from '../context-compat';
|
|
3
|
-
export const name = 'no-literal-string-in-object';
|
|
4
|
-
export const rule = {
|
|
5
|
-
meta: {
|
|
6
|
-
type: 'problem',
|
|
7
|
-
docs: {
|
|
8
|
-
description: 'Enforce translation of specific object properties',
|
|
9
|
-
url: 'https://formatjs.github.io/docs/tooling/linter#no-literal-string-in-object',
|
|
10
|
-
},
|
|
11
|
-
schema: [
|
|
12
|
-
{
|
|
13
|
-
type: 'object',
|
|
14
|
-
properties: {
|
|
15
|
-
include: {
|
|
16
|
-
type: 'array',
|
|
17
|
-
items: { type: 'string' },
|
|
18
|
-
default: ['label'],
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
additionalProperties: false,
|
|
22
|
-
},
|
|
23
|
-
],
|
|
24
|
-
messages: {
|
|
25
|
-
untranslatedProperty: 'Object property: `{{propertyKey}}` might contain an untranslated literal string',
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
defaultOptions: [],
|
|
29
|
-
create(context) {
|
|
30
|
-
const propertyVisitor = (node) => {
|
|
31
|
-
checkProperty(context, node);
|
|
32
|
-
};
|
|
33
|
-
const parserServices = getParserServices(context);
|
|
34
|
-
//@ts-expect-error defineTemplateBodyVisitor exists in Vue parser
|
|
35
|
-
if (parserServices?.defineTemplateBodyVisitor) {
|
|
36
|
-
//@ts-expect-error
|
|
37
|
-
return parserServices.defineTemplateBodyVisitor({
|
|
38
|
-
Property: propertyVisitor,
|
|
39
|
-
}, {
|
|
40
|
-
Property: propertyVisitor,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
Property: propertyVisitor,
|
|
45
|
-
};
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
function checkProperty(context, node) {
|
|
49
|
-
const config = {
|
|
50
|
-
include: ['label'],
|
|
51
|
-
...(context.options[0] || {}),
|
|
52
|
-
};
|
|
53
|
-
const propertyKey = node.key.type === TSESTree.AST_NODE_TYPES.Identifier
|
|
54
|
-
? node.key.name
|
|
55
|
-
: node.key.type === TSESTree.AST_NODE_TYPES.Literal &&
|
|
56
|
-
typeof node.key.value === 'string'
|
|
57
|
-
? node.key.value
|
|
58
|
-
: null;
|
|
59
|
-
if (!propertyKey || !config.include.includes(propertyKey)) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
checkPropertyValue(context, node.value, propertyKey);
|
|
63
|
-
}
|
|
64
|
-
function checkPropertyValue(context, node, propertyKey) {
|
|
65
|
-
if ((node.type === 'Literal' &&
|
|
66
|
-
typeof node.value === 'string' &&
|
|
67
|
-
node.value.length > 0) ||
|
|
68
|
-
(node.type === 'TemplateLiteral' &&
|
|
69
|
-
(node.quasis.length > 1 || node.quasis[0].value.raw.length > 0))) {
|
|
70
|
-
context.report({
|
|
71
|
-
node: node,
|
|
72
|
-
messageId: 'untranslatedProperty',
|
|
73
|
-
data: {
|
|
74
|
-
propertyKey: propertyKey,
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
else if (node.type === 'BinaryExpression' && node.operator === '+') {
|
|
79
|
-
checkPropertyValue(context, node.left, propertyKey);
|
|
80
|
-
checkPropertyValue(context, node.right, propertyKey);
|
|
81
|
-
}
|
|
82
|
-
else if (node.type === 'ConditionalExpression') {
|
|
83
|
-
checkPropertyValue(context, node.consequent, propertyKey);
|
|
84
|
-
checkPropertyValue(context, node.alternate, propertyKey);
|
|
85
|
-
}
|
|
86
|
-
else if (node.type === 'LogicalExpression') {
|
|
87
|
-
checkPropertyValue(context, node.left, propertyKey);
|
|
88
|
-
checkPropertyValue(context, node.right, propertyKey);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { RuleModule } from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
-
export declare const name = "no-missing-icu-plural-one-placeholders";
|
|
3
|
-
export type MessageIds = 'noMissingIcuPluralOnePlaceholders';
|
|
4
|
-
type Options = [];
|
|
5
|
-
export declare const rule: RuleModule<MessageIds, Options>;
|
|
6
|
-
export {};
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { isLiteralElement, isPluralElement, isSelectElement, isTagElement, parse, } from '@formatjs/icu-messageformat-parser';
|
|
2
|
-
import MagicString from 'magic-string';
|
|
3
|
-
import { getParserServices } from '../context-compat';
|
|
4
|
-
import { extractMessages, patchMessage } from '../util';
|
|
5
|
-
export const name = 'no-missing-icu-plural-one-placeholders';
|
|
6
|
-
function verifyAst(context, messageNode, ast) {
|
|
7
|
-
const patches = [];
|
|
8
|
-
_verifyAstAndReplace(ast);
|
|
9
|
-
if (patches.length > 0) {
|
|
10
|
-
const patchedMessage = patchMessage(messageNode, ast, content => {
|
|
11
|
-
return patches
|
|
12
|
-
.reduce((magicString, patch) => {
|
|
13
|
-
switch (patch.type) {
|
|
14
|
-
case 'prependLeft':
|
|
15
|
-
return magicString.prependLeft(patch.index, patch.content);
|
|
16
|
-
case 'remove':
|
|
17
|
-
return magicString.remove(patch.start, patch.end);
|
|
18
|
-
case 'update':
|
|
19
|
-
return magicString.update(patch.start, patch.end, patch.content);
|
|
20
|
-
}
|
|
21
|
-
}, new MagicString(content))
|
|
22
|
-
.toString();
|
|
23
|
-
});
|
|
24
|
-
context.report({
|
|
25
|
-
node: messageNode,
|
|
26
|
-
messageId: 'noMissingIcuPluralOnePlaceholders',
|
|
27
|
-
fix: patchedMessage !== null
|
|
28
|
-
? fixer => fixer.replaceText(messageNode, patchedMessage)
|
|
29
|
-
: null,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
function _verifyAstAndReplace(ast, root = true) {
|
|
33
|
-
for (const el of ast) {
|
|
34
|
-
if (isPluralElement(el) && el.options['one']) {
|
|
35
|
-
_verifyAstAndReplace(el.options['one'].value, false);
|
|
36
|
-
}
|
|
37
|
-
else if (isSelectElement(el)) {
|
|
38
|
-
for (const { value } of Object.values(el.options)) {
|
|
39
|
-
_verifyAstAndReplace(value, root);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
else if (isTagElement(el)) {
|
|
43
|
-
_verifyAstAndReplace(el.children, root);
|
|
44
|
-
}
|
|
45
|
-
else if (!root && isLiteralElement(el)) {
|
|
46
|
-
const match = el.value.match(/\b1\b/);
|
|
47
|
-
if (match && el.location) {
|
|
48
|
-
patches.push({
|
|
49
|
-
type: 'update',
|
|
50
|
-
start: el.location.start.offset,
|
|
51
|
-
end: el.location.end.offset,
|
|
52
|
-
content: el.value.replace(match[0], '#'),
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function checkNode(context, node) {
|
|
60
|
-
const msgs = extractMessages(node);
|
|
61
|
-
for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
|
|
62
|
-
if (!defaultMessage || !messageNode) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
verifyAst(context, messageNode, parse(defaultMessage, { captureLocation: true }));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
export const rule = {
|
|
69
|
-
meta: {
|
|
70
|
-
type: 'problem',
|
|
71
|
-
docs: {
|
|
72
|
-
description: 'We use `one {# item}` instead of `one {1 item}` in ICU messages as some locales use the `one` formatting for other similar numbers.',
|
|
73
|
-
url: 'https://formatjs.github.io/docs/tooling/linter#no-explicit-icu-plural',
|
|
74
|
-
},
|
|
75
|
-
fixable: 'code',
|
|
76
|
-
messages: {
|
|
77
|
-
noMissingIcuPluralOnePlaceholders: 'Use `one {# item}` instead of `one {1 item}` in ICU messages.',
|
|
78
|
-
},
|
|
79
|
-
schema: [],
|
|
80
|
-
},
|
|
81
|
-
defaultOptions: [],
|
|
82
|
-
create(context) {
|
|
83
|
-
const callExpressionVisitor = (node) => checkNode(context, node);
|
|
84
|
-
const parserServices = getParserServices(context);
|
|
85
|
-
//@ts-expect-error defineTemplateBodyVisitor exists in Vue parser
|
|
86
|
-
if (parserServices?.defineTemplateBodyVisitor) {
|
|
87
|
-
//@ts-expect-error
|
|
88
|
-
return parserServices.defineTemplateBodyVisitor({
|
|
89
|
-
CallExpression: callExpressionVisitor,
|
|
90
|
-
}, {
|
|
91
|
-
CallExpression: callExpressionVisitor,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return {
|
|
95
|
-
JSXOpeningElement: (node) => checkNode(context, node),
|
|
96
|
-
CallExpression: callExpressionVisitor,
|
|
97
|
-
};
|
|
98
|
-
},
|
|
99
|
-
};
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { isPluralElement, parse, } from '@formatjs/icu-messageformat-parser';
|
|
2
|
-
import { getParserServices } from '../context-compat';
|
|
3
|
-
import { extractMessages, getSettings } from '../util';
|
|
4
|
-
function verifyAst(ast, pluralCount = { count: 0 }) {
|
|
5
|
-
const errors = [];
|
|
6
|
-
for (const el of ast) {
|
|
7
|
-
if (isPluralElement(el)) {
|
|
8
|
-
pluralCount.count++;
|
|
9
|
-
if (pluralCount.count > 1) {
|
|
10
|
-
errors.push({ messageId: 'noMultiplePlurals', data: {} });
|
|
11
|
-
}
|
|
12
|
-
const { options } = el;
|
|
13
|
-
for (const selector of Object.keys(options)) {
|
|
14
|
-
errors.push(...verifyAst(options[selector].value, pluralCount));
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return errors;
|
|
19
|
-
}
|
|
20
|
-
function checkNode(context, node) {
|
|
21
|
-
const settings = getSettings(context);
|
|
22
|
-
const msgs = extractMessages(node, settings);
|
|
23
|
-
for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
|
|
24
|
-
if (!defaultMessage || !messageNode) {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
const errors = verifyAst(parse(defaultMessage, {
|
|
28
|
-
ignoreTag: settings.ignoreTag,
|
|
29
|
-
}));
|
|
30
|
-
for (const error of errors) {
|
|
31
|
-
context.report({
|
|
32
|
-
node,
|
|
33
|
-
...error,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
export const name = 'no-multiple-plurals';
|
|
39
|
-
export const rule = {
|
|
40
|
-
meta: {
|
|
41
|
-
type: 'problem',
|
|
42
|
-
docs: {
|
|
43
|
-
description: 'Disallow multiple plural rules in the same message',
|
|
44
|
-
url: 'https://formatjs.github.io/docs/tooling/linter#no-multiple-plurals',
|
|
45
|
-
},
|
|
46
|
-
fixable: 'code',
|
|
47
|
-
schema: [],
|
|
48
|
-
messages: {
|
|
49
|
-
noMultiplePlurals: 'Multiple plural rules in the same message',
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
defaultOptions: [],
|
|
53
|
-
create(context) {
|
|
54
|
-
const callExpressionVisitor = (node) => checkNode(context, node);
|
|
55
|
-
const parserServices = getParserServices(context);
|
|
56
|
-
//@ts-expect-error defineTemplateBodyVisitor exists in Vue parser
|
|
57
|
-
if (parserServices?.defineTemplateBodyVisitor) {
|
|
58
|
-
//@ts-expect-error
|
|
59
|
-
return 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
|
-
};
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { parse, TYPE, } from '@formatjs/icu-messageformat-parser';
|
|
2
|
-
import { getParserServices } from '../context-compat';
|
|
3
|
-
import { extractMessages, getSettings, patchMessage } from '../util';
|
|
4
|
-
function isAstValid(ast) {
|
|
5
|
-
for (const element of ast) {
|
|
6
|
-
switch (element.type) {
|
|
7
|
-
case TYPE.literal:
|
|
8
|
-
if (/\s{2,}/gm.test(element.value)) {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
break;
|
|
12
|
-
case TYPE.argument:
|
|
13
|
-
case TYPE.date:
|
|
14
|
-
case TYPE.literal:
|
|
15
|
-
case TYPE.number:
|
|
16
|
-
case TYPE.pound:
|
|
17
|
-
case TYPE.time:
|
|
18
|
-
break;
|
|
19
|
-
case TYPE.plural:
|
|
20
|
-
case TYPE.select: {
|
|
21
|
-
for (const option of Object.values(element.options)) {
|
|
22
|
-
if (!isAstValid(option.value)) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
case TYPE.tag:
|
|
29
|
-
return isAstValid(element.children);
|
|
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 TYPE.literal:
|
|
40
|
-
literalElements.push(element);
|
|
41
|
-
break;
|
|
42
|
-
case TYPE.argument:
|
|
43
|
-
case TYPE.date:
|
|
44
|
-
case TYPE.literal:
|
|
45
|
-
case TYPE.number:
|
|
46
|
-
case TYPE.pound:
|
|
47
|
-
case TYPE.time:
|
|
48
|
-
break;
|
|
49
|
-
case TYPE.plural:
|
|
50
|
-
case TYPE.select: {
|
|
51
|
-
for (const option of Object.values(element.options)) {
|
|
52
|
-
collectLiteralElements(option.value);
|
|
53
|
-
}
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
case TYPE.tag:
|
|
57
|
-
collectLiteralElements(element.children);
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
collectLiteralElements(ast);
|
|
63
|
-
// Surgically trim whitespaces in the literal element ranges.
|
|
64
|
-
// This is to preserve the original whitespaces and newlines info that are lost to parsing.
|
|
65
|
-
let trimmedFragments = [];
|
|
66
|
-
let currentOffset = 0;
|
|
67
|
-
for (const literal of literalElements) {
|
|
68
|
-
const { start, end } = literal.location;
|
|
69
|
-
const startOffset = start.offset;
|
|
70
|
-
const endOffset = end.offset;
|
|
71
|
-
trimmedFragments.push(message.slice(currentOffset, startOffset));
|
|
72
|
-
trimmedFragments.push(message.slice(startOffset, endOffset).replace(/\s{2,}/gm, ' '));
|
|
73
|
-
currentOffset = endOffset;
|
|
74
|
-
}
|
|
75
|
-
trimmedFragments.push(message.slice(currentOffset));
|
|
76
|
-
return trimmedFragments.join('');
|
|
77
|
-
}
|
|
78
|
-
function checkNode(context, node) {
|
|
79
|
-
const msgs = extractMessages(node, getSettings(context));
|
|
80
|
-
for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
|
|
81
|
-
if (!defaultMessage || !messageNode) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
let ast;
|
|
85
|
-
try {
|
|
86
|
-
ast = parse(defaultMessage, { captureLocation: true });
|
|
87
|
-
}
|
|
88
|
-
catch (e) {
|
|
89
|
-
context.report({
|
|
90
|
-
node: messageNode,
|
|
91
|
-
messageId: 'parserError',
|
|
92
|
-
data: { message: e instanceof Error ? e.message : String(e) },
|
|
93
|
-
});
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (!isAstValid(ast)) {
|
|
97
|
-
const newMessage = patchMessage(messageNode, ast, trimMultiWhitespaces);
|
|
98
|
-
context.report({
|
|
99
|
-
node: messageNode,
|
|
100
|
-
messageId: 'noMultipleWhitespaces',
|
|
101
|
-
fix: newMessage !== null
|
|
102
|
-
? fixer => fixer.replaceText(messageNode, newMessage)
|
|
103
|
-
: null,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
export const name = 'no-multiple-whitespaces';
|
|
109
|
-
export const rule = {
|
|
110
|
-
meta: {
|
|
111
|
-
type: 'problem',
|
|
112
|
-
docs: {
|
|
113
|
-
description: 'Prevents usage of multiple consecutive whitespaces in message',
|
|
114
|
-
url: 'https://formatjs.github.io/docs/tooling/linter#no-multiple-whitespaces',
|
|
115
|
-
},
|
|
116
|
-
messages: {
|
|
117
|
-
noMultipleWhitespaces: 'Multiple consecutive whitespaces are not allowed',
|
|
118
|
-
parserError: '{{message}}',
|
|
119
|
-
},
|
|
120
|
-
fixable: 'code',
|
|
121
|
-
schema: [],
|
|
122
|
-
},
|
|
123
|
-
defaultOptions: [],
|
|
124
|
-
create(context) {
|
|
125
|
-
const callExpressionVisitor = (node) => checkNode(context, node);
|
|
126
|
-
const parserServices = getParserServices(context);
|
|
127
|
-
//@ts-expect-error defineTemplateBodyVisitor exists in Vue parser
|
|
128
|
-
if (parserServices?.defineTemplateBodyVisitor) {
|
|
129
|
-
//@ts-expect-error
|
|
130
|
-
return parserServices.defineTemplateBodyVisitor({
|
|
131
|
-
CallExpression: callExpressionVisitor,
|
|
132
|
-
}, {
|
|
133
|
-
CallExpression: callExpressionVisitor,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
JSXOpeningElement: (node) => checkNode(context, node),
|
|
138
|
-
CallExpression: callExpressionVisitor,
|
|
139
|
-
};
|
|
140
|
-
},
|
|
141
|
-
};
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { isPluralElement, parse, } from '@formatjs/icu-messageformat-parser';
|
|
2
|
-
import { getParserServices } from '../context-compat';
|
|
3
|
-
import { extractMessages, getSettings } from '../util';
|
|
4
|
-
function verifyAst(ast) {
|
|
5
|
-
const errors = [];
|
|
6
|
-
for (const el of ast) {
|
|
7
|
-
if (isPluralElement(el)) {
|
|
8
|
-
if (el.offset) {
|
|
9
|
-
errors.push({ messageId: 'noOffset', data: {} });
|
|
10
|
-
}
|
|
11
|
-
const { options } = el;
|
|
12
|
-
for (const selector of Object.keys(options)) {
|
|
13
|
-
errors.push(...verifyAst(options[selector].value));
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return errors;
|
|
18
|
-
}
|
|
19
|
-
function checkNode(context, node) {
|
|
20
|
-
const settings = getSettings(context);
|
|
21
|
-
const msgs = extractMessages(node, settings);
|
|
22
|
-
for (const [{ message: { defaultMessage }, messageNode, },] of msgs) {
|
|
23
|
-
if (!defaultMessage || !messageNode) {
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
const errors = verifyAst(parse(defaultMessage, {
|
|
27
|
-
ignoreTag: settings.ignoreTag,
|
|
28
|
-
}));
|
|
29
|
-
for (const error of errors) {
|
|
30
|
-
context.report({
|
|
31
|
-
node: messageNode,
|
|
32
|
-
...error,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
export const name = 'no-offset';
|
|
38
|
-
export const rule = {
|
|
39
|
-
meta: {
|
|
40
|
-
type: 'problem',
|
|
41
|
-
docs: {
|
|
42
|
-
description: 'Disallow offset in plural rules',
|
|
43
|
-
url: 'https://formatjs.github.io/docs/tooling/linter#no-offset',
|
|
44
|
-
},
|
|
45
|
-
fixable: 'code',
|
|
46
|
-
messages: {
|
|
47
|
-
noOffset: 'offset is not allowed',
|
|
48
|
-
},
|
|
49
|
-
schema: [],
|
|
50
|
-
},
|
|
51
|
-
defaultOptions: [],
|
|
52
|
-
create(context) {
|
|
53
|
-
const callExpressionVisitor = (node) => checkNode(context, node);
|
|
54
|
-
const parserServices = getParserServices(context);
|
|
55
|
-
//@ts-expect-error defineTemplateBodyVisitor exists in Vue parser
|
|
56
|
-
if (parserServices?.defineTemplateBodyVisitor) {
|
|
57
|
-
//@ts-expect-error
|
|
58
|
-
return 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
|
-
};
|