| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659 | /** * @author Toru Nagashima <https://github.com/mysticatea> * @copyright 2017 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */'use strict'// ------------------------------------------------------------------------------// Helpers// ------------------------------------------------------------------------------const HTML_ELEMENT_NAMES = new Set(require('./html-elements.json'))const VOID_ELEMENT_NAMES = new Set(require('./void-elements.json'))const assert = require('assert')const vueEslintParser = require('vue-eslint-parser')// ------------------------------------------------------------------------------// Exports// ------------------------------------------------------------------------------module.exports = {  /**   * Register the given visitor to parser services.   * If the parser service of `vue-eslint-parser` was not found,   * this generates a warning.   *   * @param {RuleContext} context The rule context to use parser services.   * @param {Object} templateBodyVisitor The visitor to traverse the template body.   * @param {Object} scriptVisitor The visitor to traverse the script.   * @returns {Object} The merged visitor.   */  defineTemplateBodyVisitor (context, templateBodyVisitor, scriptVisitor) {    if (context.parserServices.defineTemplateBodyVisitor == null) {      context.report({        loc: { line: 1, column: 0 },        message: 'Use the latest vue-eslint-parser. See also https://github.com/vuejs/eslint-plugin-vue#what-is-the-use-the-latest-vue-eslint-parser-error'      })      return {}    }    return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)  },  /**   * Check whether the given node is the root element or not.   * @param {ASTNode} node The element node to check.   * @returns {boolean} `true` if the node is the root element.   */  isRootElement (node) {    assert(node && node.type === 'VElement')    return (      node.parent.type === 'VDocumentFragment' ||      node.parent.parent.type === 'VDocumentFragment'    )  },  /**   * Get the previous sibling element of the given element.   * @param {ASTNode} node The element node to get the previous sibling element.   * @returns {ASTNode|null} The previous sibling element.   */  prevSibling (node) {    assert(node && node.type === 'VElement')    let prevElement = null    for (const siblingNode of (node.parent && node.parent.children) || []) {      if (siblingNode === node) {        return prevElement      }      if (siblingNode.type === 'VElement') {        prevElement = siblingNode      }    }    return null  },  /**   * Check whether the given start tag has specific directive.   * @param {ASTNode} node The start tag node to check.   * @param {string} name The attribute name to check.   * @param {string} [value] The attribute value to check.   * @returns {boolean} `true` if the start tag has the directive.   */  hasAttribute (node, name, value) {    assert(node && node.type === 'VElement')    return node.startTag.attributes.some(a =>      !a.directive &&      a.key.name === name &&      (        value === undefined ||        (a.value != null && a.value.value === value)      )    )  },  /**   * Check whether the given start tag has specific directive.   * @param {ASTNode} node The start tag node to check.   * @param {string} name The directive name to check.   * @param {string} [argument] The directive argument to check.   * @returns {boolean} `true` if the start tag has the directive.   */  hasDirective (node, name, argument) {    assert(node && node.type === 'VElement')    return node.startTag.attributes.some(a =>      a.directive &&      a.key.name === name &&      (argument === undefined || a.key.argument === argument)    )  },  /**   * Check whether the given attribute has their attribute value.   * @param {ASTNode} node The attribute node to check.   * @returns {boolean} `true` if the attribute has their value.   */  hasAttributeValue (node) {    assert(node && node.type === 'VAttribute')    return (      node.value != null &&      (node.value.expression != null || node.value.syntaxError != null)    )  },  /**   * Get the attribute which has the given name.   * @param {ASTNode} node The start tag node to check.   * @param {string} name The attribute name to check.   * @param {string} [value] The attribute value to check.   * @returns {ASTNode} The found attribute.   */  getAttribute (node, name, value) {    assert(node && node.type === 'VElement')    return node.startTag.attributes.find(a =>      !a.directive &&      a.key.name === name &&      (        value === undefined ||        (a.value != null && a.value.value === value)      )    )  },  /**   * Get the directive which has the given name.   * @param {ASTNode} node The start tag node to check.   * @param {string} name The directive name to check.   * @param {string} [argument] The directive argument to check.   * @returns {ASTNode} The found directive.   */  getDirective (node, name, argument) {    assert(node && node.type === 'VElement')    return node.startTag.attributes.find(a =>      a.directive &&      a.key.name === name &&      (argument === undefined || a.key.argument === argument)    )  },  /**   * Check whether the previous sibling element has `if` or `else-if` directive.   * @param {ASTNode} node The element node to check.   * @returns {boolean} `true` if the previous sibling element has `if` or `else-if` directive.   */  prevElementHasIf (node) {    assert(node && node.type === 'VElement')    const prev = this.prevSibling(node)    return (      prev != null &&      prev.startTag.attributes.some(a =>        a.directive &&        (a.key.name === 'if' || a.key.name === 'else-if')      )    )  },  /**   * Check whether the given node is a custom component or not.   * @param {ASTNode} node The start tag node to check.   * @returns {boolean} `true` if the node is a custom component.   */  isCustomComponent (node) {    assert(node && node.type === 'VElement')    return (      (this.isHtmlElementNode(node) && !this.isHtmlWellKnownElementName(node.name)) ||      this.hasAttribute(node, 'is') ||      this.hasDirective(node, 'bind', 'is')    )  },  /**   * Check whether the given node is a HTML element or not.   * @param {ASTNode} node The node to check.   * @returns {boolean} `true` if the node is a HTML element.   */  isHtmlElementNode (node) {    assert(node && node.type === 'VElement')    return node.namespace === vueEslintParser.AST.NS.HTML  },  /**   * Check whether the given node is a SVG element or not.   * @param {ASTNode} node The node to check.   * @returns {boolean} `true` if the name is a SVG element.   */  isSvgElementNode (node) {    assert(node && node.type === 'VElement')    return node.namespace === vueEslintParser.AST.NS.SVG  },  /**   * Check whether the given name is a MathML element or not.   * @param {ASTNode} name The node to check.   * @returns {boolean} `true` if the node is a MathML element.   */  isMathMLElementNode (node) {    assert(node && node.type === 'VElement')    return node.namespace === vueEslintParser.AST.NS.MathML  },  /**   * Check whether the given name is an well-known element or not.   * @param {string} name The name to check.   * @returns {boolean} `true` if the name is an well-known element name.   */  isHtmlWellKnownElementName (name) {    assert(typeof name === 'string')    return HTML_ELEMENT_NAMES.has(name.toLowerCase())  },  /**   * Check whether the given name is a void element name or not.   * @param {string} name The name to check.   * @returns {boolean} `true` if the name is a void element name.   */  isHtmlVoidElementName (name) {    assert(typeof name === 'string')    return VOID_ELEMENT_NAMES.has(name.toLowerCase())  },  /**   * Parse member expression node to get array with all of its parts   * @param {ASTNode} MemberExpression   * @returns {Array}   */  parseMemberExpression (node) {    const members = []    let memberExpression    if (node.type === 'MemberExpression') {      memberExpression = node      while (memberExpression.type === 'MemberExpression') {        if (memberExpression.property.type === 'Identifier') {          members.push(memberExpression.property.name)        }        memberExpression = memberExpression.object      }      if (memberExpression.type === 'ThisExpression') {        members.push('this')      } else if (memberExpression.type === 'Identifier') {        members.push(memberExpression.name)      }    }    return members.reverse()  },  /**   * Gets the property name of a given node.   * @param {ASTNode} 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  },  /**   * Get all computed properties by looking at all component's properties   * @param {ObjectExpression} Object with component definition   * @return {Array} Array of computed properties in format: [{key: String, value: ASTNode}]   */  getComputedProperties (componentObject) {    const computedPropertiesNode = componentObject.properties      .find(p =>        p.type === 'Property' &&        p.key.type === 'Identifier' &&        p.key.name === 'computed' &&        p.value.type === 'ObjectExpression'      )    if (!computedPropertiesNode) { return [] }    return computedPropertiesNode.value.properties      .filter(cp => cp.type === 'Property')      .map(cp => {        const key = cp.key.name        let value        if (cp.value.type === 'FunctionExpression') {          value = cp.value.body        } else if (cp.value.type === 'ObjectExpression') {          value = cp.value.properties            .filter(p =>              p.type === 'Property' &&              p.key.type === 'Identifier' &&              p.key.name === 'get' &&              p.value.type === 'FunctionExpression'            )            .map(p => p.value.body)[0]        }        return { key, value }      })  },  /**   * Check whether the given node is a Vue component based   * on the filename and default export type   * export default {} in .vue || .jsx   * @param {ASTNode} node Node to check   * @param {string} path File name with extension   * @returns {boolean}   */  isVueComponentFile (node, path) {    const isVueFile = path.endsWith('.vue') || path.endsWith('.jsx')    return isVueFile &&      node.type === 'ExportDefaultDeclaration' &&      node.declaration.type === 'ObjectExpression'  },  /**   * Check whether given node is Vue component   * Vue.component('xxx', {}) || component('xxx', {})   * @param {ASTNode} node Node to check   * @returns {boolean}   */  isVueComponent (node) {    const callee = node.callee    const isFullVueComponent = node.type === 'CallExpression' &&      callee.type === 'MemberExpression' &&      callee.object.type === 'Identifier' &&      callee.object.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'    const isDestructedVueComponent = node.type === 'CallExpression' &&      callee.type === 'Identifier' &&      callee.name === 'component' &&      node.arguments.length >= 1 &&      node.arguments.slice(-1)[0].type === 'ObjectExpression'    return isFullVueComponent || isDestructedVueComponent  },  /**   * Check whether given node is new Vue instance   * new Vue({})   * @param {ASTNode} node Node to check   * @returns {boolean}   */  isVueInstance (node) {    const callee = node.callee    return node.type === 'NewExpression' &&      callee.type === 'Identifier' &&      callee.name === 'Vue' &&      node.arguments.length &&      node.arguments[0].type === 'ObjectExpression'  },  /**   * Check if current file is a Vue instance or component and call callback   * @param {RuleContext} context The ESLint rule context object.   * @param {Function} cb Callback function   */  executeOnVue (context, cb) {    return Object.assign(      this.executeOnVueComponent(context, cb),      this.executeOnVueInstance(context, cb)    )  },  /**   * Check if current file is a Vue instance (new Vue) and call callback   * @param {RuleContext} context The ESLint rule context object.   * @param {Function} cb Callback function   */  executeOnVueInstance (context, cb) {    const _this = this    return {      'NewExpression:exit' (node) {        // new Vue({})        if (!_this.isVueInstance(node)) return        cb(node.arguments[0])      }    }  },  /**   * Check if current file is a Vue component and call callback   * @param {RuleContext} context The ESLint rule context object.   * @param {Function} cb Callback function   */  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:exit' (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) || isDuplicateNode(node.declaration)) return        cb(node.declaration)      },      '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])      }    }  },  /**   * 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 (!name) continue      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 && item.argument.type === 'ObjectExpression') {          yield * this.iterateObjectExpression(item.argument, groupName)        }      }    }  },  /**   * Find all functions which do not always return values   * @param {boolean} treatUndefinedAsUnspecified   * @param {Function} cb Callback function   */  executeOnFunctionsWithoutReturn (treatUndefinedAsUnspecified, cb) {    let funcInfo = {      funcInfo: null,      codePath: null,      hasReturn: false,      hasReturnValue: false,      node: null    }    function isValidReturn () {      if (!funcInfo.hasReturn) {        return false      }      return !treatUndefinedAsUnspecified || funcInfo.hasReturnValue    }    return {      onCodePathStart (codePath, node) {        funcInfo = {          codePath,          funcInfo: funcInfo,          hasReturn: false,          hasReturnValue: false,          node        }      },      onCodePathEnd () {        funcInfo = funcInfo.funcInfo      },      ReturnStatement (node) {        funcInfo.hasReturn = true        funcInfo.hasReturnValue = Boolean(node.argument)      },      'ArrowFunctionExpression:exit' (node) {        if (!isValidReturn() && !node.expression) {          cb(funcInfo.node)        }      },      'FunctionExpression:exit' (node) {        if (!isValidReturn()) {          cb(funcInfo.node)        }      }    }  },  /**   * Check whether the component is declared in a single line or not.   * @param {ASTNode} node   * @returns {boolean}   */  isSingleLine (node) {    return node.loc.start.line === node.loc.end.line  },  /**   * Check whether the templateBody of the program has invalid EOF or not.   * @param {Program} node The program node to check.   * @returns {boolean} `true` if it has invalid EOF.   */  hasInvalidEOF (node) {    const body = node.templateBody    if (body == null || body.errors == null) {      return    }    return body.errors.some(error => typeof error.code === 'string' && error.code.startsWith('eof-'))  },  /**   * Parse CallExpression or MemberExpression to get simplified version without arguments   *   * @param  {Object} node The node to parse (MemberExpression | CallExpression)   * @return {String} eg. 'this.asd.qwe().map().filter().test.reduce()'   */  parseMemberOrCallExpression (node) {    const parsedCallee = []    let n = node    let isFunc    while (n.type === 'MemberExpression' || n.type === 'CallExpression') {      if (n.type === 'CallExpression') {        n = n.callee        isFunc = true      } else {        if (n.computed) {          parsedCallee.push('[]')        } else if (n.property.type === 'Identifier') {          parsedCallee.push(n.property.name + (isFunc ? '()' : ''))        }        isFunc = false        n = n.object      }    }    if (n.type === 'Identifier') {      parsedCallee.push(n.name)    }    if (n.type === 'ThisExpression') {      parsedCallee.push('this')    }    return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')  }}
 |