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
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import picomatch from 'picomatch'
|
|
2
|
-
import type {Rule} from 'eslint'
|
|
3
|
-
import {TSESTree} from '@typescript-eslint/typescript-estree'
|
|
4
|
-
|
|
5
|
-
type PropMatcher = readonly [TagNamePattern: string, PropNamePattern: string][]
|
|
6
|
-
|
|
7
|
-
type CompiledPropMatcher = readonly [
|
|
8
|
-
TagNamePattern: RegExp,
|
|
9
|
-
PropNamePattern: RegExp
|
|
10
|
-
][]
|
|
11
|
-
|
|
12
|
-
type Config = {
|
|
13
|
-
props?: {
|
|
14
|
-
include?: PropMatcher
|
|
15
|
-
exclude?: PropMatcher
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const propMatcherSchema = {
|
|
20
|
-
type: 'array',
|
|
21
|
-
items: {
|
|
22
|
-
type: 'array',
|
|
23
|
-
items: [{type: 'string'}, {type: 'string'}],
|
|
24
|
-
},
|
|
25
|
-
} as const
|
|
26
|
-
|
|
27
|
-
const defaultPropIncludePattern: PropMatcher = [
|
|
28
|
-
['*', 'aria-{label,description,details,errormessage}'],
|
|
29
|
-
['[a-z]*([a-z0-9])', '(placeholder|title)'],
|
|
30
|
-
['img', 'alt'],
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
const defaultPropExcludePattern: PropMatcher = []
|
|
34
|
-
|
|
35
|
-
function stringifyJsxTagName(tagName: TSESTree.JSXTagNameExpression): string {
|
|
36
|
-
switch (tagName.type) {
|
|
37
|
-
case TSESTree.AST_NODE_TYPES.JSXIdentifier:
|
|
38
|
-
return tagName.name
|
|
39
|
-
case TSESTree.AST_NODE_TYPES.JSXMemberExpression:
|
|
40
|
-
return `${stringifyJsxTagName(tagName.object)}.${tagName.property.name}`
|
|
41
|
-
case TSESTree.AST_NODE_TYPES.JSXNamespacedName:
|
|
42
|
-
return `${tagName.namespace.name}:${tagName.name.name}`
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function compilePropMatcher(propMatcher: PropMatcher): CompiledPropMatcher {
|
|
47
|
-
return propMatcher.map(([tagNamePattern, propNamePattern]) => {
|
|
48
|
-
return [
|
|
49
|
-
picomatch.makeRe(tagNamePattern, {contains: false}),
|
|
50
|
-
picomatch.makeRe(propNamePattern, {contains: false}),
|
|
51
|
-
]
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const rule: Rule.RuleModule = {
|
|
56
|
-
meta: {
|
|
57
|
-
type: 'problem',
|
|
58
|
-
docs: {
|
|
59
|
-
description: 'Disallow untranslated literal strings without translation.',
|
|
60
|
-
category: 'Errors',
|
|
61
|
-
recommended: false,
|
|
62
|
-
url: 'https://formatjs.io/docs/tooling/linter#no-literal-string-in-jsx',
|
|
63
|
-
},
|
|
64
|
-
schema: [
|
|
65
|
-
{
|
|
66
|
-
type: 'object',
|
|
67
|
-
properties: {
|
|
68
|
-
props: {
|
|
69
|
-
type: 'object',
|
|
70
|
-
properties: {
|
|
71
|
-
include: {
|
|
72
|
-
...propMatcherSchema,
|
|
73
|
-
},
|
|
74
|
-
exclude: {
|
|
75
|
-
...propMatcherSchema,
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
// TODO: Vue support
|
|
84
|
-
create(context) {
|
|
85
|
-
const userConfig: Config = context.options[0] || {}
|
|
86
|
-
|
|
87
|
-
const propIncludePattern: CompiledPropMatcher = compilePropMatcher([
|
|
88
|
-
...defaultPropIncludePattern,
|
|
89
|
-
...(userConfig.props?.include ?? []),
|
|
90
|
-
])
|
|
91
|
-
|
|
92
|
-
const propExcludePattern: CompiledPropMatcher = compilePropMatcher([
|
|
93
|
-
...defaultPropExcludePattern,
|
|
94
|
-
...(userConfig.props?.exclude ?? []),
|
|
95
|
-
])
|
|
96
|
-
|
|
97
|
-
const lexicalJsxStack: (TSESTree.JSXElement | TSESTree.JSXFragment)[] = []
|
|
98
|
-
|
|
99
|
-
const shouldSkipCurrentJsxAttribute = (node: TSESTree.JSXAttribute) => {
|
|
100
|
-
const currentJsxNode = lexicalJsxStack[lexicalJsxStack.length - 1]!
|
|
101
|
-
if (currentJsxNode.type === 'JSXFragment') {
|
|
102
|
-
return false
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const nameString = stringifyJsxTagName(currentJsxNode.openingElement.name)
|
|
106
|
-
const attributeName =
|
|
107
|
-
typeof node.name.name === 'string'
|
|
108
|
-
? node.name.name
|
|
109
|
-
: node.name.name.name
|
|
110
|
-
|
|
111
|
-
// match exclude
|
|
112
|
-
for (const [tagNamePattern, propNamePattern] of propExcludePattern) {
|
|
113
|
-
if (
|
|
114
|
-
tagNamePattern.test(nameString) &&
|
|
115
|
-
propNamePattern.test(attributeName)
|
|
116
|
-
) {
|
|
117
|
-
return true
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// match include
|
|
122
|
-
for (const [tagNamePattern, propNamePattern] of propIncludePattern) {
|
|
123
|
-
if (
|
|
124
|
-
tagNamePattern.test(nameString) &&
|
|
125
|
-
propNamePattern.test(attributeName)
|
|
126
|
-
) {
|
|
127
|
-
return false
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return true
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const checkJSXExpression = (
|
|
135
|
-
node: TSESTree.Expression | TSESTree.PrivateIdentifier
|
|
136
|
-
) => {
|
|
137
|
-
// Check if this is either a string literal / template literal, or the concat of them.
|
|
138
|
-
if (
|
|
139
|
-
(node.type === 'Literal' && typeof node.value === 'string') ||
|
|
140
|
-
node.type === 'TemplateLiteral'
|
|
141
|
-
) {
|
|
142
|
-
context.report({
|
|
143
|
-
node: node as any,
|
|
144
|
-
message: 'Cannot have untranslated text in JSX',
|
|
145
|
-
})
|
|
146
|
-
} else if (node.type === 'BinaryExpression' && node.operator === '+') {
|
|
147
|
-
checkJSXExpression(node.left)
|
|
148
|
-
checkJSXExpression(node.right)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
JSXElement: (node: TSESTree.Node) => {
|
|
154
|
-
lexicalJsxStack.push(node as TSESTree.JSXElement)
|
|
155
|
-
},
|
|
156
|
-
'JSXElement:exit': () => {
|
|
157
|
-
lexicalJsxStack.pop()
|
|
158
|
-
},
|
|
159
|
-
|
|
160
|
-
JSXFragment: (node: TSESTree.Node) => {
|
|
161
|
-
lexicalJsxStack.push(node as TSESTree.JSXFragment)
|
|
162
|
-
},
|
|
163
|
-
'JSXFragment:exit': () => {
|
|
164
|
-
lexicalJsxStack.pop()
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
JSXAttribute: (node: TSESTree.JSXAttribute) => {
|
|
168
|
-
if (shouldSkipCurrentJsxAttribute(node)) {
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!node.value) {
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (node.value.type === 'Literal') {
|
|
177
|
-
context.report({
|
|
178
|
-
node: node as any,
|
|
179
|
-
message: 'Cannot have untranslated text in JSX',
|
|
180
|
-
})
|
|
181
|
-
} else if (
|
|
182
|
-
node.value.type === 'JSXExpressionContainer' &&
|
|
183
|
-
node.value.expression.type !== 'JSXEmptyExpression'
|
|
184
|
-
) {
|
|
185
|
-
checkJSXExpression(node.value.expression)
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
JSXText: (node: TSESTree.JSXText) => {
|
|
190
|
-
// Ignore purely spacing fragments
|
|
191
|
-
if (!node.value.replace(/\s*/gm, '')) {
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
context.report({
|
|
196
|
-
node: node as any,
|
|
197
|
-
message: 'Cannot have untranslated text in JSX',
|
|
198
|
-
})
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
// Children expression container
|
|
202
|
-
'JSXElement > JSXExpressionContainer': (
|
|
203
|
-
node: TSESTree.JSXExpressionContainer
|
|
204
|
-
) => {
|
|
205
|
-
if (node.expression.type !== 'JSXEmptyExpression') {
|
|
206
|
-
checkJSXExpression(node.expression)
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
} as any
|
|
210
|
-
},
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export default rule
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import {Rule} from 'eslint'
|
|
2
|
-
import {TSESTree} from '@typescript-eslint/typescript-estree'
|
|
3
|
-
import {extractMessages, getSettings} from '../util'
|
|
4
|
-
import {
|
|
5
|
-
parse,
|
|
6
|
-
isPluralElement,
|
|
7
|
-
MessageFormatElement,
|
|
8
|
-
} from '@formatjs/icu-messageformat-parser'
|
|
9
|
-
|
|
10
|
-
class MultiplePlurals extends Error {
|
|
11
|
-
public message = 'Cannot specify more than 1 plural rules'
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function verifyAst(ast: MessageFormatElement[], pluralCount = {count: 0}) {
|
|
15
|
-
for (const el of ast) {
|
|
16
|
-
if (isPluralElement(el)) {
|
|
17
|
-
pluralCount.count++
|
|
18
|
-
if (pluralCount.count > 1) {
|
|
19
|
-
throw new MultiplePlurals()
|
|
20
|
-
}
|
|
21
|
-
const {options} = el
|
|
22
|
-
for (const selector of Object.keys(options)) {
|
|
23
|
-
verifyAst(options[selector].value, pluralCount)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function checkNode(context: Rule.RuleContext, node: TSESTree.Node) {
|
|
30
|
-
const settings = getSettings(context)
|
|
31
|
-
const msgs = extractMessages(node, settings)
|
|
32
|
-
|
|
33
|
-
for (const [
|
|
34
|
-
{
|
|
35
|
-
message: {defaultMessage},
|
|
36
|
-
messageNode,
|
|
37
|
-
},
|
|
38
|
-
] of msgs) {
|
|
39
|
-
if (!defaultMessage || !messageNode) {
|
|
40
|
-
continue
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
verifyAst(
|
|
44
|
-
parse(defaultMessage, {
|
|
45
|
-
ignoreTag: settings.ignoreTag,
|
|
46
|
-
})
|
|
47
|
-
)
|
|
48
|
-
} catch (e) {
|
|
49
|
-
context.report({
|
|
50
|
-
node: messageNode as any,
|
|
51
|
-
message: e instanceof Error ? e.message : String(e),
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const rule: Rule.RuleModule = {
|
|
58
|
-
meta: {
|
|
59
|
-
type: 'problem',
|
|
60
|
-
docs: {
|
|
61
|
-
description: 'Disallow multiple plural rules in the same message',
|
|
62
|
-
category: 'Errors',
|
|
63
|
-
recommended: false,
|
|
64
|
-
url: 'https://formatjs.io/docs/tooling/linter#no-multiple-plurals',
|
|
65
|
-
},
|
|
66
|
-
fixable: 'code',
|
|
67
|
-
},
|
|
68
|
-
create(context) {
|
|
69
|
-
const callExpressionVisitor = (node: TSESTree.Node) =>
|
|
70
|
-
checkNode(context, node)
|
|
71
|
-
|
|
72
|
-
if (context.parserServices.defineTemplateBodyVisitor) {
|
|
73
|
-
return context.parserServices.defineTemplateBodyVisitor(
|
|
74
|
-
{
|
|
75
|
-
CallExpression: callExpressionVisitor,
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
CallExpression: callExpressionVisitor,
|
|
79
|
-
}
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
JSXOpeningElement: (node: TSESTree.Node) => checkNode(context, node),
|
|
84
|
-
CallExpression: callExpressionVisitor,
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export default rule
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
LiteralElement,
|
|
3
|
-
MessageFormatElement,
|
|
4
|
-
parse,
|
|
5
|
-
TYPE,
|
|
6
|
-
} from '@formatjs/icu-messageformat-parser'
|
|
7
|
-
import {TSESTree} from '@typescript-eslint/typescript-estree'
|
|
8
|
-
import {Rule} from 'eslint'
|
|
9
|
-
import {extractMessages, getSettings} from '../util'
|
|
10
|
-
|
|
11
|
-
function isAstValid(ast: MessageFormatElement[]): boolean {
|
|
12
|
-
for (const element of ast) {
|
|
13
|
-
switch (element.type) {
|
|
14
|
-
case TYPE.literal:
|
|
15
|
-
if (/\s{2,}/gm.test(element.value)) {
|
|
16
|
-
return false
|
|
17
|
-
}
|
|
18
|
-
break
|
|
19
|
-
case TYPE.argument:
|
|
20
|
-
case TYPE.date:
|
|
21
|
-
case TYPE.literal:
|
|
22
|
-
case TYPE.number:
|
|
23
|
-
case TYPE.pound:
|
|
24
|
-
case TYPE.tag:
|
|
25
|
-
case TYPE.time:
|
|
26
|
-
break
|
|
27
|
-
case TYPE.plural:
|
|
28
|
-
case TYPE.select: {
|
|
29
|
-
for (const option of Object.values(element.options)) {
|
|
30
|
-
if (!isAstValid(option.value)) {
|
|
31
|
-
return false
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
break
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return true
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function trimMultiWhitespaces(
|
|
42
|
-
message: string,
|
|
43
|
-
ast: MessageFormatElement[]
|
|
44
|
-
): string {
|
|
45
|
-
const literalElements: LiteralElement[] = []
|
|
46
|
-
|
|
47
|
-
const collectLiteralElements = (elements: MessageFormatElement[]) => {
|
|
48
|
-
for (const element of elements) {
|
|
49
|
-
switch (element.type) {
|
|
50
|
-
case TYPE.literal:
|
|
51
|
-
literalElements.push(element)
|
|
52
|
-
break
|
|
53
|
-
case TYPE.argument:
|
|
54
|
-
case TYPE.date:
|
|
55
|
-
case TYPE.literal:
|
|
56
|
-
case TYPE.number:
|
|
57
|
-
case TYPE.pound:
|
|
58
|
-
case TYPE.tag:
|
|
59
|
-
case TYPE.time:
|
|
60
|
-
break
|
|
61
|
-
case TYPE.plural:
|
|
62
|
-
case TYPE.select: {
|
|
63
|
-
for (const option of Object.values(element.options)) {
|
|
64
|
-
collectLiteralElements(option.value)
|
|
65
|
-
}
|
|
66
|
-
break
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
collectLiteralElements(ast)
|
|
72
|
-
|
|
73
|
-
// Surgically trim whitespaces in the literal element ranges.
|
|
74
|
-
// This is to preserve the original whitespaces and newlines info that are lost to parsing.
|
|
75
|
-
let trimmedFragments: string[] = []
|
|
76
|
-
let currentOffset = 0
|
|
77
|
-
|
|
78
|
-
for (const literal of literalElements) {
|
|
79
|
-
const {start, end} = literal.location!
|
|
80
|
-
const startOffset = start.offset
|
|
81
|
-
const endOffset = end.offset
|
|
82
|
-
|
|
83
|
-
trimmedFragments.push(message.slice(currentOffset, startOffset))
|
|
84
|
-
trimmedFragments.push(
|
|
85
|
-
message.slice(startOffset, endOffset).replace(/\s{2,}/gm, ' ')
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
currentOffset = endOffset
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
trimmedFragments.push(message.slice(currentOffset))
|
|
92
|
-
|
|
93
|
-
return trimmedFragments.join('')
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function checkNode(context: Rule.RuleContext, node: TSESTree.Node) {
|
|
97
|
-
const msgs = extractMessages(node, getSettings(context))
|
|
98
|
-
|
|
99
|
-
for (const [
|
|
100
|
-
{
|
|
101
|
-
message: {defaultMessage},
|
|
102
|
-
messageNode,
|
|
103
|
-
},
|
|
104
|
-
] of msgs) {
|
|
105
|
-
if (!defaultMessage || !messageNode) {
|
|
106
|
-
continue
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
let ast: MessageFormatElement[]
|
|
110
|
-
try {
|
|
111
|
-
ast = parse(defaultMessage, {captureLocation: true})
|
|
112
|
-
} catch (e) {
|
|
113
|
-
context.report({
|
|
114
|
-
node: messageNode as any,
|
|
115
|
-
message: e instanceof Error ? e.message : String(e),
|
|
116
|
-
})
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!isAstValid(ast)) {
|
|
121
|
-
const reportObject: Parameters<typeof context['report']>[0] = {
|
|
122
|
-
node: messageNode as any,
|
|
123
|
-
message: 'Multiple consecutive whitespaces are not allowed',
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
messageNode.type === 'Literal' &&
|
|
128
|
-
messageNode.value &&
|
|
129
|
-
typeof messageNode.value === 'string'
|
|
130
|
-
) {
|
|
131
|
-
reportObject.fix = function (fixer) {
|
|
132
|
-
return fixer.replaceText(
|
|
133
|
-
messageNode as any,
|
|
134
|
-
JSON.stringify(
|
|
135
|
-
trimMultiWhitespaces(messageNode.value as string, ast)
|
|
136
|
-
)
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
} else if (
|
|
140
|
-
messageNode.type === 'TemplateLiteral' &&
|
|
141
|
-
messageNode.quasis.length === 1 &&
|
|
142
|
-
messageNode.expressions.length === 0
|
|
143
|
-
) {
|
|
144
|
-
reportObject.fix = function (fixer) {
|
|
145
|
-
return fixer.replaceText(
|
|
146
|
-
messageNode as any,
|
|
147
|
-
'`' +
|
|
148
|
-
trimMultiWhitespaces(messageNode.quasis[0].value.cooked, ast)
|
|
149
|
-
.replace(/\\/g, '\\\\')
|
|
150
|
-
.replace(/`/g, '\\`') +
|
|
151
|
-
'`'
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
context.report(reportObject)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const rule: Rule.RuleModule = {
|
|
162
|
-
meta: {
|
|
163
|
-
type: 'problem',
|
|
164
|
-
docs: {
|
|
165
|
-
description:
|
|
166
|
-
'Prevents usage of multiple consecutive whitespaces in message',
|
|
167
|
-
category: 'Errors',
|
|
168
|
-
recommended: false,
|
|
169
|
-
url: 'https://formatjs.io/docs/tooling/linter#no-multiple-whitespaces',
|
|
170
|
-
},
|
|
171
|
-
fixable: 'code',
|
|
172
|
-
},
|
|
173
|
-
create(context) {
|
|
174
|
-
const callExpressionVisitor = (node: TSESTree.Node) =>
|
|
175
|
-
checkNode(context, node)
|
|
176
|
-
|
|
177
|
-
if (context.parserServices.defineTemplateBodyVisitor) {
|
|
178
|
-
return context.parserServices.defineTemplateBodyVisitor(
|
|
179
|
-
{
|
|
180
|
-
CallExpression: callExpressionVisitor,
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
CallExpression: callExpressionVisitor,
|
|
184
|
-
}
|
|
185
|
-
)
|
|
186
|
-
}
|
|
187
|
-
return {
|
|
188
|
-
JSXOpeningElement: (node: TSESTree.Node) => checkNode(context, node),
|
|
189
|
-
CallExpression: callExpressionVisitor,
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export default rule
|
package/rules/no-offset.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import {Rule} from 'eslint'
|
|
2
|
-
import {TSESTree} from '@typescript-eslint/typescript-estree'
|
|
3
|
-
import {extractMessages, getSettings} from '../util'
|
|
4
|
-
import {
|
|
5
|
-
parse,
|
|
6
|
-
isPluralElement,
|
|
7
|
-
MessageFormatElement,
|
|
8
|
-
} from '@formatjs/icu-messageformat-parser'
|
|
9
|
-
|
|
10
|
-
class NoOffsetError extends Error {
|
|
11
|
-
public message = 'offset are not allowed in plural rules'
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function verifyAst(ast: MessageFormatElement[]) {
|
|
15
|
-
for (const el of ast) {
|
|
16
|
-
if (isPluralElement(el)) {
|
|
17
|
-
if (el.offset) {
|
|
18
|
-
throw new NoOffsetError()
|
|
19
|
-
}
|
|
20
|
-
const {options} = el
|
|
21
|
-
for (const selector of Object.keys(options)) {
|
|
22
|
-
verifyAst(options[selector].value)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function checkNode(context: Rule.RuleContext, node: TSESTree.Node) {
|
|
29
|
-
const settings = getSettings(context)
|
|
30
|
-
const msgs = extractMessages(node, settings)
|
|
31
|
-
|
|
32
|
-
for (const [
|
|
33
|
-
{
|
|
34
|
-
message: {defaultMessage},
|
|
35
|
-
messageNode,
|
|
36
|
-
},
|
|
37
|
-
] of msgs) {
|
|
38
|
-
if (!defaultMessage || !messageNode) {
|
|
39
|
-
continue
|
|
40
|
-
}
|
|
41
|
-
try {
|
|
42
|
-
verifyAst(
|
|
43
|
-
parse(defaultMessage, {
|
|
44
|
-
ignoreTag: settings.ignoreTag,
|
|
45
|
-
})
|
|
46
|
-
)
|
|
47
|
-
} catch (e) {
|
|
48
|
-
context.report({
|
|
49
|
-
node: messageNode as any,
|
|
50
|
-
message: (e as Error).message,
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const rule: Rule.RuleModule = {
|
|
57
|
-
meta: {
|
|
58
|
-
type: 'problem',
|
|
59
|
-
docs: {
|
|
60
|
-
description: 'Disallow offset in plural rules',
|
|
61
|
-
category: 'Errors',
|
|
62
|
-
recommended: false,
|
|
63
|
-
url: 'https://formatjs.io/docs/tooling/linter#no-offset',
|
|
64
|
-
},
|
|
65
|
-
fixable: 'code',
|
|
66
|
-
},
|
|
67
|
-
create(context) {
|
|
68
|
-
const callExpressionVisitor = (node: TSESTree.Node) =>
|
|
69
|
-
checkNode(context, node)
|
|
70
|
-
|
|
71
|
-
if (context.parserServices.defineTemplateBodyVisitor) {
|
|
72
|
-
return context.parserServices.defineTemplateBodyVisitor(
|
|
73
|
-
{
|
|
74
|
-
CallExpression: callExpressionVisitor,
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
CallExpression: callExpressionVisitor,
|
|
78
|
-
}
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
JSXOpeningElement: (node: TSESTree.Node) => checkNode(context, node),
|
|
83
|
-
CallExpression: callExpressionVisitor,
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export default rule
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import blocklistElements from '../rules/blocklist-elements'
|
|
2
|
-
import {dynamicMessage, emptyFnCall, noMatch, spreadJsx} from './fixtures'
|
|
3
|
-
import {ruleTester, vueRuleTester} from './util'
|
|
4
|
-
|
|
5
|
-
ruleTester.run('blocklist-elements', blocklistElements, {
|
|
6
|
-
valid: [
|
|
7
|
-
{
|
|
8
|
-
code: `import {defineMessage} from 'react-intl'
|
|
9
|
-
defineMessage({
|
|
10
|
-
defaultMessage: '{count, plural, one {#} other {# more}}'
|
|
11
|
-
})`,
|
|
12
|
-
options: [['selectordinal']],
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
code: `import {defineMessage} from 'react-intl'
|
|
16
|
-
defineMessage({
|
|
17
|
-
defaultMessage: '{count, plural, one {#} other {# more}} <a href="asd"></a>'
|
|
18
|
-
})`,
|
|
19
|
-
options: [['selectordinal']],
|
|
20
|
-
settings: {
|
|
21
|
-
formatjs: {
|
|
22
|
-
ignoreTag: true,
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
code: `
|
|
28
|
-
$t({
|
|
29
|
-
defaultMessage: '{count, plural, one {#} other {# more}}'
|
|
30
|
-
})`,
|
|
31
|
-
options: [['selectordinal']],
|
|
32
|
-
settings: {
|
|
33
|
-
formatjs: {
|
|
34
|
-
additionalFunctionNames: ['$t'],
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
dynamicMessage,
|
|
39
|
-
noMatch,
|
|
40
|
-
spreadJsx,
|
|
41
|
-
emptyFnCall,
|
|
42
|
-
],
|
|
43
|
-
invalid: [
|
|
44
|
-
{
|
|
45
|
-
code: `
|
|
46
|
-
import {defineMessage} from 'react-intl'
|
|
47
|
-
defineMessage({
|
|
48
|
-
defaultMessage: '{count, selectordinal, offset:1 one {#} other {# more}}'
|
|
49
|
-
})`,
|
|
50
|
-
options: [['selectordinal']],
|
|
51
|
-
errors: [
|
|
52
|
-
{
|
|
53
|
-
message: 'selectordinal element is blocklisted',
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
code: `
|
|
59
|
-
$t({
|
|
60
|
-
defaultMessage: '{count, selectordinal, offset:1 one {#} other {# more}}'
|
|
61
|
-
})`,
|
|
62
|
-
options: [['selectordinal']],
|
|
63
|
-
settings: {
|
|
64
|
-
formatjs: {
|
|
65
|
-
additionalFunctionNames: ['$t'],
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
errors: [
|
|
69
|
-
{
|
|
70
|
-
message: 'selectordinal element is blocklisted',
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
vueRuleTester.run('vue/blocklist-elements', blocklistElements, {
|
|
78
|
-
valid: [
|
|
79
|
-
{
|
|
80
|
-
code: `<template>
|
|
81
|
-
<p>{{ $formatMessage({
|
|
82
|
-
defaultMessage: '{count, plural, offset:1 one {#} other {# more} }'
|
|
83
|
-
}) }} World!</p>
|
|
84
|
-
</template>`,
|
|
85
|
-
options: [['selectordinal']],
|
|
86
|
-
},
|
|
87
|
-
`<script>${dynamicMessage}</script>`,
|
|
88
|
-
`<script>${noMatch}</script>`,
|
|
89
|
-
`<script>${emptyFnCall}</script>`,
|
|
90
|
-
],
|
|
91
|
-
invalid: [
|
|
92
|
-
{
|
|
93
|
-
code: `
|
|
94
|
-
<script>
|
|
95
|
-
intl.formatMessage({
|
|
96
|
-
defaultMessage: '{count, selectordinal, offset:1 one {#} other {# more}}'
|
|
97
|
-
})</script>`,
|
|
98
|
-
options: [['selectordinal']],
|
|
99
|
-
errors: [
|
|
100
|
-
{
|
|
101
|
-
message: 'selectordinal element is blocklisted',
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
code: `
|
|
107
|
-
<template>
|
|
108
|
-
<p>{{ $formatMessage({
|
|
109
|
-
defaultMessage: '{count, selectordinal, offset:1 one {#} other {# more} }'
|
|
110
|
-
}) }} World!</p>
|
|
111
|
-
</template>`,
|
|
112
|
-
options: [['selectordinal']],
|
|
113
|
-
errors: [
|
|
114
|
-
{
|
|
115
|
-
message: 'selectordinal element is blocklisted',
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
})
|