Skip to content

Commit 667a81d

Browse files
authored
Add splitByDots option to key-format-style rule (#289)
1 parent b1ea985 commit 667a81d

File tree

3 files changed

+134
-18
lines changed

3 files changed

+134
-18
lines changed

docs/rules/key-format-style.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,16 @@ Also, the following localization key definitions are reported as errors, because
4949
"error",
5050
"camelCase" | "kebab-case" | "snake_case",
5151
{
52-
"allowArray": false
52+
"allowArray": false,
53+
"splitByDots": false,
5354
}
5455
]
5556
}
5657
```
5758

5859
- Primary Option: Select the casing you want to apply. It set to `"camelCase"` as default
5960
- `allowArray`: If `true`, allow the use of arrays. If `false`, disallow the use of arrays. It set to `false` as default.
61+
- `splitByDots`: If `true`, check the values of the key name split by dots.
6062

6163
:+1: Examples of **correct** code for this rule with `"camelCase"`:
6264

lib/rules/key-format-style.ts

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,100 @@ function create(context: RuleContext): RuleListener {
2525
const expectCasing: CaseOption = context.options[0] ?? 'camelCase'
2626
const checker = getCasingChecker(expectCasing)
2727
const allowArray: boolean = context.options[1]?.allowArray
28+
const splitByDotsOption: boolean = context.options[1]?.splitByDots
2829

2930
function reportUnknown(reportNode: YAMLAST.YAMLNode) {
3031
context.report({
3132
message: `Unexpected object key. Use ${expectCasing} string key instead`,
3233
loc: reportNode.loc
3334
})
3435
}
35-
function verifyKey(
36-
key: string | number,
37-
reportNode: JSONAST.JSONNode | YAMLAST.YAMLNode
38-
) {
39-
if (typeof key === 'number') {
40-
if (!allowArray) {
36+
function verifyKeyForString(
37+
key: string,
38+
reportNode:
39+
| JSONAST.JSONProperty['key']
40+
| NonNullable<YAMLAST.YAMLPair['key']>
41+
): void {
42+
for (const target of splitByDotsOption && key.includes('.')
43+
? splitByDots(key, reportNode)
44+
: [{ key, loc: reportNode.loc }]) {
45+
if (!checker(target.key)) {
4146
context.report({
42-
message: `Unexpected array element`,
43-
loc: reportNode.loc
47+
message: `"{{key}}" is not {{expectCasing}}`,
48+
loc: target.loc,
49+
data: {
50+
key: target.key,
51+
expectCasing
52+
}
4453
})
4554
}
46-
} else {
47-
if (!checker(key)) {
48-
context.report({
49-
message: `"${key}" is not ${expectCasing}`,
50-
loc: reportNode.loc
55+
}
56+
}
57+
function verifyKeyForNumber(
58+
key: number,
59+
reportNode:
60+
| NonNullable<JSONAST.JSONArrayExpression['elements'][number]>
61+
| NonNullable<YAMLAST.YAMLSequence['entries'][number]>
62+
): void {
63+
if (!allowArray) {
64+
context.report({
65+
message: `Unexpected array element`,
66+
loc: reportNode.loc
67+
})
68+
}
69+
}
70+
71+
function splitByDots(
72+
key: string,
73+
reportNode:
74+
| JSONAST.JSONProperty['key']
75+
| NonNullable<YAMLAST.YAMLPair['key']>
76+
) {
77+
const result: {
78+
key: string
79+
loc: JSONAST.SourceLocation
80+
}[] = []
81+
let startIndex = 0
82+
let index
83+
while ((index = key.indexOf('.', startIndex)) >= 0) {
84+
const getLoc = buildGetLocation(startIndex, index)
85+
result.push({
86+
key: key.slice(startIndex, index),
87+
get loc() {
88+
return getLoc()
89+
}
90+
})
91+
92+
startIndex = index + 1
93+
}
94+
95+
const getLoc = buildGetLocation(startIndex, key.length)
96+
result.push({
97+
key: key.slice(startIndex, index),
98+
get loc() {
99+
return getLoc()
100+
}
101+
})
102+
103+
return result
104+
105+
function buildGetLocation(start: number, end: number) {
106+
const offset =
107+
reportNode.type === 'JSONLiteral' ||
108+
(reportNode.type === 'YAMLScalar' &&
109+
(reportNode.style === 'double-quoted' ||
110+
reportNode.style === 'single-quoted'))
111+
? reportNode.range[0] + 1
112+
: reportNode.range[0]
113+
let cachedLoc: JSONAST.SourceLocation | undefined
114+
return () => {
115+
if (cachedLoc) {
116+
return cachedLoc
117+
}
118+
const sourceCode = context.getSourceCode()
119+
return (cachedLoc = {
120+
start: sourceCode.getLocFromIndex(offset + start),
121+
end: sourceCode.getLocFromIndex(offset + end)
51122
})
52123
}
53124
}
@@ -81,7 +152,7 @@ function create(context: RuleContext): RuleListener {
81152
const key =
82153
node.key.type === 'JSONLiteral' ? `${node.key.value}` : node.key.name
83154

84-
verifyKey(key, node.key)
155+
verifyKeyForString(key, node.key)
85156
},
86157
'JSONProperty:exit'() {
87158
keyStack = keyStack.upper!
@@ -92,7 +163,7 @@ function create(context: RuleContext): RuleListener {
92163
}
93164
) {
94165
const key = node.parent.elements.indexOf(node)
95-
verifyKey(key, node)
166+
verifyKeyForNumber(key, node)
96167
}
97168
}
98169
}
@@ -147,7 +218,7 @@ function create(context: RuleContext): RuleListener {
147218
} else if (node.key.type === 'YAMLScalar') {
148219
const keyValue = node.key.value
149220
const key = typeof keyValue === 'string' ? keyValue : String(keyValue)
150-
verifyKey(key, node.key)
221+
verifyKeyForString(key, node.key)
151222
} else {
152223
reportUnknown(node)
153224
}
@@ -164,7 +235,7 @@ function create(context: RuleContext): RuleListener {
164235
return
165236
}
166237
const key = node.parent.entries.indexOf(node)
167-
verifyKey(key, node)
238+
verifyKeyForNumber(key, node)
168239
}
169240
}
170241
}
@@ -232,6 +303,9 @@ export = createRule({
232303
properties: {
233304
allowArray: {
234305
type: 'boolean'
306+
},
307+
splitByDots: {
308+
type: 'boolean'
235309
}
236310
},
237311
additionalProperties: false

tests/lib/rules/key-format-style.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,16 @@ tester.run('key-format-style', rule as never, {
200200
</i18n>
201201
<template></template>
202202
<script></script>`
203+
},
204+
{
205+
code: `{
206+
"camelCase.camelCase": {
207+
"fooBar": "kebab-value"
208+
}
209+
}
210+
`,
211+
...options.json.file,
212+
options: ['camelCase', { splitByDots: true }]
203213
}
204214
],
205215

@@ -532,6 +542,36 @@ tester.run('key-format-style', rule as never, {
532542
'"fooBar" is not SCREAMING_SNAKE_CASE',
533543
'"foo_bar" is not SCREAMING_SNAKE_CASE'
534544
]
545+
},
546+
{
547+
code: `{
548+
"kebab-case1.kebab-case2": {
549+
"foo-bar": "kebab-value"
550+
}
551+
}
552+
`,
553+
...options.json.file,
554+
options: ['camelCase', { splitByDots: true }],
555+
errors: [
556+
{
557+
message: '"kebab-case1" is not camelCase',
558+
line: 2,
559+
column: 10,
560+
endColumn: 21
561+
},
562+
{
563+
message: '"kebab-case" is not camelCase',
564+
line: 2,
565+
column: 22,
566+
endColumn: 33
567+
},
568+
{
569+
message: '"foo-bar" is not camelCase',
570+
line: 3,
571+
column: 11,
572+
endColumn: 20
573+
}
574+
]
535575
}
536576
]
537577
})

0 commit comments

Comments
 (0)