From c4e7cc341b5b2d766483071ed54e5879dca05a96 Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 2 Aug 2017 04:50:23 +0200 Subject: [PATCH 1/2] Add rule `vue/require-valid-default-prop`. fixes #117 --- docs/rules/require-valid-default-prop.md | 64 +++ lib/rules/require-valid-default-prop.js | 134 +++++++ tests/lib/rules/require-valid-default-prop.js | 378 ++++++++++++++++++ 3 files changed, 576 insertions(+) create mode 100644 docs/rules/require-valid-default-prop.md create mode 100644 lib/rules/require-valid-default-prop.js create mode 100644 tests/lib/rules/require-valid-default-prop.js diff --git a/docs/rules/require-valid-default-prop.md b/docs/rules/require-valid-default-prop.md new file mode 100644 index 000000000..564349a2a --- /dev/null +++ b/docs/rules/require-valid-default-prop.md @@ -0,0 +1,64 @@ +# Enforces prop default values to be valid (require-valid-default-prop) + +This rule is doing basic type checking between type and default value and inform about missuesed or invalid default values. +Type checking is working for all `Native types`, with requirement that `Array` and `Object` has to be a function. + +## :book: Rule Details + +:-1: Examples of **incorrect** code for this rule: + +```js +Vue.component('example', { + props: { + propA: { + type: String, + default: {} + }, + propB: { + type: String, + default: [] + }, + propC: { + type: Object, + default: [] + }, + propD: { + type: Array, + default: [] + }, + propE: { + type: Object, + default: { message: 'hello' } + } + } +}) +``` + +:+1: Examples of **correct** code for this rule: + +```js +Vue.component('example', { + props: { + // basic type check (`null` means accept any type) + propA: Number, + // multiple possible types + propB: [String, Number], + // a number with default value + propD: { + type: Number, + default: 100 + }, + // object/array defaults should be returned from a factory function + propE: { + type: Object, + default: function () { + return { message: 'hello' } + } + } + } +}) +``` + +## :wrench: Options + +Nothing. diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js new file mode 100644 index 000000000..ba15da450 --- /dev/null +++ b/lib/rules/require-valid-default-prop.js @@ -0,0 +1,134 @@ +/** + * @fileoverview Enforces prop default values to be valid. + * @author Armano + */ +'use strict' +const utils = require('../utils') + +const NATIVE_TYPES = new Set([ + 'String', + 'Number', + 'Boolean', + 'Function', + 'Object', + 'Array', + 'Symbol' +]) + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'Enforces prop default values to be valid.', + category: 'Possible Errors', + recommended: false + }, + fixable: null, + schema: [] + }, + + create (context) { + // ---------------------------------------------------------------------- + // Helpers + // ---------------------------------------------------------------------- + + function isPropertyIdentifier (node) { + return node.type === 'Property' && node.key.type === 'Identifier' + } + + function getPropertyNode (obj, name) { + return obj.properties.find(p => + isPropertyIdentifier(p) && + p.key.name === name + ) + } + + function getTypes (node) { + if (node.type === 'Identifier') { + return [node.name] + } else if (node.type === 'ArrayExpression') { + return node.elements + .filter(item => item.type === 'Identifier') + .map(item => item.name) + } + return [] + } + + function ucFirst (text) { + return text[0].toUpperCase() + text.slice(1) + } + + function getValueType (node) { + if (node.type === 'CallExpression') { // Symbol(), Number() ... + if (node.callee.type === 'Identifier' && NATIVE_TYPES.has(node.callee.name)) { + return node.callee.name + } + } else if (node.type === 'TemplateLiteral') { // String + return 'String' + } else if (node.type === 'Literal') { // String, Boolean, Number + if (node.value === null) return null + const type = ucFirst(typeof node.value) + if (NATIVE_TYPES.has(type)) { + return type + } + } else if (node.type === 'ArrayExpression') { // Array + return 'Array' + } else if (node.type === 'ObjectExpression') { // Array + return 'Object' + } + // FunctionExpression, ArrowFunctionExpression + return null + } + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + return utils.executeOnVue(context, obj => { + const props = obj.properties.find(p => + isPropertyIdentifier(p) && + p.key.name === 'props' && + p.value.type === 'ObjectExpression' + ) + if (!props) return + + const properties = props.value.properties.filter(p => + isPropertyIdentifier(p) && + p.value.type === 'ObjectExpression' + ) + + for (const prop of properties) { + const type = getPropertyNode(prop.value, 'type') + if (!type) { + return + } + + const typeNames = new Set(getTypes(type.value) + .map(item => item === 'Object' || item === 'Array' ? 'Function' : item) // Object and Array require function + .filter(item => NATIVE_TYPES.has(item))) + + if (typeNames.size === 0) { // There is no native types detected + return + } + + const def = getPropertyNode(prop.value, 'default') + if (!def) return + + const defType = getValueType(def.value) + if (typeNames.has(defType)) return + + context.report({ + node: def, + message: "Type of default value prop '{{name}}' must be a {{types}}.", + data: { + name: prop.key.name, + types: Array.from(typeNames).join(' or ').toLowerCase() + } + }) + } + }) + } +} diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js new file mode 100644 index 000000000..665d3d309 --- /dev/null +++ b/tests/lib/rules/require-valid-default-prop.js @@ -0,0 +1,378 @@ +/** + * @fileoverview Enforces prop default values to be valid. + * @author Armano + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/require-valid-default-prop') +const RuleTester = require('eslint').RuleTester + +const parserOptions = { + ecmaVersion: 6, + sourceType: 'module' +} + +function errorMessage (type) { + return [{ + message: `Type of default value prop 'foo' must be a ${type}.`, + type: 'Property', + line: 5 + }] +} + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester() +ruleTester.run('require-valid-default-prop', rule, { + + valid: [ + { + filename: 'test.vue', + code: `export default { + props: { foo: null } + }`, + parserOptions + }, + { + filename: 'test.vue', + code: `export default { + props: ['foo'] + }`, + parserOptions + }, + { + filename: 'test.vue', + code: `Vue.component('example', { + props: { + foo: null, + foo: Number, + foo: [String, Number], + foo: { type: Number, default: 100 }, + foo: { type: [String, Number], default: '' }, + foo: { type: [String, Number], default: 0 }, + foo: { type: String, default: '' }, + foo: { type: String, default: \`\` }, + foo: { type: Boolean, default: false }, + foo: { type: Object, default: () => { } }, + foo: { type: Array, default () { } }, + foo: { type: String, default () { } }, + foo: { type: Number, default () { } }, + foo: { type: Boolean, default () { } }, + foo: { type: Symbol, default () { } }, + foo: { type: Array, default () { } }, + foo: { type: Symbol, default: Symbol('a') }, + foo: { type: String, default: \`Foo\` } + } + })`, + parserOptions + } + ], + + invalid: [ + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: [Number, String], + default: {} + } + } + }`, + parserOptions, + errors: errorMessage('number or string') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: [Number, Object], + default: {} + } + } + }`, + parserOptions, + errors: errorMessage('number or function') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Number, + default: '' + } + } + }`, + parserOptions, + errors: errorMessage('number') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Number, + default: false + } + } + }`, + parserOptions, + errors: errorMessage('number') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Number, + default: {} + } + } + }`, + parserOptions, + errors: errorMessage('number') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Number, + default: [] + } + } + }`, + parserOptions, + errors: errorMessage('number') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: String, + default: 2 + } + } + }`, + parserOptions, + errors: errorMessage('string') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: String, + default: {} + } + } + }`, + parserOptions, + errors: errorMessage('string') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: String, + default: [] + } + } + }`, + parserOptions, + errors: errorMessage('string') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Boolean, + default: '' + } + } + }`, + parserOptions, + errors: errorMessage('boolean') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Boolean, + default: 5 + } + } + }`, + parserOptions, + errors: errorMessage('boolean') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Boolean, + default: {} + } + } + }`, + parserOptions, + errors: errorMessage('boolean') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Boolean, + default: [] + } + } + }`, + parserOptions, + errors: errorMessage('boolean') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Object, + default: '' + } + } + }`, + parserOptions, + errors: errorMessage('function') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Object, + default: 55 + } + } + }`, + parserOptions, + errors: errorMessage('function') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Object, + default: false + } + } + }`, + parserOptions, + errors: errorMessage('function') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Object, + default: {} + } + } + }`, + parserOptions, + errors: errorMessage('function') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Object, + default: [] + } + } + }`, + parserOptions, + errors: errorMessage('function') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Array, + default: '' + } + } + }`, + parserOptions, + errors: errorMessage('function') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Array, + default: 55 + } + } + }`, + parserOptions, + errors: errorMessage('function') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Array, + default: false + } + } + }`, + parserOptions, + errors: errorMessage('function') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Array, + default: {} + } + } + }`, + parserOptions, + errors: errorMessage('function') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Array, + default: [] + } + } + }`, + parserOptions, + errors: errorMessage('function') + } + ] +}) From b85a00cb3484105f950c4f4da9998ff83fb485ad Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 4 Aug 2017 17:36:35 +0200 Subject: [PATCH 2/2] Update doc & error message & add more tests --- docs/rules/require-valid-default-prop.md | 5 ++-- lib/rules/require-valid-default-prop.js | 8 ++--- tests/lib/rules/require-valid-default-prop.js | 29 +++++++++++++++++-- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/docs/rules/require-valid-default-prop.md b/docs/rules/require-valid-default-prop.md index 564349a2a..a571ecf33 100644 --- a/docs/rules/require-valid-default-prop.md +++ b/docs/rules/require-valid-default-prop.md @@ -1,7 +1,6 @@ -# Enforces prop default values to be valid (require-valid-default-prop) +# Enforces props default values to be valid (require-valid-default-prop) -This rule is doing basic type checking between type and default value and inform about missuesed or invalid default values. -Type checking is working for all `Native types`, with requirement that `Array` and `Object` has to be a function. +This rule checks whether the default value of each prop is valid for the given type. It should report an error when default value for type `Array` or `Object` is not returned using function. ## :book: Rule Details diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js index ba15da450..22dbe5b4d 100644 --- a/lib/rules/require-valid-default-prop.js +++ b/lib/rules/require-valid-default-prop.js @@ -1,5 +1,5 @@ /** - * @fileoverview Enforces prop default values to be valid. + * @fileoverview Enforces props default values to be valid. * @author Armano */ 'use strict' @@ -22,7 +22,7 @@ const NATIVE_TYPES = new Set([ module.exports = { meta: { docs: { - description: 'Enforces prop default values to be valid.', + description: 'Enforces props default values to be valid.', category: 'Possible Errors', recommended: false }, @@ -76,7 +76,7 @@ module.exports = { } } else if (node.type === 'ArrayExpression') { // Array return 'Array' - } else if (node.type === 'ObjectExpression') { // Array + } else if (node.type === 'ObjectExpression') { // Object return 'Object' } // FunctionExpression, ArrowFunctionExpression @@ -122,7 +122,7 @@ module.exports = { context.report({ node: def, - message: "Type of default value prop '{{name}}' must be a {{types}}.", + message: "Type of the default value for '{{name}}' prop must be a {{types}}.", data: { name: prop.key.name, types: Array.from(typeNames).join(' or ').toLowerCase() diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js index 665d3d309..8c4a84abf 100644 --- a/tests/lib/rules/require-valid-default-prop.js +++ b/tests/lib/rules/require-valid-default-prop.js @@ -1,5 +1,5 @@ /** - * @fileoverview Enforces prop default values to be valid. + * @fileoverview Enforces props default values to be valid. * @author Armano */ 'use strict' @@ -18,7 +18,7 @@ const parserOptions = { function errorMessage (type) { return [{ - message: `Type of default value prop 'foo' must be a ${type}.`, + message: `Type of the default value for 'foo' prop must be a ${type}.`, type: 'Property', line: 5 }] @@ -46,6 +46,18 @@ ruleTester.run('require-valid-default-prop', rule, { }`, parserOptions }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: [Object, Number], + default: 10 + } + } + }`, + parserOptions + }, { filename: 'test.vue', code: `Vue.component('example', { @@ -373,6 +385,19 @@ ruleTester.run('require-valid-default-prop', rule, { }`, parserOptions, errors: errorMessage('function') + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: [Object, Number], + default: {} + } + } + }`, + parserOptions, + errors: errorMessage('function or number') } ] })