comment-directive.js 10 KB

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