Skip to content

Commit

Permalink
Updated to detect Vue3 components. (#1073)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Mar 14, 2020
1 parent 2c92d3d commit a634e3c
Show file tree
Hide file tree
Showing 10 changed files with 312 additions and 64 deletions.
3 changes: 3 additions & 0 deletions docs/user-guide/README.md
Expand Up @@ -85,6 +85,9 @@ All component-related rules are applied to code that passes any of the following
* `Vue.component()` expression
* `Vue.extend()` expression
* `Vue.mixin()` expression
* `app.component()` expression
* `app.mixin()` expression
* `createApp()` expression
* `export default {}` in `.vue` or `.jsx` file

However, if you want to take advantage of the rules in any of your custom objects that are Vue components, you might need to use the special comment `// @vue/component` that marks an object in the next line as a Vue component in any file, e.g.:
Expand Down
162 changes: 100 additions & 62 deletions lib/utils/index.js
Expand Up @@ -264,7 +264,7 @@ module.exports = {
return componentsNode.value.properties
.filter(p => p.type === 'Property')
.map(node => {
const name = this.getStaticPropertyName(node)
const name = getStaticPropertyName(node)
return name ? { node, name } : null
})
.filter(comp => comp != null)
Expand Down Expand Up @@ -402,42 +402,7 @@ module.exports = {
* @param {Property|MethodDefinition|MemberExpression|Literal|TemplateLiteral|Identifier} node - The node to get.
* @return {string|null} The property name if static. Otherwise, null.
*/
getStaticPropertyName (node) {
let prop
switch (node && node.type) {
case 'Property':
case 'MethodDefinition':
prop = node.key
break
case 'MemberExpression':
prop = node.property
break
case 'Literal':
case 'TemplateLiteral':
case 'Identifier':
prop = node
break
// no default
}

switch (prop && prop.type) {
case 'Literal':
return String(prop.value)
case 'TemplateLiteral':
if (prop.expressions.length === 0 && prop.quasis.length === 1) {
return prop.quasis[0].value.cooked
}
break
case 'Identifier':
if (!node.computed) {
return prop.name
}
break
// no default
}

return null
},
getStaticPropertyName,

/**
* Get all props by looking at all component's properties
Expand All @@ -464,8 +429,8 @@ module.exports = {
.filter(prop => prop.type === 'Property')
.map(prop => {
return {
key: prop.key, value: this.unwrapTypes(prop.value), node: prop,
propName: this.getStaticPropertyName(prop)
key: prop.key, value: unwrapTypes(prop.value), node: prop,
propName: getStaticPropertyName(prop)
}
})
} else {
Expand Down Expand Up @@ -548,28 +513,52 @@ module.exports = {
const callee = node.callee

if (callee.type === 'MemberExpression') {
const calleeObject = this.unwrapTypes(callee.object)
const calleeObject = unwrapTypes(callee.object)

if (calleeObject.type === 'Identifier') {
const propName = getStaticPropertyName(callee.property)
if (calleeObject.name === 'Vue') {
// for Vue.js 2.x
// Vue.component('xxx', {}) || Vue.mixin({}) || Vue.extend('xxx', {})
const isFullVueComponentForVue2 =
['component', 'mixin', 'extend'].includes(propName) &&
isObjectArgument(node)

return isFullVueComponentForVue2
}

const isFullVueComponent = calleeObject.type === 'Identifier' &&
calleeObject.name === 'Vue' &&
callee.property.type === 'Identifier' &&
['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 &&
node.arguments.length >= 1 &&
node.arguments.slice(-1)[0].type === 'ObjectExpression'
// for Vue.js 3.x
// app.component('xxx', {}) || app.mixin({})
const isFullVueComponent =
['component', 'mixin'].includes(propName) &&
isObjectArgument(node)

return isFullVueComponent
return isFullVueComponent
}
}

if (callee.type === 'Identifier') {
const isDestructedVueComponent = callee.name === 'component' &&
node.arguments.length >= 1 &&
node.arguments.slice(-1)[0].type === 'ObjectExpression'

return isDestructedVueComponent
if (callee.name === 'component') {
// for Vue.js 2.x
// component('xxx', {})
const isDestructedVueComponent = isObjectArgument(node)
return isDestructedVueComponent
}
if (callee.name === 'createApp') {
// for Vue.js 3.x
// createApp({})
const isAppVueComponent = isObjectArgument(node)
return isAppVueComponent
}
}
}

return false

function isObjectArgument (node) {
return node.arguments.length > 0 &&
unwrapTypes(node.arguments.slice(-1)[0]).type === 'ObjectExpression'
}
},

/**
Expand All @@ -584,7 +573,7 @@ module.exports = {
callee.type === 'Identifier' &&
callee.name === 'Vue' &&
node.arguments.length &&
node.arguments[0].type === 'ObjectExpression'
unwrapTypes(node.arguments[0]).type === 'ObjectExpression'
},

/**
Expand Down Expand Up @@ -647,7 +636,7 @@ module.exports = {
'CallExpression:exit' (node) {
// Vue.component('xxx', {}) || component('xxx', {})
if (!_this.isVueComponent(node) || isDuplicateNode(node.arguments.slice(-1)[0])) return
cb(node.arguments.slice(-1)[0])
cb(unwrapTypes(node.arguments.slice(-1)[0]))
}
}
},
Expand All @@ -664,10 +653,10 @@ module.exports = {
const callee = callExpr.callee

if (callee.type === 'MemberExpression') {
const calleeObject = this.unwrapTypes(callee.object)
const calleeObject = unwrapTypes(callee.object)

if (calleeObject.type === 'Identifier' &&
calleeObject.name === 'Vue' &&
// calleeObject.name === 'Vue' && // Any names can be used in Vue.js 3.x. e.g. app.component()
callee.property === node &&
callExpr.arguments.length >= 1) {
cb(callExpr)
Expand All @@ -682,9 +671,9 @@ module.exports = {
* @param {Set} groups Name of parent group
*/
* iterateProperties (node, groups) {
const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(this.getStaticPropertyName(p.key)))
const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(getStaticPropertyName(p.key)))
for (const item of nodes) {
const name = this.getStaticPropertyName(item.key)
const name = getStaticPropertyName(item.key)
if (!name) continue

if (item.value.type === 'ArrayExpression') {
Expand All @@ -705,7 +694,7 @@ module.exports = {
* iterateArrayExpression (node, groupName) {
assert(node.type === 'ArrayExpression')
for (const item of node.elements) {
const name = this.getStaticPropertyName(item)
const name = getStaticPropertyName(item)
if (name) {
const obj = { name, groupName, node: item }
yield obj
Expand All @@ -721,7 +710,7 @@ module.exports = {
* iterateObjectExpression (node, groupName) {
assert(node.type === 'ObjectExpression')
for (const item of node.properties) {
const name = this.getStaticPropertyName(item)
const name = getStaticPropertyName(item)
if (name) {
const obj = { name, groupName, node: item.key }
yield obj
Expand Down Expand Up @@ -865,7 +854,56 @@ module.exports = {
* @param {T} node
* @return {T}
*/
unwrapTypes (node) {
return node.type === 'TSAsExpression' ? node.expression : node
unwrapTypes
}
/**
* Unwrap typescript types like "X as F"
* @template T
* @param {T} node
* @return {T}
*/
function unwrapTypes (node) {
return node.type === 'TSAsExpression' ? node.expression : node
}

/**
* Gets the property name of a given node.
* @param {Property|MethodDefinition|MemberExpression|Literal|TemplateLiteral|Identifier} node - The node to get.
* @return {string|null} The property name if static. Otherwise, null.
*/
function getStaticPropertyName (node) {
let prop
switch (node && node.type) {
case 'Property':
case 'MethodDefinition':
prop = node.key
break
case 'MemberExpression':
prop = node.property
break
case 'Literal':
case 'TemplateLiteral':
case 'Identifier':
prop = node
break
// no default
}

switch (prop && prop.type) {
case 'Literal':
return String(prop.value)
case 'TemplateLiteral':
if (prop.expressions.length === 0 && prop.quasis.length === 1) {
return prop.quasis[0].value.cooked
}
break
case 'Identifier':
if (!node.computed) {
return prop.name
}
break
// no default
}

return null
}
34 changes: 34 additions & 0 deletions tests/lib/rules/component-definition-name-casing.js
Expand Up @@ -116,6 +116,12 @@ ruleTester.run('component-definition-name-casing', rule, {
options: ['kebab-case'],
parserOptions
},
{
filename: 'test.vue',
code: `app.component('FooBar', component)`,
options: ['PascalCase'],
parserOptions
},
{
filename: 'test.vue',
code: `Vue.mixin({})`,
Expand All @@ -137,6 +143,12 @@ ruleTester.run('component-definition-name-casing', rule, {
options: ['kebab-case'],
parserOptions
},
{
filename: 'test.vue',
code: `app.component(\`fooBar\${foo}\`, component)`,
options: ['kebab-case'],
parserOptions
},
// https://github.com/vuejs/eslint-plugin-vue/issues/1018
{
filename: 'test.js',
Expand Down Expand Up @@ -292,6 +304,17 @@ ruleTester.run('component-definition-name-casing', rule, {
line: 1
}]
},
{
filename: 'test.vue',
code: `app.component('foo-bar', component)`,
output: `app.component('FooBar', component)`,
parserOptions,
errors: [{
message: 'Property name "foo-bar" is not PascalCase.',
type: 'Literal',
line: 1
}]
},
{
filename: 'test.vue',
code: `(Vue as VueConstructor<Vue>).component('foo-bar', component)`,
Expand All @@ -315,6 +338,17 @@ ruleTester.run('component-definition-name-casing', rule, {
line: 1
}]
},
{
filename: 'test.vue',
code: `app.component('foo-bar', {})`,
output: `app.component('FooBar', {})`,
parserOptions,
errors: [{
message: 'Property name "foo-bar" is not PascalCase.',
type: 'Literal',
line: 1
}]
},
{
filename: 'test.js',
code: `Vue.component('foo_bar', {})`,
Expand Down
23 changes: 23 additions & 0 deletions tests/lib/rules/match-component-file-name.js
Expand Up @@ -429,6 +429,16 @@ ruleTester.run('match-component-file-name', rule, {
options: [{ extensions: ['js'] }],
parserOptions
},
{
filename: 'MyComponent.js',
code: `
app.component('MyComponent', {
template: '<div />'
})
`,
options: [{ extensions: ['js'] }],
parserOptions
},
{
filename: 'MyComponent.js',
code: `
Expand Down Expand Up @@ -701,6 +711,19 @@ ruleTester.run('match-component-file-name', rule, {
message: 'Component name `MComponent` should match file name `MyComponent`.'
}]
},
{
filename: 'MyComponent.js',
code: `
app.component(\`MComponent\`, {
template: '<div />'
})
`,
options: [{ extensions: ['js'] }],
parserOptions,
errors: [{
message: 'Component name `MComponent` should match file name `MyComponent`.'
}]
},

// casing
{
Expand Down

0 comments on commit a634e3c

Please sign in to comment.