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
package/index.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import blocklistElements from './rules/blocklist-elements'
|
|
2
|
-
import enforceDefaultMessage from './rules/enforce-default-message'
|
|
3
|
-
import enforceDescription from './rules/enforce-description'
|
|
4
|
-
import enforceId from './rules/enforce-id'
|
|
5
|
-
import enforcePlaceholders from './rules/enforce-placeholders'
|
|
6
|
-
import noInvalidICU from './rules/no-invalid-icu'
|
|
7
|
-
import enforcePluralRules from './rules/enforce-plural-rules'
|
|
8
|
-
import noCamelCase from './rules/no-camel-case'
|
|
9
|
-
import noComplexSelectors from './rules/no-complex-selectors'
|
|
10
|
-
import noEmoji from './rules/no-emoji'
|
|
11
|
-
import noId from './rules/no-id'
|
|
12
|
-
import noMultiplePlurals from './rules/no-multiple-plurals'
|
|
13
|
-
import noMultipleWhitespaces from './rules/no-multiple-whitespaces'
|
|
14
|
-
import noOffset from './rules/no-offset'
|
|
15
|
-
import noLiteralStringInJsx from './rules/no-literal-string-in-jsx'
|
|
16
|
-
const plugin = {
|
|
17
|
-
rules: {
|
|
18
|
-
'blocklist-elements': blocklistElements,
|
|
19
|
-
'enforce-default-message': enforceDefaultMessage,
|
|
20
|
-
'enforce-description': enforceDescription,
|
|
21
|
-
'enforce-id': enforceId,
|
|
22
|
-
'enforce-placeholders': enforcePlaceholders,
|
|
23
|
-
'enforce-plural-rules': enforcePluralRules,
|
|
24
|
-
'no-camel-case': noCamelCase,
|
|
25
|
-
'no-complex-selectors': noComplexSelectors,
|
|
26
|
-
'no-emoji': noEmoji,
|
|
27
|
-
'no-id': noId,
|
|
28
|
-
'no-literal-string-in-jsx': noLiteralStringInJsx,
|
|
29
|
-
'no-multiple-plurals': noMultiplePlurals,
|
|
30
|
-
'no-multiple-whitespaces': noMultipleWhitespaces,
|
|
31
|
-
'no-invalid-icu': noInvalidICU,
|
|
32
|
-
'no-offset': noOffset,
|
|
33
|
-
},
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type Plugin = typeof plugin
|
|
37
|
-
|
|
38
|
-
module.exports = plugin
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import {Rule} from 'eslint'
|
|
2
|
-
import {extractMessages, getSettings} from '../util'
|
|
3
|
-
import {
|
|
4
|
-
parse,
|
|
5
|
-
isPluralElement,
|
|
6
|
-
MessageFormatElement,
|
|
7
|
-
isLiteralElement,
|
|
8
|
-
isArgumentElement,
|
|
9
|
-
isNumberElement,
|
|
10
|
-
isDateElement,
|
|
11
|
-
isTimeElement,
|
|
12
|
-
isSelectElement,
|
|
13
|
-
isTagElement,
|
|
14
|
-
} from '@formatjs/icu-messageformat-parser'
|
|
15
|
-
import {TSESTree} from '@typescript-eslint/typescript-estree'
|
|
16
|
-
|
|
17
|
-
class BlacklistElement extends Error {
|
|
18
|
-
public message: string
|
|
19
|
-
constructor(type: Element) {
|
|
20
|
-
super()
|
|
21
|
-
this.message = `${type} element is blocklisted`
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
enum Element {
|
|
26
|
-
literal = 'literal',
|
|
27
|
-
argument = 'argument',
|
|
28
|
-
number = 'number',
|
|
29
|
-
date = 'date',
|
|
30
|
-
time = 'time',
|
|
31
|
-
select = 'select',
|
|
32
|
-
selectordinal = 'selectordinal',
|
|
33
|
-
plural = 'plural',
|
|
34
|
-
tag = 'tag',
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function verifyAst(blocklist: Element[], ast: MessageFormatElement[]) {
|
|
38
|
-
for (const el of ast) {
|
|
39
|
-
if (isLiteralElement(el) && blocklist.includes(Element.literal)) {
|
|
40
|
-
throw new BlacklistElement(Element.literal)
|
|
41
|
-
}
|
|
42
|
-
if (isArgumentElement(el) && blocklist.includes(Element.argument)) {
|
|
43
|
-
throw new BlacklistElement(Element.argument)
|
|
44
|
-
}
|
|
45
|
-
if (isNumberElement(el) && blocklist.includes(Element.number)) {
|
|
46
|
-
throw new BlacklistElement(Element.number)
|
|
47
|
-
}
|
|
48
|
-
if (isDateElement(el) && blocklist.includes(Element.date)) {
|
|
49
|
-
throw new BlacklistElement(Element.date)
|
|
50
|
-
}
|
|
51
|
-
if (isTimeElement(el) && blocklist.includes(Element.time)) {
|
|
52
|
-
throw new BlacklistElement(Element.time)
|
|
53
|
-
}
|
|
54
|
-
if (isSelectElement(el) && blocklist.includes(Element.select)) {
|
|
55
|
-
throw new BlacklistElement(Element.select)
|
|
56
|
-
}
|
|
57
|
-
if (isTagElement(el) && blocklist.includes(Element.tag)) {
|
|
58
|
-
throw new BlacklistElement(Element.tag)
|
|
59
|
-
}
|
|
60
|
-
if (isPluralElement(el)) {
|
|
61
|
-
if (blocklist.includes(Element.plural)) {
|
|
62
|
-
throw new BlacklistElement(Element.argument)
|
|
63
|
-
}
|
|
64
|
-
if (
|
|
65
|
-
el.pluralType === 'ordinal' &&
|
|
66
|
-
blocklist.includes(Element.selectordinal)
|
|
67
|
-
) {
|
|
68
|
-
throw new BlacklistElement(Element.selectordinal)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (isSelectElement(el) || isPluralElement(el)) {
|
|
72
|
-
const {options} = el
|
|
73
|
-
for (const selector of Object.keys(options)) {
|
|
74
|
-
verifyAst(blocklist, options[selector].value)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function checkNode(context: Rule.RuleContext, node: TSESTree.Node) {
|
|
81
|
-
const settings = getSettings(context)
|
|
82
|
-
const msgs = extractMessages(node, settings)
|
|
83
|
-
if (!msgs.length) {
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const blocklist = context.options[0]
|
|
88
|
-
if (!Array.isArray(blocklist) || !blocklist.length) {
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
for (const [
|
|
92
|
-
{
|
|
93
|
-
message: {defaultMessage},
|
|
94
|
-
messageNode,
|
|
95
|
-
},
|
|
96
|
-
] of msgs) {
|
|
97
|
-
if (!defaultMessage || !messageNode) {
|
|
98
|
-
continue
|
|
99
|
-
}
|
|
100
|
-
try {
|
|
101
|
-
verifyAst(
|
|
102
|
-
context.options[0],
|
|
103
|
-
parse(defaultMessage, {
|
|
104
|
-
ignoreTag: settings.ignoreTag,
|
|
105
|
-
})
|
|
106
|
-
)
|
|
107
|
-
} catch (e) {
|
|
108
|
-
context.report({
|
|
109
|
-
node: messageNode as any,
|
|
110
|
-
message: e instanceof Error ? e.message : String(e),
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const rule: Rule.RuleModule = {
|
|
117
|
-
meta: {
|
|
118
|
-
type: 'problem',
|
|
119
|
-
docs: {
|
|
120
|
-
description: 'Disallow specific elements in ICU message format',
|
|
121
|
-
category: 'Errors',
|
|
122
|
-
recommended: false,
|
|
123
|
-
url: 'https://formatjs.io/docs/tooling/linter#blocklist-elements',
|
|
124
|
-
},
|
|
125
|
-
fixable: 'code',
|
|
126
|
-
schema: [
|
|
127
|
-
{
|
|
128
|
-
type: 'array',
|
|
129
|
-
properties: {
|
|
130
|
-
items: {
|
|
131
|
-
type: 'string',
|
|
132
|
-
enum: Object.keys(Element),
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
},
|
|
138
|
-
create(context) {
|
|
139
|
-
const callExpressionVisitor = (node: TSESTree.Node) =>
|
|
140
|
-
checkNode(context, node)
|
|
141
|
-
|
|
142
|
-
if (context.parserServices.defineTemplateBodyVisitor) {
|
|
143
|
-
return context.parserServices.defineTemplateBodyVisitor(
|
|
144
|
-
{
|
|
145
|
-
CallExpression: callExpressionVisitor,
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
CallExpression: callExpressionVisitor,
|
|
149
|
-
}
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
JSXOpeningElement: (node: TSESTree.Node) => checkNode(context, node),
|
|
154
|
-
CallExpression: callExpressionVisitor,
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export default rule
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import {Rule} from 'eslint'
|
|
2
|
-
import {extractMessages, getSettings} from '../util'
|
|
3
|
-
import {TSESTree} from '@typescript-eslint/typescript-estree'
|
|
4
|
-
|
|
5
|
-
function checkNode(context: Rule.RuleContext, node: TSESTree.Node) {
|
|
6
|
-
const msgs = extractMessages(node, getSettings(context))
|
|
7
|
-
const {
|
|
8
|
-
options: [type],
|
|
9
|
-
} = context
|
|
10
|
-
for (const [
|
|
11
|
-
{
|
|
12
|
-
message: {defaultMessage},
|
|
13
|
-
messageNode,
|
|
14
|
-
},
|
|
15
|
-
] of msgs) {
|
|
16
|
-
if (!defaultMessage) {
|
|
17
|
-
if (type === 'literal' && messageNode) {
|
|
18
|
-
context.report({
|
|
19
|
-
node: messageNode as any,
|
|
20
|
-
message: `"defaultMessage" must be:
|
|
21
|
-
- a string literal or
|
|
22
|
-
- template literal without variable`,
|
|
23
|
-
})
|
|
24
|
-
} else if (!messageNode) {
|
|
25
|
-
context.report({
|
|
26
|
-
node: node as any,
|
|
27
|
-
message: '`defaultMessage` has to be specified in message descriptor',
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const rule: Rule.RuleModule = {
|
|
35
|
-
meta: {
|
|
36
|
-
type: 'problem',
|
|
37
|
-
docs: {
|
|
38
|
-
description: 'Enforce defaultMessage in message descriptor',
|
|
39
|
-
category: 'Errors',
|
|
40
|
-
recommended: false,
|
|
41
|
-
url: 'https://formatjs.io/docs/tooling/linter#enforce-default-message',
|
|
42
|
-
},
|
|
43
|
-
fixable: 'code',
|
|
44
|
-
schema: [
|
|
45
|
-
{
|
|
46
|
-
enum: ['literal', 'anything'],
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
create(context) {
|
|
51
|
-
const callExpressionVisitor = (node: TSESTree.Node) =>
|
|
52
|
-
checkNode(context, node)
|
|
53
|
-
|
|
54
|
-
if (context.parserServices.defineTemplateBodyVisitor) {
|
|
55
|
-
return context.parserServices.defineTemplateBodyVisitor(
|
|
56
|
-
{
|
|
57
|
-
CallExpression: callExpressionVisitor,
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
CallExpression: callExpressionVisitor,
|
|
61
|
-
}
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
JSXOpeningElement: (node: TSESTree.Node) => checkNode(context, node),
|
|
66
|
-
CallExpression: callExpressionVisitor,
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export default rule
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import {Rule} from 'eslint'
|
|
2
|
-
import {extractMessages, getSettings} from '../util'
|
|
3
|
-
import {TSESTree} from '@typescript-eslint/typescript-estree'
|
|
4
|
-
|
|
5
|
-
function checkNode(context: Rule.RuleContext, node: TSESTree.Node) {
|
|
6
|
-
const msgs = extractMessages(node, getSettings(context))
|
|
7
|
-
const {
|
|
8
|
-
options: [type],
|
|
9
|
-
} = context
|
|
10
|
-
for (const [
|
|
11
|
-
{
|
|
12
|
-
message: {description},
|
|
13
|
-
descriptionNode,
|
|
14
|
-
},
|
|
15
|
-
] of msgs) {
|
|
16
|
-
if (!description) {
|
|
17
|
-
if (type === 'literal' && descriptionNode) {
|
|
18
|
-
context.report({
|
|
19
|
-
node: descriptionNode as any,
|
|
20
|
-
message:
|
|
21
|
-
'`description` has to be a string literal (not function call or variable)',
|
|
22
|
-
})
|
|
23
|
-
} else if (!descriptionNode) {
|
|
24
|
-
context.report({
|
|
25
|
-
node: node as any,
|
|
26
|
-
message: '`description` has to be specified in message descriptor',
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export default {
|
|
34
|
-
meta: {
|
|
35
|
-
type: 'problem',
|
|
36
|
-
docs: {
|
|
37
|
-
description: 'Enforce description in message descriptor',
|
|
38
|
-
category: 'Errors',
|
|
39
|
-
recommended: false,
|
|
40
|
-
url: 'https://formatjs.io/docs/tooling/linter#enforce-description',
|
|
41
|
-
},
|
|
42
|
-
fixable: 'code',
|
|
43
|
-
schema: [
|
|
44
|
-
{
|
|
45
|
-
enum: ['literal', 'anything'],
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
},
|
|
49
|
-
create(context) {
|
|
50
|
-
const callExpressionVisitor = (node: TSESTree.Node) =>
|
|
51
|
-
checkNode(context, node)
|
|
52
|
-
|
|
53
|
-
if (context.parserServices.defineTemplateBodyVisitor) {
|
|
54
|
-
return context.parserServices.defineTemplateBodyVisitor(
|
|
55
|
-
{
|
|
56
|
-
CallExpression: callExpressionVisitor,
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
CallExpression: callExpressionVisitor,
|
|
60
|
-
}
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
JSXOpeningElement: (node: TSESTree.Node) => checkNode(context, node),
|
|
65
|
-
CallExpression: callExpressionVisitor,
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
} as Rule.RuleModule
|
package/rules/enforce-id.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import {Rule} from 'eslint'
|
|
2
|
-
import {extractMessages, getSettings} from '../util'
|
|
3
|
-
import {TSESTree} from '@typescript-eslint/typescript-estree'
|
|
4
|
-
import {interpolateName} from '@formatjs/ts-transformer'
|
|
5
|
-
|
|
6
|
-
interface Opts {
|
|
7
|
-
idInterpolationPattern: string
|
|
8
|
-
idWhitelistRegexps?: RegExp[]
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function checkNode(
|
|
12
|
-
context: Rule.RuleContext,
|
|
13
|
-
node: TSESTree.Node,
|
|
14
|
-
{idInterpolationPattern, idWhitelistRegexps}: Opts
|
|
15
|
-
) {
|
|
16
|
-
const msgs = extractMessages(node, getSettings(context))
|
|
17
|
-
for (const [
|
|
18
|
-
{
|
|
19
|
-
message: {defaultMessage, description, id},
|
|
20
|
-
idPropNode,
|
|
21
|
-
descriptionNode,
|
|
22
|
-
messagePropNode,
|
|
23
|
-
},
|
|
24
|
-
] of msgs) {
|
|
25
|
-
if (!idInterpolationPattern && !idPropNode) {
|
|
26
|
-
context.report({
|
|
27
|
-
node: node as any,
|
|
28
|
-
message: `id must be specified`,
|
|
29
|
-
})
|
|
30
|
-
} else if (idInterpolationPattern) {
|
|
31
|
-
if (!defaultMessage) {
|
|
32
|
-
context.report({
|
|
33
|
-
node: node as any,
|
|
34
|
-
message: `defaultMessage must be a string literal to calculate generated IDs`,
|
|
35
|
-
})
|
|
36
|
-
} else if (!description && descriptionNode) {
|
|
37
|
-
context.report({
|
|
38
|
-
node: node as any,
|
|
39
|
-
message: `description must be a string literal to calculate generated IDs`,
|
|
40
|
-
})
|
|
41
|
-
} else {
|
|
42
|
-
if (
|
|
43
|
-
idWhitelistRegexps &&
|
|
44
|
-
id &&
|
|
45
|
-
idWhitelistRegexps.some((r: RegExp) => r.test(id))
|
|
46
|
-
) {
|
|
47
|
-
// messageId is allowlisted so skip interpolation id check
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const correctId = interpolateName(
|
|
52
|
-
{
|
|
53
|
-
resourcePath: context.getFilename(),
|
|
54
|
-
} as any,
|
|
55
|
-
idInterpolationPattern,
|
|
56
|
-
{
|
|
57
|
-
content: description
|
|
58
|
-
? `${defaultMessage}#${description}`
|
|
59
|
-
: defaultMessage,
|
|
60
|
-
}
|
|
61
|
-
)
|
|
62
|
-
if (id !== correctId) {
|
|
63
|
-
let message = `"id" does not match with hash pattern ${idInterpolationPattern}`
|
|
64
|
-
if (idWhitelistRegexps) {
|
|
65
|
-
message += ` or allowlisted patterns ["${idWhitelistRegexps
|
|
66
|
-
.map(r => r.toString())
|
|
67
|
-
.join('", "')}"]`
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
context.report({
|
|
71
|
-
node: node as any,
|
|
72
|
-
message: `${message}.
|
|
73
|
-
Expected: ${correctId}
|
|
74
|
-
Actual: ${id}`,
|
|
75
|
-
fix(fixer) {
|
|
76
|
-
if (idPropNode) {
|
|
77
|
-
if (idPropNode.type === 'JSXAttribute') {
|
|
78
|
-
return fixer.replaceText(
|
|
79
|
-
idPropNode as any,
|
|
80
|
-
`id="${correctId}"`
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
return fixer.replaceText(
|
|
84
|
-
idPropNode as any,
|
|
85
|
-
`id: '${correctId}'`
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
// Insert after default message node
|
|
89
|
-
if (messagePropNode!.type === 'JSXAttribute') {
|
|
90
|
-
return fixer.insertTextAfter(
|
|
91
|
-
messagePropNode as any,
|
|
92
|
-
` id="${correctId}"`
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
return fixer.replaceText(
|
|
96
|
-
messagePropNode as any,
|
|
97
|
-
`defaultMessage: '${defaultMessage}', id: '${correctId}'`
|
|
98
|
-
)
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export default {
|
|
108
|
-
meta: {
|
|
109
|
-
type: 'problem',
|
|
110
|
-
docs: {
|
|
111
|
-
description: 'Enforce (generated) ID in message descriptor',
|
|
112
|
-
category: 'Errors',
|
|
113
|
-
recommended: false,
|
|
114
|
-
url: 'https://formatjs.io/docs/tooling/linter#enforce-id',
|
|
115
|
-
},
|
|
116
|
-
fixable: 'code',
|
|
117
|
-
schema: [
|
|
118
|
-
{
|
|
119
|
-
type: 'object',
|
|
120
|
-
properties: {
|
|
121
|
-
idInterpolationPattern: {
|
|
122
|
-
type: 'string',
|
|
123
|
-
description:
|
|
124
|
-
'Pattern to verify ID against. Recommended value: [sha512:contenthash:base64:6]',
|
|
125
|
-
},
|
|
126
|
-
idWhitelist: {
|
|
127
|
-
type: 'array',
|
|
128
|
-
description:
|
|
129
|
-
"An array of strings with regular expressions. This array allows allowlist custom ids for messages. For example '`\\\\.`' allows any id which has dot; `'^payment_.*'` - allows any custom id which has prefix `payment_`. Be aware that any backslash \\ provided via string must be escaped with an additional backslash.",
|
|
130
|
-
items: {
|
|
131
|
-
type: 'string',
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
required: ['idInterpolationPattern'],
|
|
136
|
-
additionalProperties: false,
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
},
|
|
140
|
-
create(context) {
|
|
141
|
-
const tmp = context?.options?.[0]
|
|
142
|
-
const opts = {
|
|
143
|
-
idInterpolationPattern: tmp?.idInterpolationPattern,
|
|
144
|
-
} as Opts
|
|
145
|
-
if (Array.isArray(tmp?.idWhitelist)) {
|
|
146
|
-
const {idWhitelist} = tmp
|
|
147
|
-
opts.idWhitelistRegexps = idWhitelist.map(
|
|
148
|
-
(str: string) => new RegExp(str, 'i')
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const callExpressionVisitor = (node: TSESTree.Node) =>
|
|
153
|
-
checkNode(context, node, opts)
|
|
154
|
-
|
|
155
|
-
if (context.parserServices.defineTemplateBodyVisitor) {
|
|
156
|
-
return context.parserServices.defineTemplateBodyVisitor(
|
|
157
|
-
{
|
|
158
|
-
CallExpression: callExpressionVisitor,
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
CallExpression: callExpressionVisitor,
|
|
162
|
-
}
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
JSXOpeningElement: (node: TSESTree.Node) =>
|
|
167
|
-
checkNode(context, node, opts),
|
|
168
|
-
CallExpression: callExpressionVisitor,
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
} as Rule.RuleModule
|
|
@@ -1,161 +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
|
-
isLiteralElement,
|
|
9
|
-
isSelectElement,
|
|
10
|
-
isPoundElement,
|
|
11
|
-
isTagElement,
|
|
12
|
-
} from '@formatjs/icu-messageformat-parser'
|
|
13
|
-
|
|
14
|
-
class PlaceholderEnforcement extends Error {
|
|
15
|
-
public message: string
|
|
16
|
-
constructor(message: string) {
|
|
17
|
-
super()
|
|
18
|
-
this.message = message
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function keyExistsInExpression(
|
|
23
|
-
key: string,
|
|
24
|
-
values: TSESTree.Expression | undefined
|
|
25
|
-
) {
|
|
26
|
-
if (!values) {
|
|
27
|
-
return false
|
|
28
|
-
}
|
|
29
|
-
if (values.type !== 'ObjectExpression') {
|
|
30
|
-
return true // True bc we cannot evaluate this
|
|
31
|
-
}
|
|
32
|
-
if (values.properties.find(prop => prop.type === 'SpreadElement')) {
|
|
33
|
-
return true // True bc there's a spread element
|
|
34
|
-
}
|
|
35
|
-
return !!values.properties.find(prop => {
|
|
36
|
-
if (prop.type !== 'Property') {
|
|
37
|
-
return false
|
|
38
|
-
}
|
|
39
|
-
switch (prop.key.type) {
|
|
40
|
-
case 'Identifier':
|
|
41
|
-
return prop.key.name === key
|
|
42
|
-
case 'Literal':
|
|
43
|
-
return prop.key.value === key
|
|
44
|
-
}
|
|
45
|
-
return false
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function verifyAst(
|
|
50
|
-
ast: MessageFormatElement[],
|
|
51
|
-
values: TSESTree.Expression | undefined,
|
|
52
|
-
ignoreList: Set<string>
|
|
53
|
-
) {
|
|
54
|
-
for (const el of ast) {
|
|
55
|
-
if (isLiteralElement(el) || isPoundElement(el)) {
|
|
56
|
-
continue
|
|
57
|
-
}
|
|
58
|
-
const key = el.value
|
|
59
|
-
if (!ignoreList.has(key) && !keyExistsInExpression(key, values)) {
|
|
60
|
-
throw new PlaceholderEnforcement(
|
|
61
|
-
`Missing value for placeholder "${el.value}"`
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (isPluralElement(el) || isSelectElement(el)) {
|
|
66
|
-
for (const selector of Object.keys(el.options)) {
|
|
67
|
-
verifyAst(el.options[selector].value, values, ignoreList)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (isTagElement(el)) {
|
|
72
|
-
verifyAst(el.children, values, ignoreList)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function checkNode(context: Rule.RuleContext, node: TSESTree.Node) {
|
|
78
|
-
const settings = getSettings(context)
|
|
79
|
-
const msgs = extractMessages(node, {
|
|
80
|
-
excludeMessageDeclCalls: true,
|
|
81
|
-
...settings,
|
|
82
|
-
})
|
|
83
|
-
const {
|
|
84
|
-
options: [opt],
|
|
85
|
-
} = context
|
|
86
|
-
const ignoreList = new Set<string>(opt?.ignoreList || [])
|
|
87
|
-
for (const [
|
|
88
|
-
{
|
|
89
|
-
message: {defaultMessage},
|
|
90
|
-
messageNode,
|
|
91
|
-
},
|
|
92
|
-
values,
|
|
93
|
-
] of msgs) {
|
|
94
|
-
if (!defaultMessage || !messageNode) {
|
|
95
|
-
continue
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
verifyAst(
|
|
99
|
-
parse(defaultMessage, {
|
|
100
|
-
ignoreTag: settings.ignoreTag,
|
|
101
|
-
}),
|
|
102
|
-
values,
|
|
103
|
-
ignoreList
|
|
104
|
-
)
|
|
105
|
-
} catch (e) {
|
|
106
|
-
context.report({
|
|
107
|
-
node: messageNode as any,
|
|
108
|
-
message: e instanceof Error ? e.message : String(e),
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const rule: Rule.RuleModule = {
|
|
115
|
-
meta: {
|
|
116
|
-
type: 'problem',
|
|
117
|
-
docs: {
|
|
118
|
-
description:
|
|
119
|
-
'Enforce that all messages with placeholders have enough passed-in values',
|
|
120
|
-
category: 'Errors',
|
|
121
|
-
recommended: true,
|
|
122
|
-
url: 'https://formatjs.io/docs/tooling/linter#enforce-placeholders',
|
|
123
|
-
},
|
|
124
|
-
fixable: 'code',
|
|
125
|
-
schema: [
|
|
126
|
-
{
|
|
127
|
-
type: 'object',
|
|
128
|
-
properties: {
|
|
129
|
-
ignoreList: {
|
|
130
|
-
type: 'array',
|
|
131
|
-
items: {
|
|
132
|
-
type: 'string',
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
additionalProperties: false,
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
},
|
|
140
|
-
create(context) {
|
|
141
|
-
const callExpressionVisitor = (node: TSESTree.Node) =>
|
|
142
|
-
checkNode(context, node)
|
|
143
|
-
|
|
144
|
-
if (context.parserServices.defineTemplateBodyVisitor) {
|
|
145
|
-
return context.parserServices.defineTemplateBodyVisitor(
|
|
146
|
-
{
|
|
147
|
-
CallExpression: callExpressionVisitor,
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
CallExpression: callExpressionVisitor,
|
|
151
|
-
}
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
return {
|
|
155
|
-
JSXOpeningElement: (node: TSESTree.Node) => checkNode(context, node),
|
|
156
|
-
CallExpression: callExpressionVisitor,
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export default rule
|