index.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  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 VOID_ELEMENT_NAMES = new Set(require('./void-elements.json'))
  12. const assert = require('assert')
  13. const vueEslintParser = require('vue-eslint-parser')
  14. // ------------------------------------------------------------------------------
  15. // Exports
  16. // ------------------------------------------------------------------------------
  17. module.exports = {
  18. /**
  19. * Register the given visitor to parser services.
  20. * If the parser service of `vue-eslint-parser` was not found,
  21. * this generates a warning.
  22. *
  23. * @param {RuleContext} context The rule context to use parser services.
  24. * @param {Object} templateBodyVisitor The visitor to traverse the template body.
  25. * @param {Object} scriptVisitor The visitor to traverse the script.
  26. * @returns {Object} The merged visitor.
  27. */
  28. defineTemplateBodyVisitor (context, templateBodyVisitor, scriptVisitor) {
  29. if (context.parserServices.defineTemplateBodyVisitor == null) {
  30. context.report({
  31. loc: { line: 1, column: 0 },
  32. 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'
  33. })
  34. return {}
  35. }
  36. return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
  37. },
  38. /**
  39. * Check whether the given node is the root element or not.
  40. * @param {ASTNode} node The element node to check.
  41. * @returns {boolean} `true` if the node is the root element.
  42. */
  43. isRootElement (node) {
  44. assert(node && node.type === 'VElement')
  45. return (
  46. node.parent.type === 'VDocumentFragment' ||
  47. node.parent.parent.type === 'VDocumentFragment'
  48. )
  49. },
  50. /**
  51. * Get the previous sibling element of the given element.
  52. * @param {ASTNode} node The element node to get the previous sibling element.
  53. * @returns {ASTNode|null} The previous sibling element.
  54. */
  55. prevSibling (node) {
  56. assert(node && node.type === 'VElement')
  57. let prevElement = null
  58. for (const siblingNode of (node.parent && node.parent.children) || []) {
  59. if (siblingNode === node) {
  60. return prevElement
  61. }
  62. if (siblingNode.type === 'VElement') {
  63. prevElement = siblingNode
  64. }
  65. }
  66. return null
  67. },
  68. /**
  69. * Check whether the given start tag has specific directive.
  70. * @param {ASTNode} node The start tag node to check.
  71. * @param {string} name The attribute name to check.
  72. * @param {string} [value] The attribute value to check.
  73. * @returns {boolean} `true` if the start tag has the directive.
  74. */
  75. hasAttribute (node, name, value) {
  76. assert(node && node.type === 'VElement')
  77. return node.startTag.attributes.some(a =>
  78. !a.directive &&
  79. a.key.name === name &&
  80. (
  81. value === undefined ||
  82. (a.value != null && a.value.value === value)
  83. )
  84. )
  85. },
  86. /**
  87. * Check whether the given start tag has specific directive.
  88. * @param {ASTNode} node The start tag node to check.
  89. * @param {string} name The directive name to check.
  90. * @param {string} [argument] The directive argument to check.
  91. * @returns {boolean} `true` if the start tag has the directive.
  92. */
  93. hasDirective (node, name, argument) {
  94. assert(node && node.type === 'VElement')
  95. return node.startTag.attributes.some(a =>
  96. a.directive &&
  97. a.key.name === name &&
  98. (argument === undefined || a.key.argument === argument)
  99. )
  100. },
  101. /**
  102. * Check whether the given attribute has their attribute value.
  103. * @param {ASTNode} node The attribute node to check.
  104. * @returns {boolean} `true` if the attribute has their value.
  105. */
  106. hasAttributeValue (node) {
  107. assert(node && node.type === 'VAttribute')
  108. return (
  109. node.value != null &&
  110. (node.value.expression != null || node.value.syntaxError != null)
  111. )
  112. },
  113. /**
  114. * Get the attribute which has the given name.
  115. * @param {ASTNode} node The start tag node to check.
  116. * @param {string} name The attribute name to check.
  117. * @param {string} [value] The attribute value to check.
  118. * @returns {ASTNode} The found attribute.
  119. */
  120. getAttribute (node, name, value) {
  121. assert(node && node.type === 'VElement')
  122. return node.startTag.attributes.find(a =>
  123. !a.directive &&
  124. a.key.name === name &&
  125. (
  126. value === undefined ||
  127. (a.value != null && a.value.value === value)
  128. )
  129. )
  130. },
  131. /**
  132. * Get the directive which has the given name.
  133. * @param {ASTNode} node The start tag node to check.
  134. * @param {string} name The directive name to check.
  135. * @param {string} [argument] The directive argument to check.
  136. * @returns {ASTNode} The found directive.
  137. */
  138. getDirective (node, name, argument) {
  139. assert(node && node.type === 'VElement')
  140. return node.startTag.attributes.find(a =>
  141. a.directive &&
  142. a.key.name === name &&
  143. (argument === undefined || a.key.argument === argument)
  144. )
  145. },
  146. /**
  147. * Check whether the previous sibling element has `if` or `else-if` directive.
  148. * @param {ASTNode} node The element node to check.
  149. * @returns {boolean} `true` if the previous sibling element has `if` or `else-if` directive.
  150. */
  151. prevElementHasIf (node) {
  152. assert(node && node.type === 'VElement')
  153. const prev = this.prevSibling(node)
  154. return (
  155. prev != null &&
  156. prev.startTag.attributes.some(a =>
  157. a.directive &&
  158. (a.key.name === 'if' || a.key.name === 'else-if')
  159. )
  160. )
  161. },
  162. /**
  163. * Check whether the given node is a custom component or not.
  164. * @param {ASTNode} node The start tag node to check.
  165. * @returns {boolean} `true` if the node is a custom component.
  166. */
  167. isCustomComponent (node) {
  168. assert(node && node.type === 'VElement')
  169. return (
  170. (this.isHtmlElementNode(node) && !this.isHtmlWellKnownElementName(node.name)) ||
  171. this.hasAttribute(node, 'is') ||
  172. this.hasDirective(node, 'bind', 'is')
  173. )
  174. },
  175. /**
  176. * Check whether the given node is a HTML element or not.
  177. * @param {ASTNode} node The node to check.
  178. * @returns {boolean} `true` if the node is a HTML element.
  179. */
  180. isHtmlElementNode (node) {
  181. assert(node && node.type === 'VElement')
  182. return node.namespace === vueEslintParser.AST.NS.HTML
  183. },
  184. /**
  185. * Check whether the given node is a SVG element or not.
  186. * @param {ASTNode} node The node to check.
  187. * @returns {boolean} `true` if the name is a SVG element.
  188. */
  189. isSvgElementNode (node) {
  190. assert(node && node.type === 'VElement')
  191. return node.namespace === vueEslintParser.AST.NS.SVG
  192. },
  193. /**
  194. * Check whether the given name is a MathML element or not.
  195. * @param {ASTNode} name The node to check.
  196. * @returns {boolean} `true` if the node is a MathML element.
  197. */
  198. isMathMLElementNode (node) {
  199. assert(node && node.type === 'VElement')
  200. return node.namespace === vueEslintParser.AST.NS.MathML
  201. },
  202. /**
  203. * Check whether the given name is an well-known element or not.
  204. * @param {string} name The name to check.
  205. * @returns {boolean} `true` if the name is an well-known element name.
  206. */
  207. isHtmlWellKnownElementName (name) {
  208. assert(typeof name === 'string')
  209. return HTML_ELEMENT_NAMES.has(name.toLowerCase())
  210. },
  211. /**
  212. * Check whether the given name is a void element name or not.
  213. * @param {string} name The name to check.
  214. * @returns {boolean} `true` if the name is a void element name.
  215. */
  216. isHtmlVoidElementName (name) {
  217. assert(typeof name === 'string')
  218. return VOID_ELEMENT_NAMES.has(name.toLowerCase())
  219. },
  220. /**
  221. * Parse member expression node to get array with all of its parts
  222. * @param {ASTNode} MemberExpression
  223. * @returns {Array}
  224. */
  225. parseMemberExpression (node) {
  226. const members = []
  227. let memberExpression
  228. if (node.type === 'MemberExpression') {
  229. memberExpression = node
  230. while (memberExpression.type === 'MemberExpression') {
  231. if (memberExpression.property.type === 'Identifier') {
  232. members.push(memberExpression.property.name)
  233. }
  234. memberExpression = memberExpression.object
  235. }
  236. if (memberExpression.type === 'ThisExpression') {
  237. members.push('this')
  238. } else if (memberExpression.type === 'Identifier') {
  239. members.push(memberExpression.name)
  240. }
  241. }
  242. return members.reverse()
  243. },
  244. /**
  245. * Gets the property name of a given node.
  246. * @param {ASTNode} node - The node to get.
  247. * @return {string|null} The property name if static. Otherwise, null.
  248. */
  249. getStaticPropertyName (node) {
  250. let prop
  251. switch (node && node.type) {
  252. case 'Property':
  253. case 'MethodDefinition':
  254. prop = node.key
  255. break
  256. case 'MemberExpression':
  257. prop = node.property
  258. break
  259. case 'Literal':
  260. case 'TemplateLiteral':
  261. case 'Identifier':
  262. prop = node
  263. break
  264. // no default
  265. }
  266. switch (prop && prop.type) {
  267. case 'Literal':
  268. return String(prop.value)
  269. case 'TemplateLiteral':
  270. if (prop.expressions.length === 0 && prop.quasis.length === 1) {
  271. return prop.quasis[0].value.cooked
  272. }
  273. break
  274. case 'Identifier':
  275. if (!node.computed) {
  276. return prop.name
  277. }
  278. break
  279. // no default
  280. }
  281. return null
  282. },
  283. /**
  284. * Get all computed properties by looking at all component's properties
  285. * @param {ObjectExpression} Object with component definition
  286. * @return {Array} Array of computed properties in format: [{key: String, value: ASTNode}]
  287. */
  288. getComputedProperties (componentObject) {
  289. const computedPropertiesNode = componentObject.properties
  290. .find(p =>
  291. p.type === 'Property' &&
  292. p.key.type === 'Identifier' &&
  293. p.key.name === 'computed' &&
  294. p.value.type === 'ObjectExpression'
  295. )
  296. if (!computedPropertiesNode) { return [] }
  297. return computedPropertiesNode.value.properties
  298. .filter(cp => cp.type === 'Property')
  299. .map(cp => {
  300. const key = cp.key.name
  301. let value
  302. if (cp.value.type === 'FunctionExpression') {
  303. value = cp.value.body
  304. } else if (cp.value.type === 'ObjectExpression') {
  305. value = cp.value.properties
  306. .filter(p =>
  307. p.type === 'Property' &&
  308. p.key.type === 'Identifier' &&
  309. p.key.name === 'get' &&
  310. p.value.type === 'FunctionExpression'
  311. )
  312. .map(p => p.value.body)[0]
  313. }
  314. return { key, value }
  315. })
  316. },
  317. /**
  318. * Check whether the given node is a Vue component based
  319. * on the filename and default export type
  320. * export default {} in .vue || .jsx
  321. * @param {ASTNode} node Node to check
  322. * @param {string} path File name with extension
  323. * @returns {boolean}
  324. */
  325. isVueComponentFile (node, path) {
  326. const isVueFile = path.endsWith('.vue') || path.endsWith('.jsx')
  327. return isVueFile &&
  328. node.type === 'ExportDefaultDeclaration' &&
  329. node.declaration.type === 'ObjectExpression'
  330. },
  331. /**
  332. * Check whether given node is Vue component
  333. * Vue.component('xxx', {}) || component('xxx', {})
  334. * @param {ASTNode} node Node to check
  335. * @returns {boolean}
  336. */
  337. isVueComponent (node) {
  338. const callee = node.callee
  339. const isFullVueComponent = node.type === 'CallExpression' &&
  340. callee.type === 'MemberExpression' &&
  341. callee.object.type === 'Identifier' &&
  342. callee.object.name === 'Vue' &&
  343. callee.property.type === 'Identifier' &&
  344. ['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 &&
  345. node.arguments.length >= 1 &&
  346. node.arguments.slice(-1)[0].type === 'ObjectExpression'
  347. const isDestructedVueComponent = node.type === 'CallExpression' &&
  348. callee.type === 'Identifier' &&
  349. callee.name === 'component' &&
  350. node.arguments.length >= 1 &&
  351. node.arguments.slice(-1)[0].type === 'ObjectExpression'
  352. return isFullVueComponent || isDestructedVueComponent
  353. },
  354. /**
  355. * Check whether given node is new Vue instance
  356. * new Vue({})
  357. * @param {ASTNode} node Node to check
  358. * @returns {boolean}
  359. */
  360. isVueInstance (node) {
  361. const callee = node.callee
  362. return node.type === 'NewExpression' &&
  363. callee.type === 'Identifier' &&
  364. callee.name === 'Vue' &&
  365. node.arguments.length &&
  366. node.arguments[0].type === 'ObjectExpression'
  367. },
  368. /**
  369. * Check if current file is a Vue instance or component and call callback
  370. * @param {RuleContext} context The ESLint rule context object.
  371. * @param {Function} cb Callback function
  372. */
  373. executeOnVue (context, cb) {
  374. return Object.assign(
  375. this.executeOnVueComponent(context, cb),
  376. this.executeOnVueInstance(context, cb)
  377. )
  378. },
  379. /**
  380. * Check if current file is a Vue instance (new Vue) and call callback
  381. * @param {RuleContext} context The ESLint rule context object.
  382. * @param {Function} cb Callback function
  383. */
  384. executeOnVueInstance (context, cb) {
  385. const _this = this
  386. return {
  387. 'NewExpression:exit' (node) {
  388. // new Vue({})
  389. if (!_this.isVueInstance(node)) return
  390. cb(node.arguments[0])
  391. }
  392. }
  393. },
  394. /**
  395. * Check if current file is a Vue component and call callback
  396. * @param {RuleContext} context The ESLint rule context object.
  397. * @param {Function} cb Callback function
  398. */
  399. executeOnVueComponent (context, cb) {
  400. const filePath = context.getFilename()
  401. const sourceCode = context.getSourceCode()
  402. const _this = this
  403. const componentComments = sourceCode.getAllComments().filter(comment => /@vue\/component/g.test(comment.value))
  404. const foundNodes = []
  405. const isDuplicateNode = (node) => {
  406. if (foundNodes.some(el => el.loc.start.line === node.loc.start.line)) return true
  407. foundNodes.push(node)
  408. return false
  409. }
  410. return {
  411. 'ObjectExpression:exit' (node) {
  412. if (!componentComments.some(el => el.loc.end.line === node.loc.start.line - 1) || isDuplicateNode(node)) return
  413. cb(node)
  414. },
  415. 'ExportDefaultDeclaration:exit' (node) {
  416. // export default {} in .vue || .jsx
  417. if (!_this.isVueComponentFile(node, filePath) || isDuplicateNode(node.declaration)) return
  418. cb(node.declaration)
  419. },
  420. 'CallExpression:exit' (node) {
  421. // Vue.component('xxx', {}) || component('xxx', {})
  422. if (!_this.isVueComponent(node) || isDuplicateNode(node.arguments.slice(-1)[0])) return
  423. cb(node.arguments.slice(-1)[0])
  424. }
  425. }
  426. },
  427. /**
  428. * Return generator with all properties
  429. * @param {ASTNode} node Node to check
  430. * @param {string} groupName Name of parent group
  431. */
  432. * iterateProperties (node, groups) {
  433. const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(this.getStaticPropertyName(p.key)))
  434. for (const item of nodes) {
  435. const name = this.getStaticPropertyName(item.key)
  436. if (!name) continue
  437. if (item.value.type === 'ArrayExpression') {
  438. yield * this.iterateArrayExpression(item.value, name)
  439. } else if (item.value.type === 'ObjectExpression') {
  440. yield * this.iterateObjectExpression(item.value, name)
  441. } else if (item.value.type === 'FunctionExpression') {
  442. yield * this.iterateFunctionExpression(item.value, name)
  443. }
  444. }
  445. },
  446. /**
  447. * Return generator with all elements inside ArrayExpression
  448. * @param {ASTNode} node Node to check
  449. * @param {string} groupName Name of parent group
  450. */
  451. * iterateArrayExpression (node, groupName) {
  452. assert(node.type === 'ArrayExpression')
  453. for (const item of node.elements) {
  454. const name = this.getStaticPropertyName(item)
  455. if (name) {
  456. const obj = { name, groupName, node: item }
  457. yield obj
  458. }
  459. }
  460. },
  461. /**
  462. * Return generator with all elements inside ObjectExpression
  463. * @param {ASTNode} node Node to check
  464. * @param {string} groupName Name of parent group
  465. */
  466. * iterateObjectExpression (node, groupName) {
  467. assert(node.type === 'ObjectExpression')
  468. for (const item of node.properties) {
  469. const name = this.getStaticPropertyName(item)
  470. if (name) {
  471. const obj = { name, groupName, node: item.key }
  472. yield obj
  473. }
  474. }
  475. },
  476. /**
  477. * Return generator with all elements inside FunctionExpression
  478. * @param {ASTNode} node Node to check
  479. * @param {string} groupName Name of parent group
  480. */
  481. * iterateFunctionExpression (node, groupName) {
  482. assert(node.type === 'FunctionExpression')
  483. if (node.body.type === 'BlockStatement') {
  484. for (const item of node.body.body) {
  485. if (item.type === 'ReturnStatement' && item.argument && item.argument.type === 'ObjectExpression') {
  486. yield * this.iterateObjectExpression(item.argument, groupName)
  487. }
  488. }
  489. }
  490. },
  491. /**
  492. * Find all functions which do not always return values
  493. * @param {boolean} treatUndefinedAsUnspecified
  494. * @param {Function} cb Callback function
  495. */
  496. executeOnFunctionsWithoutReturn (treatUndefinedAsUnspecified, cb) {
  497. let funcInfo = {
  498. funcInfo: null,
  499. codePath: null,
  500. hasReturn: false,
  501. hasReturnValue: false,
  502. node: null
  503. }
  504. function isValidReturn () {
  505. if (!funcInfo.hasReturn) {
  506. return false
  507. }
  508. return !treatUndefinedAsUnspecified || funcInfo.hasReturnValue
  509. }
  510. return {
  511. onCodePathStart (codePath, node) {
  512. funcInfo = {
  513. codePath,
  514. funcInfo: funcInfo,
  515. hasReturn: false,
  516. hasReturnValue: false,
  517. node
  518. }
  519. },
  520. onCodePathEnd () {
  521. funcInfo = funcInfo.funcInfo
  522. },
  523. ReturnStatement (node) {
  524. funcInfo.hasReturn = true
  525. funcInfo.hasReturnValue = Boolean(node.argument)
  526. },
  527. 'ArrowFunctionExpression:exit' (node) {
  528. if (!isValidReturn() && !node.expression) {
  529. cb(funcInfo.node)
  530. }
  531. },
  532. 'FunctionExpression:exit' (node) {
  533. if (!isValidReturn()) {
  534. cb(funcInfo.node)
  535. }
  536. }
  537. }
  538. },
  539. /**
  540. * Check whether the component is declared in a single line or not.
  541. * @param {ASTNode} node
  542. * @returns {boolean}
  543. */
  544. isSingleLine (node) {
  545. return node.loc.start.line === node.loc.end.line
  546. },
  547. /**
  548. * Check whether the templateBody of the program has invalid EOF or not.
  549. * @param {Program} node The program node to check.
  550. * @returns {boolean} `true` if it has invalid EOF.
  551. */
  552. hasInvalidEOF (node) {
  553. const body = node.templateBody
  554. if (body == null || body.errors == null) {
  555. return
  556. }
  557. return body.errors.some(error => typeof error.code === 'string' && error.code.startsWith('eof-'))
  558. },
  559. /**
  560. * Parse CallExpression or MemberExpression to get simplified version without arguments
  561. *
  562. * @param {Object} node The node to parse (MemberExpression | CallExpression)
  563. * @return {String} eg. 'this.asd.qwe().map().filter().test.reduce()'
  564. */
  565. parseMemberOrCallExpression (node) {
  566. const parsedCallee = []
  567. let n = node
  568. let isFunc
  569. while (n.type === 'MemberExpression' || n.type === 'CallExpression') {
  570. if (n.type === 'CallExpression') {
  571. n = n.callee
  572. isFunc = true
  573. } else {
  574. if (n.computed) {
  575. parsedCallee.push('[]')
  576. } else if (n.property.type === 'Identifier') {
  577. parsedCallee.push(n.property.name + (isFunc ? '()' : ''))
  578. }
  579. isFunc = false
  580. n = n.object
  581. }
  582. }
  583. if (n.type === 'Identifier') {
  584. parsedCallee.push(n.name)
  585. }
  586. if (n.type === 'ThisExpression') {
  587. parsedCallee.push('this')
  588. }
  589. return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
  590. }
  591. }