diff --git a/.changeset/grumpy-forks-brake.md b/.changeset/grumpy-forks-brake.md new file mode 100644 index 00000000..3a5d9a1c --- /dev/null +++ b/.changeset/grumpy-forks-brake.md @@ -0,0 +1,5 @@ +--- +"@intlify/eslint-plugin-vue-i18n": minor +--- + +feat: `no-deprecated-modulo-syntax` rule diff --git a/docs/rules/index.md b/docs/rules/index.md index 3291d81b..80651d6a 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -11,6 +11,7 @@ | [@intlify/vue-i18n/no-deprecated-i18n-component](./no-deprecated-i18n-component.html) | disallow using deprecated `` components (in Vue I18n 9.0.0+) | :black_nib: | | [@intlify/vue-i18n/no-deprecated-i18n-place-attr](./no-deprecated-i18n-place-attr.html) | disallow using deprecated `place` attribute (Removed in Vue I18n 9.0.0+) | | | [@intlify/vue-i18n/no-deprecated-i18n-places-prop](./no-deprecated-i18n-places-prop.html) | disallow using deprecated `places` prop (Removed in Vue I18n 9.0.0+) | | +| [@intlify/vue-i18n/no-deprecated-modulo-syntax](./no-deprecated-modulo-syntax.html) | enforce modulo interpolation to be named interpolation | :black_nib: | | [@intlify/vue-i18n/no-html-messages](./no-html-messages.html) | disallow use HTML localization messages | :star: | | [@intlify/vue-i18n/no-i18n-t-path-prop](./no-i18n-t-path-prop.html) | disallow using `path` prop with `` | :black_nib: | | [@intlify/vue-i18n/no-missing-keys](./no-missing-keys.html) | disallow missing locale message key at localization methods | :star: | diff --git a/docs/rules/no-deprecated-modulo-syntax.md b/docs/rules/no-deprecated-modulo-syntax.md new file mode 100644 index 00000000..61941f24 --- /dev/null +++ b/docs/rules/no-deprecated-modulo-syntax.md @@ -0,0 +1,56 @@ +--- +title: '@intlify/vue-i18n/no-deprecated-modulo-syntax' +description: enforce modulo interpolation to be named interpolation +since: v3.0.0 +--- + +# @intlify/vue-i18n/no-deprecated-modulo-syntax + +> enforce modulo interpolation to be named interpolation + +- :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. + +This rule enforces modulo interpolation to be named interpolation + +## :book: Rule Details + +:-1: Examples of **incorrect** code for this rule: + +locale messages: + + + +```json +/* eslint @intlify/vue-i18n/no-deprecated-modulo-syntax: 'error' */ +{ + /* ✗ BAD */ + "hello": "%{msg} world" +} +``` + + + +:+1: Examples of **correct** code for this rule: + +locale messages (for vue-i18n v9+): + + + +```json +/* eslint @intlify/vue-i18n/no-deprecated-modulo-syntax: 'error' */ +{ + /* ✓ GOOD */ + "hello": "{msg} world" +} +``` + + + +## :rocket: Version + +This rule was introduced in `@intlify/eslint-plugin-vue-i18n` v3.0.0 + +## :mag: Implementation + +- [Rule source](https://github.com/intlify/eslint-plugin-vue-i18n/blob/master/lib/rules/no-deprecated-modulo-syntax.ts) +- [Test source](https://github.com/intlify/eslint-plugin-vue-i18n/tree/master/tests/lib/rules/no-deprecated-modulo-syntax.ts) diff --git a/lib/index.ts b/lib/index.ts index 2e6afd45..dbd5a844 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -10,6 +10,7 @@ import keyFormatStyle from './rules/key-format-style' import noDeprecatedI18nComponent from './rules/no-deprecated-i18n-component' import noDeprecatedI18nPlaceAttr from './rules/no-deprecated-i18n-place-attr' import noDeprecatedI18nPlacesProp from './rules/no-deprecated-i18n-places-prop' +import noDeprecatedModuloSyntax from './rules/no-deprecated-modulo-syntax' import noDuplicateKeysInLocale from './rules/no-duplicate-keys-in-locale' import noDynamicKeys from './rules/no-dynamic-keys' import noHtmlMessages from './rules/no-html-messages' @@ -41,6 +42,7 @@ export = { 'no-deprecated-i18n-component': noDeprecatedI18nComponent, 'no-deprecated-i18n-place-attr': noDeprecatedI18nPlaceAttr, 'no-deprecated-i18n-places-prop': noDeprecatedI18nPlacesProp, + 'no-deprecated-modulo-syntax': noDeprecatedModuloSyntax, 'no-duplicate-keys-in-locale': noDuplicateKeysInLocale, 'no-dynamic-keys': noDynamicKeys, 'no-html-messages': noHtmlMessages, diff --git a/lib/rules/no-deprecated-modulo-syntax.ts b/lib/rules/no-deprecated-modulo-syntax.ts new file mode 100644 index 00000000..6ba42155 --- /dev/null +++ b/lib/rules/no-deprecated-modulo-syntax.ts @@ -0,0 +1,132 @@ +/** + * @author kazuya kawaguchi (a.k.a. kazupon) + */ +import type { AST as JSONAST } from 'jsonc-eslint-parser' +import type { AST as YAMLAST } from 'yaml-eslint-parser' +import type { RuleContext, RuleListener } from '../types' +import type { GetReportOffset } from '../utils/rule' +import type { CustomBlockVisitorFactory } from '../types/vue-parser-services' +import { extname } from 'node:path' +import debugBuilder from 'debug' +import { defineCustomBlocksVisitor, getLocaleMessages } from '../utils/index' +import { + getMessageSyntaxVersions, + NodeTypes +} from '../utils/message-compiler/utils' +import { parse } from '../utils/message-compiler/parser' +import { traverseNode } from '../utils/message-compiler/traverser' +import { + createRule, + defineCreateVisitorForJson, + defineCreateVisitorForYaml +} from '../utils/rule' +import { getFilename, getSourceCode } from '../utils/compat' + +const debug = debugBuilder('eslint-plugin-vue-i18n:no-deprecated-modulo-syntax') + +function create(context: RuleContext): RuleListener { + const filename = getFilename(context) + const sourceCode = getSourceCode(context) + const messageSyntaxVersions = getMessageSyntaxVersions(context) + + function verifyForV9( + message: string, + reportNode: JSONAST.JSONStringLiteral | YAMLAST.YAMLScalar, + getReportOffset: GetReportOffset + ) { + const { ast, errors } = parse(message) + if (errors.length) { + return + } + traverseNode(ast, node => { + if (node.type !== NodeTypes.Named || !node.modulo) { + return + } + let range: [number, number] | null = null + const start = getReportOffset(node.loc!.start.offset) + const end = getReportOffset(node.loc!.end.offset) + if (start != null && end != null) { + // Subtract `%` length (1), because we want to fix modulo + range = [start - 1, end] + } + context.report({ + loc: range + ? { + start: sourceCode.getLocFromIndex(range[0]), + end: sourceCode.getLocFromIndex(range[1]) + } + : reportNode.loc, + message: + 'The modulo interpolation must be enforced to named interpolation.', + fix(fixer) { + return range ? fixer.removeRange([range[0], range[0] + 1]) : null + } + }) + }) + } + + function verifyMessage( + message: string, + reportNode: JSONAST.JSONStringLiteral | YAMLAST.YAMLScalar, + getReportOffset: GetReportOffset + ) { + if (messageSyntaxVersions.reportIfMissingSetting()) { + return + } + if (messageSyntaxVersions.v9) { + verifyForV9(message, reportNode, getReportOffset) + } else if (messageSyntaxVersions.v8) { + return + } + } + + const createVisitorForJson = defineCreateVisitorForJson(verifyMessage) + const createVisitorForYaml = defineCreateVisitorForYaml(verifyMessage) + + if (extname(filename) === '.vue') { + return defineCustomBlocksVisitor( + context, + createVisitorForJson, + createVisitorForYaml + ) + } else if ( + sourceCode.parserServices.isJSON || + sourceCode.parserServices.isYAML + ) { + const localeMessages = getLocaleMessages(context) + const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename) + if (!targetLocaleMessage) { + debug(`ignore ${filename} in no-deprecated-modulo-syntax`) + return {} + } + + if (sourceCode.parserServices.isJSON) { + return createVisitorForJson( + context as Parameters[0] + ) + } else if (sourceCode.parserServices.isYAML) { + return createVisitorForYaml( + context as Parameters[0] + ) + } + return {} + } else { + debug(`ignore ${filename} in no-deprecated-modulo-syntax`) + return {} + } +} + +export = createRule({ + meta: { + type: 'problem', + docs: { + description: 'enforce modulo interpolation to be named interpolation', + category: 'Recommended', + url: 'https://eslint-plugin-vue-i18n.intlify.dev/rules/no-deprecated-modulo-syntax.html', + recommended: false + }, + fixable: 'code', + schema: [] + }, + create +}) diff --git a/lib/rules/prefer-linked-key-with-paren.ts b/lib/rules/prefer-linked-key-with-paren.ts index 6120e449..b22685f2 100644 --- a/lib/rules/prefer-linked-key-with-paren.ts +++ b/lib/rules/prefer-linked-key-with-paren.ts @@ -3,20 +3,26 @@ */ import type { AST as JSONAST } from 'jsonc-eslint-parser' import type { AST as YAMLAST } from 'yaml-eslint-parser' -import { extname } from 'path' -import { defineCustomBlocksVisitor, getLocaleMessages } from '../utils/index' -import debugBuilder from 'debug' import type { RuleContext, RuleListener } from '../types' +import type { GetReportOffset } from '../utils/rule' +import type { CustomBlockVisitorFactory } from '../types/vue-parser-services' +import { extname } from 'node:path' +import debugBuilder from 'debug' +import { + createRule, + defineCreateVisitorForJson, + defineCreateVisitorForYaml +} from '../utils/rule' +import { defineCustomBlocksVisitor, getLocaleMessages } from '../utils/index' import { getMessageSyntaxVersions, - getReportIndex, NodeTypes } from '../utils/message-compiler/utils' import { parse } from '../utils/message-compiler/parser' import { parse as parseForV8 } from '../utils/message-compiler/parser-v8' import { traverseNode } from '../utils/message-compiler/traverser' -import { createRule } from '../utils/rule' import { getFilename, getSourceCode } from '../utils/compat' + const debug = debugBuilder( 'eslint-plugin-vue-i18n:prefer-linked-key-with-paren' ) @@ -31,8 +37,6 @@ function getSingleQuote(node: JSONAST.JSONStringLiteral | YAMLAST.YAMLScalar) { return "'" } -type GetReportOffset = (offset: number) => number | null - function create(context: RuleContext): RuleListener { const filename = getFilename(context) const sourceCode = getSourceCode(context) @@ -141,80 +145,9 @@ function create(context: RuleContext): RuleListener { verifyForV8(message, reportNode, getReportOffset) } } - /** - * Create node visitor for JSON - */ - function createVisitorForJson(): RuleListener { - function verifyExpression(node: JSONAST.JSONExpression) { - if (node.type !== 'JSONLiteral' || typeof node.value !== 'string') { - return - } - verifyMessage(node.value, node as JSONAST.JSONStringLiteral, offset => - getReportIndex(node, offset) - ) - } - return { - JSONProperty(node: JSONAST.JSONProperty) { - verifyExpression(node.value) - }, - JSONArrayExpression(node: JSONAST.JSONArrayExpression) { - for (const element of node.elements) { - if (element) verifyExpression(element) - } - } - } - } - - /** - * Create node visitor for YAML - */ - function createVisitorForYaml(): RuleListener { - const yamlKeyNodes = new Set() - function withinKey(node: YAMLAST.YAMLNode) { - for (const keyNode of yamlKeyNodes) { - if ( - keyNode.range[0] <= node.range[0] && - node.range[0] < keyNode.range[1] - ) { - return true - } - } - return false - } - function verifyContent(node: YAMLAST.YAMLContent | YAMLAST.YAMLWithMeta) { - const valueNode = node.type === 'YAMLWithMeta' ? node.value : node - if ( - !valueNode || - valueNode.type !== 'YAMLScalar' || - typeof valueNode.value !== 'string' - ) { - return - } - verifyMessage(valueNode.value, valueNode, offset => - getReportIndex(valueNode, offset) - ) - } - return { - YAMLPair(node: YAMLAST.YAMLPair) { - if (withinKey(node)) { - return - } - if (node.key != null) { - yamlKeyNodes.add(node.key) - } - if (node.value) verifyContent(node.value) - }, - YAMLSequence(node: YAMLAST.YAMLSequence) { - if (withinKey(node)) { - return - } - for (const entry of node.entries) { - if (entry) verifyContent(entry) - } - } - } - } + const createVisitorForJson = defineCreateVisitorForJson(verifyMessage) + const createVisitorForYaml = defineCreateVisitorForYaml(verifyMessage) if (extname(filename) === '.vue') { return defineCustomBlocksVisitor( @@ -234,9 +167,13 @@ function create(context: RuleContext): RuleListener { } if (sourceCode.parserServices.isJSON) { - return createVisitorForJson() + return createVisitorForJson( + context as Parameters[0] + ) } else if (sourceCode.parserServices.isYAML) { - return createVisitorForYaml() + return createVisitorForYaml( + context as Parameters[0] + ) } return {} } else { diff --git a/lib/types/vue-parser-services.ts b/lib/types/vue-parser-services.ts index 097cd962..f2a703f4 100644 --- a/lib/types/vue-parser-services.ts +++ b/lib/types/vue-parser-services.ts @@ -55,4 +55,4 @@ export type CustomBlockVisitorFactory = ( context: RuleContext & { parserServices: SourceCode['parserServices'] & { customBlock: VElement } } -) => RuleListener | null +) => RuleListener diff --git a/lib/utils/message-compiler/parser-v8.ts b/lib/utils/message-compiler/parser-v8.ts index 174b442e..100cc62e 100644 --- a/lib/utils/message-compiler/parser-v8.ts +++ b/lib/utils/message-compiler/parser-v8.ts @@ -212,6 +212,9 @@ function parseAST(code: string, errors: CompileError[]): ResourceNode { key: trimmedKeyValue, ...ctx.getNodeLoc(endOffset - 1, placeholderEndOffset) } + if (key === '%{') { + namedNode.modulo = true + } if (!/^[a-zA-Z][a-zA-Z0-9_$]*$/.test(namedNode.key)) { errors.push( ctx.createCompileError('Unexpected placeholder key', endOffset) diff --git a/lib/utils/rule.ts b/lib/utils/rule.ts index cb9c1acd..8539c131 100644 --- a/lib/utils/rule.ts +++ b/lib/utils/rule.ts @@ -1,4 +1,99 @@ -import type { RuleModule } from '../types' +import type { RuleModule, RuleListener } from '../types' +import type { AST as JSONAST } from 'jsonc-eslint-parser' +import type { AST as YAMLAST } from 'yaml-eslint-parser' +import type { CustomBlockVisitorFactory } from '../types/vue-parser-services' +import { getReportIndex } from '../utils/message-compiler/utils' + +export type GetReportOffset = (offset: number) => number | null +export type VerifyMessage = ( + message: string, + reportNode: JSONAST.JSONStringLiteral | YAMLAST.YAMLScalar, + getReportOffset: GetReportOffset +) => void + export function createRule(module: RuleModule) { return module } + +/** + * Define create node visitor for JSON + */ +export function defineCreateVisitorForJson( + verifyMessage: VerifyMessage +): CustomBlockVisitorFactory { + return function (): RuleListener { + function verifyExpression(node: JSONAST.JSONExpression) { + if (node.type !== 'JSONLiteral' || typeof node.value !== 'string') { + return + } + verifyMessage(node.value, node as JSONAST.JSONStringLiteral, offset => + getReportIndex(node, offset) + ) + } + return { + JSONProperty(node: JSONAST.JSONProperty) { + verifyExpression(node.value) + }, + JSONArrayExpression(node: JSONAST.JSONArrayExpression) { + for (const element of node.elements) { + if (element) verifyExpression(element) + } + } + } + } +} + +/** + * Define Create node visitor for YAML + */ +export function defineCreateVisitorForYaml( + verifyMessage: VerifyMessage +): CustomBlockVisitorFactory { + return function (): RuleListener { + const yamlKeyNodes = new Set() + function withinKey(node: YAMLAST.YAMLNode) { + for (const keyNode of yamlKeyNodes) { + if ( + keyNode.range[0] <= node.range[0] && + node.range[0] < keyNode.range[1] + ) { + return true + } + } + return false + } + function verifyContent(node: YAMLAST.YAMLContent | YAMLAST.YAMLWithMeta) { + const valueNode = node.type === 'YAMLWithMeta' ? node.value : node + if ( + !valueNode || + valueNode.type !== 'YAMLScalar' || + typeof valueNode.value !== 'string' + ) { + return + } + verifyMessage(valueNode.value, valueNode, offset => + getReportIndex(valueNode, offset) + ) + } + return { + YAMLPair(node: YAMLAST.YAMLPair) { + if (withinKey(node)) { + return + } + if (node.key != null) { + yamlKeyNodes.add(node.key) + } + + if (node.value) verifyContent(node.value) + }, + YAMLSequence(node: YAMLAST.YAMLSequence) { + if (withinKey(node)) { + return + } + for (const entry of node.entries) { + if (entry) verifyContent(entry) + } + } + } + } +} diff --git a/package.json b/package.json index 94ef5140..1c007f80 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "lint:prettier": "prettier --check .", "prerelease": "pnpm run test && pnpm run build", "release": "changeset publish", - "test": "mocha --require jiti/register \"./tests/lib/**/*.ts\"", + "test": "mocha --require jiti/register \"./tests/lib/**/*.ts\" --timeout 5000", "test:debug": "mocha --require jiti/register \"./tests/lib/**/*.ts\"", "test:coverage": "nyc mocha --require jiti/register \"./tests/lib/**/*.ts\" --timeout 60000", "test:integrations": "mocha --require jiti/register \"./tests/integrations/*.ts\" --timeout 60000", @@ -60,8 +60,8 @@ }, "dependencies": { "@eslint/eslintrc": "^3.0.0", - "@intlify/core-base": "beta", - "@intlify/message-compiler": "beta", + "@intlify/core-base": "^9.12.0", + "@intlify/message-compiler": "^9.12.0", "debug": "^4.3.4", "eslint-compat-utils": "^0.5.0", "glob": "^10.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64aabc33..367797e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ dependencies: specifier: ^3.0.0 version: 3.0.0 '@intlify/core-base': - specifier: beta - version: 9.3.0-beta.27 + specifier: ^9.12.0 + version: 9.12.0 '@intlify/message-compiler': - specifier: beta - version: 9.3.0-beta.27 + specifier: ^9.12.0 + version: 9.12.0 debug: specifier: ^4.3.4 version: 4.3.4(supports-color@8.1.1) @@ -1134,42 +1134,25 @@ packages: /@humanwhocodes/object-schema@2.0.3: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - /@intlify/core-base@9.3.0-beta.27: - resolution: {integrity: sha512-hWI8dZh9rRLxDt1IqPJQnXgMW5KZrNX2Z4uJCN348gsPVvsN8eB/J71TcNJs+C1mfIjQPwtmzUWPNhTewi8QGg==} + /@intlify/core-base@9.12.0: + resolution: {integrity: sha512-6EnWQXHnCh2bMiXT5N/IWwkcYQXjmF8nnEZ3YhTm23h1ZfOylz83D7pJYhcU8CsTiEdgbGiNdqyZPKwrHw03Ng==} engines: {node: '>= 16'} dependencies: - '@intlify/devtools-if': 9.3.0-beta.27 - '@intlify/message-compiler': 9.3.0-beta.27 - '@intlify/shared': 9.3.0-beta.27 - '@intlify/vue-devtools': 9.3.0-beta.27 + '@intlify/message-compiler': 9.12.0 + '@intlify/shared': 9.12.0 dev: false - /@intlify/devtools-if@9.3.0-beta.27: - resolution: {integrity: sha512-hA0rBQmVy7fN6UGZa+o0DrFTYnUvqD3E2Vqw35XV+huLlMQ2Zui9IpVhWozSy+ov/bdRFIhaxW11DAEG3BHa4w==} + /@intlify/message-compiler@9.12.0: + resolution: {integrity: sha512-2c6VwhvVJ1nur+2cN2NjdrmrV6vXjvyxYVvtUYMXKsWSUwoNURHGds0xJVJmWxbF8qV9oGepcVV6xl9bvadEIg==} engines: {node: '>= 16'} dependencies: - '@intlify/shared': 9.3.0-beta.27 - dev: false - - /@intlify/message-compiler@9.3.0-beta.27: - resolution: {integrity: sha512-GC8rSbd7V67Zu+a9Z0bpV4riBek11YCURJU50YaEhV4Ub2JHEPtoYxK5r2eIsq/kp+M2hJyGLiC4NJUrGa2VwQ==} - engines: {node: '>= 16'} - dependencies: - '@intlify/shared': 9.3.0-beta.27 - source-map-js: 1.0.2 - dev: false - - /@intlify/shared@9.3.0-beta.27: - resolution: {integrity: sha512-hPMsmVCs+ZUVHHU5VORG6LopzXZT7zmyVNqc9OQG80YpA/N4lT/pkJ4B6DTNIsv2C7mwfGM7RdK+0qPki43YgA==} - engines: {node: '>= 16'} + '@intlify/shared': 9.12.0 + source-map-js: 1.2.0 dev: false - /@intlify/vue-devtools@9.3.0-beta.27: - resolution: {integrity: sha512-XqES1UsntZjyo5sLgmVol42RK7YpAos3bvCh6NbzFMjPZVJf08PYs+KsUvYi2y0D3OSfmXbZSSh4A5s2O9MT0w==} + /@intlify/shared@9.12.0: + resolution: {integrity: sha512-uBcH55x5CfZynnerWHQxrXbT6yD6j6T7Nt+R2+dHAOAneoMd6BoGvfEzfYscE94rgmjoDqdr+PdGDBLk5I5EjA==} engines: {node: '>= 16'} - dependencies: - '@intlify/core-base': 9.3.0-beta.27 - '@intlify/shared': 9.3.0-beta.27 dev: false /@isaacs/cliui@8.0.2: @@ -4775,15 +4758,9 @@ packages: yargs: 15.4.1 dev: true - /source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - dev: false - /source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} - dev: true /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} diff --git a/tests/fixtures/no-deprecated-modulo-syntax/test.json b/tests/fixtures/no-deprecated-modulo-syntax/test.json new file mode 100644 index 00000000..19765bd5 --- /dev/null +++ b/tests/fixtures/no-deprecated-modulo-syntax/test.json @@ -0,0 +1 @@ +null diff --git a/tests/fixtures/no-deprecated-modulo-syntax/test.yaml b/tests/fixtures/no-deprecated-modulo-syntax/test.yaml new file mode 100644 index 00000000..19765bd5 --- /dev/null +++ b/tests/fixtures/no-deprecated-modulo-syntax/test.yaml @@ -0,0 +1 @@ +null diff --git a/tests/integrations/flat-config/eslint.config.js b/tests/integrations/flat-config/eslint.config.js index 4ab5e454..ae73e5da 100644 --- a/tests/integrations/flat-config/eslint.config.js +++ b/tests/integrations/flat-config/eslint.config.js @@ -10,7 +10,8 @@ export default [ }, settings: { 'vue-i18n': { - localeDir: './src/resources/*.json' + localeDir: './src/resources/*.json', + messageSyntaxVersion: '^9.0.0' } } } diff --git a/tests/integrations/legacy-config/.eslintrc.cjs b/tests/integrations/legacy-config/.eslintrc.cjs index 9cf0c4a8..cfe0e2f6 100644 --- a/tests/integrations/legacy-config/.eslintrc.cjs +++ b/tests/integrations/legacy-config/.eslintrc.cjs @@ -8,7 +8,8 @@ module.exports = { }, settings: { 'vue-i18n': { - localeDir: `./src/resources/*.json` + localeDir: `./src/resources/*.json`, + messageSyntaxVersion: '^9.0.0' } } } diff --git a/tests/lib/rules/no-deprecated-modulo-syntax.ts b/tests/lib/rules/no-deprecated-modulo-syntax.ts new file mode 100644 index 00000000..448d0b90 --- /dev/null +++ b/tests/lib/rules/no-deprecated-modulo-syntax.ts @@ -0,0 +1,169 @@ +/** + * @author kazuya kawaguchi (a.k.a. kazupon) + */ +import { join } from 'node:path' +import { RuleTester } from '../eslint-compat' +import { getRuleTesterTestCaseOptions } from '../test-utils' +import rule from '../../../lib/rules/no-deprecated-modulo-syntax' +import * as vueParser from 'vue-eslint-parser' + +const FIXTURES_ROOT = join( + __dirname, + '../../fixtures/no-deprecated-modulo-syntax' +) + +const options = getRuleTesterTestCaseOptions(FIXTURES_ROOT) + +const tester = new RuleTester({ + languageOptions: { parser: vueParser, ecmaVersion: 2015 } +}) + +tester.run('no-deprecated-module-syntax', rule as never, { + valid: [ + // text only + { + code: ` + { + "hello": "world" + } + `, + ...options.json() + }, + // named interpolation + { + code: ` + { + "hello": "{msg} world" + } + `, + ...options.json() + }, + // list interpolation + { + code: ` + { + "hello": "{0} world" + } + `, + ...options.json() + }, + // linked messages + { + code: ` + { + "hello": "@:{'baz'} world" + } + `, + ...options.json() + }, + // pluralization + { + code: ` + { + "hello": "world1 | world2" + } + `, + ...options.json() + }, + // yaml + { + code: ` + hello: '{msg} world' + `, + ...options.yaml() + }, + // SFC + { + code: ` + + { hello: "{msg} world" } + + + hello: '{msg} world' + + `, + ...options.vue() + } + ], + + invalid: [ + // modulo for json + { + code: `{ + "hello": "%{msg} world" + } + `, + ...options.json(), + output: `{ + "hello": "{msg} world" + } + `, + errors: [ + { + message: + 'The modulo interpolation must be enforced to named interpolation.', + line: 2, + column: 19, + endLine: 2, + endColumn: 25 + } + ] + }, + // modulo for yaml + { + code: `hello: "%{msg} world" + `, + ...options.yaml(), + output: `hello: "{msg} world" + `, + errors: [ + { + message: + 'The modulo interpolation must be enforced to named interpolation.', + line: 1, + column: 9, + endLine: 1, + endColumn: 15 + } + ] + }, + // modulo for SFC + { + code: ` + + { "hello": "%{msg} world" } + + + hello: '%{msg} world' + + `, + ...options.vue(), + output: ` + + { "hello": "{msg} world" } + + + hello: '{msg} world' + + `, + errors: [ + { + message: + 'The modulo interpolation must be enforced to named interpolation.', + line: 3, + column: 19, + endLine: 3, + endColumn: 25 + }, + { + message: + 'The modulo interpolation must be enforced to named interpolation.', + line: 6, + column: 15, + endLine: 6, + endColumn: 21 + } + ] + } + ] +}) diff --git a/tests/lib/rules/prefer-linked-key-with-paren.ts b/tests/lib/rules/prefer-linked-key-with-paren.ts index c26a146a..c45c68f1 100644 --- a/tests/lib/rules/prefer-linked-key-with-paren.ts +++ b/tests/lib/rules/prefer-linked-key-with-paren.ts @@ -3,57 +3,16 @@ */ import { join } from 'node:path' import { RuleTester, TEST_RULE_ID_PREFIX } from '../eslint-compat' +import { getRuleTesterTestCaseOptions } from '../test-utils' import rule from '../../../lib/rules/prefer-linked-key-with-paren' import * as vueParser from 'vue-eslint-parser' -import * as jsonParser from 'jsonc-eslint-parser' -import * as yamlParser from 'yaml-eslint-parser' const FIXTURES_ROOT = join( __dirname, '../../fixtures/prefer-linked-key-with-paren' ) -const options = { - json(messageSyntaxVersion: string | null = '^9.0.0') { - return { - languageOptions: { parser: jsonParser }, - filename: join(FIXTURES_ROOT, 'test.json'), - settings: { - 'vue-i18n': { - localeDir: { - pattern: `${FIXTURES_ROOT}/*.{json,yaml,yml}` - }, - messageSyntaxVersion - } - } - } - }, - yaml(messageSyntaxVersion: string | null = '^9.0.0') { - return { - languageOptions: { parser: yamlParser }, - filename: join(FIXTURES_ROOT, 'test.yaml'), - settings: { - 'vue-i18n': { - localeDir: { - pattern: `${FIXTURES_ROOT}/*.{json,yaml,yml}` - }, - messageSyntaxVersion - } - } - } - }, - vue(messageSyntaxVersion: string | null = '^9.0.0') { - return { - languageOptions: { parser: vueParser }, - filename: join(FIXTURES_ROOT, 'test.vue'), - settings: { - 'vue-i18n': { - messageSyntaxVersion - } - } - } - } -} +const options = getRuleTesterTestCaseOptions(FIXTURES_ROOT) const tester = new RuleTester({ languageOptions: { parser: vueParser, ecmaVersion: 2015 } diff --git a/tests/lib/test-utils.ts b/tests/lib/test-utils.ts index 1e890c16..65a8eec3 100644 --- a/tests/lib/test-utils.ts +++ b/tests/lib/test-utils.ts @@ -12,6 +12,53 @@ type LanguageOptions = { parser: object } +export function getRuleTesterTestCaseOptions( + root: string, + filebaseName: string = 'test' +) { + return { + json(messageSyntaxVersion: string | null = '^9.0.0') { + return { + languageOptions: { parser: jsonParser }, + filename: join(root, `${filebaseName}.json`), + settings: { + 'vue-i18n': { + localeDir: { + pattern: `${root}/*.{json,yaml,yml}` + }, + messageSyntaxVersion + } + } + } + }, + yaml(messageSyntaxVersion: string | null = '^9.0.0') { + return { + languageOptions: { parser: yamlParser }, + filename: join(root, `${filebaseName}.yaml`), + settings: { + 'vue-i18n': { + localeDir: { + pattern: `${root}/*.{json,yaml,yml}` + }, + messageSyntaxVersion + } + } + } + }, + vue(messageSyntaxVersion: string | null = '^9.0.0') { + return { + languageOptions: { parser: vueParser }, + filename: join(root, `${filebaseName}.vue`), + settings: { + 'vue-i18n': { + messageSyntaxVersion + } + } + } + } + } +} + export function getTestCasesFromFixtures(testOptions: { eslint?: string cwd: string