Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/plugins/l10n/rules/enforce-ellipsis-vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import type { Rule } from 'eslint'

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

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

create(context) {
return vueUtils.defineTemplateBodyVisitor(context, enforceEllipsis.create(context))
return defineTemplateBodyVisitor(context, enforceEllipsis.create(context))
},
})
4 changes: 2 additions & 2 deletions lib/plugins/l10n/rules/non-breaking-space-vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import type { Rule } from 'eslint'

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

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

create(context) {
return vueUtils.defineTemplateBodyVisitor(context, nonBreakingSpace.create(context))
return defineTemplateBodyVisitor(context, nonBreakingSpace.create(context))
},
})
71 changes: 39 additions & 32 deletions lib/plugins/nextcloud-vue/rules/no-deprecated-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Rule } from 'eslint'
import type { AST } from 'vue-eslint-parser'

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

export default {
meta: {
Expand Down Expand Up @@ -66,8 +68,8 @@ export default {
'tertiary-no-background',
]

return vueUtils.defineTemplateBodyVisitor(context, {
'VElement VAttribute:has(VIdentifier[name="type"])': function(node) {
return defineTemplateBodyVisitor(context, {
'VElement VAttribute:has(VIdentifier[name="type"])': function(node: AST.VAttribute | AST.VDirective) {
if (![
'ncactions',
'ncappnavigationnew',
Expand All @@ -85,13 +87,13 @@ export default {

const hasNativeType = node.parent.attributes.find((attr) => (
attr.key.name === 'native-type'
|| (attr.key.type === 'VDirectiveKey' && attr.key.argument && attr.key.argument.name === 'native-type')))
|| (attr.key.type === 'VDirectiveKey' && attr.key.argument && attr.key.argument.type === 'VIdentifier' && attr.key.argument.name === 'native-type')))

const isLiteral = node.value.type === 'VLiteral' && legacyTypes.includes(node.value.value)
const isExpression = node.value.type === 'VExpressionContainer'
&& node.value.expression.type === 'ConditionalExpression'
&& (legacyTypes.includes(node.value.expression.consequent.value)
|| legacyTypes.includes(node.value.expression.alternate.value)
&& (('value' in node.value.expression.consequent && legacyTypes.includes(node.value.expression.consequent.value.toString()))
|| ('value' in node.value.expression.alternate && legacyTypes.includes(node.value.expression.alternate.value.toString()))
)

/**
Expand All @@ -114,7 +116,7 @@ export default {
}
},

'VElement VAttribute:has(VIdentifier[name="native-type"])': function(node) {
'VElement VAttribute:has(VIdentifier[name="native-type"])': function(node: AST.VAttribute | AST.VDirective) {
if (![
'ncbutton',
'ncdialogbutton',
Expand All @@ -140,7 +142,7 @@ export default {
})
},

'VElement VAttribute:has(VIdentifier[name="aria-hidden"])': function(node) {
'VElement VAttribute:has(VIdentifier[name="aria-hidden"])': function(node: AST.VAttribute | AST.VDirective) {
if (node.parent.parent.name.startsWith('ncaction')
|| node.parent.parent.name === 'ncbutton') {
if (!isAriaHiddenValid) {
Expand All @@ -155,7 +157,7 @@ export default {
}
},

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

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

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

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

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

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

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

'VElement VAttribute:has(VIdentifier[name="can-close"])': function(node) {
'VElement VAttribute:has(VIdentifier[name="can-close"])': function(node: AST.VAttribute | AST.VDirective) {
if (![
'ncdialog',
'ncmodal',
Expand All @@ -258,7 +260,7 @@ export default {
})
},

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

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

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

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

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

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

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

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

const isExpression = node.value.type === 'VExpressionContainer' && node.value.expression?.type === 'ConditionalExpression'
&& (node.value.expression.consequent.value === 'arrowRight' || node.value.expression.alternate.value === 'arrowRight')
const isExpression = node.value.type === 'VExpressionContainer'
&& node.value.expression?.type === 'ConditionalExpression'
&& (
('value' in node.value.expression.consequent && node.value.expression.consequent.value === 'arrowRight')
|| ('value' in node.value.expression.alternate && node.value.expression.alternate.value === 'arrowRight')
)

/**
* if it is a literal with a deprecated value -> we migrate
Expand All @@ -357,24 +363,25 @@ export default {
if (node.key.type === 'VIdentifier') {
return fixer.replaceTextRange(node.value.range, '"arrowEnd"')
} else if (node.key.type === 'VDirectiveKey') {
return (node.value.expression.consequent.value === 'arrowRight')
? fixer.replaceTextRange(node.value.expression.consequent.range, '\'arrowEnd\'')
: fixer.replaceTextRange(node.value.expression.alternate.range, '\'arrowEnd\'')
const expression = (node.value as AST.VExpressionContainer).expression as AST.ESLintConditionalExpression
return (expression.consequent as AST.ESLintLiteral).value === 'arrowRight'
? fixer.replaceTextRange(expression.consequent.range, '\'arrowEnd\'')
: fixer.replaceTextRange(expression.alternate.range, '\'arrowEnd\'')
}
},
})
}
},

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

'VElement VAttribute:has(VIdentifier[name="exact"])': function(node) {
'VElement VAttribute:has(VIdentifier[name="exact"])': function(node: AST.VAttribute | AST.VDirective) {
if (![
'ncactionrouter',
'ncappnavigationitem',
Expand All @@ -396,7 +403,7 @@ export default {
})
},

'VElement VAttribute:has(VIdentifier[name="checked"])': function(node) {
'VElement VAttribute:has(VIdentifier[name="checked"])': function(node: AST.VAttribute | AST.VDirective) {
if (![
'ncactioncheckbox',
'ncactionradio',
Expand Down Expand Up @@ -429,7 +436,7 @@ export default {
})
},

'VElement VAttribute:has(VIdentifier[name="value"])': function(node) {
'VElement VAttribute:has(VIdentifier[name="value"])': function(node: AST.VAttribute | AST.VDirective) {
if (![
'ncactioninput',
'ncactiontexteditable',
Expand Down
62 changes: 62 additions & 0 deletions lib/plugins/nextcloud-vue/utils/vue-template-visitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Rule } from 'eslint'

import { extname } from 'node:path'

type TemplateVisitor = { [key: string]: (...args: unknown[]) => void }

type TemplateVisitorOptions = { templateBodyTriggerSelector: 'Program' | 'Program:exit' }

/**
* @see https://github.com/vuejs/vue-eslint-parser/blob/5ff1a4fda76b07608cc17687a976c2309f5648e2/src/parser-services.ts#L46
*/
interface ParserServices {
/**
* Define handlers to traverse the template body.
*
* @param templateBodyVisitor The template body handlers.
* @param scriptVisitor The script handlers.
* @param options The options.
*/
defineTemplateBodyVisitor(
templateBodyVisitor: TemplateVisitor,
scriptVisitor?: Rule.NodeListener,
options?: TemplateVisitorOptions,
): Rule.RuleListener
}

/**
* Register the given visitor to parser services.
* If the parser service of `vue-eslint-parser` was not found,
* this generates a warning.
*
* @param context - The rule context to use parser services.
* @param templateBodyVisitor - The visitor to traverse the template body.
* @param scriptVisitor - The visitor to traverse the script.
* @param options - The options.
*
* @return The merged visitor.
* @see https://github.com/vuejs/eslint-plugin-vue/blob/745fd4e1f3719c3a2f93bd3531da5e886c16f008/lib/utils/index.js#L2281-L2315
*/
export function defineTemplateBodyVisitor(context: Rule.RuleContext, templateBodyVisitor: TemplateVisitor, scriptVisitor?: Rule.RuleListener, options?: TemplateVisitorOptions) {
const sourceCode = context.sourceCode
if (!sourceCode.parserServices?.defineTemplateBodyVisitor) {
const filename = context.filename
if (extname(filename) === '.vue') {
context.report({
loc: {
line: 1,
column: 0,
},
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.',
})
}
return {}
}
return (sourceCode.parserServices as ParserServices)
.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor, options)
}
Loading