index.js 26 KB

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