block-lang.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. const lang = new Set(
  56. Array.isArray(option.lang) ? option.lang : option.lang ? [option.lang] : []
  57. )
  58. let hasDefault = false
  59. for (const def of DEFAULT_LANGUAGES[blockName] || []) {
  60. if (lang.has(def)) {
  61. lang.delete(def)
  62. hasDefault = true
  63. }
  64. }
  65. if (lang.size === 0) {
  66. return {
  67. lang,
  68. allowNoLang: true
  69. }
  70. }
  71. return {
  72. lang,
  73. allowNoLang: hasDefault || Boolean(option.allowNoLang)
  74. }
  75. }
  76. /**
  77. * Normalizes a given options.
  78. * @param { UserOptions } options An option to parse.
  79. * @returns {Options} Normalized option.
  80. */
  81. function normalizeOptions(options) {
  82. if (!options) {
  83. return {}
  84. }
  85. /** @type {Options} */
  86. const normalized = {}
  87. for (const blockName of Object.keys(options)) {
  88. const value = options[blockName]
  89. if (value) {
  90. normalized[blockName] = normalizeOption(blockName, value)
  91. }
  92. }
  93. return normalized
  94. }
  95. // ------------------------------------------------------------------------------
  96. // Rule Definition
  97. // ------------------------------------------------------------------------------
  98. module.exports = {
  99. meta: {
  100. type: 'suggestion',
  101. docs: {
  102. description: 'disallow use other than available `lang`',
  103. categories: undefined,
  104. url: 'https://eslint.vuejs.org/rules/block-lang.html'
  105. },
  106. schema: [
  107. {
  108. type: 'object',
  109. patternProperties: {
  110. '^(?:\\S+)$': {
  111. oneOf: [
  112. {
  113. type: 'object',
  114. properties: {
  115. lang: {
  116. anyOf: [
  117. { type: 'string' },
  118. {
  119. type: 'array',
  120. items: {
  121. type: 'string'
  122. },
  123. uniqueItems: true,
  124. additionalItems: false
  125. }
  126. ]
  127. },
  128. allowNoLang: { type: 'boolean' }
  129. },
  130. additionalProperties: false
  131. }
  132. ]
  133. }
  134. },
  135. minProperties: 1,
  136. additionalProperties: false
  137. }
  138. ],
  139. messages: {
  140. expected:
  141. "Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'.",
  142. missing: "The 'lang' attribute of '<{{tag}}>' is missing.",
  143. unexpected: "Do not specify the 'lang' attribute of '<{{tag}}>'.",
  144. useOrNot:
  145. "Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'. Or, not specifying the `lang` attribute is allowed.",
  146. unexpectedDefault:
  147. "Do not explicitly specify the default language for the 'lang' attribute of '<{{tag}}>'."
  148. }
  149. },
  150. /** @param {RuleContext} context */
  151. create(context) {
  152. const options = normalizeOptions(
  153. context.options[0] || {
  154. script: { allowNoLang: true },
  155. template: { allowNoLang: true },
  156. style: { allowNoLang: true }
  157. }
  158. )
  159. if (!Object.keys(options).length) {
  160. // empty
  161. return {}
  162. }
  163. /**
  164. * @param {VElement} element
  165. * @returns {void}
  166. */
  167. function verify(element) {
  168. const tag = element.name
  169. const option = options[tag]
  170. if (!option) {
  171. return
  172. }
  173. const lang = utils.getAttribute(element, 'lang')
  174. if (lang == null || lang.value == null) {
  175. if (!option.allowNoLang) {
  176. context.report({
  177. node: element.startTag,
  178. messageId: 'missing',
  179. data: {
  180. tag
  181. }
  182. })
  183. }
  184. return
  185. }
  186. if (!option.lang.has(lang.value.value)) {
  187. let messageId
  188. if (!option.allowNoLang) {
  189. messageId = 'expected'
  190. } else if (option.lang.size === 0) {
  191. if ((DEFAULT_LANGUAGES[tag] || []).includes(lang.value.value)) {
  192. messageId = 'unexpectedDefault'
  193. } else {
  194. messageId = 'unexpected'
  195. }
  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. }