Skip to content

Commit 9e11cd3

Browse files
authored
Add key-name-casing rule (#27)
1 parent 40e6abd commit 9e11cd3

File tree

8 files changed

+793
-0
lines changed

8 files changed

+793
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ The rules with the following star :star: are included in the config.
137137

138138
| Rule ID | Description | Fixable | JSON | JSONC | JSON5 |
139139
|:--------|:------------|:-------:|:----:|:-----:|:-----:|
140+
| [jsonc/key-name-casing](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/key-name-casing.html) | enforce naming convention to property key names | | | | |
140141
| [jsonc/no-bigint-literals](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-bigint-literals.html) | disallow BigInt literals | | :star: | :star: | :star: |
141142
| [jsonc/no-comments](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-comments.html) | disallow comments | | :star: | | |
142143
| [jsonc/no-number-props](https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-number-props.html) | disallow number property keys | :wrench: | :star: | :star: | :star: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The rules with the following star :star: are included in the `plugin:jsonc/recom
1313

1414
| Rule ID | Description | Fixable | JSON | JSONC | JSON5 |
1515
|:--------|:------------|:-------:|:----:|:-----:|:-----:|
16+
| [jsonc/key-name-casing](./key-name-casing.md) | enforce naming convention to property key names | | | | |
1617
| [jsonc/no-bigint-literals](./no-bigint-literals.md) | disallow BigInt literals | | :star: | :star: | :star: |
1718
| [jsonc/no-comments](./no-comments.md) | disallow comments | | :star: | | |
1819
| [jsonc/no-number-props](./no-number-props.md) | disallow number property keys | :wrench: | :star: | :star: | :star: |

docs/rules/key-name-casing.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "jsonc/key-name-casing"
5+
description: "enforce naming convention to property key names"
6+
---
7+
# jsonc/key-name-casing
8+
9+
> enforce naming convention to property key names
10+
11+
## :book: Rule Details
12+
13+
This rule enforces a naming convention to property key names.
14+
15+
<eslint-code-block>
16+
17+
```json5
18+
/* eslint jsonc/key-name-casing: 'error' */
19+
{
20+
/* ✓ GOOD */
21+
"camelCase": "camelCase",
22+
23+
/* ✗ BAD */
24+
"PascalCase": "PascalCase",
25+
"SCREAMING_SNAKE_CASE": "SCREAMING_SNAKE_CASE",
26+
"kebab-case": "kebab-case",
27+
"snake_case": "snake_case"
28+
}
29+
```
30+
31+
</eslint-code-block>
32+
33+
## :wrench: Options
34+
35+
Nothing.
36+
37+
```json5
38+
{
39+
"jsonc/key-name-casing": ["error", {
40+
"camelCase": true,
41+
"PascalCase": false,
42+
"SCREAMING_SNAKE_CASE": false,
43+
"kebab-case": false,
44+
"snake_case": false,
45+
"ignores": []
46+
}]
47+
}
48+
```
49+
50+
- `"camelCase"` ... if `true`, allows camelCase naming. default `true`
51+
- `"PascalCase"` ... if `true`, allows PascalCase naming. default `false`
52+
- `"SCREAMING_SNAKE_CASE"` ... if `true`, allows SCREAMING_SNAKE_CASE naming. default `false`
53+
- `"kebab-case"` ... if `true`, allows kebab-case naming. default `false`
54+
- `"snake_case"` ... if `true`, allows snake_case naming. default `false`
55+
- `"ignores"` ... you can specify the patterns to ignore in the array.
56+
57+
## Implementation
58+
59+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-jsonc/blob/master/lib/rules/key-name-casing.ts)
60+
- [Test source](https://github.com/ota-meshi/eslint-plugin-jsonc/blob/master/tests/lib/rules/key-name-casing.js)

lib/rules/key-name-casing.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import type { AST } from "jsonc-eslint-parser"
2+
import type { RuleListener } from "../types"
3+
import { createRule } from "../utils"
4+
import type { CasingKind } from "../utils/casing"
5+
import { getChecker, allowedCaseOptions } from "../utils/casing"
6+
7+
type Option = {
8+
[key in CasingKind]?: boolean
9+
} & {
10+
ignores?: string[]
11+
}
12+
13+
export default createRule("key-name-casing", {
14+
meta: {
15+
docs: {
16+
description: "enforce naming convention to property key names",
17+
recommended: null,
18+
extensionRule: false,
19+
},
20+
schema: [
21+
{
22+
type: "object",
23+
properties: {
24+
camelCase: {
25+
type: "boolean",
26+
default: true,
27+
},
28+
// eslint-disable-next-line @typescript-eslint/naming-convention -- option
29+
PascalCase: {
30+
type: "boolean",
31+
default: false,
32+
},
33+
// eslint-disable-next-line @typescript-eslint/naming-convention -- option
34+
SCREAMING_SNAKE_CASE: {
35+
type: "boolean",
36+
default: false,
37+
},
38+
// eslint-disable-next-line @typescript-eslint/naming-convention -- option
39+
"kebab-case": {
40+
type: "boolean",
41+
default: false,
42+
},
43+
// eslint-disable-next-line @typescript-eslint/naming-convention -- option
44+
snake_case: {
45+
type: "boolean",
46+
default: false,
47+
},
48+
ignores: {
49+
type: "array",
50+
items: {
51+
type: "string",
52+
},
53+
uniqueItems: true,
54+
additionalItems: false,
55+
},
56+
},
57+
additionalProperties: false,
58+
},
59+
],
60+
messages: {
61+
doesNotMatchFormat:
62+
"Property name `{{name}}` must match one of the following formats: {{formats}}",
63+
},
64+
type: "layout",
65+
},
66+
create(context) {
67+
if (!context.parserServices.isJSON) {
68+
return {} as RuleListener
69+
}
70+
const sourceCode = context.getSourceCode()
71+
const option: Option = { ...context.options[0] }
72+
if (option.camelCase !== false) {
73+
option.camelCase = true
74+
}
75+
const ignores = option.ignores
76+
? option.ignores.map((ignore) => new RegExp(ignore))
77+
: []
78+
const formats = Object.keys(option)
79+
.filter((key): key is CasingKind =>
80+
allowedCaseOptions.includes(key as CasingKind),
81+
)
82+
.filter((key) => option[key])
83+
84+
const checkers: ((str: string) => boolean)[] = formats.map(getChecker)
85+
86+
/**
87+
* Check whether a given name is a valid.
88+
*/
89+
function isValid(name: string): boolean {
90+
if (ignores.some((regex) => regex.test(name))) {
91+
return true
92+
}
93+
return checkers.length ? checkers.some((c) => c(name)) : true
94+
}
95+
96+
return {
97+
JSONProperty(node: AST.JSONProperty) {
98+
const name =
99+
node.key.type === "JSONLiteral" &&
100+
typeof node.key.value === "string"
101+
? node.key.value
102+
: sourceCode.text.slice(...node.key.range)
103+
if (!isValid(name)) {
104+
context.report({
105+
loc: node.key.loc,
106+
messageId: "doesNotMatchFormat",
107+
data: {
108+
name,
109+
formats: formats.join(", "),
110+
},
111+
})
112+
}
113+
},
114+
}
115+
},
116+
})

0 commit comments

Comments
 (0)