Skip to content

Commit 514b897

Browse files
committed
fix: use vue-eslint-parser directly
Resolves a breaking change with eslint-plugin-vue. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 7c6a6d2 commit 514b897

6 files changed

Lines changed: 118 additions & 46 deletions

File tree

lib/plugins/l10n/rules/enforce-ellipsis-vue.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import type { Rule } from 'eslint'
77

8-
import * as vueUtils from 'eslint-plugin-vue/lib/utils/index.js'
8+
import { defineTemplateBodyVisitor } from '../../nextcloud-vue/utils/vue-template-visitor.ts'
99
import enforceEllipsis from './enforce-ellipsis.ts'
1010

1111
const defineRule = (r: Rule.RuleModule) => r
@@ -14,6 +14,6 @@ export default defineRule({
1414
...enforceEllipsis,
1515

1616
create(context) {
17-
return vueUtils.defineTemplateBodyVisitor(context, enforceEllipsis.create(context))
17+
return defineTemplateBodyVisitor(context, enforceEllipsis.create(context))
1818
},
1919
})

lib/plugins/l10n/rules/non-breaking-space-vue.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import type { Rule } from 'eslint'
77

8-
import * as vueUtils from 'eslint-plugin-vue/lib/utils/index.js'
8+
import { defineTemplateBodyVisitor } from '../../nextcloud-vue/utils/vue-template-visitor.ts'
99
import nonBreakingSpace from './non-breaking-space.ts'
1010

1111
const defineRule = (r: Rule.RuleModule) => r
@@ -14,6 +14,6 @@ export default defineRule({
1414
...nonBreakingSpace,
1515

1616
create(context) {
17-
return vueUtils.defineTemplateBodyVisitor(context, nonBreakingSpace.create(context))
17+
return defineTemplateBodyVisitor(context, nonBreakingSpace.create(context))
1818
},
1919
})

lib/plugins/nextcloud-vue/rules/no-deprecated-props.ts

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5+
56
import type { Rule } from 'eslint'
7+
import type { AST } from 'vue-eslint-parser'
68

7-
import * as vueUtils from 'eslint-plugin-vue/lib/utils/index.js'
89
import { createLibVersionValidator } from '../utils/lib-version-parser.ts'
10+
import { defineTemplateBodyVisitor } from '../utils/vue-template-visitor.ts'
911

1012
export default {
1113
meta: {
@@ -66,8 +68,8 @@ export default {
6668
'tertiary-no-background',
6769
]
6870

69-
return vueUtils.defineTemplateBodyVisitor(context, {
70-
'VElement VAttribute:has(VIdentifier[name="type"])': function(node) {
71+
return defineTemplateBodyVisitor(context, {
72+
'VElement VAttribute:has(VIdentifier[name="type"])': function(node: AST.VAttribute | AST.VDirective) {
7173
if (![
7274
'ncactions',
7375
'ncappnavigationnew',
@@ -85,13 +87,13 @@ export default {
8587

8688
const hasNativeType = node.parent.attributes.find((attr) => (
8789
attr.key.name === 'native-type'
88-
|| (attr.key.type === 'VDirectiveKey' && attr.key.argument && attr.key.argument.name === 'native-type')))
90+
|| (attr.key.type === 'VDirectiveKey' && attr.key.argument && attr.key.argument.type === 'VIdentifier' && attr.key.argument.name === 'native-type')))
8991

9092
const isLiteral = node.value.type === 'VLiteral' && legacyTypes.includes(node.value.value)
9193
const isExpression = node.value.type === 'VExpressionContainer'
9294
&& node.value.expression.type === 'ConditionalExpression'
93-
&& (legacyTypes.includes(node.value.expression.consequent.value)
94-
|| legacyTypes.includes(node.value.expression.alternate.value)
95+
&& (('value' in node.value.expression.consequent && legacyTypes.includes(node.value.expression.consequent.value.toString()))
96+
|| ('value' in node.value.expression.alternate && legacyTypes.includes(node.value.expression.alternate.value.toString()))
9597
)
9698

9799
/**
@@ -114,7 +116,7 @@ export default {
114116
}
115117
},
116118

117-
'VElement VAttribute:has(VIdentifier[name="native-type"])': function(node) {
119+
'VElement VAttribute:has(VIdentifier[name="native-type"])': function(node: AST.VAttribute | AST.VDirective) {
118120
if (![
119121
'ncbutton',
120122
'ncdialogbutton',
@@ -140,7 +142,7 @@ export default {
140142
})
141143
},
142144

143-
'VElement VAttribute:has(VIdentifier[name="aria-hidden"])': function(node) {
145+
'VElement VAttribute:has(VIdentifier[name="aria-hidden"])': function(node: AST.VAttribute | AST.VDirective) {
144146
if (node.parent.parent.name.startsWith('ncaction')
145147
|| node.parent.parent.name === 'ncbutton') {
146148
if (!isAriaHiddenValid) {
@@ -155,7 +157,7 @@ export default {
155157
}
156158
},
157159

158-
'VElement[name="ncappcontent"] VAttribute:has(VIdentifier[name="allow-swipe-navigation"])': function(node) {
160+
'VElement[name="ncappcontent"] VAttribute:has(VIdentifier[name="allow-swipe-navigation"])': function(node: AST.VAttribute | AST.VDirective) {
159161
if (!isDisableSwipeValid) {
160162
context.report({ node, messageId: 'outdatedVueLibrary' })
161163
return
@@ -167,7 +169,7 @@ export default {
167169
})
168170
},
169171

170-
'VElement[name="ncavatar"] VAttribute:has(VIdentifier[name="show-user-status"])': function(node) {
172+
'VElement[name="ncavatar"] VAttribute:has(VIdentifier[name="show-user-status"])': function(node: AST.VAttribute | AST.VDirective) {
171173
if (!isDefaultBooleanFalseValid) {
172174
context.report({ node, messageId: 'outdatedVueLibrary' })
173175
return
@@ -179,7 +181,7 @@ export default {
179181
})
180182
},
181183

182-
'VElement[name="ncavatar"] VAttribute:has(VIdentifier[name="show-user-status-compact"])': function(node) {
184+
'VElement[name="ncavatar"] VAttribute:has(VIdentifier[name="show-user-status-compact"])': function(node: AST.VAttribute | AST.VDirective) {
183185
if (!isDefaultBooleanFalseValid) {
184186
context.report({ node, messageId: 'outdatedVueLibrary' })
185187
return
@@ -191,7 +193,7 @@ export default {
191193
})
192194
},
193195

194-
'VElement[name="ncavatar"] VAttribute:has(VIdentifier[name="allow-placeholder"])': function(node) {
196+
'VElement[name="ncavatar"] VAttribute:has(VIdentifier[name="allow-placeholder"])': function(node: AST.VAttribute | AST.VDirective) {
195197
if (!isDefaultBooleanFalseValid) {
196198
context.report({ node, messageId: 'outdatedVueLibrary' })
197199
return
@@ -203,7 +205,7 @@ export default {
203205
})
204206
},
205207

206-
'VElement[name="ncdatetimepicker"] VAttribute:has(VIdentifier[name="formatter"])': function(node) {
208+
'VElement[name="ncdatetimepicker"] VAttribute:has(VIdentifier[name="formatter"])': function(node: AST.VAttribute | AST.VDirective) {
207209
if (!isDateTimePickerFormatValid) {
208210
context.report({ node, messageId: 'outdatedVueLibrary' })
209211
return
@@ -215,7 +217,7 @@ export default {
215217
})
216218
},
217219

218-
'VElement[name="ncdatetimepicker"] VAttribute:has(VIdentifier[name="lang"])': function(node) {
220+
'VElement[name="ncdatetimepicker"] VAttribute:has(VIdentifier[name="lang"])': function(node: AST.VAttribute | AST.VDirective) {
219221
if (!isVue3Valid) {
220222
// Do not throw for v8.X.X
221223
return
@@ -227,7 +229,7 @@ export default {
227229
})
228230
},
229231

230-
'VElement[name="ncdatetimepicker"] VAttribute:has(VIdentifier[name="range"])': function(node) {
232+
'VElement[name="ncdatetimepicker"] VAttribute:has(VIdentifier[name="range"])': function(node: AST.VAttribute | AST.VDirective) {
231233
if (!isDateTimePickerFormatValid) {
232234
context.report({ node, messageId: 'outdatedVueLibrary' })
233235
return
@@ -239,7 +241,7 @@ export default {
239241
})
240242
},
241243

242-
'VElement VAttribute:has(VIdentifier[name="can-close"])': function(node) {
244+
'VElement VAttribute:has(VIdentifier[name="can-close"])': function(node: AST.VAttribute | AST.VDirective) {
243245
if (![
244246
'ncdialog',
245247
'ncmodal',
@@ -258,7 +260,7 @@ export default {
258260
})
259261
},
260262

261-
'VElement[name="ncpopover"] VAttribute:has(VIdentifier[name="close-on-click-outside"])': function(node) {
263+
'VElement[name="ncpopover"] VAttribute:has(VIdentifier[name="close-on-click-outside"])': function(node: AST.VAttribute | AST.VDirective) {
262264
if (!isVue3Valid) {
263265
// Do not throw for v8.X.X
264266
return
@@ -270,7 +272,7 @@ export default {
270272
})
271273
},
272274

273-
'VElement[name="ncmodal"] VAttribute:has(VIdentifier[name="enable-swipe"])': function(node) {
275+
'VElement[name="ncmodal"] VAttribute:has(VIdentifier[name="enable-swipe"])': function(node: AST.VAttribute | AST.VDirective) {
274276
if (!isDisableSwipeValid) {
275277
context.report({ node, messageId: 'outdatedVueLibrary' })
276278
return
@@ -282,7 +284,7 @@ export default {
282284
})
283285
},
284286

285-
'VElement[name="ncmodal"] VAttribute:has(VIdentifier[name="close-button-contained"])': function(node) {
287+
'VElement[name="ncmodal"] VAttribute:has(VIdentifier[name="close-button-contained"])': function(node: AST.VAttribute | AST.VDirective) {
286288
if (!isCloseButtonOutsideValid) {
287289
context.report({ node, messageId: 'outdatedVueLibrary' })
288290
return
@@ -294,7 +296,7 @@ export default {
294296
})
295297
},
296298

297-
'VElement[name="ncpopover"] VAttribute:has(VIdentifier[name="focus-trap"])': function(node) {
299+
'VElement[name="ncpopover"] VAttribute:has(VIdentifier[name="focus-trap"])': function(node: AST.VAttribute | AST.VDirective) {
298300
if (!isNcPopoverNoFocusTrapValid) {
299301
context.report({ node, messageId: 'outdatedVueLibrary' })
300302
return
@@ -306,7 +308,7 @@ export default {
306308
})
307309
},
308310

309-
'VElement[name="ncselect"] VAttribute:has(VIdentifier[name="close-on-select"])': function(node) {
311+
'VElement[name="ncselect"] VAttribute:has(VIdentifier[name="close-on-select"])': function(node: AST.VAttribute | AST.VDirective) {
310312
if (!isNcSelectKeepOpenValid) {
311313
context.report({ node, messageId: 'outdatedVueLibrary' })
312314
return
@@ -318,7 +320,7 @@ export default {
318320
})
319321
},
320322

321-
'VElement[name="ncselect"] VAttribute:has(VIdentifier[name="user-select"])': function(node) {
323+
'VElement[name="ncselect"] VAttribute:has(VIdentifier[name="user-select"])': function(node: AST.VAttribute | AST.VDirective) {
322324
if (!isNcSelectUsersValid) {
323325
context.report({ node, messageId: 'outdatedVueLibrary' })
324326
return
@@ -330,7 +332,7 @@ export default {
330332
})
331333
},
332334

333-
'VElement VAttribute:has(VIdentifier[name="trailing-button-icon"])': function(node) {
335+
'VElement VAttribute:has(VIdentifier[name="trailing-button-icon"])': function(node: AST.VAttribute | AST.VDirective) {
334336
if (node.parent.parent.name !== 'nctextfield') {
335337
return
336338
}
@@ -342,8 +344,12 @@ export default {
342344

343345
const isLiteral = node.value.type === 'VLiteral' && node.value.value === 'arrowRight'
344346

345-
const isExpression = node.value.type === 'VExpressionContainer' && node.value.expression?.type === 'ConditionalExpression'
346-
&& (node.value.expression.consequent.value === 'arrowRight' || node.value.expression.alternate.value === 'arrowRight')
347+
const isExpression = node.value.type === 'VExpressionContainer'
348+
&& node.value.expression?.type === 'ConditionalExpression'
349+
&& (
350+
('value' in node.value.expression.consequent && node.value.expression.consequent.value === 'arrowRight')
351+
|| ('value' in node.value.expression.alternate && node.value.expression.alternate.value === 'arrowRight')
352+
)
347353

348354
/**
349355
* if it is a literal with a deprecated value -> we migrate
@@ -357,24 +363,25 @@ export default {
357363
if (node.key.type === 'VIdentifier') {
358364
return fixer.replaceTextRange(node.value.range, '"arrowEnd"')
359365
} else if (node.key.type === 'VDirectiveKey') {
360-
return (node.value.expression.consequent.value === 'arrowRight')
361-
? fixer.replaceTextRange(node.value.expression.consequent.range, '\'arrowEnd\'')
362-
: fixer.replaceTextRange(node.value.expression.alternate.range, '\'arrowEnd\'')
366+
const expression = (node.value as AST.VExpressionContainer).expression as AST.ESLintConditionalExpression
367+
return (expression.consequent as AST.ESLintLiteral).value === 'arrowRight'
368+
? fixer.replaceTextRange(expression.consequent.range, '\'arrowEnd\'')
369+
: fixer.replaceTextRange(expression.alternate.range, '\'arrowEnd\'')
363370
}
364371
},
365372
})
366373
}
367374
},
368375

369-
'VElement[name="ncsettingssection"] VAttribute:has(VIdentifier[name="limit-width"])': function(node) {
376+
'VElement[name="ncsettingssection"] VAttribute:has(VIdentifier[name="limit-width"])': function(node: AST.VAttribute | AST.VDirective) {
370377
// This was deprecated in 8.13.0 (Nextcloud 30+), before first supported version by plugin
371378
context.report({
372379
node,
373380
messageId: 'removeLimitWidth',
374381
})
375382
},
376383

377-
'VElement VAttribute:has(VIdentifier[name="exact"])': function(node) {
384+
'VElement VAttribute:has(VIdentifier[name="exact"])': function(node: AST.VAttribute | AST.VDirective) {
378385
if (![
379386
'ncactionrouter',
380387
'ncappnavigationitem',
@@ -396,7 +403,7 @@ export default {
396403
})
397404
},
398405

399-
'VElement VAttribute:has(VIdentifier[name="checked"])': function(node) {
406+
'VElement VAttribute:has(VIdentifier[name="checked"])': function(node: AST.VAttribute | AST.VDirective) {
400407
if (![
401408
'ncactioncheckbox',
402409
'ncactionradio',
@@ -429,7 +436,7 @@ export default {
429436
})
430437
},
431438

432-
'VElement VAttribute:has(VIdentifier[name="value"])': function(node) {
439+
'VElement VAttribute:has(VIdentifier[name="value"])': function(node: AST.VAttribute | AST.VDirective) {
433440
if (![
434441
'ncactioninput',
435442
'ncactiontexteditable',
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { Rule } from 'eslint'
7+
8+
import { extname } from 'node:path'
9+
10+
type TemplateVisitor = { [key: string]: (...args: unknown[]) => void }
11+
12+
type TemplateVisitorOptions = { templateBodyTriggerSelector: 'Program' | 'Program:exit' }
13+
14+
/**
15+
* @see https://github.com/vuejs/vue-eslint-parser/blob/5ff1a4fda76b07608cc17687a976c2309f5648e2/src/parser-services.ts#L46
16+
*/
17+
interface ParserServices {
18+
/**
19+
* Define handlers to traverse the template body.
20+
*
21+
* @param templateBodyVisitor The template body handlers.
22+
* @param scriptVisitor The script handlers.
23+
* @param options The options.
24+
*/
25+
defineTemplateBodyVisitor(
26+
templateBodyVisitor: TemplateVisitor,
27+
scriptVisitor?: Rule.NodeListener,
28+
options?: TemplateVisitorOptions,
29+
): Rule.RuleListener
30+
}
31+
32+
/**
33+
* Register the given visitor to parser services.
34+
* If the parser service of `vue-eslint-parser` was not found,
35+
* this generates a warning.
36+
*
37+
* @param context - The rule context to use parser services.
38+
* @param templateBodyVisitor - The visitor to traverse the template body.
39+
* @param scriptVisitor - The visitor to traverse the script.
40+
* @param options - The options.
41+
*
42+
* @return The merged visitor.
43+
* @see https://github.com/vuejs/eslint-plugin-vue/blob/745fd4e1f3719c3a2f93bd3531da5e886c16f008/lib/utils/index.js#L2281-L2315
44+
*/
45+
export function defineTemplateBodyVisitor(context: Rule.RuleContext, templateBodyVisitor: TemplateVisitor, scriptVisitor?: Rule.RuleListener, options?: TemplateVisitorOptions) {
46+
const sourceCode = context.sourceCode
47+
if (!sourceCode.parserServices?.defineTemplateBodyVisitor) {
48+
const filename = context.filename
49+
if (extname(filename) === '.vue') {
50+
context.report({
51+
loc: {
52+
line: 1,
53+
column: 0,
54+
},
55+
message: 'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.',
56+
})
57+
}
58+
return {}
59+
}
60+
return (sourceCode.parserServices as ParserServices)
61+
.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor, options)
62+
}

0 commit comments

Comments
 (0)