From e863e779d5ac7fced96ab7303159cf60adbb0cb4 Mon Sep 17 00:00:00 2001 From: y Date: Mon, 11 Dec 2017 04:12:43 +0900 Subject: [PATCH 1/6] [New] Add `prop-name-casing` --- docs/rules/prop-name-casing.md | 35 ++++++ lib/rules/prop-name-casing.js | 73 +++++++++++ lib/utils/casing.js | 13 ++ tests/lib/rules/prop-name-casing.js | 180 ++++++++++++++++++++++++++++ tests/lib/utils/casing.js | 9 ++ 5 files changed, 310 insertions(+) create mode 100644 docs/rules/prop-name-casing.md create mode 100644 lib/rules/prop-name-casing.js create mode 100644 tests/lib/rules/prop-name-casing.js diff --git a/docs/rules/prop-name-casing.md b/docs/rules/prop-name-casing.md new file mode 100644 index 000000000..fcd8207f9 --- /dev/null +++ b/docs/rules/prop-name-casing.md @@ -0,0 +1,35 @@ +# enforce specific casing for the Prop name in Vue components(prop-name-casing) + +This rule would enforce proper casing of props in vue components(camelCase). + +## :book: Rule Details + +(https://vuejs.org/v2/style-guide/#Prop-name-casing-strongly-recommended). + +:+1: Examples of **correct** code for `camelCase`: + +```js +export default { + props: { + greetingText: String + } +} +``` + +:-1: Examples of **incorrect** code for `camelCase`: + +```js +export default { + props: { + 'greeting-text': String + } +} +``` + +## :wrench: Options + +Default casing is set to `camelCase`. + +``` +"vue/prop-name-casing": ["error", "camelCase|snake_case"] +``` diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js new file mode 100644 index 000000000..23e04a483 --- /dev/null +++ b/lib/rules/prop-name-casing.js @@ -0,0 +1,73 @@ +/** + * @fileoverview Requires specific casing for the Prop name in Vue components + * @author Yu Kimura + */ +'use strict' + +const utils = require('../utils') +const casing = require('../utils/casing') +const allowedCaseOptions = ['camelCase', 'snake_case'] + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +function create (context) { + const options = context.options[0] + const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'camelCase' + const converter = casing.getConverter(caseType) + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + return utils.executeOnVue(context, (obj) => { + const node = obj.properties.find(p => + p.type === 'Property' && + p.key.type === 'Identifier' && + p.key.name === 'props' + ) + if (!node) return + + const items = node.value.type === 'ObjectExpression' ? node.value.properties : node.value.elements + for (const item of items) { + if (item.type !== 'Property') { + return + } + + const propName = item.key.type === 'Literal' ? item.key.value : item.key.name + const convertedName = converter(propName) + if (convertedName !== propName) { + context.report({ + node: item, + message: 'Prop "{{name}}" is not {{caseType}}.', + data: { + name: propName, + caseType: caseType + } + }) + } + } + }) +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'enforce specific casing for the Prop name in Vue components', + category: 'strongly-recommended' + }, + fixable: null, // or "code" or "whitespace" + schema: [ + { + enum: allowedCaseOptions + } + ] + }, + + create +} diff --git a/lib/utils/casing.js b/lib/utils/casing.js index 2baca2356..db73a5a23 100644 --- a/lib/utils/casing.js +++ b/lib/utils/casing.js @@ -14,6 +14,18 @@ function kebabCase (str) { .toLowerCase() } +/** + * Convert text to snake_case + * @param {string} str Text to be converted + * @return {string} + */ +function snakeCase (str) { + return str + .replace(/([a-z])([A-Z])/g, match => match[0] + '_' + match[1]) + .replace(invalidChars, '_') + .toLowerCase() +} + /** * Convert text to camelCase * @param {string} str Text to be converted @@ -42,6 +54,7 @@ function pascalCase (str) { const convertersMap = { 'kebab-case': kebabCase, + 'snake_case': snakeCase, 'camelCase': camelCase, 'PascalCase': pascalCase } diff --git a/tests/lib/rules/prop-name-casing.js b/tests/lib/rules/prop-name-casing.js new file mode 100644 index 000000000..fcab592a8 --- /dev/null +++ b/tests/lib/rules/prop-name-casing.js @@ -0,0 +1,180 @@ +/** + * @fileoverview Define a style for the name property casing for consistency purposes + * @author Yu Kimura + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/prop-name-casing') +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const parserOptions = { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: { experimentalObjectRestSpread: true } +} + +const ruleTester = new RuleTester() +ruleTester.run('prop-name-casing', rule, { + + valid: [ + { + filename: 'test.vue', + code: ` + export default { + props: ['greetingText'] + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: ['greetingText'] + } + `, + options: ['camelCase'], + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: ['greetingText'] + } + `, + options: ['snake_case'], + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + greetingText: String + } + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + greetingText: String + } + } + `, + options: ['camelCase'], + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + greeting_text: String + } + } + `, + options: ['snake_case'], + parserOptions + } + ], + + invalid: [ + { + filename: 'test.vue', + code: ` + export default { + props: { + greeting_text: String + } + } + `, + parserOptions, + errors: [{ + message: 'Prop "greeting_text" is not camelCase.', + type: 'Property', + line: 4 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + greeting_text: String + } + } + `, + options: ['camelCase'], + parserOptions, + errors: [{ + message: 'Prop "greeting_text" is not camelCase.', + type: 'Property', + line: 4 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + greetingText: String + } + } + `, + options: ['snake_case'], + parserOptions, + errors: [{ + message: 'Prop "greetingText" is not snake_case.', + type: 'Property', + line: 4 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + 'greeting-text': String + } + } + `, + options: ['camelCase'], + parserOptions, + errors: [{ + message: 'Prop "greeting-text" is not camelCase.', + type: 'Property', + line: 4 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + 'greeting-text': String + } + } + `, + options: ['snake_case'], + parserOptions, + errors: [{ + message: 'Prop "greeting-text" is not snake_case.', + type: 'Property', + line: 4 + }] + } + ] +}) diff --git a/tests/lib/utils/casing.js b/tests/lib/utils/casing.js index 743d24fd4..e9cbe921a 100644 --- a/tests/lib/utils/casing.js +++ b/tests/lib/utils/casing.js @@ -32,4 +32,13 @@ describe('getConverter()', () => { assert.ok(converter('FooBar') === 'foo-bar') assert.ok(converter('Foo1Bar') === 'foo1bar') }) + + it('should conver string to snake_case', () => { + const converter = casing.getConverter('snake_case') + + assert.ok(converter('fooBar') === 'foo_bar') + assert.ok(converter('foo-bar') === 'foo_bar') + assert.ok(converter('FooBar') === 'foo_bar') + assert.ok(converter('Foo1Bar') === 'foo1bar') + }) }) From fc2f68947eb6ea7ae5a766f4f556e1aea748371c Mon Sep 17 00:00:00 2001 From: y Date: Thu, 4 Jan 2018 15:23:30 +0900 Subject: [PATCH 2/6] fix message and category --- lib/rules/prop-name-casing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js index 23e04a483..2c49b2c69 100644 --- a/lib/rules/prop-name-casing.js +++ b/lib/rules/prop-name-casing.js @@ -40,7 +40,7 @@ function create (context) { if (convertedName !== propName) { context.report({ node: item, - message: 'Prop "{{name}}" is not {{caseType}}.', + message: 'Prop "{{name}}" is not in {{caseType}}.', data: { name: propName, caseType: caseType @@ -59,7 +59,7 @@ module.exports = { meta: { docs: { description: 'enforce specific casing for the Prop name in Vue components', - category: 'strongly-recommended' + category: 'upcoming' }, fixable: null, // or "code" or "whitespace" schema: [ From 551e90e4cdb6244a88a21bd5e0778e6a51ea288d Mon Sep 17 00:00:00 2001 From: y Date: Thu, 4 Jan 2018 15:27:07 +0900 Subject: [PATCH 3/6] fix category --- lib/rules/prop-name-casing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js index 2c49b2c69..f72ab0c67 100644 --- a/lib/rules/prop-name-casing.js +++ b/lib/rules/prop-name-casing.js @@ -59,7 +59,7 @@ module.exports = { meta: { docs: { description: 'enforce specific casing for the Prop name in Vue components', - category: 'upcoming' + category: 'upcoming' // 'strongly-recommended' }, fixable: null, // or "code" or "whitespace" schema: [ From a5ca853b08eaa6774638f9a0432d1f89df56875e Mon Sep 17 00:00:00 2001 From: y Date: Thu, 4 Jan 2018 15:31:11 +0900 Subject: [PATCH 4/6] fix test message --- tests/lib/rules/prop-name-casing.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/lib/rules/prop-name-casing.js b/tests/lib/rules/prop-name-casing.js index fcab592a8..4679d6747 100644 --- a/tests/lib/rules/prop-name-casing.js +++ b/tests/lib/rules/prop-name-casing.js @@ -103,7 +103,7 @@ ruleTester.run('prop-name-casing', rule, { `, parserOptions, errors: [{ - message: 'Prop "greeting_text" is not camelCase.', + message: 'Prop "greeting_text" is not in camelCase.', type: 'Property', line: 4 }] @@ -120,7 +120,7 @@ ruleTester.run('prop-name-casing', rule, { options: ['camelCase'], parserOptions, errors: [{ - message: 'Prop "greeting_text" is not camelCase.', + message: 'Prop "greeting_text" is not in camelCase.', type: 'Property', line: 4 }] @@ -137,7 +137,7 @@ ruleTester.run('prop-name-casing', rule, { options: ['snake_case'], parserOptions, errors: [{ - message: 'Prop "greetingText" is not snake_case.', + message: 'Prop "greetingText" is not in snake_case.', type: 'Property', line: 4 }] @@ -154,7 +154,7 @@ ruleTester.run('prop-name-casing', rule, { options: ['camelCase'], parserOptions, errors: [{ - message: 'Prop "greeting-text" is not camelCase.', + message: 'Prop "greeting-text" is not in camelCase.', type: 'Property', line: 4 }] @@ -171,7 +171,7 @@ ruleTester.run('prop-name-casing', rule, { options: ['snake_case'], parserOptions, errors: [{ - message: 'Prop "greeting-text" is not snake_case.', + message: 'Prop "greeting-text" is not in snake_case.', type: 'Property', line: 4 }] From f33d47d57bb14cd2b2184940da436bde65b974af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sajn=C3=B3g?= Date: Sat, 6 Jan 2018 23:11:45 +0100 Subject: [PATCH 5/6] Set category to undefined --- lib/rules/prop-name-casing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js index f72ab0c67..d5df99c54 100644 --- a/lib/rules/prop-name-casing.js +++ b/lib/rules/prop-name-casing.js @@ -59,7 +59,7 @@ module.exports = { meta: { docs: { description: 'enforce specific casing for the Prop name in Vue components', - category: 'upcoming' // 'strongly-recommended' + category: undefined // 'strongly-recommended' }, fixable: null, // or "code" or "whitespace" schema: [ From 605a152a72d50be52b08e0443a4d17327fef9578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sajn=C3=B3g?= Date: Sun, 28 Jan 2018 23:51:36 +0100 Subject: [PATCH 6/6] Add more tests and fix edge case scenario --- lib/rules/prop-name-casing.js | 4 +++- tests/lib/rules/prop-name-casing.js | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js index d5df99c54..02070af22 100644 --- a/lib/rules/prop-name-casing.js +++ b/lib/rules/prop-name-casing.js @@ -25,8 +25,10 @@ function create (context) { const node = obj.properties.find(p => p.type === 'Property' && p.key.type === 'Identifier' && - p.key.name === 'props' + p.key.name === 'props' && + (p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression') ) + if (!node) return const items = node.value.type === 'ObjectExpression' ? node.value.properties : node.value.elements diff --git a/tests/lib/rules/prop-name-casing.js b/tests/lib/rules/prop-name-casing.js index 4679d6747..3df60c8ba 100644 --- a/tests/lib/rules/prop-name-casing.js +++ b/tests/lib/rules/prop-name-casing.js @@ -34,6 +34,26 @@ ruleTester.run('prop-name-casing', rule, { `, parserOptions }, + { + filename: 'test.vue', + code: ` + export default { + props: some_props + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + ...some_props, + } + } + `, + parserOptions + }, { filename: 'test.vue', code: `