comment-directive.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. /**
  2. * @author Toru Nagashima <https://github.com/mysticatea>
  3. */
  4. /* eslint-disable eslint-plugin/report-message-format */
  5. 'use strict'
  6. // ------------------------------------------------------------------------------
  7. // Requirements
  8. // ------------------------------------------------------------------------------
  9. const utils = require('../utils')
  10. /**
  11. * @typedef {object} RuleAndLocation
  12. * @property {string} RuleAndLocation.ruleId
  13. * @property {number} RuleAndLocation.index
  14. * @property {string} [RuleAndLocation.key]
  15. */
  16. // -----------------------------------------------------------------------------
  17. // Helpers
  18. // -----------------------------------------------------------------------------
  19. const COMMENT_DIRECTIVE_B = /^\s*(eslint-(?:en|dis)able)(?:\s+|$)/
  20. const COMMENT_DIRECTIVE_L = /^\s*(eslint-disable(?:-next)?-line)(?:\s+|$)/
  21. /**
  22. * Remove the ignored part from a given directive comment and trim it.
  23. * @param {string} value The comment text to strip.
  24. * @returns {string} The stripped text.
  25. */
  26. function stripDirectiveComment(value) {
  27. return value.split(/\s-{2,}\s/u)[0]
  28. }
  29. /**
  30. * Parse a given comment.
  31. * @param {RegExp} pattern The RegExp pattern to parse.
  32. * @param {string} comment The comment value to parse.
  33. * @returns {({type:string,rules:RuleAndLocation[]})|null} The parsing result.
  34. */
  35. function parse(pattern, comment) {
  36. const text = stripDirectiveComment(comment)
  37. const match = pattern.exec(text)
  38. if (match == null) {
  39. return null
  40. }
  41. const type = match[1]
  42. /** @type {RuleAndLocation[]} */
  43. const rules = []
  44. const rulesRe = /([^,\s]+)[,\s]*/g
  45. let startIndex = match[0].length
  46. rulesRe.lastIndex = startIndex
  47. let res
  48. while ((res = rulesRe.exec(text))) {
  49. const ruleId = res[1].trim()
  50. rules.push({
  51. ruleId,
  52. index: startIndex
  53. })
  54. startIndex = rulesRe.lastIndex
  55. }
  56. return { type, rules }
  57. }
  58. /**
  59. * Enable rules.
  60. * @param {RuleContext} context The rule context.
  61. * @param {{line:number,column:number}} loc The location information to enable.
  62. * @param { 'block' | 'line' } group The group to enable.
  63. * @param {string | null} rule The rule ID to enable.
  64. * @returns {void}
  65. */
  66. function enable(context, loc, group, rule) {
  67. if (!rule) {
  68. context.report({
  69. loc,
  70. messageId: group === 'block' ? 'enableBlock' : 'enableLine'
  71. })
  72. } else {
  73. context.report({
  74. loc,
  75. messageId: group === 'block' ? 'enableBlockRule' : 'enableLineRule',
  76. data: { rule }
  77. })
  78. }
  79. }
  80. /**
  81. * Disable rules.
  82. * @param {RuleContext} context The rule context.
  83. * @param {{line:number,column:number}} loc The location information to disable.
  84. * @param { 'block' | 'line' } group The group to disable.
  85. * @param {string | null} rule The rule ID to disable.
  86. * @param {string} key The disable directive key.
  87. * @returns {void}
  88. */
  89. function disable(context, loc, group, rule, key) {
  90. if (!rule) {
  91. context.report({
  92. loc,
  93. messageId: group === 'block' ? 'disableBlock' : 'disableLine',
  94. data: { key }
  95. })
  96. } else {
  97. context.report({
  98. loc,
  99. messageId: group === 'block' ? 'disableBlockRule' : 'disableLineRule',
  100. data: { rule, key }
  101. })
  102. }
  103. }
  104. /**
  105. * Process a given comment token.
  106. * If the comment is `eslint-disable` or `eslint-enable` then it reports the comment.
  107. * @param {RuleContext} context The rule context.
  108. * @param {Token} comment The comment token to process.
  109. * @param {boolean} reportUnusedDisableDirectives To report unused eslint-disable comments.
  110. * @returns {void}
  111. */
  112. function processBlock(context, comment, reportUnusedDisableDirectives) {
  113. const parsed = parse(COMMENT_DIRECTIVE_B, comment.value)
  114. if (parsed != null) {
  115. if (parsed.type === 'eslint-disable') {
  116. if (parsed.rules.length) {
  117. const rules = reportUnusedDisableDirectives
  118. ? reportUnusedRules(context, comment, parsed.type, parsed.rules)
  119. : parsed.rules
  120. for (const rule of rules) {
  121. disable(
  122. context,
  123. comment.loc.start,
  124. 'block',
  125. rule.ruleId,
  126. rule.key || '*'
  127. )
  128. }
  129. } else {
  130. const key = reportUnusedDisableDirectives
  131. ? reportUnused(context, comment, parsed.type)
  132. : ''
  133. disable(context, comment.loc.start, 'block', null, key)
  134. }
  135. } else {
  136. if (parsed.rules.length) {
  137. for (const rule of parsed.rules) {
  138. enable(context, comment.loc.start, 'block', rule.ruleId)
  139. }
  140. } else {
  141. enable(context, comment.loc.start, 'block', null)
  142. }
  143. }
  144. }
  145. }
  146. /**
  147. * Process a given comment token.
  148. * If the comment is `eslint-disable-line` or `eslint-disable-next-line` then it reports the comment.
  149. * @param {RuleContext} context The rule context.
  150. * @param {Token} comment The comment token to process.
  151. * @param {boolean} reportUnusedDisableDirectives To report unused eslint-disable comments.
  152. * @returns {void}
  153. */
  154. function processLine(context, comment, reportUnusedDisableDirectives) {
  155. const parsed = parse(COMMENT_DIRECTIVE_L, comment.value)
  156. if (parsed != null && comment.loc.start.line === comment.loc.end.line) {
  157. const line =
  158. comment.loc.start.line + (parsed.type === 'eslint-disable-line' ? 0 : 1)
  159. const column = -1
  160. if (parsed.rules.length) {
  161. const rules = reportUnusedDisableDirectives
  162. ? reportUnusedRules(context, comment, parsed.type, parsed.rules)
  163. : parsed.rules
  164. for (const rule of rules) {
  165. disable(context, { line, column }, 'line', rule.ruleId, rule.key || '')
  166. enable(context, { line: line + 1, column }, 'line', rule.ruleId)
  167. }
  168. } else {
  169. const key = reportUnusedDisableDirectives
  170. ? reportUnused(context, comment, parsed.type)
  171. : ''
  172. disable(context, { line, column }, 'line', null, key)
  173. enable(context, { line: line + 1, column }, 'line', null)
  174. }
  175. }
  176. }
  177. /**
  178. * Reports unused disable directive.
  179. * Do not check the use of directives here. Filter the directives used with postprocess.
  180. * @param {RuleContext} context The rule context.
  181. * @param {Token} comment The comment token to report.
  182. * @param {string} kind The comment directive kind.
  183. * @returns {string} The report key
  184. */
  185. function reportUnused(context, comment, kind) {
  186. const loc = comment.loc
  187. context.report({
  188. loc,
  189. messageId: 'unused',
  190. data: { kind }
  191. })
  192. return locToKey(loc.start)
  193. }
  194. /**
  195. * Reports unused disable directive rules.
  196. * Do not check the use of directives here. Filter the directives used with postprocess.
  197. * @param {RuleContext} context The rule context.
  198. * @param {Token} comment The comment token to report.
  199. * @param {string} kind The comment directive kind.
  200. * @param {RuleAndLocation[]} rules To report rule.
  201. * @returns { { ruleId: string, key: string }[] }
  202. */
  203. function reportUnusedRules(context, comment, kind, rules) {
  204. const sourceCode = context.getSourceCode()
  205. const commentStart = comment.range[0] + 4 /* <!-- */
  206. return rules.map((rule) => {
  207. const start = sourceCode.getLocFromIndex(commentStart + rule.index)
  208. const end = sourceCode.getLocFromIndex(
  209. commentStart + rule.index + rule.ruleId.length
  210. )
  211. context.report({
  212. loc: { start, end },
  213. messageId: 'unusedRule',
  214. data: { rule: rule.ruleId, kind }
  215. })
  216. return {
  217. ruleId: rule.ruleId,
  218. key: locToKey(start)
  219. }
  220. })
  221. }
  222. /**
  223. * Gets the key of location
  224. * @param {Position} location The location
  225. * @returns {string} The key
  226. */
  227. function locToKey(location) {
  228. return `line:${location.line},column${location.column}`
  229. }
  230. /**
  231. * Extracts the top-level elements in document fragment.
  232. * @param {VDocumentFragment} documentFragment The document fragment.
  233. * @returns {VElement[]} The top-level elements
  234. */
  235. function extractTopLevelHTMLElements(documentFragment) {
  236. return documentFragment.children.filter(utils.isVElement)
  237. }
  238. /**
  239. * Extracts the top-level comments in document fragment.
  240. * @param {VDocumentFragment} documentFragment The document fragment.
  241. * @returns {Token[]} The top-level comments
  242. */
  243. function extractTopLevelDocumentFragmentComments(documentFragment) {
  244. const elements = extractTopLevelHTMLElements(documentFragment)
  245. return documentFragment.comments.filter((comment) =>
  246. elements.every(
  247. (element) =>
  248. comment.range[1] <= element.range[0] ||
  249. element.range[1] <= comment.range[0]
  250. )
  251. )
  252. }
  253. // -----------------------------------------------------------------------------
  254. // Rule Definition
  255. // -----------------------------------------------------------------------------
  256. module.exports = {
  257. meta: {
  258. type: 'problem',
  259. docs: {
  260. description: 'support comment-directives in `<template>`', // eslint-disable-line eslint-plugin/require-meta-docs-description
  261. categories: ['base'],
  262. url: 'https://eslint.vuejs.org/rules/comment-directive.html'
  263. },
  264. schema: [
  265. {
  266. type: 'object',
  267. properties: {
  268. reportUnusedDisableDirectives: {
  269. type: 'boolean'
  270. }
  271. },
  272. additionalProperties: false
  273. }
  274. ],
  275. messages: {
  276. disableBlock: '--block {{key}}',
  277. enableBlock: '++block',
  278. disableLine: '--line {{key}}',
  279. enableLine: '++line',
  280. disableBlockRule: '-block {{rule}} {{key}}',
  281. enableBlockRule: '+block {{rule}}',
  282. disableLineRule: '-line {{rule}} {{key}}',
  283. enableLineRule: '+line {{rule}}',
  284. clear: 'clear',
  285. unused: 'Unused {{kind}} directive (no problems were reported).',
  286. unusedRule:
  287. "Unused {{kind}} directive (no problems were reported from '{{rule}}')."
  288. }
  289. },
  290. /**
  291. * @param {RuleContext} context - The rule context.
  292. * @returns {RuleListener} AST event handlers.
  293. */
  294. create(context) {
  295. const options = context.options[0] || {}
  296. /** @type {boolean} */
  297. const reportUnusedDisableDirectives = options.reportUnusedDisableDirectives
  298. const documentFragment =
  299. context.parserServices.getDocumentFragment &&
  300. context.parserServices.getDocumentFragment()
  301. return {
  302. Program(node) {
  303. if (node.templateBody) {
  304. // Send directives to the post-process.
  305. for (const comment of node.templateBody.comments) {
  306. processBlock(context, comment, reportUnusedDisableDirectives)
  307. processLine(context, comment, reportUnusedDisableDirectives)
  308. }
  309. // Send a clear mark to the post-process.
  310. context.report({
  311. loc: node.templateBody.loc.end,
  312. messageId: 'clear'
  313. })
  314. }
  315. if (documentFragment) {
  316. // Send directives to the post-process.
  317. for (const comment of extractTopLevelDocumentFragmentComments(
  318. documentFragment
  319. )) {
  320. processBlock(context, comment, reportUnusedDisableDirectives)
  321. processLine(context, comment, reportUnusedDisableDirectives)
  322. }
  323. // Send a clear mark to the post-process.
  324. for (const element of extractTopLevelHTMLElements(documentFragment)) {
  325. context.report({
  326. loc: element.loc.end,
  327. messageId: 'clear'
  328. })
  329. }
  330. }
  331. }
  332. }
  333. }
  334. }