Skip to content

Commit

Permalink
Add prefer-sfc-lang-attr rule (#267)
Browse files Browse the repository at this point in the history
* Add `prefer-sfc-lang-attr` rule

* fix
  • Loading branch information
ota-meshi authored Nov 18, 2021
1 parent adbe0fe commit 863848b
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
| [@intlify/vue-i18n/<wbr>no-dynamic-keys](./no-dynamic-keys.html) | disallow localization dynamic keys at localization methods | |
| [@intlify/vue-i18n/<wbr>no-missing-keys-in-other-locales](./no-missing-keys-in-other-locales.html) | disallow missing locale message keys in other locales | |
| [@intlify/vue-i18n/<wbr>no-unused-keys](./no-unused-keys.html) | disallow unused localization keys | :black_nib: |
| [@intlify/vue-i18n/<wbr>prefer-sfc-lang-attr](./prefer-sfc-lang-attr.html) | require lang attribute on `<i18n>` block | :black_nib: |

## Stylistic Issues

Expand Down
65 changes: 65 additions & 0 deletions docs/rules/prefer-sfc-lang-attr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
title: '@intlify/vue-i18n/prefer-sfc-lang-attr'
description: require lang attribute on `<i18n>` block
---

# @intlify/vue-i18n/prefer-sfc-lang-attr

> require lang attribute on `<i18n>` block
- :black_nib:️ The `--fix` option on the [command line](http://eslint.org/docs/user-guide/command-line-interface#fix) can automatically fix some of the problems reported by this rule.

## :book: Rule Details

This rule enforce `lang` attribute to be specified to `<i18n>` custom block.

:-1: Examples of **incorrect** code for this rule:

locale messages:

<eslint-code-block fix>

<!-- eslint-skip -->

```vue
<i18n>
{
"en": {
"message": "hello!"
}
}
</i18n>
<script>
/* eslint @intlify/vue-i18n/prefer-sfc-lang-attr: 'error' */
</script>
```

</eslint-code-block>

:+1: Examples of **correct** code for this rule:

locale messages:

<eslint-code-block fix>

<!-- eslint-skip -->

```vue
<i18n lang="json">
{
"en": {
"message": "hello!"
}
}
</i18n>
<script>
/* eslint @intlify/vue-i18n/prefer-sfc-lang-attr: 'error' */
</script>
```

</eslint-code-block>

## :mag: Implementation

- [Rule source](https://github.com/intlify/eslint-plugin-vue-i18n/blob/master/lib/rules/prefer-sfc-lang-attr.ts)
- [Test source](https://github.com/intlify/eslint-plugin-vue-i18n/tree/master/tests/lib/rules/prefer-sfc-lang-attr.ts)
2 changes: 2 additions & 0 deletions lib/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import noRawText from './rules/no-raw-text'
import noUnusedKeys from './rules/no-unused-keys'
import noVHtml from './rules/no-v-html'
import preferLinkedKeyWithParen from './rules/prefer-linked-key-with-paren'
import preferSfcLangAttr from './rules/prefer-sfc-lang-attr'
import validMessageSyntax from './rules/valid-message-syntax'

export = {
Expand All @@ -30,5 +31,6 @@ export = {
'no-unused-keys': noUnusedKeys,
'no-v-html': noVHtml,
'prefer-linked-key-with-paren': preferLinkedKeyWithParen,
'prefer-sfc-lang-attr': preferSfcLangAttr,
'valid-message-syntax': validMessageSyntax
}
61 changes: 61 additions & 0 deletions lib/rules/prefer-sfc-lang-attr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { getAttribute, isI18nBlock } from '../utils/index'
import type { RuleContext, RuleListener } from '../types'

function create(context: RuleContext): RuleListener {
const df = context.parserServices.getDocumentFragment?.()
if (!df) {
return {}
}

return {
Program() {
for (const i18n of df.children.filter(isI18nBlock)) {
const srcAttrs = getAttribute(i18n, 'src')
if (srcAttrs != null) {
continue
}
const langAttrs = getAttribute(i18n, 'lang')
if (
langAttrs == null ||
langAttrs.value == null ||
!langAttrs.value.value
) {
context.report({
loc: (langAttrs?.value ?? langAttrs ?? i18n.startTag).loc,
messageId: 'required',
fix(fixer) {
if (langAttrs) {
return fixer.replaceTextRange(langAttrs.range, 'lang="json"')
}
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
const closeToken = tokenStore.getLastToken(i18n.startTag)
const beforeToken = tokenStore.getTokenBefore(closeToken)
return fixer.insertTextBeforeRange(
closeToken.range,
(beforeToken.range[1] < closeToken.range[0] ? '' : ' ') +
'lang="json" '
)
}
})
}
}
}
}
}

export = {
meta: {
type: 'suggestion',
docs: {
description: 'require lang attribute on `<i18n>` block',
category: 'Best Practices',
recommended: false
},
fixable: 'code',
schema: [],
messages: {
required: '`lang` attribute is required.'
}
},
create
}
2 changes: 2 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as casing from './utils/casing'
import * as collectKeys from './utils/collect-keys'
import * as collectLinkedKeys from './utils/collect-linked-keys'
import * as defaultTimeouts from './utils/default-timeouts'
import * as getCwd from './utils/get-cwd'
import * as globSync from './utils/glob-sync'
import * as globUtils from './utils/glob-utils'
import * as ignoredPaths from './utils/ignored-paths'
Expand All @@ -22,6 +23,7 @@ export = {
'collect-keys': collectKeys,
'collect-linked-keys': collectLinkedKeys,
'default-timeouts': defaultTimeouts,
'get-cwd': getCwd,
'glob-sync': globSync,
'glob-utils': globUtils,
'ignored-paths': ignoredPaths,
Expand Down
127 changes: 127 additions & 0 deletions tests/lib/rules/prefer-sfc-lang-attr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { RuleTester } from 'eslint'
import rule = require('../../../lib/rules/prefer-sfc-lang-attr')

new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
}).run('prefer-sfc-lang-attr', rule as never, {
valid: [
{
filename: 'test.vue',
code: `
<i18n lang="json">{}</i18n>
<template></template>
<script></script>`
},
{
filename: 'test.vue',
code: `
<i18n lang="json5">{}</i18n>
<template></template>
<script></script>`
},
{
filename: 'test.vue',
code: `
<i18n lang="yaml">{}</i18n>
<template></template>
<script></script>`
}
],
invalid: [
{
filename: 'test.vue',
code: `
<i18n>{}</i18n>
<template></template>
<script></script>`,
output: `
<i18n lang="json" >{}</i18n>
<template></template>
<script></script>`,
errors: [
{
message: '`lang` attribute is required.',
line: 2,
column: 7,
endLine: 2,
endColumn: 13
}
]
},
{
filename: 'test.vue',
code: `
<i18n>{}</i18n>
<template></template>`,
output: `
<i18n lang="json" >{}</i18n>
<template></template>`,
errors: [
{
message: '`lang` attribute is required.',
line: 2,
column: 7,
endLine: 2,
endColumn: 13
}
]
},
{
filename: 'test.vue',
code: `
<i18n locale="en" >{}</i18n>
<template></template>
<script></script>`,
output: `
<i18n locale="en" lang="json" >{}</i18n>
<template></template>
<script></script>`,
errors: [
{
message: '`lang` attribute is required.',
line: 2,
column: 7,
endLine: 2,
endColumn: 26
}
]
},
{
filename: 'test.vue',
code: `
<i18n lang>{}</i18n>
<template></template>`,
output: `
<i18n lang="json">{}</i18n>
<template></template>`,
errors: [
{
message: '`lang` attribute is required.',
line: 2,
column: 13,
endLine: 2,
endColumn: 17
}
]
},
{
filename: 'test.vue',
code: `
<i18n lang="">{}</i18n>
<template></template>`,
output: `
<i18n lang="json">{}</i18n>
<template></template>`,
errors: [
{
message: '`lang` attribute is required.',
line: 2,
column: 18,
endLine: 2,
endColumn: 20
}
]
}
]
})

0 comments on commit 863848b

Please sign in to comment.