Skip to content

Commit 39fb944

Browse files
committed
feat: node-deprecated-modulo-syntax rule
1 parent e827a23 commit 39fb944

File tree

8 files changed

+437
-82
lines changed

8 files changed

+437
-82
lines changed
+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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 { extname } from 'node:path'
7+
import { defineCustomBlocksVisitor, getLocaleMessages } from '../utils/index'
8+
import debugBuilder from 'debug'
9+
import type { RuleContext, RuleListener } from '../types'
10+
import {
11+
getMessageSyntaxVersions,
12+
getReportIndex,
13+
NodeTypes
14+
} from '../utils/message-compiler/utils'
15+
import { parse } from '../utils/message-compiler/parser'
16+
import { traverseNode } from '../utils/message-compiler/traverser'
17+
import { createRule } from '../utils/rule'
18+
import { getFilename, getSourceCode } from '../utils/compat'
19+
const debug = debugBuilder('eslint-plugin-vue-i18n:no-deprecated-modulo-syntax')
20+
21+
type GetReportOffset = (offset: number) => number | null
22+
23+
function create(context: RuleContext): RuleListener {
24+
const filename = getFilename(context)
25+
const sourceCode = getSourceCode(context)
26+
const messageSyntaxVersions = getMessageSyntaxVersions(context)
27+
28+
function verifyForV9(
29+
message: string,
30+
reportNode: JSONAST.JSONStringLiteral | YAMLAST.YAMLScalar,
31+
getReportOffset: GetReportOffset
32+
) {
33+
const { ast, errors } = parse(message)
34+
if (errors.length) {
35+
return
36+
}
37+
traverseNode(ast, node => {
38+
if (node.type !== NodeTypes.Named || !node.modulo) {
39+
return
40+
}
41+
let range: [number, number] | null = null
42+
const start = getReportOffset(node.loc!.start.offset)
43+
const end = getReportOffset(node.loc!.end.offset)
44+
if (start != null && end != null) {
45+
// Subtract `%` length (1), because we want to fix modulo
46+
range = [start - 1, end]
47+
}
48+
context.report({
49+
loc: range
50+
? {
51+
start: sourceCode.getLocFromIndex(range[0]),
52+
end: sourceCode.getLocFromIndex(range[1])
53+
}
54+
: reportNode.loc,
55+
message:
56+
'The modulo interpolation must be enforced to named interpolation.',
57+
fix(fixer) {
58+
return range ? fixer.removeRange([range[0], range[0] + 1]) : null
59+
}
60+
})
61+
})
62+
}
63+
64+
function verifyMessage(
65+
message: string,
66+
reportNode: JSONAST.JSONStringLiteral | YAMLAST.YAMLScalar,
67+
getReportOffset: GetReportOffset
68+
) {
69+
if (messageSyntaxVersions.reportIfMissingSetting()) {
70+
return
71+
}
72+
if (messageSyntaxVersions.v9 && messageSyntaxVersions.v8) {
73+
// This rule cannot support two versions in the same project.
74+
return
75+
}
76+
77+
if (messageSyntaxVersions.v9) {
78+
verifyForV9(message, reportNode, getReportOffset)
79+
} else if (messageSyntaxVersions.v8) {
80+
return
81+
}
82+
}
83+
84+
/**
85+
* Create node visitor for JSON
86+
*/
87+
function createVisitorForJson(): RuleListener {
88+
function verifyExpression(node: JSONAST.JSONExpression) {
89+
if (node.type !== 'JSONLiteral' || typeof node.value !== 'string') {
90+
return
91+
}
92+
verifyMessage(node.value, node as JSONAST.JSONStringLiteral, offset =>
93+
getReportIndex(node, offset)
94+
)
95+
}
96+
return {
97+
JSONProperty(node: JSONAST.JSONProperty) {
98+
verifyExpression(node.value)
99+
},
100+
JSONArrayExpression(node: JSONAST.JSONArrayExpression) {
101+
for (const element of node.elements) {
102+
if (element) verifyExpression(element)
103+
}
104+
}
105+
}
106+
}
107+
108+
/**
109+
* Create node visitor for YAML
110+
*/
111+
function createVisitorForYaml(): RuleListener {
112+
const yamlKeyNodes = new Set<YAMLAST.YAMLContent | YAMLAST.YAMLWithMeta>()
113+
function withinKey(node: YAMLAST.YAMLNode) {
114+
for (const keyNode of yamlKeyNodes) {
115+
if (
116+
keyNode.range[0] <= node.range[0] &&
117+
node.range[0] < keyNode.range[1]
118+
) {
119+
return true
120+
}
121+
}
122+
return false
123+
}
124+
function verifyContent(node: YAMLAST.YAMLContent | YAMLAST.YAMLWithMeta) {
125+
const valueNode = node.type === 'YAMLWithMeta' ? node.value : node
126+
if (
127+
!valueNode ||
128+
valueNode.type !== 'YAMLScalar' ||
129+
typeof valueNode.value !== 'string'
130+
) {
131+
return
132+
}
133+
verifyMessage(valueNode.value, valueNode, offset =>
134+
getReportIndex(valueNode, offset)
135+
)
136+
}
137+
return {
138+
YAMLPair(node: YAMLAST.YAMLPair) {
139+
if (withinKey(node)) {
140+
return
141+
}
142+
if (node.key != null) {
143+
yamlKeyNodes.add(node.key)
144+
}
145+
146+
if (node.value) verifyContent(node.value)
147+
},
148+
YAMLSequence(node: YAMLAST.YAMLSequence) {
149+
if (withinKey(node)) {
150+
return
151+
}
152+
for (const entry of node.entries) {
153+
if (entry) verifyContent(entry)
154+
}
155+
}
156+
}
157+
}
158+
159+
if (extname(filename) === '.vue') {
160+
return defineCustomBlocksVisitor(
161+
context,
162+
createVisitorForJson,
163+
createVisitorForYaml
164+
)
165+
} else if (
166+
sourceCode.parserServices.isJSON ||
167+
sourceCode.parserServices.isYAML
168+
) {
169+
const localeMessages = getLocaleMessages(context)
170+
const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename)
171+
if (!targetLocaleMessage) {
172+
debug(`ignore ${filename} in no-deprecated-modulo-syntax`)
173+
return {}
174+
}
175+
176+
if (sourceCode.parserServices.isJSON) {
177+
return createVisitorForJson()
178+
} else if (sourceCode.parserServices.isYAML) {
179+
return createVisitorForYaml()
180+
}
181+
return {}
182+
} else {
183+
debug(`ignore ${filename} in no-deprecated-modulo-syntax`)
184+
return {}
185+
}
186+
}
187+
188+
export = createRule({
189+
meta: {
190+
type: 'problem',
191+
docs: {
192+
description: 'enforce modulo interpolation to be named interpolation',
193+
category: 'Recommended',
194+
url: 'https://eslint-plugin-vue-i18n.intlify.dev/rules/no-deprecated-modulo-syntax.html',
195+
recommended: true
196+
},
197+
fixable: 'code',
198+
schema: []
199+
},
200+
create
201+
})

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)