Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit c5ada10

Browse files
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‎

Lines changed: 2 additions & 1 deletion
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‎

Lines changed: 91 additions & 0 deletions
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‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 224 additions & 0 deletions
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‎

Lines changed: 12 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /