diff --git a/docs/rules/prop-specificity.md b/docs/rules/prop-specificity.md new file mode 100644 index 000000000..d77c78931 --- /dev/null +++ b/docs/rules/prop-specificity.md @@ -0,0 +1,42 @@ +# Prop definitions should be detailed (prop-specificity) + +In committed code, prop definitions should always be as detailed as possible, specifying at least type(s). + +## :book: Rule Details + +This rule enforces that a `props` statement contains type definition. + +:-1: Examples of **incorrect** code for this rule: + +```js +export default { + props: ['status'] +} +``` + +:+1: Examples of **correct** code for this rule: + +```js +export default { + props: { + status: String + } +} +``` + +```js +export default { + props: { + status: { + type: String, + required: true, + validate: function (value) { + return ['syncing', 'synced', 'version-conflict', 'error'].indexOf(value) !== -1 + } + } + } +} +``` +## :wrench: Options + +Nothing. diff --git a/lib/rules/prop-specificity.js b/lib/rules/prop-specificity.js new file mode 100644 index 000000000..149e42f29 --- /dev/null +++ b/lib/rules/prop-specificity.js @@ -0,0 +1,107 @@ +/** + * @fileoverview Prop definitions should be detailed + * @author Armano + */ +'use strict' + +const utils = require('../utils') + +function create (context) { + // variables should be defined here + + // ---------------------------------------------------------------------- + // Helpers + // ---------------------------------------------------------------------- + + function getPropTypes (componentProperties) { + const node = componentProperties + .filter(p => + p.key.type === 'Identifier' && + p.key.name === 'props' + )[0] + + if (!node) { + return { + type: null, + nodes: [] + } + } + + let nodes = [] + const type = node.value.type + if (type === 'ObjectExpression') { // props: { + nodes = node.value.properties.map(cp => { + const key = cp.key.name + let hasType = true + if (cp.value.type === 'ObjectExpression') { // foo: { + hasType = !!(cp.value.properties + .filter(p => + p.key.type === 'Identifier' && + p.key.name === 'type' && + ( + p.value.type !== 'ArrayExpression' || + p.value.elements.length > 0 + ) + )[0]) + } else if (cp.value.type === 'ArrayExpression') { // foo: [ + hasType = cp.value.elements.length > 0 + } + return { key, node: cp, hasType } + }) + } else if (type === 'ArrayExpression') { // props: [ + // nodes = node.elements + } + + return { + type, + node, + nodes + } + } + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + return utils.executeOnVueComponent(context, (obj) => { + const data = getPropTypes(obj.properties) + if (data.type === 'ObjectExpression') { + data.nodes.forEach(cp => { + if (!cp.hasType) { + context.report({ + node: cp.node || data.node, + message: 'Prop "{{name}}" definitions should always be as detailed with at least type(s).', + data: { + name: cp.key + } + }) + } + }) + } else if (data.type === 'ArrayExpression') { + context.report({ + node: data.node, + message: 'Props definitions should always be an object and detailed with at least type(s).' + }) + } + }) +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'Prop definitions should be detailed', + category: 'Best Practices', + recommended: false + }, + fixable: null, // or "code" or "whitespace" + schema: [ + // fill in your schema + ] + }, + + create +} diff --git a/tests/lib/rules/prop-specificity.js b/tests/lib/rules/prop-specificity.js new file mode 100644 index 000000000..49b5df7d5 --- /dev/null +++ b/tests/lib/rules/prop-specificity.js @@ -0,0 +1,121 @@ +/** + * @fileoverview Prop definitions should be detailed + * @author Armano + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/prop-specificity') + +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +var ruleTester = new RuleTester() +ruleTester.run('prop-specificity', rule, { + + valid: [ + { + filename: 'test.vue', + code: ` + export default { + props: { + foo: String + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + foo: [String, Number] + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + foo: { + type: String + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + } + ], + + invalid: [ + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'] + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [{ + message: 'Props definitions should always be an object and detailed with at least type(s).', + line: 3 + }] + }, + { + filename: 'test.js', + code: ` + new Vue({ + props: ['foo'] + }) + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [{ + message: 'Props definitions should always be an object and detailed with at least type(s).', + line: 3 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + foo: { + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [{ + message: 'Prop "foo" definitions should always be as detailed with at least type(s).', + line: 4 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + foo: { + type: [] + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [{ + message: 'Prop "foo" definitions should always be as detailed with at least type(s).', + line: 4 + }] + } + ] +})