11/**
22 * @author Yosuke Ota
33 */
4- import type { AST as VAST } from 'vue-eslint-parser'
54import type { AST as JSONAST } from 'jsonc-eslint-parser'
65import type { AST as YAMLAST } from 'yaml-eslint-parser'
76import { extname } from 'path'
8- import { getLocaleMessages } from '../utils/index'
7+ import { defineCustomBlocksVisitor , getLocaleMessages } from '../utils/index'
98import debugBuilder from 'debug'
109import type { RuleContext , RuleListener } from '../types'
1110import { getCasingChecker } from '../utils/casing'
12- import { LocaleMessage } from '../utils/locale-messages'
11+ import type { LocaleMessage } from '../utils/locale-messages'
1312const debug = debugBuilder ( 'eslint-plugin-vue-i18n:key-format-style' )
1413
1514const allowedCaseOptions = [ 'camelCase' , 'kebab-case' , 'snake_case' ] as const
1615type CaseOption = typeof allowedCaseOptions [ number ]
17- const unknownKey = Symbol ( 'unknown key' )
18- type UnknownKey = typeof unknownKey
1916
2017function create ( context : RuleContext ) : RuleListener {
2118 const filename = context . getFilename ( )
2219 const expectCasing : CaseOption = context . options [ 0 ] ?? 'camelCase'
2320 const checker = getCasingChecker ( expectCasing )
2421 const allowArray : boolean = context . options [ 1 ] ?. allowArray
2522
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+ }
2649 /**
27- * Create node visitor
50+ * Create node visitor for JSON
2851 */
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 {
4155 type KeyStack = {
4256 inLocale : boolean
43- node ?: N
57+ node ?: JSONAST . JSONNode
4458 upper ?: KeyStack
4559 }
4660 let keyStack : KeyStack = {
4761 inLocale : targetLocaleMessage . localeKey === 'file'
4862 }
4963 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 ) {
5865 const { inLocale } = keyStack
5966 keyStack = {
6067 node,
@@ -64,182 +71,122 @@ function create(context: RuleContext): RuleListener {
6471 if ( ! inLocale ) {
6572 return
6673 }
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 )
8779 } ,
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 !
11382 } ,
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
12286 }
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 )
12890 }
129- } )
91+ }
13092 }
13193
13294 /**
13395 * Create node visitor for YAML
13496 */
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 ) {
139112 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 ]
145115 ) {
146116 return true
147117 }
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
158128 }
159- if ( node . type === 'YAMLPair' ) {
160- yamlKeyNodes . add ( node . key )
161- return true
129+ if ( ! inLocale ) {
130+ return
162131 }
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
174135 }
175- return unknownKey
176- } else if ( parent . type === 'YAMLSequence' ) {
177- return parent . entries . indexOf ( node as never )
136+ yamlKeyNodes . add ( node . key )
178137 }
179138
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 !
181151 } ,
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 )
185162 }
186- } )
163+ }
187164 }
188165
189166 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 { }
205176 }
177+ return createVisitorForJson ( targetLocaleMessage )
178+ } ,
179+ ctx => {
206180 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 { }
240186 }
187+ return createVisitorForYaml ( targetLocaleMessage )
241188 }
242- }
189+ )
243190 } else if ( context . parserServices . isJSON || context . parserServices . isYAML ) {
244191 const localeMessages = getLocaleMessages ( context )
245192 const targetLocaleMessage = localeMessages . findExistLocaleMessage ( filename )
@@ -249,19 +196,9 @@ function create(context: RuleContext): RuleListener {
249196 }
250197
251198 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 )
258200 } 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 )
265202 }
266203 return { }
267204 } else {
0 commit comments