Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated to detect Vue3 components. #1073

Merged
merged 1 commit into from Mar 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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