You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
224 lines
5.6 KiB
224 lines
5.6 KiB
/** |
|
* @fileoverview Disallow use other than available `lang` |
|
* @author Yosuke Ota |
|
*/ |
|
'use strict' |
|
const utils = require('../utils') |
|
|
|
/** |
|
* @typedef {object} BlockOptions |
|
* @property {Set<string>} 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 |
|
* <template lang="html"></template> |
|
* <style lang="css"></style> |
|
* <script lang="js"></script> |
|
* <script lang="javascript"></script> |
|
* @type {Record<string, string[] | undefined>} |
|
*/ |
|
const DEFAULT_LANGUAGES = { |
|
template: ['html'], |
|
style: ['css'], |
|
script: ['js', 'javascript'] |
|
} |
|
|
|
/** |
|
* @param {NonNullable<BlockOptions['lang']>} 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 |
|
}) |
|
} |
|
}
|
|
|