Repository: vuejs/eslint-plugin-vue (js)

Issue:
Rule Proposal: sfc-language
<!--
  Before proposing new rule, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22

searched for: 'lang', 'language', 'typescript', 'ts', 'script'
-->

**Please describe what the rule should do:**
<!-- A clear and concise description of what the rule should do. -->
Limit the languages used in all Vue SFC files for template, script and style blocks.

**What category should the rule belong to?**
<!-- (place an "X" next to just one item) -->
[ ] Enforces code style (layout)
[ ] Warns about a potential error (problem)
[ ] Suggests an alternate way of doing something (suggestion)
[ X] Other (please specify:)
Enforces code language.

**Provide 2-3 code examples that this rule should warn about:**
Using rule config:
 ```json5
{
    "script": "ts",
// "style": "scss",
// "template": "whatever",
}
```
```vue
// SomeComponent.vue

// Error: Missing attribute `lang="ts"`
<script>
// Error: Wrong lang attribute, expected `lang="ts"`
<script lang="js">

// Correct
<script lang="ts">
```

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

We're using Typescript and Vue SFC files, but it is very easy to create a new component file and forget to add a `lang="ts"` attribute to the script tag, and missing out on some type checking. 

We also use `<style lang="scss">`.


Pull request description:
Add `vue/block-lang` rule.
This PR adds `vue/block-lang` rule.
`vue/block-lang` rule that  disallows the use of languages other than those available in the your application for the lang attribute of block elements.

close #1458

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 0cc9bf16..f7a2e01d 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 00000000..9b342c0b
--- /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: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## :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"
+      }
+    }
+  ]
+}
+```
+
+<eslint-code-block :rules="{'vue/block-lang': ['error', { script: { lang: 'ts' } }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<script lang="ts">
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/block-lang': ['error', { script: { lang: 'ts' } }]}">
+
+```vue
+<!-- ✗ BAD -->
+<script>
+</script>
+```
+
+</eslint-code-block>
+
+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 `<template>`, `<style>` and `<script>`, it will be enforced to not specify `lang` attribute.  
+This is to prevent unintended problems with [Vetur](https://vuejs.github.io/vetur/).
+
+See also [Vetur - Syntax Highlighting](https://vuejs.github.io/vetur/guide/highlighting.html).
+:::
+
+### `{ script: { lang: 'js' } }`
+
+Same as `{ script: { allowNoLang: true } }`.
+
+<eslint-code-block :rules="{'vue/block-lang': ['error', { script: { lang: 'js' } }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<script>
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/block-lang': ['error', { script: { lang: 'js' } }]}">
+
+```vue
+<!-- ✗ BAD -->
+<script lang="js">
+</script>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/block-lang.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/block-lang.js)
diff --git a/lib/index.js b/lib/index.js
index 87f80c9a..1fe6e4eb 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -12,6 +12,7 @@ module.exports = {
     'arrow-spacing': require('./rules/arrow-spacing'),
     'attribute-hyphenation': require('./rules/attribute-hyphenation'),
     'attributes-order': require('./rules/attributes-order'),
+    'block-lang': require('./rules/block-lang'),
     'block-spacing': require('./rules/block-spacing'),
     'block-tag-newline': require('./rules/block-tag-newline'),
     'brace-style': require('./rules/brace-style'),
diff --git a/lib/rules/block-lang.js b/lib/rules/block-lang.js
new file mode 100644
index 00000000..f1383c83
--- /dev/null
+++ b/lib/rules/block-lang.js
@@ -0,0 +1,224 @@
+/**
+ * @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
+    })
+  }
+}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 510e016a..68a5bbee 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.
@@ -1706,7 +1718,7 @@ module.exports = {
           const callName = getStaticPropertyName(mem)
           if (
             callName &&
-            /^push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill$/u.exec(
+            /^(?:push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)$/u.exec(
               callName
             )
           ) {


Test patch:
diff --git a/tests/lib/rules/block-lang.js b/tests/lib/rules/block-lang.js
new file mode 100644
index 00000000..235ea778
--- /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: `<template></template>
+      <script lang="ts"></script>`,
+      options: [{ script: { lang: 'ts' } }]
+    },
+    {
+      code: `<template></template>
+      <script></script>`,
+      options: [{ script: { lang: 'js' } }]
+    },
+    {
+      code: '<i18n></i18n><i18n lang="json"></i18n>',
+      options: [{ i18n: { lang: 'json', allowNoLang: true } }]
+    },
+    {
+      code: `<template></template>
+      <script></script>
+      <style></style>`
+    }
+  ],
+  invalid: [
+    {
+      code: `<template></template>
+      <script lang="js"></script>`,
+      options: [{ script: { lang: 'ts' } }],
+      errors: [
+        {
+          message: `Only "ts" can be used for the 'lang' attribute of '<script>'.`,
+          line: 2,
+          column: 15
+        }
+      ]
+    },
+    {
+      code: `<template></template>
+      <script lang="js"></script>`,
+      options: [{ script: { lang: ['ts'] } }],
+      errors: [
+        {
+          message: `Only "ts" can be used for the 'lang' attribute of '<script>'.`,
+          line: 2,
+          column: 15
+        }
+      ]
+    },
+    {
+      code: `<template></template>
+      <script></script>`,
+      options: [{ script: { lang: 'ts' } }],
+      errors: [
+        {
+          message: "The 'lang' attribute of '<script>' is missing.",
+          line: 2,
+          column: 7
+        }
+      ]
+    },
+    {
+      code: `<template></template>
+      <script lang=""></script>`,
+      options: [{ script: { lang: 'ts' } }],
+      errors: [
+        {
+          message: `Only "ts" can be used for the 'lang' attribute of '<script>'.`,
+          line: 2,
+          column: 15
+        }
+      ]
+    },
+    {
+      code: '<template></template><script lang="ts"></script>',
+      options: [{ script: { lang: 'js' } }],
+      errors: [
+        {
+          message: "Do not specify the 'lang' attribute of '<script>'.",
+          line: 1,
+          column: 30
+        }
+      ]
+    },
+    {
+      code: '<template></template><script lang="js"></script>',
+      options: [{ script: { lang: 'js' } }],
+      errors: [
+        {
+          message:
+            "Do not explicitly specify the default language for the 'lang' attribute of '<script>'.",
+          line: 1,
+          column: 30
+        }
+      ]
+    },
+    {
+      code: '<template></template><script lang="ts"></script>',
+      options: [{ script: {} }],
+      errors: [
+        {
+          message: "Do not specify the 'lang' attribute of '<script>'.",
+          line: 1,
+          column: 30
+        }
+      ]
+    },
+    {
+      code: `<i18n></i18n>
+      <i18n lang="json"></i18n>`,
+      options: [{ i18n: { lang: 'json' } }],
+      errors: [
+        {
+          message: "The 'lang' attribute of '<i18n>' is missing.",
+          line: 1,
+          column: 1
+        }
+      ]
+    },
+    {
+      code: `<i18n></i18n>
+      <i18n lang="yaml"></i18n>`,
+      options: [{ i18n: { lang: 'json', allowNoLang: true } }],
+      errors: [
+        {
+          message:
+            "Only \"json\" can be used for the 'lang' attribute of '<i18n>'. Or, not specifying the `lang` attribute is allowed.",
+          line: 2,
+          column: 13
+        }
+      ]
+    },
+    {
+      code: `<i18n></i18n>
+      <i18n lang="toml"></i18n>`,
+      options: [{ i18n: { lang: ['json', 'yaml'], allowNoLang: true } }],
+      errors: [
+        {
+          message:
+            'Only "json", and "yaml" can be used for the \'lang\' attribute of \'<i18n>\'. Or, not specifying the `lang` attribute is allowed.',
+          line: 2,
+          column: 13
+        }
+      ]
+    },
+
+    {
+      code: `<template lang="pug"></template>
+      <script lang="ts"></script>
+      <style lang="stylus"></style>`,
+      errors: [
+        {
+          message: "Do not specify the 'lang' attribute of '<template>'.",
+          line: 1,
+          column: 11
+        },
+        {
+          message: "Do not specify the 'lang' attribute of '<script>'.",
+          line: 2,
+          column: 15
+        },
+        {
+          message: "Do not specify the 'lang' attribute of '<style>'.",
+          line: 3,
+          column: 14
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-mutating-props.js b/tests/lib/rules/no-mutating-props.js
index 2dcc68c5..c0d6c273 100644
--- a/tests/lib/rules/no-mutating-props.js
+++ b/tests/lib/rules/no-mutating-props.js
@@ -318,6 +318,19 @@ ruleTester.run('no-mutating-props', rule, {
         value.value++
         </script>
       `
+    },
+    {
+      // https://github.com/vuejs/eslint-plugin-vue/issues/1579
+      filename: 'test.vue',
+      code: `
+      <script>
+        export default {
+          props: ['prop'],
+          setup(props) {
+            props.prop.sortAscending()
+          }
+        }
+      </script>`
     }
   ],