Skip to content

Commit

Permalink
Add vue/block-lang rule. (#1586)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Jul 30, 2021
1 parent 7664259 commit c5ada10
Show file tree
Hide file tree
Showing 6 changed files with 511 additions and 1 deletion.
3 changes: 2 additions & 1 deletion docs/rules/README.md
Expand Up @@ -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 | |
Expand Down
91 changes: 91 additions & 0 deletions 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)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -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'),
Expand Down
224 changes: 224 additions & 0 deletions 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
})
}
}
12 changes: 12 additions & 0 deletions lib/utils/index.js
Expand Up @@ -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.
Expand Down

0 comments on commit c5ada10

Please sign in to comment.