diff --git a/README.md b/README.md index 855ca4200..17efac0a4 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Create `.eslintrc.*` file to configure rules. See also: [http://eslint.org/docs/ Example **.eslintrc.js**: -```javascript +```js module.exports = { extends: [ 'eslint:recommended', @@ -44,7 +44,35 @@ module.exports = { } ``` -## ⚙ Configs +### Attention + +All component-related rules are being applied to code that passes any of the following checks: + +* `Vue.component()` expression +* `export default {}` in `.vue` or `.jsx` file + +If you however want to take advantage of our rules in any of your custom objects that are Vue components, you might need to use special comment `// @vue/component` that marks object in the next line as a Vue component in any file, e.g.: + +```js +// @vue/component +const CustomComponent = { + name: 'custom-component', + template: '
' +} +``` +```js +Vue.component('AsyncComponent', (resolve, reject) => { + setTimeout(() => { + // @vue/component + resolve({ + name: 'async-component', + template: '
' + }) + }, 500) +}) +``` + +## :gear: Configs This plugin provides two predefined configs: - `plugin:vue/base` - contains necessary settings for this plugin to work properly diff --git a/lib/utils/index.js b/lib/utils/index.js index 3a3428268..ccac6cdd4 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -431,17 +431,30 @@ module.exports = { */ executeOnVueComponent (context, cb) { const filePath = context.getFilename() + const sourceCode = context.getSourceCode() const _this = this + const componentComments = sourceCode.getAllComments().filter(comment => /@vue\/component/g.test(comment.value)) + const foundNodes = [] + + const isDuplicateNode = (node) => { + if (foundNodes.some(el => el.loc.start.line === node.loc.start.line)) return true + foundNodes.push(node) + return false + } return { + ObjectExpression (node) { + if (!componentComments.some(el => el.loc.end.line === node.loc.start.line - 1) || isDuplicateNode(node)) return + cb(node) + }, 'ExportDefaultDeclaration:exit' (node) { // export default {} in .vue || .jsx - if (!_this.isVueComponentFile(node, filePath)) return + if (!_this.isVueComponentFile(node, filePath) || isDuplicateNode(node.declaration)) return cb(node.declaration) }, 'CallExpression:exit' (node) { // Vue.component('xxx', {}) || component('xxx', {}) - if (!_this.isVueComponent(node)) return + if (!_this.isVueComponent(node) || isDuplicateNode(node.arguments.slice(-1)[0])) return cb(node.arguments.slice(-1)[0]) } } diff --git a/tests/lib/utils/vue-component.js b/tests/lib/utils/vue-component.js new file mode 100644 index 000000000..38b696333 --- /dev/null +++ b/tests/lib/utils/vue-component.js @@ -0,0 +1,277 @@ +/** + * @author Armano + */ +'use strict' + +const utils = require('../../../lib/utils/index') + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = { + create (context) { + return utils.executeOnVueComponent(context, obj => { + context.report({ + node: obj, + message: 'Component detected.' + }) + }) + }, + meta: { + fixable: null, + schema: [] + } +} + +const RuleTester = require('eslint').RuleTester +const parserOptions = { + ecmaVersion: 6, + sourceType: 'module' +} + +function makeError (line) { + return { + message: 'Component detected.', + line, + type: 'ObjectExpression' + } +} + +function validTests (ext) { + return [ + { + filename: `test.${ext}`, + code: `export const foo = {}`, + parserOptions + }, + { + filename: `test.${ext}`, + code: `export var foo = {}`, + parserOptions + }, + { + filename: `test.${ext}`, + code: `const foo = {}`, + parserOptions + }, + { + filename: `test.${ext}`, + code: `var foo = {}`, + parserOptions + }, + { + filename: `test.${ext}`, + code: `let foo = {}`, + parserOptions + }, + { + filename: `test.${ext}`, + code: `foo({ })`, + parserOptions + }, + { + filename: `test.${ext}`, + code: `foo(() => { return {} })`, + parserOptions + }, + { + filename: `test.${ext}`, + code: `Vue.component('async-example', function (resolve, reject) { })`, + parserOptions + }, + { + filename: `test.${ext}`, + code: `Vue.component('async-example', function (resolve, reject) { resolve({}) })`, + parserOptions + }, + { + filename: `test.${ext}`, + code: `new Vue({ })`, + parserOptions + }, + { + filename: `test.${ext}`, + code: `{ + foo: {} + }`, + parserOptions + } + ] +} + +function invalidTests (ext) { + return [ + { + filename: `test.${ext}`, + code: ` + Vue.component('async-example', function (resolve, reject) { + // @vue/component + resolve({}) + }) + // ${ext} + `, + parserOptions, + errors: [makeError(4)] + }, + { + filename: `test.${ext}`, + code: `Vue.component({})`, + parserOptions, + errors: [makeError(1)] + }, + { + filename: `test.${ext}`, + code: ` + // @vue/component + export default { } + // ${ext} + `, + parserOptions, + errors: [makeError(3)] + }, + { + filename: `test.${ext}`, + code: ` + /* @vue/component */ + export default { } + // ${ext} + `, + parserOptions, + errors: [makeError(3)] + }, + { + filename: `test.${ext}`, + code: ` + /* + * ext: ${ext} + * @vue/component + */ + export default { } + // ${ext} + `, + parserOptions, + errors: [makeError(6)] + }, + { + filename: `test.${ext}`, + code: ` + // @vue/component + export default { } + // @vue/component + export var a = { } + // ${ext} + `, + parserOptions, + errors: [makeError(3), makeError(5)] + }, + { + filename: `test.${ext}`, + code: ` + /* @vue/component */ + export const foo = { } + /* @vue/component */ + export default { } + // ${ext} + `, + parserOptions, + errors: [makeError(3), makeError(5)] + }, + { + filename: `test.${ext}`, + code: ` + export default { } + // @vue/component + export let foo = { } + // ${ext} + `, + parserOptions, + errors: (ext === 'js' ? [] : [makeError(2)]).concat([makeError(4)]) + }, + { + filename: `test.${ext}`, + code: ` + let foo = { } + // @vue/component + export let bar = { } + // ${ext} + `, + parserOptions, + errors: [makeError(4)] + }, + { + filename: `test.${ext}`, + code: ` + export var dar = { } + // @vue/component + foo({ }) + bar({ }) + // ${ext} + `, + parserOptions, + errors: [makeError(4)] + }, + { + filename: `test.${ext}`, + code: ` + foo({ }) + export default { + test: {}, + // @vue/component + foo: { } + } + bar({ }) + // ${ext} + `, + parserOptions, + errors: (ext === 'js' ? [] : [makeError(3)]).concat([makeError(6)]) + }, + { + filename: `test.${ext}`, + code: ` + export default { + bar () { + return {} + }, + foo () { + // @vue/component + return {} + } + } + // ${ext} + `, + parserOptions, + errors: (ext === 'js' ? [] : [makeError(2)]).concat([makeError(8)]) + } + ] +} + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester() +ruleTester.run('vue-component', rule, { + + valid: [ + { + filename: 'test.js', + code: `export default { }`, + parserOptions + } + ].concat(validTests('js')).concat(validTests('jsx')).concat(validTests('vue')), + invalid: [ + { + filename: 'test.vue', + code: `export default { }`, + parserOptions, + errors: [makeError(1)] + }, + { + filename: 'test.jsx', + code: `export default { }`, + parserOptions, + errors: [makeError(1)] + } + ].concat(invalidTests('js')).concat(invalidTests('jsx')).concat(invalidTests('vue')) +})