block-lang.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. /**
  2. * @fileoverview Disallow use other than available `lang`
  3. * @author Yosuke Ota
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. /**
  8. * @typedef {object} BlockOptions
  9. * @property {Set<string>} lang
  10. * @property {boolean} allowNoLang
  11. */
  12. /**
  13. * @typedef { { [element: string]: BlockOptions | undefined } } Options
  14. */
  15. /**
  16. * @typedef {object} UserBlockOptions
  17. * @property {string[] | string} [lang]
  18. * @property {boolean} [allowNoLang]
  19. */
  20. /**
  21. * @typedef { { [element: string]: UserBlockOptions | undefined } } UserOptions
  22. */
  23. /**
  24. * https://vuejs.github.io/vetur/guide/highlighting.html
  25. * <template lang="html"></template>
  26. * <style lang="css"></style>
  27. * <script lang="js"></script>
  28. * <script lang="javascript"></script>
  29. * @type {Record<string, string[] | undefined>}
  30. */
  31. const DEFAULT_LANGUAGES = {
  32. template: ['html'],
  33. style: ['css'],
  34. script: ['js', 'javascript']
  35. }
  36. /**
  37. * @param {NonNullable<BlockOptions['lang']>} lang
  38. */
  39. function getAllowsLangPhrase(lang) {
  40. const langs = [...lang].map((s) => `"${s}"`)
  41. switch (langs.length) {
  42. case 1:
  43. return langs[0]
  44. default:
  45. return `${langs.slice(0, -1).join(', ')}, and ${langs[langs.length - 1]}`
  46. }
  47. }
  48. /**
  49. * Normalizes a given option.
  50. * @param {string} blockName The block name.
  51. * @param {UserBlockOptions} option An option to parse.
  52. * @returns {BlockOptions} Normalized option.
  53. */
  54. function normalizeOption(blockName, option) {
  55. /** @type {Set<string>} */
  56. let lang
  57. if (Array.isArray(option.lang)) {
  58. lang = new Set(option.lang)
  59. } else if (typeof option.lang === 'string') {
  60. lang = new Set([option.lang])
  61. } else {
  62. lang = new Set()
  63. }
  64. let hasDefault = false
  65. for (const def of DEFAULT_LANGUAGES[blockName] || []) {
  66. if (lang.has(def)) {
  67. lang.delete(def)
  68. hasDefault = true
  69. }
  70. }
  71. if (lang.size === 0) {
  72. return {
  73. lang,
  74. allowNoLang: true
  75. }
  76. }
  77. return {
  78. lang,
  79. allowNoLang: hasDefault || Boolean(option.allowNoLang)
  80. }
  81. }
  82. /**
  83. * Normalizes a given options.
  84. * @param { UserOptions } options An option to parse.
  85. * @returns {Options} Normalized option.
  86. */
  87. function normalizeOptions(options) {
  88. if (!options) {
  89. return {}
  90. }
  91. /** @type {Options} */
  92. const normalized = {}
  93. for (const blockName of Object.keys(options)) {
  94. const value = options[blockName]
  95. if (value) {
  96. normalized[blockName] = normalizeOption(blockName, value)
  97. }
  98. }
  99. return normalized
  100. }
  101. module.exports = {
  102. meta: {
  103. type: 'suggestion',
  104. docs: {
  105. description: 'disallow use other than available `lang`',
  106. categories: undefined,
  107. url: 'https://eslint.vuejs.org/rules/block-lang.html'
  108. },
  109. schema: [
  110. {
  111. type: 'object',
  112. patternProperties: {
  113. '^(?:\\S+)$': {
  114. oneOf: [
  115. {
  116. type: 'object',
  117. properties: {
  118. lang: {
  119. anyOf: [
  120. { type: 'string' },
  121. {
  122. type: 'array',
  123. items: {
  124. type: 'string'
  125. },
  126. uniqueItems: true,
  127. additionalItems: false
  128. }
  129. ]
  130. },
  131. allowNoLang: { type: 'boolean' }
  132. },
  133. additionalProperties: false
  134. }
  135. ]
  136. }
  137. },
  138. minProperties: 1,
  139. additionalProperties: false
  140. }
  141. ],
  142. messages: {
  143. expected:
  144. "Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'.",
  145. missing: "The 'lang' attribute of '<{{tag}}>' is missing.",
  146. unexpected: "Do not specify the 'lang' attribute of '<{{tag}}>'.",
  147. useOrNot:
  148. "Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'. Or, not specifying the `lang` attribute is allowed.",
  149. unexpectedDefault:
  150. "Do not explicitly specify the default language for the 'lang' attribute of '<{{tag}}>'."
  151. }
  152. },
  153. /** @param {RuleContext} context */
  154. create(context) {
  155. const options = normalizeOptions(
  156. context.options[0] || {
  157. script: { allowNoLang: true },
  158. template: { allowNoLang: true },
  159. style: { allowNoLang: true }
  160. }
  161. )
  162. if (Object.keys(options).length === 0) {
  163. return {}
  164. }
  165. /**
  166. * @param {VElement} element
  167. * @returns {void}
  168. */
  169. function verify(element) {
  170. const tag = element.name
  171. const option = options[tag]
  172. if (!option) {
  173. return
  174. }
  175. const lang = utils.getAttribute(element, 'lang')
  176. if (lang == null || lang.value == null) {
  177. if (!option.allowNoLang) {
  178. context.report({
  179. node: element.startTag,
  180. messageId: 'missing',
  181. data: {
  182. tag
  183. }
  184. })
  185. }
  186. return
  187. }
  188. if (!option.lang.has(lang.value.value)) {
  189. let messageId
  190. if (!option.allowNoLang) {
  191. messageId = 'expected'
  192. } else if (option.lang.size === 0) {
  193. messageId = (DEFAULT_LANGUAGES[tag] || []).includes(lang.value.value)
  194. ? 'unexpectedDefault'
  195. : 'unexpected'
  196. } else {
  197. messageId = 'useOrNot'
  198. }
  199. context.report({
  200. node: lang,
  201. messageId,
  202. data: {
  203. tag,
  204. allows: getAllowsLangPhrase(option.lang)
  205. }
  206. })
  207. }
  208. }
  209. return utils.defineDocumentVisitor(context, {
  210. 'VDocumentFragment > VElement': verify
  211. })
  212. }
  213. }