index.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  1. /**
  2. * @author Toru Nagashima <https://github.com/mysticatea>
  3. * @copyright 2017 Toru Nagashima. All rights reserved.
  4. * See LICENSE file in root directory for full license.
  5. */
  6. 'use strict'
  7. /**
  8. * @typedef {import('vue-eslint-parser').AST.ESLintArrayExpression} ArrayExpression
  9. * @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
  10. * @typedef {import('vue-eslint-parser').AST.ESLintIdentifier} Identifier
  11. * @typedef {import('vue-eslint-parser').AST.ESLintLiteral} Literal
  12. * @typedef {import('vue-eslint-parser').AST.ESLintMemberExpression} MemberExpression
  13. * @typedef {import('vue-eslint-parser').AST.ESLintMethodDefinition} MethodDefinition
  14. * @typedef {import('vue-eslint-parser').AST.ESLintObjectExpression} ObjectExpression
  15. * @typedef {import('vue-eslint-parser').AST.ESLintProperty} Property
  16. * @typedef {import('vue-eslint-parser').AST.ESLintTemplateLiteral} TemplateLiteral
  17. */
  18. /**
  19. * @typedef { {key: Literal | null, value: null, node: ArrayExpression['elements'][0], propName: string} } ComponentArrayProp
  20. * @typedef { {key: Property['key'], value: Property['value'], node: Property, propName: string} } ComponentObjectProp
  21. */
  22. // ------------------------------------------------------------------------------
  23. // Helpers
  24. // ------------------------------------------------------------------------------
  25. const HTML_ELEMENT_NAMES = new Set(require('./html-elements.json'))
  26. const SVG_ELEMENT_NAMES = new Set(require('./svg-elements.json'))
  27. const VOID_ELEMENT_NAMES = new Set(require('./void-elements.json'))
  28. const assert = require('assert')
  29. const path = require('path')
  30. const vueEslintParser = require('vue-eslint-parser')
  31. /**
  32. * Wrap the rule context object to override methods which access to tokens (such as getTokenAfter).
  33. * @param {RuleContext} context The rule context object.
  34. * @param {TokenStore} tokenStore The token store object for template.
  35. */
  36. function wrapContextToOverrideTokenMethods (context, tokenStore) {
  37. const sourceCode = new Proxy(context.getSourceCode(), {
  38. get (object, key) {
  39. return key in tokenStore ? tokenStore[key] : object[key]
  40. }
  41. })
  42. return {
  43. __proto__: context,
  44. getSourceCode () {
  45. return sourceCode
  46. }
  47. }
  48. }
  49. // ------------------------------------------------------------------------------
  50. // Exports
  51. // ------------------------------------------------------------------------------
  52. module.exports = {
  53. /**
  54. * Register the given visitor to parser services.
  55. * If the parser service of `vue-eslint-parser` was not found,
  56. * this generates a warning.
  57. *
  58. * @param {RuleContext} context The rule context to use parser services.
  59. * @param {Object} templateBodyVisitor The visitor to traverse the template body.
  60. * @param {Object} [scriptVisitor] The visitor to traverse the script.
  61. * @returns {Object} The merged visitor.
  62. */
  63. defineTemplateBodyVisitor (context, templateBodyVisitor, scriptVisitor) {
  64. if (context.parserServices.defineTemplateBodyVisitor == null) {
  65. context.report({
  66. loc: { line: 1, column: 0 },
  67. message: 'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error'
  68. })
  69. return {}
  70. }
  71. return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
  72. },
  73. /**
  74. * Wrap a given core rule to apply it to Vue.js template.
  75. * @param {Rule} coreRule The core rule implementation to wrap.
  76. * @param {Object|undefined} options The option of this rule.
  77. * @param {string|undefined} options.category The category of this rule.
  78. * @param {boolean|undefined} options.skipDynamicArguments If `true`, skip validation within dynamic arguments.
  79. * @returns {Rule} The wrapped rule implementation.
  80. */
  81. wrapCoreRule (coreRule, options) {
  82. const { category, skipDynamicArguments } = options || {}
  83. return {
  84. create (context) {
  85. const tokenStore =
  86. context.parserServices.getTemplateBodyTokenStore &&
  87. context.parserServices.getTemplateBodyTokenStore()
  88. // The `context.getSourceCode()` cannot access the tokens of templates.
  89. // So override the methods which access to tokens by the `tokenStore`.
  90. if (tokenStore) {
  91. context = wrapContextToOverrideTokenMethods(context, tokenStore)
  92. }
  93. // Move `Program` handlers to `VElement[parent.type!='VElement']`
  94. const handlers = coreRule.create(context)
  95. if (handlers.Program) {
  96. handlers["VElement[parent.type!='VElement']"] = handlers.Program
  97. delete handlers.Program
  98. }
  99. if (handlers['Program:exit']) {
  100. handlers["VElement[parent.type!='VElement']:exit"] = handlers['Program:exit']
  101. delete handlers['Program:exit']
  102. }
  103. if (skipDynamicArguments) {
  104. let withinDynamicArguments = false
  105. for (const name of Object.keys(handlers)) {
  106. const original = handlers[name]
  107. handlers[name] = (...args) => {
  108. if (withinDynamicArguments) return
  109. original(...args)
  110. }
  111. }
  112. handlers['VDirectiveKey > VExpressionContainer'] = () => { withinDynamicArguments = true }
  113. handlers['VDirectiveKey > VExpressionContainer:exit'] = () => { withinDynamicArguments = false }
  114. }
  115. // Apply the handlers to templates.
  116. return module.exports.defineTemplateBodyVisitor(context, handlers)
  117. },
  118. meta: Object.assign({}, coreRule.meta, {
  119. docs: Object.assign({}, coreRule.meta.docs, {
  120. category,
  121. url: `https://eslint.vuejs.org/rules/${path.basename(coreRule.meta.docs.url || '')}.html`
  122. })
  123. })
  124. }
  125. },
  126. /**
  127. * Check whether the given node is the root element or not.
  128. * @param {ASTNode} node The element node to check.
  129. * @returns {boolean} `true` if the node is the root element.
  130. */
  131. isRootElement (node) {
  132. assert(node && node.type === 'VElement')
  133. return (
  134. node.parent.type === 'VDocumentFragment' ||
  135. node.parent.parent.type === 'VDocumentFragment'
  136. )
  137. },
  138. /**
  139. * Get the previous sibling element of the given element.
  140. * @param {ASTNode} node The element node to get the previous sibling element.
  141. * @returns {ASTNode|null} The previous sibling element.
  142. */
  143. prevSibling (node) {
  144. assert(node && node.type === 'VElement')
  145. let prevElement = null
  146. for (const siblingNode of (node.parent && node.parent.children) || []) {
  147. if (siblingNode === node) {
  148. return prevElement
  149. }
  150. if (siblingNode.type === 'VElement') {
  151. prevElement = siblingNode
  152. }
  153. }
  154. return null
  155. },
  156. /**
  157. * Check whether the given start tag has specific directive.
  158. * @param {ASTNode} node The start tag node to check.
  159. * @param {string} name The attribute name to check.
  160. * @param {string} [value] The attribute value to check.
  161. * @returns {boolean} `true` if the start tag has the attribute.
  162. */
  163. hasAttribute (node, name, value) {
  164. assert(node && node.type === 'VElement')
  165. return Boolean(this.getAttribute(node, name, value))
  166. },
  167. /**
  168. * Check whether the given start tag has specific directive.
  169. * @param {ASTNode} node The start tag node to check.
  170. * @param {string} name The directive name to check.
  171. * @param {string} [argument] The directive argument to check.
  172. * @returns {boolean} `true` if the start tag has the directive.
  173. */
  174. hasDirective (node, name, argument) {
  175. assert(node && node.type === 'VElement')
  176. return Boolean(this.getDirective(node, name, argument))
  177. },
  178. /**
  179. * Check whether the given attribute has their attribute value.
  180. * @param {ASTNode} node The attribute node to check.
  181. * @returns {boolean} `true` if the attribute has their value.
  182. */
  183. hasAttributeValue (node) {
  184. assert(node && node.type === 'VAttribute')
  185. return (
  186. node.value != null &&
  187. (node.value.expression != null || node.value.syntaxError != null)
  188. )
  189. },
  190. /**
  191. * Get the attribute which has the given name.
  192. * @param {ASTNode} node The start tag node to check.
  193. * @param {string} name The attribute name to check.
  194. * @param {string} [value] The attribute value to check.
  195. * @returns {ASTNode} The found attribute.
  196. */
  197. getAttribute (node, name, value) {
  198. assert(node && node.type === 'VElement')
  199. return node.startTag.attributes.find(a =>
  200. !a.directive &&
  201. a.key.name === name &&
  202. (
  203. value === undefined ||
  204. (a.value != null && a.value.value === value)
  205. )
  206. )
  207. },
  208. /**
  209. * Get the directive which has the given name.
  210. * @param {ASTNode} node The start tag node to check.
  211. * @param {string} name The directive name to check.
  212. * @param {string} [argument] The directive argument to check.
  213. * @returns {ASTNode} The found directive.
  214. */
  215. getDirective (node, name, argument) {
  216. assert(node && node.type === 'VElement')
  217. return node.startTag.attributes.find(a =>
  218. a.directive &&
  219. a.key.name.name === name &&
  220. (argument === undefined || (a.key.argument && a.key.argument.name) === argument)
  221. )
  222. },
  223. /**
  224. * Returns the list of all registered components
  225. * @param {ASTNode} componentObject
  226. * @returns {Array} Array of ASTNodes
  227. */
  228. getRegisteredComponents (componentObject) {
  229. const componentsNode = componentObject.properties
  230. .find(p =>
  231. p.type === 'Property' &&
  232. p.key.type === 'Identifier' &&
  233. p.key.name === 'components' &&
  234. p.value.type === 'ObjectExpression'
  235. )
  236. if (!componentsNode) { return [] }
  237. return componentsNode.value.properties
  238. .filter(p => p.type === 'Property')
  239. .map(node => {
  240. const name = this.getStaticPropertyName(node)
  241. return name ? { node, name } : null
  242. })
  243. .filter(comp => comp != null)
  244. },
  245. /**
  246. * Check whether the previous sibling element has `if` or `else-if` directive.
  247. * @param {ASTNode} node The element node to check.
  248. * @returns {boolean} `true` if the previous sibling element has `if` or `else-if` directive.
  249. */
  250. prevElementHasIf (node) {
  251. assert(node && node.type === 'VElement')
  252. const prev = this.prevSibling(node)
  253. return (
  254. prev != null &&
  255. prev.startTag.attributes.some(a =>
  256. a.directive &&
  257. (a.key.name.name === 'if' || a.key.name.name === 'else-if')
  258. )
  259. )
  260. },
  261. /**
  262. * Check whether the given node is a custom component or not.
  263. * @param {ASTNode} node The start tag node to check.
  264. * @returns {boolean} `true` if the node is a custom component.
  265. */
  266. isCustomComponent (node) {
  267. assert(node && node.type === 'VElement')
  268. return (
  269. (this.isHtmlElementNode(node) && !this.isHtmlWellKnownElementName(node.rawName)) ||
  270. this.hasAttribute(node, 'is') ||
  271. this.hasDirective(node, 'bind', 'is')
  272. )
  273. },
  274. /**
  275. * Check whether the given node is a HTML element or not.
  276. * @param {ASTNode} node The node to check.
  277. * @returns {boolean} `true` if the node is a HTML element.
  278. */
  279. isHtmlElementNode (node) {
  280. assert(node && node.type === 'VElement')
  281. return node.namespace === vueEslintParser.AST.NS.HTML
  282. },
  283. /**
  284. * Check whether the given node is a SVG element or not.
  285. * @param {ASTNode} node The node to check.
  286. * @returns {boolean} `true` if the name is a SVG element.
  287. */
  288. isSvgElementNode (node) {
  289. assert(node && node.type === 'VElement')
  290. return node.namespace === vueEslintParser.AST.NS.SVG
  291. },
  292. /**
  293. * Check whether the given name is a MathML element or not.
  294. * @param {ASTNode} node The node to check.
  295. * @returns {boolean} `true` if the node is a MathML element.
  296. */
  297. isMathMLElementNode (node) {
  298. assert(node && node.type === 'VElement')
  299. return node.namespace === vueEslintParser.AST.NS.MathML
  300. },
  301. /**
  302. * Check whether the given name is an well-known element or not.
  303. * @param {string} name The name to check.
  304. * @returns {boolean} `true` if the name is an well-known element name.
  305. */
  306. isHtmlWellKnownElementName (name) {
  307. assert(typeof name === 'string')
  308. return HTML_ELEMENT_NAMES.has(name)
  309. },
  310. /**
  311. * Check whether the given name is an well-known SVG element or not.
  312. * @param {string} name The name to check.
  313. * @returns {boolean} `true` if the name is an well-known SVG element name.
  314. */
  315. isSvgWellKnownElementName (name) {
  316. assert(typeof name === 'string')
  317. return SVG_ELEMENT_NAMES.has(name)
  318. },
  319. /**
  320. * Check whether the given name is a void element name or not.
  321. * @param {string} name The name to check.
  322. * @returns {boolean} `true` if the name is a void element name.
  323. */
  324. isHtmlVoidElementName (name) {
  325. assert(typeof name === 'string')
  326. return VOID_ELEMENT_NAMES.has(name)
  327. },
  328. /**
  329. * Parse member expression node to get array with all of its parts
  330. * @param {ASTNode} node MemberExpression
  331. * @returns {Array}
  332. */
  333. parseMemberExpression (node) {
  334. const members = []
  335. let memberExpression
  336. if (node.type === 'MemberExpression') {
  337. memberExpression = node
  338. while (memberExpression.type === 'MemberExpression') {
  339. if (memberExpression.property.type === 'Identifier') {
  340. members.push(memberExpression.property.name)
  341. }
  342. memberExpression = memberExpression.object
  343. }
  344. if (memberExpression.type === 'ThisExpression') {
  345. members.push('this')
  346. } else if (memberExpression.type === 'Identifier') {
  347. members.push(memberExpression.name)
  348. }
  349. }
  350. return members.reverse()
  351. },
  352. /**
  353. * Gets the property name of a given node.
  354. * @param {Property|MethodDefinition|MemberExpression|Literal|TemplateLiteral|Identifier} node - The node to get.
  355. * @return {string|null} The property name if static. Otherwise, null.
  356. */
  357. getStaticPropertyName (node) {
  358. let prop
  359. switch (node && node.type) {
  360. case 'Property':
  361. case 'MethodDefinition':
  362. prop = node.key
  363. break
  364. case 'MemberExpression':
  365. prop = node.property
  366. break
  367. case 'Literal':
  368. case 'TemplateLiteral':
  369. case 'Identifier':
  370. prop = node
  371. break
  372. // no default
  373. }
  374. switch (prop && prop.type) {
  375. case 'Literal':
  376. return String(prop.value)
  377. case 'TemplateLiteral':
  378. if (prop.expressions.length === 0 && prop.quasis.length === 1) {
  379. return prop.quasis[0].value.cooked
  380. }
  381. break
  382. case 'Identifier':
  383. if (!node.computed) {
  384. return prop.name
  385. }
  386. break
  387. // no default
  388. }
  389. return null
  390. },
  391. /**
  392. * Get all props by looking at all component's properties
  393. * @param {ObjectExpression} componentObject Object with component definition
  394. * @return {(ComponentArrayProp | ComponentObjectProp)[]} Array of component props in format: [{key?: String, value?: ASTNode, node: ASTNod}]
  395. */
  396. getComponentProps (componentObject) {
  397. const propsNode = componentObject.properties
  398. .find(p =>
  399. p.type === 'Property' &&
  400. p.key.type === 'Identifier' &&
  401. p.key.name === 'props' &&
  402. (p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression')
  403. )
  404. if (!propsNode) {
  405. return []
  406. }
  407. let props
  408. if (propsNode.value.type === 'ObjectExpression') {
  409. props = propsNode.value.properties
  410. .filter(prop => prop.type === 'Property')
  411. .map(prop => {
  412. return {
  413. key: prop.key, value: this.unwrapTypes(prop.value), node: prop,
  414. propName: this.getStaticPropertyName(prop)
  415. }
  416. })
  417. } else {
  418. props = propsNode.value.elements
  419. .filter(prop => prop)
  420. .map(prop => {
  421. const key = prop.type === 'Literal' && typeof prop.value === 'string' ? prop : null
  422. return { key, value: null, node: prop, propName: key != null ? prop.value : null }
  423. })
  424. }
  425. return props
  426. },
  427. /**
  428. * Get all computed properties by looking at all component's properties
  429. * @param {ObjectExpression} componentObject Object with component definition
  430. * @return {Array} Array of computed properties in format: [{key: String, value: ASTNode}]
  431. */
  432. getComputedProperties (componentObject) {
  433. const computedPropertiesNode = componentObject.properties
  434. .find(p =>
  435. p.type === 'Property' &&
  436. p.key.type === 'Identifier' &&
  437. p.key.name === 'computed' &&
  438. p.value.type === 'ObjectExpression'
  439. )
  440. if (!computedPropertiesNode) { return [] }
  441. return computedPropertiesNode.value.properties
  442. .filter(cp => cp.type === 'Property')
  443. .map(cp => {
  444. const key = cp.key.name
  445. let value
  446. if (cp.value.type === 'FunctionExpression') {
  447. value = cp.value.body
  448. } else if (cp.value.type === 'ObjectExpression') {
  449. value = cp.value.properties
  450. .filter(p =>
  451. p.type === 'Property' &&
  452. p.key.type === 'Identifier' &&
  453. p.key.name === 'get' &&
  454. p.value.type === 'FunctionExpression'
  455. )
  456. .map(p => p.value.body)[0]
  457. }
  458. return { key, value }
  459. })
  460. },
  461. isVueFile (path) {
  462. return path.endsWith('.vue') || path.endsWith('.jsx')
  463. },
  464. /**
  465. * Check whether the given node is a Vue component based
  466. * on the filename and default export type
  467. * export default {} in .vue || .jsx
  468. * @param {ASTNode} node Node to check
  469. * @param {string} path File name with extension
  470. * @returns {boolean}
  471. */
  472. isVueComponentFile (node, path) {
  473. return this.isVueFile(path) &&
  474. node.type === 'ExportDefaultDeclaration' &&
  475. node.declaration.type === 'ObjectExpression'
  476. },
  477. /**
  478. * Check whether given node is Vue component
  479. * Vue.component('xxx', {}) || component('xxx', {})
  480. * @param {ASTNode} node Node to check
  481. * @returns {boolean}
  482. */
  483. isVueComponent (node) {
  484. if (node.type === 'CallExpression') {
  485. const callee = node.callee
  486. if (callee.type === 'MemberExpression') {
  487. const calleeObject = this.unwrapTypes(callee.object)
  488. const isFullVueComponent = calleeObject.type === 'Identifier' &&
  489. calleeObject.name === 'Vue' &&
  490. callee.property.type === 'Identifier' &&
  491. ['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 &&
  492. node.arguments.length >= 1 &&
  493. node.arguments.slice(-1)[0].type === 'ObjectExpression'
  494. return isFullVueComponent
  495. }
  496. if (callee.type === 'Identifier') {
  497. const isDestructedVueComponent = callee.name === 'component' &&
  498. node.arguments.length >= 1 &&
  499. node.arguments.slice(-1)[0].type === 'ObjectExpression'
  500. return isDestructedVueComponent
  501. }
  502. }
  503. return false
  504. },
  505. /**
  506. * Check whether given node is new Vue instance
  507. * new Vue({})
  508. * @param {ASTNode} node Node to check
  509. * @returns {boolean}
  510. */
  511. isVueInstance (node) {
  512. const callee = node.callee
  513. return node.type === 'NewExpression' &&
  514. callee.type === 'Identifier' &&
  515. callee.name === 'Vue' &&
  516. node.arguments.length &&
  517. node.arguments[0].type === 'ObjectExpression'
  518. },
  519. /**
  520. * Check if current file is a Vue instance or component and call callback
  521. * @param {RuleContext} context The ESLint rule context object.
  522. * @param {Function} cb Callback function
  523. */
  524. executeOnVue (context, cb) {
  525. return Object.assign(
  526. this.executeOnVueComponent(context, cb),
  527. this.executeOnVueInstance(context, cb)
  528. )
  529. },
  530. /**
  531. * Check if current file is a Vue instance (new Vue) and call callback
  532. * @param {RuleContext} context The ESLint rule context object.
  533. * @param {Function} cb Callback function
  534. */
  535. executeOnVueInstance (context, cb) {
  536. const _this = this
  537. return {
  538. 'NewExpression:exit' (node) {
  539. // new Vue({})
  540. if (!_this.isVueInstance(node)) return
  541. cb(node.arguments[0])
  542. }
  543. }
  544. },
  545. /**
  546. * Check if current file is a Vue component and call callback
  547. * @param {RuleContext} context The ESLint rule context object.
  548. * @param {Function} cb Callback function
  549. */
  550. executeOnVueComponent (context, cb) {
  551. const filePath = context.getFilename()
  552. const sourceCode = context.getSourceCode()
  553. const _this = this
  554. const componentComments = sourceCode.getAllComments().filter(comment => /@vue\/component/g.test(comment.value))
  555. const foundNodes = []
  556. const isDuplicateNode = (node) => {
  557. if (foundNodes.some(el => el.loc.start.line === node.loc.start.line)) return true
  558. foundNodes.push(node)
  559. return false
  560. }
  561. return {
  562. 'ObjectExpression:exit' (node) {
  563. if (!componentComments.some(el => el.loc.end.line === node.loc.start.line - 1) || isDuplicateNode(node)) return
  564. cb(node)
  565. },
  566. 'ExportDefaultDeclaration:exit' (node) {
  567. // export default {} in .vue || .jsx
  568. if (!_this.isVueComponentFile(node, filePath) || isDuplicateNode(node.declaration)) return
  569. cb(node.declaration)
  570. },
  571. 'CallExpression:exit' (node) {
  572. // Vue.component('xxx', {}) || component('xxx', {})
  573. if (!_this.isVueComponent(node) || isDuplicateNode(node.arguments.slice(-1)[0])) return
  574. cb(node.arguments.slice(-1)[0])
  575. }
  576. }
  577. },
  578. /**
  579. * Check call `Vue.component` and call callback.
  580. * @param {RuleContext} _context The ESLint rule context object.
  581. * @param {Function} cb Callback function
  582. */
  583. executeOnCallVueComponent (_context, cb) {
  584. return {
  585. "CallExpression > MemberExpression > Identifier[name='component']": (node) => {
  586. const callExpr = node.parent.parent
  587. const callee = callExpr.callee
  588. if (callee.type === 'MemberExpression') {
  589. const calleeObject = this.unwrapTypes(callee.object)
  590. if (calleeObject.type === 'Identifier' &&
  591. calleeObject.name === 'Vue' &&
  592. callee.property === node &&
  593. callExpr.arguments.length >= 1) {
  594. cb(callExpr)
  595. }
  596. }
  597. }
  598. }
  599. },
  600. /**
  601. * Return generator with all properties
  602. * @param {ASTNode} node Node to check
  603. * @param {Set} groups Name of parent group
  604. */
  605. * iterateProperties (node, groups) {
  606. const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(this.getStaticPropertyName(p.key)))
  607. for (const item of nodes) {
  608. const name = this.getStaticPropertyName(item.key)
  609. if (!name) continue
  610. if (item.value.type === 'ArrayExpression') {
  611. yield * this.iterateArrayExpression(item.value, name)
  612. } else if (item.value.type === 'ObjectExpression') {
  613. yield * this.iterateObjectExpression(item.value, name)
  614. } else if (item.value.type === 'FunctionExpression') {
  615. yield * this.iterateFunctionExpression(item.value, name)
  616. }
  617. }
  618. },
  619. /**
  620. * Return generator with all elements inside ArrayExpression
  621. * @param {ASTNode} node Node to check
  622. * @param {string} groupName Name of parent group
  623. */
  624. * iterateArrayExpression (node, groupName) {
  625. assert(node.type === 'ArrayExpression')
  626. for (const item of node.elements) {
  627. const name = this.getStaticPropertyName(item)
  628. if (name) {
  629. const obj = { name, groupName, node: item }
  630. yield obj
  631. }
  632. }
  633. },
  634. /**
  635. * Return generator with all elements inside ObjectExpression
  636. * @param {ASTNode} node Node to check
  637. * @param {string} groupName Name of parent group
  638. */
  639. * iterateObjectExpression (node, groupName) {
  640. assert(node.type === 'ObjectExpression')
  641. for (const item of node.properties) {
  642. const name = this.getStaticPropertyName(item)
  643. if (name) {
  644. const obj = { name, groupName, node: item.key }
  645. yield obj
  646. }
  647. }
  648. },
  649. /**
  650. * Return generator with all elements inside FunctionExpression
  651. * @param {ASTNode} node Node to check
  652. * @param {string} groupName Name of parent group
  653. */
  654. * iterateFunctionExpression (node, groupName) {
  655. assert(node.type === 'FunctionExpression')
  656. if (node.body.type === 'BlockStatement') {
  657. for (const item of node.body.body) {
  658. if (item.type === 'ReturnStatement' && item.argument && item.argument.type === 'ObjectExpression') {
  659. yield * this.iterateObjectExpression(item.argument, groupName)
  660. }
  661. }
  662. }
  663. },
  664. /**
  665. * Find all functions which do not always return values
  666. * @param {boolean} treatUndefinedAsUnspecified
  667. * @param {Function} cb Callback function
  668. */
  669. executeOnFunctionsWithoutReturn (treatUndefinedAsUnspecified, cb) {
  670. let funcInfo = {
  671. funcInfo: null,
  672. codePath: null,
  673. hasReturn: false,
  674. hasReturnValue: false,
  675. node: null
  676. }
  677. function isReachable (segment) {
  678. return segment.reachable
  679. }
  680. function isValidReturn () {
  681. if (funcInfo.codePath.currentSegments.some(isReachable)) {
  682. return false
  683. }
  684. return !treatUndefinedAsUnspecified || funcInfo.hasReturnValue
  685. }
  686. return {
  687. onCodePathStart (codePath, node) {
  688. funcInfo = {
  689. codePath,
  690. funcInfo: funcInfo,
  691. hasReturn: false,
  692. hasReturnValue: false,
  693. node
  694. }
  695. },
  696. onCodePathEnd () {
  697. funcInfo = funcInfo.funcInfo
  698. },
  699. ReturnStatement (node) {
  700. funcInfo.hasReturn = true
  701. funcInfo.hasReturnValue = Boolean(node.argument)
  702. },
  703. 'ArrowFunctionExpression:exit' (node) {
  704. if (!isValidReturn() && !node.expression) {
  705. cb(funcInfo.node)
  706. }
  707. },
  708. 'FunctionExpression:exit' (node) {
  709. if (!isValidReturn()) {
  710. cb(funcInfo.node)
  711. }
  712. }
  713. }
  714. },
  715. /**
  716. * Check whether the component is declared in a single line or not.
  717. * @param {ASTNode} node
  718. * @returns {boolean}
  719. */
  720. isSingleLine (node) {
  721. return node.loc.start.line === node.loc.end.line
  722. },
  723. /**
  724. * Check whether the templateBody of the program has invalid EOF or not.
  725. * @param {Program} node The program node to check.
  726. * @returns {boolean} `true` if it has invalid EOF.
  727. */
  728. hasInvalidEOF (node) {
  729. const body = node.templateBody
  730. if (body == null || body.errors == null) {
  731. return
  732. }
  733. return body.errors.some(error => typeof error.code === 'string' && error.code.startsWith('eof-'))
  734. },
  735. /**
  736. * Parse CallExpression or MemberExpression to get simplified version without arguments
  737. *
  738. * @param {ASTNode} node The node to parse (MemberExpression | CallExpression)
  739. * @return {String} eg. 'this.asd.qwe().map().filter().test.reduce()'
  740. */
  741. parseMemberOrCallExpression (node) {
  742. const parsedCallee = []
  743. let n = node
  744. let isFunc
  745. while (n.type === 'MemberExpression' || n.type === 'CallExpression') {
  746. if (n.type === 'CallExpression') {
  747. n = n.callee
  748. isFunc = true
  749. } else {
  750. if (n.computed) {
  751. parsedCallee.push('[]' + (isFunc ? '()' : ''))
  752. } else if (n.property.type === 'Identifier') {
  753. parsedCallee.push(n.property.name + (isFunc ? '()' : ''))
  754. }
  755. isFunc = false
  756. n = n.object
  757. }
  758. }
  759. if (n.type === 'Identifier') {
  760. parsedCallee.push(n.name)
  761. }
  762. if (n.type === 'ThisExpression') {
  763. parsedCallee.push('this')
  764. }
  765. return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
  766. },
  767. /**
  768. * Unwrap typescript types like "X as F"
  769. * @template T
  770. * @param {T} node
  771. * @return {T}
  772. */
  773. unwrapTypes (node) {
  774. return node.type === 'TSAsExpression' ? node.expression : node
  775. }
  776. }