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..02070af22 --- /dev/null +++ b/lib/rules/prop-name-casing.js @@ -0,0 +1,75 @@ +/** + * @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' && + (p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression') + ) + + 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 in {{caseType}}.', + data: { + name: propName, + caseType: caseType + } + }) + } + } + }) +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'enforce specific casing for the Prop name in Vue components', + category: undefined // '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..3df60c8ba --- /dev/null +++ b/tests/lib/rules/prop-name-casing.js @@ -0,0 +1,200 @@ +/** + * @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: some_props + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + ...some_props, + } + } + `, + 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 in 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 in camelCase.', + type: 'Property', + line: 4 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + greetingText: String + } + } + `, + options: ['snake_case'], + parserOptions, + errors: [{ + message: 'Prop "greetingText" is not in 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 in 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 in 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') + }) })