Skip to content

Commit 296e6f6

Browse files
kazuponota-meshi
andauthoredApr 14, 2024··
feat: no-deprecated-modulo-syntax rule (#499)
* feat: node-deprecated-modulo-syntax rule * refactor * fix: add modulo to AST node * fix: unit test timeout * docs: add no-deprecated-modulo-syntax * fix: update docs docs and lib with generate script * test: fix: add message syntax version * Create grumpy-forks-brake.md * docs: fix * Update grumpy-forks-brake.md --------- Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>
1 parent e325ab2 commit 296e6f6

18 files changed

+556
-169
lines changed
 

‎.changeset/grumpy-forks-brake.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@intlify/eslint-plugin-vue-i18n": minor
3+
---
4+
5+
feat: `no-deprecated-modulo-syntax` rule

‎docs/rules/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
| [@intlify/vue-i18n/<wbr>no-deprecated-i18n-component](./no-deprecated-i18n-component.html) | disallow using deprecated `<i18n>` components (in Vue I18n 9.0.0+) | :black_nib: |
1212
| [@intlify/vue-i18n/<wbr>no-deprecated-i18n-place-attr](./no-deprecated-i18n-place-attr.html) | disallow using deprecated `place` attribute (Removed in Vue I18n 9.0.0+) | |
1313
| [@intlify/vue-i18n/<wbr>no-deprecated-i18n-places-prop](./no-deprecated-i18n-places-prop.html) | disallow using deprecated `places` prop (Removed in Vue I18n 9.0.0+) | |
14+
| [@intlify/vue-i18n/<wbr>no-deprecated-modulo-syntax](./no-deprecated-modulo-syntax.html) | enforce modulo interpolation to be named interpolation | :black_nib: |
1415
| [@intlify/vue-i18n/<wbr>no-html-messages](./no-html-messages.html) | disallow use HTML localization messages | :star: |
1516
| [@intlify/vue-i18n/<wbr>no-i18n-t-path-prop](./no-i18n-t-path-prop.html) | disallow using `path` prop with `<i18n-t>` | :black_nib: |
1617
| [@intlify/vue-i18n/<wbr>no-missing-keys](./no-missing-keys.html) | disallow missing locale message key at localization methods | :star: |
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
title: '@intlify/vue-i18n/no-deprecated-modulo-syntax'
3+
description: enforce modulo interpolation to be named interpolation
4+
since: v3.0.0
5+
---
6+
7+
# @intlify/vue-i18n/no-deprecated-modulo-syntax
8+
9+
> enforce modulo interpolation to be named interpolation
10+
11+
- :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.
12+
13+
This rule enforces modulo interpolation to be named interpolation
14+
15+
## :book: Rule Details
16+
17+
:-1: Examples of **incorrect** code for this rule:
18+
19+
locale messages:
20+
21+
<eslint-code-block fix language="json">
22+
23+
```json
24+
/* eslint @intlify/vue-i18n/no-deprecated-modulo-syntax: 'error' */
25+
{
26+
/* ✗ BAD */
27+
"hello": "%{msg} world"
28+
}
29+
```
30+
31+
</eslint-code-block>
32+
33+
:+1: Examples of **correct** code for this rule:
34+
35+
locale messages (for vue-i18n v9+):
36+
37+
<eslint-code-block fix message-syntax-version="^9" language="json">
38+
39+
```json
40+
/* eslint @intlify/vue-i18n/no-deprecated-modulo-syntax: 'error' */
41+
{
42+
/* ✓ GOOD */
43+
"hello": "{msg} world"
44+
}
45+
```
46+
47+
</eslint-code-block>
48+
49+
## :rocket: Version
50+
51+
This rule was introduced in `@intlify/eslint-plugin-vue-i18n` v3.0.0
52+
53+
## :mag: Implementation
54+
55+
- [Rule source](https://github.com/intlify/eslint-plugin-vue-i18n/blob/master/lib/rules/no-deprecated-modulo-syntax.ts)
56+
- [Test source](https://github.com/intlify/eslint-plugin-vue-i18n/tree/master/tests/lib/rules/no-deprecated-modulo-syntax.ts)

‎lib/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import keyFormatStyle from './rules/key-format-style'
1010
import noDeprecatedI18nComponent from './rules/no-deprecated-i18n-component'
1111
import noDeprecatedI18nPlaceAttr from './rules/no-deprecated-i18n-place-attr'
1212
import noDeprecatedI18nPlacesProp from './rules/no-deprecated-i18n-places-prop'
13+
import noDeprecatedModuloSyntax from './rules/no-deprecated-modulo-syntax'
1314
import noDuplicateKeysInLocale from './rules/no-duplicate-keys-in-locale'
1415
import noDynamicKeys from './rules/no-dynamic-keys'
1516
import noHtmlMessages from './rules/no-html-messages'
@@ -41,6 +42,7 @@ export = {
4142
'no-deprecated-i18n-component': noDeprecatedI18nComponent,
4243
'no-deprecated-i18n-place-attr': noDeprecatedI18nPlaceAttr,
4344
'no-deprecated-i18n-places-prop': noDeprecatedI18nPlacesProp,
45+
'no-deprecated-modulo-syntax': noDeprecatedModuloSyntax,
4446
'no-duplicate-keys-in-locale': noDuplicateKeysInLocale,
4547
'no-dynamic-keys': noDynamicKeys,
4648
'no-html-messages': noHtmlMessages,
+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
* @author kazuya kawaguchi (a.k.a. kazupon)
3+
*/
4+
import type { AST as JSONAST } from 'jsonc-eslint-parser'
5+
import type { AST as YAMLAST } from 'yaml-eslint-parser'
6+
import type { RuleContext, RuleListener } from '../types'
7+
import type { GetReportOffset } from '../utils/rule'
8+
import type { CustomBlockVisitorFactory } from '../types/vue-parser-services'
9+
import { extname } from 'node:path'
10+
import debugBuilder from 'debug'
11+
import { defineCustomBlocksVisitor, getLocaleMessages } from '../utils/index'
12+
import {
13+
getMessageSyntaxVersions,
14+
NodeTypes
15+
} from '../utils/message-compiler/utils'
16+
import { parse } from '../utils/message-compiler/parser'
17+
import { traverseNode } from '../utils/message-compiler/traverser'
18+
import {
19+
createRule,
20+
defineCreateVisitorForJson,
21+
defineCreateVisitorForYaml
22+
} from '../utils/rule'
23+
import { getFilename, getSourceCode } from '../utils/compat'
24+
25+
const debug = debugBuilder('eslint-plugin-vue-i18n:no-deprecated-modulo-syntax')
26+
27+
function create(context: RuleContext): RuleListener {
28+
const filename = getFilename(context)
29+
const sourceCode = getSourceCode(context)
30+
const messageSyntaxVersions = getMessageSyntaxVersions(context)
31+
32+
function verifyForV9(
33+
message: string,
34+
reportNode: JSONAST.JSONStringLiteral | YAMLAST.YAMLScalar,
35+
getReportOffset: GetReportOffset
36+
) {
37+
const { ast, errors } = parse(message)
38+
if (errors.length) {
39+
return
40+
}
41+
traverseNode(ast, node => {
42+
if (node.type !== NodeTypes.Named || !node.modulo) {
43+
return
44+
}
45+
let range: [number, number] | null = null
46+
const start = getReportOffset(node.loc!.start.offset)
47+
const end = getReportOffset(node.loc!.end.offset)
48+
if (start != null && end != null) {
49+
// Subtract `%` length (1), because we want to fix modulo
50+
range = [start - 1, end]
51+
}
52+
context.report({
53+
loc: range
54+
? {
55+
start: sourceCode.getLocFromIndex(range[0]),
56+
end: sourceCode.getLocFromIndex(range[1])
57+
}
58+
: reportNode.loc,
59+
message:
60+
'The modulo interpolation must be enforced to named interpolation.',
61+
fix(fixer) {
62+
return range ? fixer.removeRange([range[0], range[0] + 1]) : null
63+
}
64+
})
65+
})
66+
}
67+
68+
function verifyMessage(
69+
message: string,
70+
reportNode: JSONAST.JSONStringLiteral | YAMLAST.YAMLScalar,
71+
getReportOffset: GetReportOffset
72+
) {
73+
if (messageSyntaxVersions.reportIfMissingSetting()) {
74+
return
75+
}
76+
if (messageSyntaxVersions.v9) {
77+
verifyForV9(message, reportNode, getReportOffset)
78+
} else if (messageSyntaxVersions.v8) {
79+
return
80+
}
81+
}
82+
83+
const createVisitorForJson = defineCreateVisitorForJson(verifyMessage)
84+
const createVisitorForYaml = defineCreateVisitorForYaml(verifyMessage)
85+
86+
if (extname(filename) === '.vue') {
87+
return defineCustomBlocksVisitor(
88+
context,
89+
createVisitorForJson,
90+
createVisitorForYaml
91+
)
92+
} else if (
93+
sourceCode.parserServices.isJSON ||
94+
sourceCode.parserServices.isYAML
95+
) {
96+
const localeMessages = getLocaleMessages(context)
97+
const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename)
98+
if (!targetLocaleMessage) {
99+
debug(`ignore ${filename} in no-deprecated-modulo-syntax`)
100+
return {}
101+
}
102+
103+
if (sourceCode.parserServices.isJSON) {
104+
return createVisitorForJson(
105+
context as Parameters<CustomBlockVisitorFactory>[0]
106+
)
107+
} else if (sourceCode.parserServices.isYAML) {
108+
return createVisitorForYaml(
109+
context as Parameters<CustomBlockVisitorFactory>[0]
110+
)
111+
}
112+
return {}
113+
} else {
114+
debug(`ignore ${filename} in no-deprecated-modulo-syntax`)
115+
return {}
116+
}
117+
}
118+
119+
export = createRule({
120+
meta: {
121+
type: 'problem',
122+
docs: {
123+
description: 'enforce modulo interpolation to be named interpolation',
124+
category: 'Recommended',
125+
url: 'https://eslint-plugin-vue-i18n.intlify.dev/rules/no-deprecated-modulo-syntax.html',
126+
recommended: false
127+
},
128+
fixable: 'code',
129+
schema: []
130+
},
131+
create
132+
})

‎lib/rules/prefer-linked-key-with-paren.ts

+19-82
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,26 @@
33
*/
44
import type { AST as JSONAST } from 'jsonc-eslint-parser'
55
import type { AST as YAMLAST } from 'yaml-eslint-parser'
6-
import { extname } from 'path'
7-
import { defineCustomBlocksVisitor, getLocaleMessages } from '../utils/index'
8-
import debugBuilder from 'debug'
96
import type { RuleContext, RuleListener } from '../types'
7+
import type { GetReportOffset } from '../utils/rule'
8+
import type { CustomBlockVisitorFactory } from '../types/vue-parser-services'
9+
import { extname } from 'node:path'
10+
import debugBuilder from 'debug'
11+
import {
12+
createRule,
13+
defineCreateVisitorForJson,
14+
defineCreateVisitorForYaml
15+
} from '../utils/rule'
16+
import { defineCustomBlocksVisitor, getLocaleMessages } from '../utils/index'
1017
import {
1118
getMessageSyntaxVersions,
12-
getReportIndex,
1319
NodeTypes
1420
} from '../utils/message-compiler/utils'
1521
import { parse } from '../utils/message-compiler/parser'
1622
import { parse as parseForV8 } from '../utils/message-compiler/parser-v8'
1723
import { traverseNode } from '../utils/message-compiler/traverser'
18-
import { createRule } from '../utils/rule'
1924
import { getFilename, getSourceCode } from '../utils/compat'
25+
2026
const debug = debugBuilder(
2127
'eslint-plugin-vue-i18n:prefer-linked-key-with-paren'
2228
)
@@ -31,8 +37,6 @@ function getSingleQuote(node: JSONAST.JSONStringLiteral | YAMLAST.YAMLScalar) {
3137
return "'"
3238
}
3339

34-
type GetReportOffset = (offset: number) => number | null
35-
3640
function create(context: RuleContext): RuleListener {
3741
const filename = getFilename(context)
3842
const sourceCode = getSourceCode(context)
@@ -141,80 +145,9 @@ function create(context: RuleContext): RuleListener {
141145
verifyForV8(message, reportNode, getReportOffset)
142146
}
143147
}
144-
/**
145-
* Create node visitor for JSON
146-
*/
147-
function createVisitorForJson(): RuleListener {
148-
function verifyExpression(node: JSONAST.JSONExpression) {
149-
if (node.type !== 'JSONLiteral' || typeof node.value !== 'string') {
150-
return
151-
}
152-
verifyMessage(node.value, node as JSONAST.JSONStringLiteral, offset =>
153-
getReportIndex(node, offset)
154-
)
155-
}
156-
return {
157-
JSONProperty(node: JSONAST.JSONProperty) {
158-
verifyExpression(node.value)
159-
},
160-
JSONArrayExpression(node: JSONAST.JSONArrayExpression) {
161-
for (const element of node.elements) {
162-
if (element) verifyExpression(element)
163-
}
164-
}
165-
}
166-
}
167-
168-
/**
169-
* Create node visitor for YAML
170-
*/
171-
function createVisitorForYaml(): RuleListener {
172-
const yamlKeyNodes = new Set<YAMLAST.YAMLContent | YAMLAST.YAMLWithMeta>()
173-
function withinKey(node: YAMLAST.YAMLNode) {
174-
for (const keyNode of yamlKeyNodes) {
175-
if (
176-
keyNode.range[0] <= node.range[0] &&
177-
node.range[0] < keyNode.range[1]
178-
) {
179-
return true
180-
}
181-
}
182-
return false
183-
}
184-
function verifyContent(node: YAMLAST.YAMLContent | YAMLAST.YAMLWithMeta) {
185-
const valueNode = node.type === 'YAMLWithMeta' ? node.value : node
186-
if (
187-
!valueNode ||
188-
valueNode.type !== 'YAMLScalar' ||
189-
typeof valueNode.value !== 'string'
190-
) {
191-
return
192-
}
193-
verifyMessage(valueNode.value, valueNode, offset =>
194-
getReportIndex(valueNode, offset)
195-
)
196-
}
197-
return {
198-
YAMLPair(node: YAMLAST.YAMLPair) {
199-
if (withinKey(node)) {
200-
return
201-
}
202-
if (node.key != null) {
203-
yamlKeyNodes.add(node.key)
204-
}
205148

206-
if (node.value) verifyContent(node.value)
207-
},
208-
YAMLSequence(node: YAMLAST.YAMLSequence) {
209-
if (withinKey(node)) {
210-
return
211-
}
212-
for (const entry of node.entries) {
213-
if (entry) verifyContent(entry)
214-
}
215-
}
216-
}
217-
}
149+
const createVisitorForJson = defineCreateVisitorForJson(verifyMessage)
150+
const createVisitorForYaml = defineCreateVisitorForYaml(verifyMessage)
218151

219152
if (extname(filename) === '.vue') {
220153
return defineCustomBlocksVisitor(
@@ -234,9 +167,13 @@ function create(context: RuleContext): RuleListener {
234167
}
235168

236169
if (sourceCode.parserServices.isJSON) {
237-
return createVisitorForJson()
170+
return createVisitorForJson(
171+
context as Parameters<CustomBlockVisitorFactory>[0]
172+
)
238173
} else if (sourceCode.parserServices.isYAML) {
239-
return createVisitorForYaml()
174+
return createVisitorForYaml(
175+
context as Parameters<CustomBlockVisitorFactory>[0]
176+
)
240177
}
241178
return {}
242179
} else {

‎lib/types/vue-parser-services.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ export type CustomBlockVisitorFactory = (
5555
context: RuleContext & {
5656
parserServices: SourceCode['parserServices'] & { customBlock: VElement }
5757
}
58-
) => RuleListener | null
58+
) => RuleListener

‎lib/utils/message-compiler/parser-v8.ts

+3
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ function parseAST(code: string, errors: CompileError[]): ResourceNode {
212212
key: trimmedKeyValue,
213213
...ctx.getNodeLoc(endOffset - 1, placeholderEndOffset)
214214
}
215+
if (key === '%{') {
216+
namedNode.modulo = true
217+
}
215218
if (!/^[a-zA-Z][a-zA-Z0-9_$]*$/.test(namedNode.key)) {
216219
errors.push(
217220
ctx.createCompileError('Unexpected placeholder key', endOffset)

‎lib/utils/rule.ts

+96-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,99 @@
1-
import type { RuleModule } from '../types'
1+
import type { RuleModule, RuleListener } from '../types'
2+
import type { AST as JSONAST } from 'jsonc-eslint-parser'
3+
import type { AST as YAMLAST } from 'yaml-eslint-parser'
4+
import type { CustomBlockVisitorFactory } from '../types/vue-parser-services'
5+
import { getReportIndex } from '../utils/message-compiler/utils'
6+
7+
export type GetReportOffset = (offset: number) => number | null
8+
export type VerifyMessage = (
9+
message: string,
10+
reportNode: JSONAST.JSONStringLiteral | YAMLAST.YAMLScalar,
11+
getReportOffset: GetReportOffset
12+
) => void
13+
214
export function createRule(module: RuleModule) {
315
return module
416
}
17+
18+
/**
19+
* Define create node visitor for JSON
20+
*/
21+
export function defineCreateVisitorForJson(
22+
verifyMessage: VerifyMessage
23+
): CustomBlockVisitorFactory {
24+
return function (): RuleListener {
25+
function verifyExpression(node: JSONAST.JSONExpression) {
26+
if (node.type !== 'JSONLiteral' || typeof node.value !== 'string') {
27+
return
28+
}
29+
verifyMessage(node.value, node as JSONAST.JSONStringLiteral, offset =>
30+
getReportIndex(node, offset)
31+
)
32+
}
33+
return {
34+
JSONProperty(node: JSONAST.JSONProperty) {
35+
verifyExpression(node.value)
36+
},
37+
JSONArrayExpression(node: JSONAST.JSONArrayExpression) {
38+
for (const element of node.elements) {
39+
if (element) verifyExpression(element)
40+
}
41+
}
42+
}
43+
}
44+
}
45+
46+
/**
47+
* Define Create node visitor for YAML
48+
*/
49+
export function defineCreateVisitorForYaml(
50+
verifyMessage: VerifyMessage
51+
): CustomBlockVisitorFactory {
52+
return function (): RuleListener {
53+
const yamlKeyNodes = new Set<YAMLAST.YAMLContent | YAMLAST.YAMLWithMeta>()
54+
function withinKey(node: YAMLAST.YAMLNode) {
55+
for (const keyNode of yamlKeyNodes) {
56+
if (
57+
keyNode.range[0] <= node.range[0] &&
58+
node.range[0] < keyNode.range[1]
59+
) {
60+
return true
61+
}
62+
}
63+
return false
64+
}
65+
function verifyContent(node: YAMLAST.YAMLContent | YAMLAST.YAMLWithMeta) {
66+
const valueNode = node.type === 'YAMLWithMeta' ? node.value : node
67+
if (
68+
!valueNode ||
69+
valueNode.type !== 'YAMLScalar' ||
70+
typeof valueNode.value !== 'string'
71+
) {
72+
return
73+
}
74+
verifyMessage(valueNode.value, valueNode, offset =>
75+
getReportIndex(valueNode, offset)
76+
)
77+
}
78+
return {
79+
YAMLPair(node: YAMLAST.YAMLPair) {
80+
if (withinKey(node)) {
81+
return
82+
}
83+
if (node.key != null) {
84+
yamlKeyNodes.add(node.key)
85+
}
86+
87+
if (node.value) verifyContent(node.value)
88+
},
89+
YAMLSequence(node: YAMLAST.YAMLSequence) {
90+
if (withinKey(node)) {
91+
return
92+
}
93+
for (const entry of node.entries) {
94+
if (entry) verifyContent(entry)
95+
}
96+
}
97+
}
98+
}
99+
}

‎package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"lint:prettier": "prettier --check .",
5151
"prerelease": "pnpm run test && pnpm run build",
5252
"release": "changeset publish",
53-
"test": "mocha --require jiti/register \"./tests/lib/**/*.ts\"",
53+
"test": "mocha --require jiti/register \"./tests/lib/**/*.ts\" --timeout 5000",
5454
"test:debug": "mocha --require jiti/register \"./tests/lib/**/*.ts\"",
5555
"test:coverage": "nyc mocha --require jiti/register \"./tests/lib/**/*.ts\" --timeout 60000",
5656
"test:integrations": "mocha --require jiti/register \"./tests/integrations/*.ts\" --timeout 60000",
@@ -60,8 +60,8 @@
6060
},
6161
"dependencies": {
6262
"@eslint/eslintrc": "^3.0.0",
63-
"@intlify/core-base": "beta",
64-
"@intlify/message-compiler": "beta",
63+
"@intlify/core-base": "^9.12.0",
64+
"@intlify/message-compiler": "^9.12.0",
6565
"debug": "^4.3.4",
6666
"eslint-compat-utils": "^0.5.0",
6767
"glob": "^10.3.3",

‎pnpm-lock.yaml

+14-37
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
null

‎tests/integrations/flat-config/eslint.config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export default [
1010
},
1111
settings: {
1212
'vue-i18n': {
13-
localeDir: './src/resources/*.json'
13+
localeDir: './src/resources/*.json',
14+
messageSyntaxVersion: '^9.0.0'
1415
}
1516
}
1617
}

‎tests/integrations/legacy-config/.eslintrc.cjs

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ module.exports = {
88
},
99
settings: {
1010
'vue-i18n': {
11-
localeDir: `./src/resources/*.json`
11+
localeDir: `./src/resources/*.json`,
12+
messageSyntaxVersion: '^9.0.0'
1213
}
1314
}
1415
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* @author kazuya kawaguchi (a.k.a. kazupon)
3+
*/
4+
import { join } from 'node:path'
5+
import { RuleTester } from '../eslint-compat'
6+
import { getRuleTesterTestCaseOptions } from '../test-utils'
7+
import rule from '../../../lib/rules/no-deprecated-modulo-syntax'
8+
import * as vueParser from 'vue-eslint-parser'
9+
10+
const FIXTURES_ROOT = join(
11+
__dirname,
12+
'../../fixtures/no-deprecated-modulo-syntax'
13+
)
14+
15+
const options = getRuleTesterTestCaseOptions(FIXTURES_ROOT)
16+
17+
const tester = new RuleTester({
18+
languageOptions: { parser: vueParser, ecmaVersion: 2015 }
19+
})
20+
21+
tester.run('no-deprecated-module-syntax', rule as never, {
22+
valid: [
23+
// text only
24+
{
25+
code: `
26+
{
27+
"hello": "world"
28+
}
29+
`,
30+
...options.json()
31+
},
32+
// named interpolation
33+
{
34+
code: `
35+
{
36+
"hello": "{msg} world"
37+
}
38+
`,
39+
...options.json()
40+
},
41+
// list interpolation
42+
{
43+
code: `
44+
{
45+
"hello": "{0} world"
46+
}
47+
`,
48+
...options.json()
49+
},
50+
// linked messages
51+
{
52+
code: `
53+
{
54+
"hello": "@:{'baz'} world"
55+
}
56+
`,
57+
...options.json()
58+
},
59+
// pluralization
60+
{
61+
code: `
62+
{
63+
"hello": "world1 | world2"
64+
}
65+
`,
66+
...options.json()
67+
},
68+
// yaml
69+
{
70+
code: `
71+
hello: '{msg} world'
72+
`,
73+
...options.yaml()
74+
},
75+
// SFC
76+
{
77+
code: `
78+
<i18n lang="json5">
79+
{ hello: "{msg} world" }
80+
</i18n>
81+
<i18n lang="yaml">
82+
hello: '{msg} world'
83+
</i18n>
84+
`,
85+
...options.vue()
86+
}
87+
],
88+
89+
invalid: [
90+
// modulo for json
91+
{
92+
code: `{
93+
"hello": "%{msg} world"
94+
}
95+
`,
96+
...options.json(),
97+
output: `{
98+
"hello": "{msg} world"
99+
}
100+
`,
101+
errors: [
102+
{
103+
message:
104+
'The modulo interpolation must be enforced to named interpolation.',
105+
line: 2,
106+
column: 19,
107+
endLine: 2,
108+
endColumn: 25
109+
}
110+
]
111+
},
112+
// modulo for yaml
113+
{
114+
code: `hello: "%{msg} world"
115+
`,
116+
...options.yaml(),
117+
output: `hello: "{msg} world"
118+
`,
119+
errors: [
120+
{
121+
message:
122+
'The modulo interpolation must be enforced to named interpolation.',
123+
line: 1,
124+
column: 9,
125+
endLine: 1,
126+
endColumn: 15
127+
}
128+
]
129+
},
130+
// modulo for SFC
131+
{
132+
code: `
133+
<i18n>
134+
{ "hello": "%{msg} world" }
135+
</i18n>
136+
<i18n lang="yaml">
137+
hello: '%{msg} world'
138+
</i18n>
139+
`,
140+
...options.vue(),
141+
output: `
142+
<i18n>
143+
{ "hello": "{msg} world" }
144+
</i18n>
145+
<i18n lang="yaml">
146+
hello: '{msg} world'
147+
</i18n>
148+
`,
149+
errors: [
150+
{
151+
message:
152+
'The modulo interpolation must be enforced to named interpolation.',
153+
line: 3,
154+
column: 19,
155+
endLine: 3,
156+
endColumn: 25
157+
},
158+
{
159+
message:
160+
'The modulo interpolation must be enforced to named interpolation.',
161+
line: 6,
162+
column: 15,
163+
endLine: 6,
164+
endColumn: 21
165+
}
166+
]
167+
}
168+
]
169+
})

‎tests/lib/rules/prefer-linked-key-with-paren.ts

+2-43
Original file line numberDiff line numberDiff line change
@@ -3,57 +3,16 @@
33
*/
44
import { join } from 'node:path'
55
import { RuleTester, TEST_RULE_ID_PREFIX } from '../eslint-compat'
6+
import { getRuleTesterTestCaseOptions } from '../test-utils'
67
import rule from '../../../lib/rules/prefer-linked-key-with-paren'
78
import * as vueParser from 'vue-eslint-parser'
8-
import * as jsonParser from 'jsonc-eslint-parser'
9-
import * as yamlParser from 'yaml-eslint-parser'
109

1110
const FIXTURES_ROOT = join(
1211
__dirname,
1312
'../../fixtures/prefer-linked-key-with-paren'
1413
)
1514

16-
const options = {
17-
json(messageSyntaxVersion: string | null = '^9.0.0') {
18-
return {
19-
languageOptions: { parser: jsonParser },
20-
filename: join(FIXTURES_ROOT, 'test.json'),
21-
settings: {
22-
'vue-i18n': {
23-
localeDir: {
24-
pattern: `${FIXTURES_ROOT}/*.{json,yaml,yml}`
25-
},
26-
messageSyntaxVersion
27-
}
28-
}
29-
}
30-
},
31-
yaml(messageSyntaxVersion: string | null = '^9.0.0') {
32-
return {
33-
languageOptions: { parser: yamlParser },
34-
filename: join(FIXTURES_ROOT, 'test.yaml'),
35-
settings: {
36-
'vue-i18n': {
37-
localeDir: {
38-
pattern: `${FIXTURES_ROOT}/*.{json,yaml,yml}`
39-
},
40-
messageSyntaxVersion
41-
}
42-
}
43-
}
44-
},
45-
vue(messageSyntaxVersion: string | null = '^9.0.0') {
46-
return {
47-
languageOptions: { parser: vueParser },
48-
filename: join(FIXTURES_ROOT, 'test.vue'),
49-
settings: {
50-
'vue-i18n': {
51-
messageSyntaxVersion
52-
}
53-
}
54-
}
55-
}
56-
}
15+
const options = getRuleTesterTestCaseOptions(FIXTURES_ROOT)
5716

5817
const tester = new RuleTester({
5918
languageOptions: { parser: vueParser, ecmaVersion: 2015 }

‎tests/lib/test-utils.ts

+47
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,53 @@ type LanguageOptions = {
1212
parser: object
1313
}
1414

15+
export function getRuleTesterTestCaseOptions(
16+
root: string,
17+
filebaseName: string = 'test'
18+
) {
19+
return {
20+
json(messageSyntaxVersion: string | null = '^9.0.0') {
21+
return {
22+
languageOptions: { parser: jsonParser },
23+
filename: join(root, `${filebaseName}.json`),
24+
settings: {
25+
'vue-i18n': {
26+
localeDir: {
27+
pattern: `${root}/*.{json,yaml,yml}`
28+
},
29+
messageSyntaxVersion
30+
}
31+
}
32+
}
33+
},
34+
yaml(messageSyntaxVersion: string | null = '^9.0.0') {
35+
return {
36+
languageOptions: { parser: yamlParser },
37+
filename: join(root, `${filebaseName}.yaml`),
38+
settings: {
39+
'vue-i18n': {
40+
localeDir: {
41+
pattern: `${root}/*.{json,yaml,yml}`
42+
},
43+
messageSyntaxVersion
44+
}
45+
}
46+
}
47+
},
48+
vue(messageSyntaxVersion: string | null = '^9.0.0') {
49+
return {
50+
languageOptions: { parser: vueParser },
51+
filename: join(root, `${filebaseName}.vue`),
52+
settings: {
53+
'vue-i18n': {
54+
messageSyntaxVersion
55+
}
56+
}
57+
}
58+
}
59+
}
60+
}
61+
1562
export function getTestCasesFromFixtures(testOptions: {
1663
eslint?: string
1764
cwd: string

0 commit comments

Comments
 (0)
Please sign in to comment.