1
1
/**
2
2
* @author Yosuke Ota
3
3
*/
4
- import type { AST as VAST } from 'vue-eslint-parser'
5
4
import type { AST as JSONAST } from 'jsonc-eslint-parser'
6
5
import type { AST as YAMLAST } from 'yaml-eslint-parser'
7
6
import { extname } from 'path'
8
- import { getLocaleMessages } from '../utils/index'
7
+ import { defineCustomBlocksVisitor , getLocaleMessages } from '../utils/index'
9
8
import debugBuilder from 'debug'
10
9
import type { RuleContext , RuleListener } from '../types'
11
10
import { getCasingChecker } from '../utils/casing'
12
- import { LocaleMessage } from '../utils/locale-messages'
11
+ import type { LocaleMessage } from '../utils/locale-messages'
13
12
const debug = debugBuilder ( 'eslint-plugin-vue-i18n:key-format-style' )
14
13
15
14
const allowedCaseOptions = [ 'camelCase' , 'kebab-case' , 'snake_case' ] as const
16
15
type CaseOption = typeof allowedCaseOptions [ number ]
17
- const unknownKey = Symbol ( 'unknown key' )
18
- type UnknownKey = typeof unknownKey
19
16
20
17
function create ( context : RuleContext ) : RuleListener {
21
18
const filename = context . getFilename ( )
22
19
const expectCasing : CaseOption = context . options [ 0 ] ?? 'camelCase'
23
20
const checker = getCasingChecker ( expectCasing )
24
21
const allowArray : boolean = context . options [ 1 ] ?. allowArray
25
22
23
+ function reportUnknown ( reportNode : YAMLAST . YAMLNode ) {
24
+ context . report ( {
25
+ message : `Unexpected object key. Use ${ expectCasing } string key instead` ,
26
+ loc : reportNode . loc
27
+ } )
28
+ }
29
+ function verifyKey (
30
+ key : string | number ,
31
+ reportNode : JSONAST . JSONNode | YAMLAST . YAMLNode
32
+ ) {
33
+ if ( typeof key === 'number' ) {
34
+ if ( ! allowArray ) {
35
+ context . report ( {
36
+ message : `Unexpected array element` ,
37
+ loc : reportNode . loc
38
+ } )
39
+ }
40
+ } else {
41
+ if ( ! checker ( key ) ) {
42
+ context . report ( {
43
+ message : `"${ key } " is not ${ expectCasing } ` ,
44
+ loc : reportNode . loc
45
+ } )
46
+ }
47
+ }
48
+ }
26
49
/**
27
- * Create node visitor
50
+ * Create node visitor for JSON
28
51
*/
29
- function createVisitor < N extends JSONAST . JSONNode | YAMLAST . YAMLNode > (
30
- targetLocaleMessage : LocaleMessage ,
31
- {
32
- skipNode,
33
- resolveKey,
34
- resolveReportNode
35
- } : {
36
- skipNode : ( node : N ) => boolean
37
- resolveKey : ( node : N ) => string | number | UnknownKey | null
38
- resolveReportNode : ( node : N ) => N
39
- }
40
- ) {
52
+ function createVisitorForJson (
53
+ targetLocaleMessage : LocaleMessage
54
+ ) : RuleListener {
41
55
type KeyStack = {
42
56
inLocale : boolean
43
- node ?: N
57
+ node ?: JSONAST . JSONNode
44
58
upper ?: KeyStack
45
59
}
46
60
let keyStack : KeyStack = {
47
61
inLocale : targetLocaleMessage . localeKey === 'file'
48
62
}
49
63
return {
50
- enterNode ( node : N ) {
51
- if ( skipNode ( node ) ) {
52
- return
53
- }
54
- const key = resolveKey ( node )
55
- if ( key == null ) {
56
- return
57
- }
64
+ JSONProperty ( node : JSONAST . JSONProperty ) {
58
65
const { inLocale } = keyStack
59
66
keyStack = {
60
67
node,
@@ -64,182 +71,122 @@ function create(context: RuleContext): RuleListener {
64
71
if ( ! inLocale ) {
65
72
return
66
73
}
67
- if ( key === unknownKey ) {
68
- context . report ( {
69
- message : `Unexpected object key. Use ${ expectCasing } string key instead` ,
70
- loc : resolveReportNode ( node ) . loc
71
- } )
72
- } else if ( typeof key === 'number' ) {
73
- if ( ! allowArray ) {
74
- context . report ( {
75
- message : `Unexpected array element` ,
76
- loc : resolveReportNode ( node ) . loc
77
- } )
78
- }
79
- } else {
80
- if ( ! checker ( key ) ) {
81
- context . report ( {
82
- message : `"${ key } " is not ${ expectCasing } ` ,
83
- loc : resolveReportNode ( node ) . loc
84
- } )
85
- }
86
- }
74
+
75
+ const key =
76
+ node . key . type === 'JSONLiteral' ? `${ node . key . value } ` : node . key . name
77
+
78
+ verifyKey ( key , node . key )
87
79
} ,
88
- leaveNode ( node : N ) {
89
- if ( keyStack . node === node ) {
90
- keyStack = keyStack . upper !
91
- }
92
- }
93
- }
94
- }
95
- /**
96
- * Create node visitor for JSON
97
- */
98
- function createVisitorForJson ( targetLocaleMessage : LocaleMessage ) {
99
- return createVisitor < JSONAST . JSONNode > ( targetLocaleMessage , {
100
- skipNode ( node ) {
101
- if (
102
- node . type === 'Program' ||
103
- node . type === 'JSONExpressionStatement' ||
104
- node . type === 'JSONProperty'
105
- ) {
106
- return true
107
- }
108
- const parent = node . parent !
109
- if ( parent . type === 'JSONProperty' && parent . key === node ) {
110
- return true
111
- }
112
- return false
80
+ 'JSONProperty:exit' ( ) {
81
+ keyStack = keyStack . upper !
113
82
} ,
114
- resolveKey ( node ) {
115
- const parent = node . parent !
116
- if ( parent . type === 'JSONProperty' ) {
117
- return parent . key . type === 'JSONLiteral'
118
- ? `${ parent . key . value } `
119
- : parent . key . name
120
- } else if ( parent . type === 'JSONArrayExpression' ) {
121
- return parent . elements . indexOf ( node as never )
83
+ 'JSONArrayExpression > *' (
84
+ node : JSONAST . JSONArrayExpression [ 'elements' ] [ number ] & {
85
+ parent : JSONAST . JSONArrayExpression
122
86
}
123
- return null
124
- } ,
125
- resolveReportNode ( node ) {
126
- const parent = node . parent !
127
- return parent . type === 'JSONProperty' ? parent . key : node
87
+ ) {
88
+ const key = node . parent . elements . indexOf ( node )
89
+ verifyKey ( key , node )
128
90
}
129
- } )
91
+ }
130
92
}
131
93
132
94
/**
133
95
* Create node visitor for YAML
134
96
*/
135
- function createVisitorForYaml ( targetLocaleMessage : LocaleMessage ) {
136
- const yamlKeyNodes = new Set ( )
137
- return createVisitor < YAMLAST . YAMLNode > ( targetLocaleMessage , {
138
- skipNode ( node ) {
97
+ function createVisitorForYaml (
98
+ targetLocaleMessage : LocaleMessage
99
+ ) : RuleListener {
100
+ const yamlKeyNodes = new Set < YAMLAST . YAMLContent | YAMLAST . YAMLWithMeta > ( )
101
+
102
+ type KeyStack = {
103
+ inLocale : boolean
104
+ node ?: YAMLAST . YAMLNode
105
+ upper ?: KeyStack
106
+ }
107
+ let keyStack : KeyStack = {
108
+ inLocale : targetLocaleMessage . localeKey === 'file'
109
+ }
110
+ function withinKey ( node : YAMLAST . YAMLNode ) {
111
+ for ( const keyNode of yamlKeyNodes ) {
139
112
if (
140
- node . type === 'Program' ||
141
- node . type === 'YAMLDocument' ||
142
- node . type === 'YAMLDirective' ||
143
- node . type === 'YAMLAnchor' ||
144
- node . type === 'YAMLTag'
113
+ keyNode . range [ 0 ] <= node . range [ 0 ] &&
114
+ node . range [ 0 ] < keyNode . range [ 1 ]
145
115
) {
146
116
return true
147
117
}
148
-
149
- if ( yamlKeyNodes . has ( node ) ) {
150
- // within key node
151
- return true
152
- }
153
- const parent = node . parent
154
- if ( yamlKeyNodes . has ( parent ) ) {
155
- // within key node
156
- yamlKeyNodes . add ( node )
157
- return true
118
+ }
119
+ return false
120
+ }
121
+ return {
122
+ YAMLPair ( node : YAMLAST . YAMLPair ) {
123
+ const { inLocale } = keyStack
124
+ keyStack = {
125
+ node,
126
+ inLocale : true ,
127
+ upper : keyStack
158
128
}
159
- if ( node . type === 'YAMLPair' ) {
160
- yamlKeyNodes . add ( node . key )
161
- return true
129
+ if ( ! inLocale ) {
130
+ return
162
131
}
163
- return false
164
- } ,
165
- resolveKey ( node ) {
166
- const parent = node . parent !
167
- if ( parent . type === 'YAMLPair' ) {
168
- if ( parent . key == null ) {
169
- return unknownKey
170
- }
171
- if ( parent . key . type === 'YAMLScalar' ) {
172
- const key = parent . key . value
173
- return typeof key === 'string' ? key : String ( key )
132
+ if ( node . key != null ) {
133
+ if ( withinKey ( node ) ) {
134
+ return
174
135
}
175
- return unknownKey
176
- } else if ( parent . type === 'YAMLSequence' ) {
177
- return parent . entries . indexOf ( node as never )
136
+ yamlKeyNodes . add ( node . key )
178
137
}
179
138
180
- return null
139
+ if ( node . key == null ) {
140
+ reportUnknown ( node )
141
+ } else if ( node . key . type === 'YAMLScalar' ) {
142
+ const keyValue = node . key . value
143
+ const key = typeof keyValue === 'string' ? keyValue : String ( keyValue )
144
+ verifyKey ( key , node . key )
145
+ } else {
146
+ reportUnknown ( node )
147
+ }
148
+ } ,
149
+ 'YAMLPair:exit' ( ) {
150
+ keyStack = keyStack . upper !
181
151
} ,
182
- resolveReportNode ( node ) {
183
- const parent = node . parent !
184
- return parent . type === 'YAMLPair' ? parent . key || parent : node
152
+ 'YAMLSequence > *' (
153
+ node : YAMLAST . YAMLSequence [ 'entries' ] [ number ] & {
154
+ parent : YAMLAST . YAMLSequence
155
+ }
156
+ ) {
157
+ if ( withinKey ( node ) ) {
158
+ return
159
+ }
160
+ const key = node . parent . entries . indexOf ( node )
161
+ verifyKey ( key , node )
185
162
}
186
- } )
163
+ }
187
164
}
188
165
189
166
if ( extname ( filename ) === '.vue' ) {
190
- return {
191
- Program ( ) {
192
- const documentFragment =
193
- context . parserServices . getDocumentFragment &&
194
- context . parserServices . getDocumentFragment ( )
195
- /** @type {VElement[] } */
196
- const i18nBlocks =
197
- ( documentFragment &&
198
- documentFragment . children . filter (
199
- ( node ) : node is VAST . VElement =>
200
- node . type === 'VElement' && node . name === 'i18n'
201
- ) ) ||
202
- [ ]
203
- if ( ! i18nBlocks . length ) {
204
- return
167
+ return defineCustomBlocksVisitor (
168
+ context ,
169
+ ctx => {
170
+ const localeMessages = getLocaleMessages ( context )
171
+ const targetLocaleMessage = localeMessages . findBlockLocaleMessage (
172
+ ctx . parserServices . customBlock
173
+ )
174
+ if ( ! targetLocaleMessage ) {
175
+ return { }
205
176
}
177
+ return createVisitorForJson ( targetLocaleMessage )
178
+ } ,
179
+ ctx => {
206
180
const localeMessages = getLocaleMessages ( context )
207
-
208
- for ( const block of i18nBlocks ) {
209
- if (
210
- block . startTag . attributes . some (
211
- attr => ! attr . directive && attr . key . name === 'src'
212
- )
213
- ) {
214
- continue
215
- }
216
-
217
- const targetLocaleMessage = localeMessages . findBlockLocaleMessage (
218
- block
219
- )
220
- if ( ! targetLocaleMessage ) {
221
- continue
222
- }
223
- const parserLang = targetLocaleMessage . getParserLang ( )
224
-
225
- let visitor
226
- if ( parserLang === 'json' ) {
227
- visitor = createVisitorForJson ( targetLocaleMessage )
228
- } else if ( parserLang === 'yaml' ) {
229
- visitor = createVisitorForYaml ( targetLocaleMessage )
230
- }
231
-
232
- if ( visitor == null ) {
233
- return
234
- }
235
-
236
- targetLocaleMessage . traverseNodes ( {
237
- enterNode : visitor . enterNode ,
238
- leaveNode : visitor . leaveNode
239
- } )
181
+ const targetLocaleMessage = localeMessages . findBlockLocaleMessage (
182
+ ctx . parserServices . customBlock
183
+ )
184
+ if ( ! targetLocaleMessage ) {
185
+ return { }
240
186
}
187
+ return createVisitorForYaml ( targetLocaleMessage )
241
188
}
242
- }
189
+ )
243
190
} else if ( context . parserServices . isJSON || context . parserServices . isYAML ) {
244
191
const localeMessages = getLocaleMessages ( context )
245
192
const targetLocaleMessage = localeMessages . findExistLocaleMessage ( filename )
@@ -249,19 +196,9 @@ function create(context: RuleContext): RuleListener {
249
196
}
250
197
251
198
if ( context . parserServices . isJSON ) {
252
- const { enterNode, leaveNode } = createVisitorForJson ( targetLocaleMessage )
253
-
254
- return {
255
- '[type=/^JSON/]' : enterNode ,
256
- '[type=/^JSON/]:exit' : leaveNode
257
- }
199
+ return createVisitorForJson ( targetLocaleMessage )
258
200
} else if ( context . parserServices . isYAML ) {
259
- const { enterNode, leaveNode } = createVisitorForYaml ( targetLocaleMessage )
260
-
261
- return {
262
- '[type=/^YAML/]' : enterNode ,
263
- '[type=/^YAML/]:exit' : leaveNode
264
- }
201
+ return createVisitorForYaml ( targetLocaleMessage )
265
202
}
266
203
return { }
267
204
} else {
0 commit comments