diff --git a/docs/rules/README.md b/docs/rules/README.md
index 0cc9bf16f..f7a2e01d5 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -279,13 +279,14 @@ For example:
```json
{
"rules": {
- "vue/block-tag-newline": "error"
+ "vue/block-lang": "error"
}
}
```
| Rule ID | Description | |
|:--------|:------------|:---|
+| [vue/block-lang](./block-lang.md) | disallow use other than available `lang` | |
| [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: |
| [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | |
diff --git a/docs/rules/block-lang.md b/docs/rules/block-lang.md
new file mode 100644
index 000000000..9b342c0b2
--- /dev/null
+++ b/docs/rules/block-lang.md
@@ -0,0 +1,91 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/block-lang
+description: disallow use other than available `lang`
+---
+# vue/block-lang
+
+> disallow use other than available `lang`
+
+- :exclamation: ***This rule has not been released yet.***
+
+## :book: Rule Details
+
+This rule disallows the use of languages other than those available in the your application for the lang attribute of block elements.
+
+## :wrench: Options
+
+```json
+{
+ "vue/block-lang": ["error",
+ {
+ "script": {
+ "lang": "ts"
+ }
+ }
+ ]
+}
+```
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+Specify the block name for the key of the option object.
+You can use the object as a value and use the following properties:
+
+- `lang` ... Specifies the available value for the `lang` attribute of the block. If multiple languages are available, specify them as an array. If you do not specify it, will disallow any language.
+- `allowNoLang` ... If `true`, allows the `lang` attribute not to be specified (allows the use of the default language of block).
+
+::: warning Note
+If the default language is specified for `lang` option of ``, `
+ *
+ *
+ * @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
+ })
+ }
+}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index bce6ccea4..68a5bbee9 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -239,6 +239,18 @@ module.exports = {
*/
defineTemplateBodyVisitor,
+ /**
+ * Register the given visitor to parser services.
+ * If the parser service of `vue-eslint-parser` was not found,
+ * this generates a warning.
+ *
+ * @param {RuleContext} context The rule context to use parser services.
+ * @param {TemplateListener} documentVisitor The visitor to traverse the document.
+ * @param { { triggerSelector: "Program" | "Program:exit" } } [options] The options.
+ * @returns {RuleListener} The merged visitor.
+ */
+ defineDocumentVisitor,
+
/**
* Wrap a given core rule to apply it to Vue.js template.
* @param {string} coreRuleName The name of the core rule implementation to wrap.
diff --git a/tests/lib/rules/block-lang.js b/tests/lib/rules/block-lang.js
new file mode 100644
index 000000000..235ea7783
--- /dev/null
+++ b/tests/lib/rules/block-lang.js
@@ -0,0 +1,181 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/block-lang')
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('block-lang', rule, {
+ valid: [
+ {
+ code: `
+ `,
+ options: [{ script: { lang: 'ts' } }]
+ },
+ {
+ code: `
+ `,
+ options: [{ script: { lang: 'js' } }]
+ },
+ {
+ code: '',
+ options: [{ i18n: { lang: 'json', allowNoLang: true } }]
+ },
+ {
+ code: `
+
+ `
+ }
+ ],
+ invalid: [
+ {
+ code: `
+ `,
+ options: [{ script: { lang: 'ts' } }],
+ errors: [
+ {
+ message: `Only "ts" can be used for the 'lang' attribute of '`,
+ options: [{ script: { lang: ['ts'] } }],
+ errors: [
+ {
+ message: `Only "ts" can be used for the 'lang' attribute of '`,
+ options: [{ script: { lang: 'ts' } }],
+ errors: [
+ {
+ message: "The 'lang' attribute of '`,
+ options: [{ script: { lang: 'ts' } }],
+ errors: [
+ {
+ message: `Only "ts" can be used for the 'lang' attribute of '',
+ options: [{ script: { lang: 'js' } }],
+ errors: [
+ {
+ message: "Do not specify the 'lang' attribute of '',
+ options: [{ script: { lang: 'js' } }],
+ errors: [
+ {
+ message:
+ "Do not explicitly specify the default language for the 'lang' attribute of '',
+ options: [{ script: {} }],
+ errors: [
+ {
+ message: "Do not specify the 'lang' attribute of '
+ `,
+ errors: [
+ {
+ message: "Do not specify the 'lang' attribute of ''.",
+ line: 1,
+ column: 11
+ },
+ {
+ message: "Do not specify the 'lang' attribute of '