From 863848b200e19486edc1e081075d8b0d41e5d96a Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Thu, 18 Nov 2021 22:36:59 +0900 Subject: [PATCH] Add `prefer-sfc-lang-attr` rule (#267) * Add `prefer-sfc-lang-attr` rule * fix --- docs/rules/README.md | 1 + docs/rules/prefer-sfc-lang-attr.md | 65 ++++++++++++ lib/rules.ts | 2 + lib/rules/prefer-sfc-lang-attr.ts | 61 ++++++++++++ lib/utils.ts | 2 + tests/lib/rules/prefer-sfc-lang-attr.ts | 127 ++++++++++++++++++++++++ 6 files changed, 258 insertions(+) create mode 100644 docs/rules/prefer-sfc-lang-attr.md create mode 100644 lib/rules/prefer-sfc-lang-attr.ts create mode 100644 tests/lib/rules/prefer-sfc-lang-attr.ts diff --git a/docs/rules/README.md b/docs/rules/README.md index da53312d..2ecfb9f5 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -28,6 +28,7 @@ | [@intlify/vue-i18n/no-dynamic-keys](./no-dynamic-keys.html) | disallow localization dynamic keys at localization methods | | | [@intlify/vue-i18n/no-missing-keys-in-other-locales](./no-missing-keys-in-other-locales.html) | disallow missing locale message keys in other locales | | | [@intlify/vue-i18n/no-unused-keys](./no-unused-keys.html) | disallow unused localization keys | :black_nib: | +| [@intlify/vue-i18n/prefer-sfc-lang-attr](./prefer-sfc-lang-attr.html) | require lang attribute on `` block | :black_nib: | ## Stylistic Issues diff --git a/docs/rules/prefer-sfc-lang-attr.md b/docs/rules/prefer-sfc-lang-attr.md new file mode 100644 index 00000000..26d0e7f9 --- /dev/null +++ b/docs/rules/prefer-sfc-lang-attr.md @@ -0,0 +1,65 @@ +--- +title: '@intlify/vue-i18n/prefer-sfc-lang-attr' +description: require lang attribute on `` block +--- + +# @intlify/vue-i18n/prefer-sfc-lang-attr + +> require lang attribute on `` 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 `` custom block. + +:-1: Examples of **incorrect** code for this rule: + +locale messages: + + + + + +```vue + +{ + "en": { + "message": "hello!" + } +} + + +``` + + + +:+1: Examples of **correct** code for this rule: + +locale messages: + + + + + +```vue + +{ + "en": { + "message": "hello!" + } +} + + +``` + + + +## :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) diff --git a/lib/rules.ts b/lib/rules.ts index 0c81ee95..b3906097 100644 --- a/lib/rules.ts +++ b/lib/rules.ts @@ -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 = { @@ -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 } diff --git a/lib/rules/prefer-sfc-lang-attr.ts b/lib/rules/prefer-sfc-lang-attr.ts new file mode 100644 index 00000000..4c212705 --- /dev/null +++ b/lib/rules/prefer-sfc-lang-attr.ts @@ -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 `` block', + category: 'Best Practices', + recommended: false + }, + fixable: 'code', + schema: [], + messages: { + required: '`lang` attribute is required.' + } + }, + create +} diff --git a/lib/utils.ts b/lib/utils.ts index a6d4c711..408b2874 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -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' @@ -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, diff --git a/tests/lib/rules/prefer-sfc-lang-attr.ts b/tests/lib/rules/prefer-sfc-lang-attr.ts new file mode 100644 index 00000000..f0ef9ed8 --- /dev/null +++ b/tests/lib/rules/prefer-sfc-lang-attr.ts @@ -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: ` + {} + + ` + }, + { + filename: 'test.vue', + code: ` + {} + + ` + }, + { + filename: 'test.vue', + code: ` + {} + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + {} + + `, + output: ` + {} + + `, + errors: [ + { + message: '`lang` attribute is required.', + line: 2, + column: 7, + endLine: 2, + endColumn: 13 + } + ] + }, + { + filename: 'test.vue', + code: ` + {} + `, + output: ` + {} + `, + errors: [ + { + message: '`lang` attribute is required.', + line: 2, + column: 7, + endLine: 2, + endColumn: 13 + } + ] + }, + { + filename: 'test.vue', + code: ` + {} + + `, + output: ` + {} + + `, + errors: [ + { + message: '`lang` attribute is required.', + line: 2, + column: 7, + endLine: 2, + endColumn: 26 + } + ] + }, + { + filename: 'test.vue', + code: ` + {} + `, + output: ` + {} + `, + errors: [ + { + message: '`lang` attribute is required.', + line: 2, + column: 13, + endLine: 2, + endColumn: 17 + } + ] + }, + { + filename: 'test.vue', + code: ` + {} + `, + output: ` + {} + `, + errors: [ + { + message: '`lang` attribute is required.', + line: 2, + column: 18, + endLine: 2, + endColumn: 20 + } + ] + } + ] +})