Skip to content

Commit c5ada10

Browse files
authored
Add vue/block-lang rule. (#1586)
1 parent 7664259 commit c5ada10

File tree

6 files changed

+511
-1
lines changed

6 files changed

+511
-1
lines changed

docs/rules/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -279,13 +279,14 @@ For example:
279279
```json
280280
{
281281
"rules": {
282-
"vue/block-tag-newline": "error"
282+
"vue/block-lang": "error"
283283
}
284284
}
285285
```
286286

287287
| Rule ID | Description | |
288288
|:--------|:------------|:---|
289+
| [vue/block-lang](./block-lang.md) | disallow use other than available `lang` | |
289290
| [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: |
290291
| [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
291292
| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | |

docs/rules/block-lang.md

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/block-lang
5+
description: disallow use other than available `lang`
6+
---
7+
# vue/block-lang
8+
9+
> disallow use other than available `lang`
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
## :book: Rule Details
14+
15+
This rule disallows the use of languages other than those available in the your application for the lang attribute of block elements.
16+
17+
## :wrench: Options
18+
19+
```json
20+
{
21+
"vue/block-lang": ["error",
22+
{
23+
"script": {
24+
"lang": "ts"
25+
}
26+
}
27+
]
28+
}
29+
```
30+
31+
<eslint-code-block :rules="{'vue/block-lang': ['error', { script: { lang: 'ts' } }]}">
32+
33+
```vue
34+
<!-- ✓ GOOD -->
35+
<script lang="ts">
36+
</script>
37+
```
38+
39+
</eslint-code-block>
40+
41+
<eslint-code-block :rules="{'vue/block-lang': ['error', { script: { lang: 'ts' } }]}">
42+
43+
```vue
44+
<!-- ✗ BAD -->
45+
<script>
46+
</script>
47+
```
48+
49+
</eslint-code-block>
50+
51+
Specify the block name for the key of the option object.
52+
You can use the object as a value and use the following properties:
53+
54+
- `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.
55+
- `allowNoLang` ... If `true`, allows the `lang` attribute not to be specified (allows the use of the default language of block).
56+
57+
::: warning Note
58+
If the default language is specified for `lang` option of `<template>`, `<style>` and `<script>`, it will be enforced to not specify `lang` attribute.
59+
This is to prevent unintended problems with [Vetur](https://vuejs.github.io/vetur/).
60+
61+
See also [Vetur - Syntax Highlighting](https://vuejs.github.io/vetur/guide/highlighting.html).
62+
:::
63+
64+
### `{ script: { lang: 'js' } }`
65+
66+
Same as `{ script: { allowNoLang: true } }`.
67+
68+
<eslint-code-block :rules="{'vue/block-lang': ['error', { script: { lang: 'js' } }]}">
69+
70+
```vue
71+
<!-- ✓ GOOD -->
72+
<script>
73+
</script>
74+
```
75+
76+
</eslint-code-block>
77+
78+
<eslint-code-block :rules="{'vue/block-lang': ['error', { script: { lang: 'js' } }]}">
79+
80+
```vue
81+
<!-- ✗ BAD -->
82+
<script lang="js">
83+
</script>
84+
```
85+
86+
</eslint-code-block>
87+
88+
## :mag: Implementation
89+
90+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/block-lang.js)
91+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/block-lang.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module.exports = {
1212
'arrow-spacing': require('./rules/arrow-spacing'),
1313
'attribute-hyphenation': require('./rules/attribute-hyphenation'),
1414
'attributes-order': require('./rules/attributes-order'),
15+
'block-lang': require('./rules/block-lang'),
1516
'block-spacing': require('./rules/block-spacing'),
1617
'block-tag-newline': require('./rules/block-tag-newline'),
1718
'brace-style': require('./rules/brace-style'),

lib/rules/block-lang.js

+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/**
2+
* @fileoverview Disallow use other than available `lang`
3+
* @author Yosuke Ota
4+
*/
5+
'use strict'
6+
const utils = require('../utils')
7+
8+
/**
9+
* @typedef {object} BlockOptions
10+
* @property {Set<string>} lang
11+
* @property {boolean} allowNoLang
12+
*/
13+
/**
14+
* @typedef { { [element: string]: BlockOptions | undefined } } Options
15+
*/
16+
/**
17+
* @typedef {object} UserBlockOptions
18+
* @property {string[] | string} [lang]
19+
* @property {boolean} [allowNoLang]
20+
*/
21+
/**
22+
* @typedef { { [element: string]: UserBlockOptions | undefined } } UserOptions
23+
*/
24+
25+
/**
26+
* https://vuejs.github.io/vetur/guide/highlighting.html
27+
* <template lang="html"></template>
28+
* <style lang="css"></style>
29+
* <script lang="js"></script>
30+
* <script lang="javascript"></script>
31+
* @type {Record<string, string[] | undefined>}
32+
*/
33+
const DEFAULT_LANGUAGES = {
34+
template: ['html'],
35+
style: ['css'],
36+
script: ['js', 'javascript']
37+
}
38+
39+
/**
40+
* @param {NonNullable<BlockOptions['lang']>} lang
41+
*/
42+
function getAllowsLangPhrase(lang) {
43+
const langs = [...lang].map((s) => `"${s}"`)
44+
switch (langs.length) {
45+
case 1:
46+
return langs[0]
47+
default:
48+
return `${langs.slice(0, -1).join(', ')}, and ${langs[langs.length - 1]}`
49+
}
50+
}
51+
52+
/**
53+
* Normalizes a given option.
54+
* @param {string} blockName The block name.
55+
* @param { UserBlockOptions } option An option to parse.
56+
* @returns {BlockOptions} Normalized option.
57+
*/
58+
function normalizeOption(blockName, option) {
59+
const lang = new Set(
60+
Array.isArray(option.lang) ? option.lang : option.lang ? [option.lang] : []
61+
)
62+
let hasDefault = false
63+
for (const def of DEFAULT_LANGUAGES[blockName] || []) {
64+
if (lang.has(def)) {
65+
lang.delete(def)
66+
hasDefault = true
67+
}
68+
}
69+
if (lang.size === 0) {
70+
return {
71+
lang,
72+
allowNoLang: true
73+
}
74+
}
75+
return {
76+
lang,
77+
allowNoLang: hasDefault || Boolean(option.allowNoLang)
78+
}
79+
}
80+
/**
81+
* Normalizes a given options.
82+
* @param { UserOptions } options An option to parse.
83+
* @returns {Options} Normalized option.
84+
*/
85+
function normalizeOptions(options) {
86+
if (!options) {
87+
return {}
88+
}
89+
90+
/** @type {Options} */
91+
const normalized = {}
92+
93+
for (const blockName of Object.keys(options)) {
94+
const value = options[blockName]
95+
if (value) {
96+
normalized[blockName] = normalizeOption(blockName, value)
97+
}
98+
}
99+
100+
return normalized
101+
}
102+
103+
// ------------------------------------------------------------------------------
104+
// Rule Definition
105+
// ------------------------------------------------------------------------------
106+
107+
module.exports = {
108+
meta: {
109+
type: 'suggestion',
110+
docs: {
111+
description: 'disallow use other than available `lang`',
112+
categories: undefined,
113+
url: 'https://eslint.vuejs.org/rules/block-lang.html'
114+
},
115+
schema: [
116+
{
117+
type: 'object',
118+
patternProperties: {
119+
'^(?:\\S+)$': {
120+
oneOf: [
121+
{
122+
type: 'object',
123+
properties: {
124+
lang: {
125+
anyOf: [
126+
{ type: 'string' },
127+
{
128+
type: 'array',
129+
items: {
130+
type: 'string'
131+
},
132+
uniqueItems: true,
133+
additionalItems: false
134+
}
135+
]
136+
},
137+
allowNoLang: { type: 'boolean' }
138+
},
139+
additionalProperties: false
140+
}
141+
]
142+
}
143+
},
144+
minProperties: 1,
145+
additionalProperties: false
146+
}
147+
],
148+
messages: {
149+
expected:
150+
"Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'.",
151+
missing: "The 'lang' attribute of '<{{tag}}>' is missing.",
152+
unexpected: "Do not specify the 'lang' attribute of '<{{tag}}>'.",
153+
useOrNot:
154+
"Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'. Or, not specifying the `lang` attribute is allowed.",
155+
unexpectedDefault:
156+
"Do not explicitly specify the default language for the 'lang' attribute of '<{{tag}}>'."
157+
}
158+
},
159+
/** @param {RuleContext} context */
160+
create(context) {
161+
const options = normalizeOptions(
162+
context.options[0] || {
163+
script: { allowNoLang: true },
164+
template: { allowNoLang: true },
165+
style: { allowNoLang: true }
166+
}
167+
)
168+
if (!Object.keys(options).length) {
169+
// empty
170+
return {}
171+
}
172+
173+
/**
174+
* @param {VElement} element
175+
* @returns {void}
176+
*/
177+
function verify(element) {
178+
const tag = element.name
179+
const option = options[tag]
180+
if (!option) {
181+
return
182+
}
183+
const lang = utils.getAttribute(element, 'lang')
184+
if (lang == null || lang.value == null) {
185+
if (!option.allowNoLang) {
186+
context.report({
187+
node: element.startTag,
188+
messageId: 'missing',
189+
data: {
190+
tag
191+
}
192+
})
193+
}
194+
return
195+
}
196+
if (!option.lang.has(lang.value.value)) {
197+
let messageId
198+
if (!option.allowNoLang) {
199+
messageId = 'expected'
200+
} else if (option.lang.size === 0) {
201+
if ((DEFAULT_LANGUAGES[tag] || []).includes(lang.value.value)) {
202+
messageId = 'unexpectedDefault'
203+
} else {
204+
messageId = 'unexpected'
205+
}
206+
} else {
207+
messageId = 'useOrNot'
208+
}
209+
context.report({
210+
node: lang,
211+
messageId,
212+
data: {
213+
tag,
214+
allows: getAllowsLangPhrase(option.lang)
215+
}
216+
})
217+
}
218+
}
219+
220+
return utils.defineDocumentVisitor(context, {
221+
'VDocumentFragment > VElement': verify
222+
})
223+
}
224+
}

lib/utils/index.js

+12
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,18 @@ module.exports = {
239239
*/
240240
defineTemplateBodyVisitor,
241241

242+
/**
243+
* Register the given visitor to parser services.
244+
* If the parser service of `vue-eslint-parser` was not found,
245+
* this generates a warning.
246+
*
247+
* @param {RuleContext} context The rule context to use parser services.
248+
* @param {TemplateListener} documentVisitor The visitor to traverse the document.
249+
* @param { { triggerSelector: "Program" | "Program:exit" } } [options] The options.
250+
* @returns {RuleListener} The merged visitor.
251+
*/
252+
defineDocumentVisitor,
253+
242254
/**
243255
* Wrap a given core rule to apply it to Vue.js template.
244256
* @param {string} coreRuleName The name of the core rule implementation to wrap.

0 commit comments

Comments
 (0)