From ab2b88c282b8c425a1403db78408cbec2513a391 Mon Sep 17 00:00:00 2001 From: Christian Schuller Date: Thu, 25 Apr 2024 21:09:42 +0200 Subject: [PATCH] Feature: Add "json-es/use-camelcase" rule Based on the ESLint camelcase rule. --- README.md | 17 +++ package.json | 4 +- rules/.eslintrc | 7 ++ rules/index.js | 2 + rules/use-camelcase.js | 84 ++++++++++++++ test/rules/plugin/use-camelcase.test.mjs | 141 +++++++++++++++++++++++ 6 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 rules/.eslintrc create mode 100644 rules/use-camelcase.js create mode 100644 test/rules/plugin/use-camelcase.test.mjs diff --git a/README.md b/README.md index 6e1d9e6..a250a1b 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,23 @@ Based on the recommended rules with stylistic aspects. |🔧| no-multiple-empty-lines | | | | sort-keys |Alternative with fix [eslint-plugin-sort-keys-fix] | +## Optional Rules + +### use-camelcase +The ESLint camelcase rule does not work with JSON files. + +A custom 'use-camelcase' [rule](./rules/use-camelcase.js) is available. +Based on the ESLint camelcase rule with minor adjustments. + +__Configuration__ +```json +{ + "rules": { + "json-es/use-camelcase": ["error", {"allow": ["FOO", "[regex]*"]}] + } +} +``` + [ESLint]: https://eslint.org/ [custom parser]: https://eslint.org/docs/developer-guide/working-with-custom-parsers [eslint-plugin-json]: https://github.com/azeemba/eslint-plugin-json diff --git a/package.json b/package.json index 6a02540..a5f66e4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "A JSON parser for ESLint.", "main": "index.js", "scripts": { - "lint": "eslint lib", + "lint": "npm run lint:lib && npm run lint:rules", + "lint:lib": "eslint lib", + "lint:rules": "eslint rules", "coverage": "c8 --check-coverage --lines 100 npm test", "test": "vitest run", "testui": "vitest --ui", diff --git a/rules/.eslintrc b/rules/.eslintrc new file mode 100644 index 0000000..b129d15 --- /dev/null +++ b/rules/.eslintrc @@ -0,0 +1,7 @@ +{ + "env": { + "es6": true, + "node": true + }, + "extends": ["eslint:recommended"] +} diff --git a/rules/index.js b/rules/index.js index 9d8d5e3..60f1b86 100644 --- a/rules/index.js +++ b/rules/index.js @@ -1,7 +1,9 @@ const noComments = require('./no-comments'); const useValidJson = require('./use-valid-json'); +const useCamelCase = require('./use-camelcase'); module.exports = { 'no-comments': noComments, + 'use-camelcase': useCamelCase, 'use-valid-json': useValidJson } diff --git a/rules/use-camelcase.js b/rules/use-camelcase.js new file mode 100644 index 0000000..df2876a --- /dev/null +++ b/rules/use-camelcase.js @@ -0,0 +1,84 @@ +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +// Base on eslint camelcase rule. +// https://github.com/eslint/eslint/blob/1579ce05cbb523cb5b04ff77fab06ba1ecd18dce/lib/rules/camelcase.js +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Every property should be camelCase.', + category: 'Possible Errors', + recommended: false + }, + schema: [ + { + type: "object", + properties: { + allow: { + type: "array", + items: { + type: "string" + }, + minItems: 0, + uniqueItems: true + } + }, + additionalProperties: false + } + ] + }, + create: function(context) { + const options = context.options[0] || {}; + const allow = options.allow || []; + + /** + * Checks if a string contains an underscore and isn't all upper-case + * @param {string} name The string to check. + * @returns {boolean} if the string is underscored + * @private + */ + function isUnderscored(name) { + return name.includes('_') || name.includes('-') || name === name.toUpperCase(); + } + + /** + * + * Checks if a string match the ignore list + * @param {string} name The string to check. + * @returns {boolean} if the string is ignored + * @private + */ + function isAllowed(name) { + return allow.some( + entry => name === entry || name.match(new RegExp(entry, "u")) + ); + } + + /** + * Checks if a given name is good or not. + * @param {string} name The name to check. + * @returns {boolean} `true` if the name is good. + * @private + */ + function isGoodName(name) { + return !isUnderscored(name) || isAllowed(name); + } + + return { + [["ObjectExpression > Property"]](node) { + const keyName = node.key.value || ''; + + if (!isGoodName(keyName)) { + context.report({ + node, + message: `Identifier '${keyName}' is not in camel case.` + }); + } + } + }; + } +}; diff --git a/test/rules/plugin/use-camelcase.test.mjs b/test/rules/plugin/use-camelcase.test.mjs new file mode 100644 index 0000000..e2ca0f7 --- /dev/null +++ b/test/rules/plugin/use-camelcase.test.mjs @@ -0,0 +1,141 @@ +import {describe, expect, test} from 'vitest' +import {linter} from '../../testSandbox.js'; + +const config = { + parser: 'eslint-plugin-json-es', + rules: { + 'json-es/use-camelcase': ['error'] + } +}; + +describe('use-camelcase', () => { + describe('correct', () => { + test('camelCase', () => { + // Given + const json = JSON.stringify({'camelCase': 'value'}); + + // When + const messages = linter.verify(json, config, {filename: 'test.json'}); + + // Then + expect(messages.length).toEqual(0); + }); + + test('alllowercase', () => { + // Given + const json = JSON.stringify({'alllowercase': 'value'}); + + // When + const messages = linter.verify(json, config, {filename: 'test.json'}); + + // Then + expect(messages.length).toEqual(0); + }); + }); + + describe('correct + allow', () => { + test('_privateField', () => { + // Given + const json = JSON.stringify({'_privateField': 'value'}); + + const allowConfig = structuredClone(config); + allowConfig.rules['json-es/use-camelcase'] = ['error', {allow: ['_privateField']}]; + + // When + const messages = linter.verify(json, allowConfig, {filename: 'test.json'}); + + // Then + expect(messages.length).toEqual(0); + }); + + test('A_CONSTANT', () => { + // Given + const json = JSON.stringify({'A_CONSTANT': 'value'}); + + const allowConfig = structuredClone(config); + allowConfig.rules['json-es/use-camelcase'] = ['error', {allow: ['[A-Z_]*']}]; + + // When + const messages = linter.verify(json, allowConfig, {filename: 'test.json'}); + + // Then + expect(messages.length).toEqual(0); + }); + }); + + describe('incorrect', () => { + test('UPPER_case', () => { + // Given + const json = JSON.stringify({'UPPER_case': 'value'}); + + // When + const messages = linter.verify(json, config, {filename: 'test.json'}); + + // Then + const expectedMessage = { + severity: 2, + ruleId: 'json-es/use-camelcase', + message: 'Identifier \'UPPER_case\' is not in camel case.' + }; + + expect(messages.length).toEqual(1); + expect(messages[0]).toMatchObject(expectedMessage); + }); + + test('A_CONSTANT', () => { + // Given + const json = JSON.stringify({'A_CONSTANT': 'value'}); + + // When + const messages = linter.verify(json, config, {filename: 'test.json'}); + + // Then + const expectedMessage = { + severity: 2, + ruleId: 'json-es/use-camelcase', + message: 'Identifier \'A_CONSTANT\' is not in camel case.' + }; + + expect(messages.length).toEqual(1); + expect(messages[0]).toMatchObject(expectedMessage); + }); + + test('kebab-case', () => { + // Given + const json = JSON.stringify({'kebab-case': 'value'}); + + // When + const messages = linter.verify(json, config, {filename: 'test.json'}); + + // Then + const expectedMessage = { + severity: 2, + ruleId: 'json-es/use-camelcase', + message: 'Identifier \'kebab-case\' is not in camel case.' + }; + + expect(messages.length).toEqual(1); + expect(messages[0]).toMatchObject(expectedMessage); + }); + + test('ALLUPPERCASE', () => { + // Given + const json = JSON.stringify({'ALLUPPERCASE': 'value'}); + + // When + const messages = linter.verify(json, config, {filename: 'test.json'}); + + // Then + const expectedMessage = { + severity: 2, + ruleId: 'json-es/use-camelcase', + message: 'Identifier \'ALLUPPERCASE\' is not in camel case.' + }; + + expect(messages.length).toEqual(1); + expect(messages[0]).toMatchObject(expectedMessage); + }); + }); +}); + +