/** * @fileoverview Disallow use other than available `lang` * @author Yosuke Ota */ 'use strict' const utils = require('../utils') /** * @typedef {object} BlockOptions * @property {Set} lang * @property {boolean} allowNoLang */ /** * @typedef { { [element: string]: BlockOptions | undefined } } Options */ /** * @typedef {object} UserBlockOptions * @property {string[] | string} [lang] * @property {boolean} [allowNoLang] */ /** * @typedef { { [element: string]: UserBlockOptions | undefined } } UserOptions */ /** * https://vuejs.github.io/vetur/guide/highlighting.html * * * * * @type {Record} */ const DEFAULT_LANGUAGES = { template: ['html'], style: ['css'], script: ['js', 'javascript'] } /** * @param {NonNullable} lang */ function getAllowsLangPhrase(lang) { const langs = [...lang].map((s) => `"${s}"`) switch (langs.length) { case 1: return langs[0] default: return `${langs.slice(0, -1).join(', ')}, and ${langs[langs.length - 1]}` } } /** * Normalizes a given option. * @param {string} blockName The block name. * @param { UserBlockOptions } option An option to parse. * @returns {BlockOptions} Normalized option. */ function normalizeOption(blockName, option) { const lang = new Set( Array.isArray(option.lang) ? option.lang : option.lang ? [option.lang] : [] ) let hasDefault = false for (const def of DEFAULT_LANGUAGES[blockName] || []) { if (lang.has(def)) { lang.delete(def) hasDefault = true } } if (lang.size === 0) { return { lang, allowNoLang: true } } return { lang, allowNoLang: hasDefault || Boolean(option.allowNoLang) } } /** * Normalizes a given options. * @param { UserOptions } options An option to parse. * @returns {Options} Normalized option. */ function normalizeOptions(options) { if (!options) { return {} } /** @type {Options} */ const normalized = {} for (const blockName of Object.keys(options)) { const value = options[blockName] if (value) { normalized[blockName] = normalizeOption(blockName, value) } } return normalized } // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ module.exports = { meta: { type: 'suggestion', docs: { description: 'disallow use other than available `lang`', categories: undefined, url: 'https://eslint.vuejs.org/rules/block-lang.html' }, schema: [ { type: 'object', patternProperties: { '^(?:\\S+)$': { oneOf: [ { type: 'object', properties: { lang: { anyOf: [ { type: 'string' }, { type: 'array', items: { type: 'string' }, uniqueItems: true, additionalItems: false } ] }, allowNoLang: { type: 'boolean' } }, additionalProperties: false } ] } }, minProperties: 1, additionalProperties: false } ], messages: { expected: "Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'.", missing: "The 'lang' attribute of '<{{tag}}>' is missing.", unexpected: "Do not specify the 'lang' attribute of '<{{tag}}>'.", useOrNot: "Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'. Or, not specifying the `lang` attribute is allowed.", unexpectedDefault: "Do not explicitly specify the default language for the 'lang' attribute of '<{{tag}}>'." } }, /** @param {RuleContext} context */ create(context) { const options = normalizeOptions( context.options[0] || { script: { allowNoLang: true }, template: { allowNoLang: true }, style: { allowNoLang: true } } ) if (!Object.keys(options).length) { // empty return {} } /** * @param {VElement} element * @returns {void} */ function verify(element) { const tag = element.name const option = options[tag] if (!option) { return } const lang = utils.getAttribute(element, 'lang') if (lang == null || lang.value == null) { if (!option.allowNoLang) { context.report({ node: element.startTag, messageId: 'missing', data: { tag } }) } return } if (!option.lang.has(lang.value.value)) { let messageId if (!option.allowNoLang) { messageId = 'expected' } else if (option.lang.size === 0) { if ((DEFAULT_LANGUAGES[tag] || []).includes(lang.value.value)) { messageId = 'unexpectedDefault' } else { messageId = 'unexpected' } } else { messageId = 'useOrNot' } context.report({ node: lang, messageId, data: { tag, allows: getAllowsLangPhrase(option.lang) } }) } } return utils.defineDocumentVisitor(context, { 'VDocumentFragment > VElement': verify }) } }