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.
228 lines
5.5 KiB
228 lines
5.5 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) {
|
|
/** @type {Set<string>} */
|
|
let lang
|
|
|
|
if (Array.isArray(option.lang)) {
|
|
lang = new Set(option.lang)
|
|
} else if (typeof option.lang === 'string') {
|
|
lang = new Set([option.lang])
|
|
} else {
|
|
lang = new Set()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 === 0) {
|
|
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) {
|
|
messageId = (DEFAULT_LANGUAGES[tag] || []).includes(lang.value.value)
|
|
? 'unexpectedDefault'
|
|
: 'unexpected'
|
|
} else {
|
|
messageId = 'useOrNot'
|
|
}
|
|
context.report({
|
|
node: lang,
|
|
messageId,
|
|
data: {
|
|
tag,
|
|
allows: getAllowsLangPhrase(option.lang)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
return utils.defineDocumentVisitor(context, {
|
|
'VDocumentFragment > VElement': verify
|
|
})
|
|
}
|
|
}
|