From 39c9df59256580a3189d1e3edf3a5515d41b474f Mon Sep 17 00:00:00 2001 From: Armano Date: Tue, 1 Aug 2017 16:18:25 +0200 Subject: [PATCH] Add rules: `no-dupe-keys` and `no-reserved-keys`. (#88) fixes #86 --- docs/rules/no-dupe-keys.md | 78 ++++++++++++ docs/rules/no-reservered-keys.md | 54 +++++++++ lib/rules/no-dupe-keys.js | 68 +++++++++++ lib/rules/no-reservered-keys.js | 74 ++++++++++++ lib/utils/index.js | 67 +++++++++++ lib/utils/vue-reserved.json | 4 + package-lock.json | 6 +- tests/lib/rules/no-dupe-keys.js | 165 ++++++++++++++++++++++++++ tests/lib/rules/no-reservered-keys.js | 94 +++++++++++++++ 9 files changed, 607 insertions(+), 3 deletions(-) create mode 100644 docs/rules/no-dupe-keys.md create mode 100644 docs/rules/no-reservered-keys.md create mode 100644 lib/rules/no-dupe-keys.js create mode 100644 lib/rules/no-reservered-keys.js create mode 100644 lib/utils/vue-reserved.json create mode 100644 tests/lib/rules/no-dupe-keys.js create mode 100644 tests/lib/rules/no-reservered-keys.js diff --git a/docs/rules/no-dupe-keys.md b/docs/rules/no-dupe-keys.md new file mode 100644 index 000000000..990001881 --- /dev/null +++ b/docs/rules/no-dupe-keys.md @@ -0,0 +1,78 @@ +# Prevents duplication of field names (no-dupe-keys) + +This rule prevents to use duplicated names. + +## :book: Rule Details + +This rule is aimed at preventing duplicated property names. + +:-1: Examples of **incorrect** code for this rule: + +```js +export default { + props: { + foo: String + }, + computed: { + foo: { + get () { + } + } + }, + data: { + foo: null + }, + methods: { + foo () { + } + } +} +``` + +:+1: Examples of **correct** code for this rule: + +```js +export default { + props: ['foo'], + computed: { + bar () { + } + }, + data () { + return { + dat: null + } + }, + methods: { + test () { + } + } +} +``` + +## :wrench: Options + +This rule has an object option: + +`"groups"`: [] (default) array of additional groups to search for duplicates. + +### Example: + +``` +vue/no-dupe-keys: [2, { + groups: ['asyncComputed'] +}] +``` + +:-1: Examples of **incorrect** code for this configuration + +```js +export default { + computed: { + foo () {} + }, + asyncComputed: { + foo () {} + } +} +``` diff --git a/docs/rules/no-reservered-keys.md b/docs/rules/no-reservered-keys.md new file mode 100644 index 000000000..4ec1aaa1f --- /dev/null +++ b/docs/rules/no-reservered-keys.md @@ -0,0 +1,54 @@ +# Prevent overwrite reserved keys (no-reservered-keys) + +This rule prevents to use reserved names from to avoid conflicts and unexpected behavior. + +## Rule Details + +:-1: Examples of **incorrect** code for this rule: + +```js +export default { + props: { + $el: String + }, + computed: { + $on: { + get () { + } + } + }, + data: { + _foo: null + }, + methods: { + $nextTick () { + } + } +} +``` + +## :wrench: Options + +This rule has an object option: + +`"reserved"`: [] (default) array of dissalowed names inside `groups`. + +`"groups"`: [] (default) array of additional groups to search for duplicates. + +### Example: + +``` +vue/no-dupe-keys: [2, { + reserved: ['foo'] +}] +``` + +:-1: Examples of **incorrect** code for this configuration + +```js +export default { + computed: { + foo () {} + } +} +``` diff --git a/lib/rules/no-dupe-keys.js b/lib/rules/no-dupe-keys.js new file mode 100644 index 000000000..e9bcdcbab --- /dev/null +++ b/lib/rules/no-dupe-keys.js @@ -0,0 +1,68 @@ +/** + * @fileoverview Prevents duplication of field names. + * @author Armano + */ +'use strict' + +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +const GROUP_NAMES = ['props', 'computed', 'data', 'methods'] + +function create (context) { + const usedNames = [] + + const options = context.options[0] || {} + const groups = new Set(GROUP_NAMES.concat(options.groups || [])) + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + return utils.executeOnVue(context, (obj) => { + const properties = utils.iterateProperties(obj, groups) + for (const o of properties) { + if (usedNames.indexOf(o.name) !== -1) { + context.report({ + node: o.node, + message: "Duplicated key '{{name}}'.", + data: { + name: o.name + } + }) + } + usedNames.push(o.name) + } + }) +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'Prevents duplication of field names.', + category: 'Possible Errors', + recommended: false + }, + fixable: null, // or "code" or "whitespace" + schema: [ + { + type: 'object', + properties: { + groups: { + type: 'array' + } + }, + additionalProperties: false + } + ] + }, + + create +} diff --git a/lib/rules/no-reservered-keys.js b/lib/rules/no-reservered-keys.js new file mode 100644 index 000000000..c0ba613f2 --- /dev/null +++ b/lib/rules/no-reservered-keys.js @@ -0,0 +1,74 @@ +/** + * @fileoverview Prevent overwrite reserved keys + * @author Armano + */ +'use strict' + +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +const RESERVED_KEYS = require('../utils/vue-reserved.json') +const GROUP_NAMES = ['props', 'computed', 'data', 'methods'] + +function create (context) { + const options = context.options[0] || {} + const reservedKeys = new Set(RESERVED_KEYS.concat(options.reserved || [])) + const groups = new Set(GROUP_NAMES.concat(options.groups || [])) + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + return utils.executeOnVue(context, (obj) => { + const properties = utils.iterateProperties(obj, groups) + for (const o of properties) { + if (o.groupName === 'data' && o.name[0] === '_') { + context.report({ + node: o.node, + message: "Keys starting with with '_' are reserved in '{{name}}' group.", + data: { + name: o.name + } + }) + } else if (reservedKeys.has(o.name)) { + context.report({ + node: o.node, + message: "Key '{{name}}' is reserved.", + data: { + name: o.name + } + }) + } + } + }) +} + +module.exports = { + meta: { + docs: { + description: 'Prevent overwrite reserved keys.', + category: 'Possible Errors', + recommended: false + }, + fixable: null, // or "code" or "whitespace" + schema: [ + { + type: 'object', + properties: { + reserved: { + type: 'array' + }, + groups: { + type: 'array' + } + }, + additionalProperties: false + } + ] + }, + + create +} diff --git a/lib/utils/index.js b/lib/utils/index.js index ccf3b9157..2cf39ddc4 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -453,5 +453,72 @@ module.exports = { cb(node.arguments.slice(-1)[0]) } } + }, + + /** + * Return generator with all properties + * @param {ASTNode} node Node to check + * @param {string} groupName Name of parent group + */ + * iterateProperties (node, groups) { + const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(this.getStaticPropertyName(p.key))) + for (const item of nodes) { + const name = this.getStaticPropertyName(item.key) + if (item.value.type === 'ArrayExpression') { + yield * this.iterateArrayExpression(item.value, name) + } else if (item.value.type === 'ObjectExpression') { + yield * this.iterateObjectExpression(item.value, name) + } else if (item.value.type === 'FunctionExpression') { + yield * this.iterateFunctionExpression(item.value, name) + } + } + }, + + /** + * Return generator with all elements inside ArrayExpression + * @param {ASTNode} node Node to check + * @param {string} groupName Name of parent group + */ + * iterateArrayExpression (node, groupName) { + assert(node.type === 'ArrayExpression') + for (const item of node.elements) { + const name = this.getStaticPropertyName(item) + if (name) { + const obj = { name, groupName, node: item } + yield obj + } + } + }, + + /** + * Return generator with all elements inside ObjectExpression + * @param {ASTNode} node Node to check + * @param {string} groupName Name of parent group + */ + * iterateObjectExpression (node, groupName) { + assert(node.type === 'ObjectExpression') + for (const item of node.properties) { + const name = this.getStaticPropertyName(item) + if (name) { + const obj = { name, groupName, node: item.key } + yield obj + } + } + }, + + /** + * Return generator with all elements inside FunctionExpression + * @param {ASTNode} node Node to check + * @param {string} groupName Name of parent group + */ + * iterateFunctionExpression (node, groupName) { + assert(node.type === 'FunctionExpression') + if (node.body.type === 'BlockStatement') { + for (const item of node.body.body) { + if (item.type === 'ReturnStatement' && item.argument.type === 'ObjectExpression') { + yield * this.iterateObjectExpression(item.argument, groupName) + } + } + } } } diff --git a/lib/utils/vue-reserved.json b/lib/utils/vue-reserved.json new file mode 100644 index 000000000..03bcdcba9 --- /dev/null +++ b/lib/utils/vue-reserved.json @@ -0,0 +1,4 @@ +[ + "$data", "$props", "$el", "$options", "$parent", "$root", "$children", "$slots", "$scopedSlots", "$refs", "$isServer", "$attrs", "$listeners", + "$watch", "$set", "$delete", "$on", "$once", "$off", "$emit", "$mount", "$forceUpdate", "$nextTick", "$destroy" +] diff --git a/package-lock.json b/package-lock.json index 177793b26..3496e7ced 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3198,9 +3198,9 @@ "dev": true }, "vue-eslint-parser": { - "version": "2.0.0-beta.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.0-beta.0.tgz", - "integrity": "sha512-GfJgr4S0kEBsT2k24yqBaYIq1cKHXwiajrEzF+6gpr56pRFY/5FibDwCTEo4QmNqugtG5XSKwlwwygDMMH8g5g==", + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.0-beta.2.tgz", + "integrity": "sha512-bcoX+jDpF7xY3nbPK9W6yBiH7I0WOFA1SU0LnYkusZzY6ZRGbJWOiyNpl1TWAkNmLH0Az2uGVVnzsBVB1aSl6w==", "requires": { "debug": "2.6.8", "eslint-scope": "3.7.1", diff --git a/tests/lib/rules/no-dupe-keys.js b/tests/lib/rules/no-dupe-keys.js new file mode 100644 index 000000000..fbb314c90 --- /dev/null +++ b/tests/lib/rules/no-dupe-keys.js @@ -0,0 +1,165 @@ +/** + * @fileoverview Prevents duplication of field names. + * @author Armano + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-dupe-keys') +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester() +ruleTester.run('no-dupe-keys', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + computed: { + bar () { + } + }, + data () { + return { + dat: null + } + }, + methods: { + _foo () {}, + test () { + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + + { + filename: 'test.vue', + code: ` + export default { + ...foo(), + props: { + ...foo(), + foo: String + }, + computed: { + ...mapGetters({ + test: 'getTest' + }), + bar: { + get () { + } + } + }, + data: { + ...foo(), + dat: null + }, + methods: { + ...foo(), + test () { + } + } + } + `, + parserOptions: { ecmaVersion: 8, sourceType: 'module', ecmaFeatures: { experimentalObjectRestSpread: true }} + } + ], + + invalid: [ + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + computed: { + foo () { + } + }, + data () { + return { + foo: null + } + }, + methods: { + foo () { + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [{ + message: 'Duplicated key \'foo\'.', + line: 5 + }, { + message: 'Duplicated key \'foo\'.', + line: 10 + }, { + message: 'Duplicated key \'foo\'.', + line: 14 + }] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + foo: String + }, + computed: { + foo: { + get () { + } + } + }, + data: { + foo: null + }, + methods: { + foo () { + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [{ + message: 'Duplicated key \'foo\'.', + line: 7 + }, { + message: 'Duplicated key \'foo\'.', + line: 13 + }, { + message: 'Duplicated key \'foo\'.', + line: 16 + }] + }, + { + filename: 'test.js', + code: ` + new Vue({ + foo: { + bar: String + }, + data: { + bar: null + }, + }) + `, + options: [{ groups: ['foo'] }], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + message: 'Duplicated key \'bar\'.', + line: 7 + }] + } + ] +}) diff --git a/tests/lib/rules/no-reservered-keys.js b/tests/lib/rules/no-reservered-keys.js new file mode 100644 index 000000000..378a840c2 --- /dev/null +++ b/tests/lib/rules/no-reservered-keys.js @@ -0,0 +1,94 @@ +/** + * @fileoverview Prevent overwrite reserved keys + * @author Armano + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-reservered-keys') +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester() +ruleTester.run('no-reservered-keys', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + computed: { + bar () { + } + }, + data () { + return { + dat: null + } + }, + methods: { + _foo () {}, + test () { + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + } + ], + + invalid: [ + { + filename: 'test.js', + code: ` + new Vue({ + props: { + $el: String + } + }) + `, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + message: "Key '$el' is reserved.", + line: 4 + }] + }, + { + filename: 'test.js', + code: ` + new Vue({ + data: { + _foo: String + } + }) + `, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + message: "Keys starting with with '_' are reserved in '_foo' group.", + line: 4 + }] + }, + { + filename: 'test.js', + code: ` + new Vue({ + foo: { + bar: String + } + }) + `, + options: [{ reserved: ['bar'], groups: ['foo'] }], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + message: "Key 'bar' is reserved.", + line: 4 + }] + } + ] +})