UNPKG

@nextcloud/eslint-config

Version:

Eslint shared config for nextcloud apps and libraries

519 lines (518 loc) 23.5 kB
/*! * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ import { createLibVersionValidator } from "../utils/lib-version-parser.js"; import { defineTemplateBodyVisitor } from "../utils/vue-template-visitor.js"; /* * Deprecated props can be authored in either kebab-case or camelCase: * - camelCase is forced by `vue/attribute-hyphenation: never` shipped rule; * - kebab-case might come from legacy code and other presets. * A single `VAttribute` visitor normalizes the raw attribute name with `toCamelCase()` * and dispatches to the handler registered under the canonical camelCase name. */ /** * Convert an attribute name to camelCase, matching Vue's own prop normalization, * so a template attribute written in either casing maps to a single canonical name. * * @param name - the raw attribute name as written in the template */ const toCamelCase = (name) => name.replace(/-(\w)/g, (_, char) => char.toUpperCase()); /** * Resolve the raw (case-preserving) name of a template attribute: the argument * for a directive binding (`:foo` / `v-model:foo`), or the key otherwise. * Returns `undefined` when there is no static name (e.g. a dynamic argument). * * Must use `rawName`, not `name`: Vue lowercases `name` in the AST (`focusTrap` * becomes `focustrap`), which `toCamelCase()` cannot restore, so it would no * longer match the camelCase handler key. `rawName` keeps the original casing. * * @param node - the attribute or directive node */ function getRawName(node) { if (node.key.type === 'VDirectiveKey') { return node.key.argument?.type === 'VIdentifier' ? node.key.argument.rawName : undefined; } return node.key.rawName; } export default { meta: { docs: { description: 'Deprecated `@nextcloud/vue` properties', }, type: 'problem', fixable: 'code', messages: { outdatedVueLibrary: 'Installed @nextcloud/vue library is outdated and does not support all reported errors. Install latest compatible version', useTypeInstead: 'Using `native-type` for button variant is deprecated - use `type` instead.', useVariantInstead: 'Using `type` for button variant is deprecated - use `variant` instead.', useDisableSwipeForNavInstead: 'Using `allow-swipe-navigation` is deprecated - use `disable-swipe` instead', useHideStatusInstead: 'Using `show-user-status` is deprecated - use `hide-status` instead', useVerboseStatusInstead: 'Using `show-user-status-compact` is deprecated - use `verbose-status` instead', useNoPlaceholderInstead: 'Using `allow-placeholder` is deprecated - use `no-placeholder` instead', useFormatInstead: 'Using `formatter` is deprecated - use `format` instead', useLocaleInstead: 'Using `lang` is deprecated - use `locale` instead', useTypeDateRangeInstead: 'Using `range` is deprecated - use `type` with `date-range` or `datetime-range` instead', useNoCloseInstead: 'Using `can-close` is deprecated - use `no-close` instead', useNoCloseOnClickOutsideInstead: 'Using `close-on-click-outside` is deprecated - use `no-close-on-click-outside` instead', useDisableSwipeForModalInstead: 'Using `enable-swipe` is deprecated - use `disable-swipe` instead', useNoFocusTrapInstead: 'Using `focus-trap` is deprecated - use `no-focus-trap` instead', useKeepOpenInstead: 'Using `close-on-select` is deprecated - use `keep-open` instead', useNcSelectUsersInstead: 'Using `user-select` is deprecated - use `NcSelectUsers` component instead', useArrowEndInstead: 'Using `arrowRight` as `trailing-button-icon` value is deprecated - use `arrowEnd` instead', removeAriaHidden: 'Using `aria-hidden` is deprecated - remove prop from components, otherwise root element will inherit incorrect attribute.', removeLimitWidth: 'Using `limit-width` is deprecated - remove prop from components, otherwise root element will inherit incorrect attribute.', removeExact: 'Using `exact` is deprecated - consult Vue Router documentation for alternatives.', useCloseButtonOutsideInstead: 'Using `close-button-contained` is deprecated - use `close-button-outside` instead', useModelValueInsteadChecked: 'Using `checked` is deprecated - use `model-value` or `v-model` instead', useModelValueInsteadValue: 'Using `value` is deprecated - use `model-value` or `v-model` instead', }, }, create(context) { const versionSatisfies = createLibVersionValidator(context); const isVue3Valid = versionSatisfies('9.0.0'); // #6651 const isAriaHiddenValid = versionSatisfies('8.2.0'); // #4835 const isModelValueValid = versionSatisfies('8.20.0'); // #6172 const isDisableSwipeValid = versionSatisfies('8.23.0'); // #6452 const isVariantTypeValid = versionSatisfies('8.24.0'); // #6472 const isDefaultBooleanFalseValid = versionSatisfies('8.24.0'); // #6656 const isDateTimePickerFormatValid = versionSatisfies('8.25.0'); // #6738 const isNcSelectKeepOpenValid = versionSatisfies('8.25.0'); // #6791 const isNcPopoverNoFocusTrapValid = versionSatisfies('8.26.0'); // #6808 const isNcSelectUsersValid = versionSatisfies('8.25.0'); // #6791 const isNcTextFieldArrowEndValid = versionSatisfies('8.28.0'); // #7002 const isCloseButtonOutsideValid = versionSatisfies('8.32.0'); // #7553 const legacyTypes = [ 'primary', 'error', 'warning', 'success', 'secondary', 'tertiary', 'tertiary-no-background', ]; /* * Deprecated prop handlers keyed by their canonical camelCase name. Each * handler receives the matched attribute node and verifies the owning * component and installed library version itself. */ const handlers = { type: function (node) { if (![ 'ncactions', 'ncappnavigationnew', 'ncbutton', 'ncchip', 'ncdialogbutton', ].includes(node.parent.parent.name)) { return; } if (!isVariantTypeValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } const hasNativeType = node.parent.attributes.some((attr) => { const rawName = getRawName(attr); return rawName !== undefined && toCamelCase(rawName) === 'nativeType'; }); const isLiteral = node.value.type === 'VLiteral' && legacyTypes.includes(node.value.value); const isExpression = node.value.type === 'VExpressionContainer' && node.value.expression.type === 'ConditionalExpression' && (('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()))); /** * if it is a literal with a deprecated value -> we migrate * if it is an expression with a defined deprecated value -> we migrate * if it is unknown value (e.g. computed), but there is also the `native-type` we assume it is legacy and we migrate */ if (isLiteral || isExpression || hasNativeType) { context.report({ node, messageId: 'useVariantInstead', fix: (fixer) => { if (node.key.type === 'VIdentifier') { return fixer.replaceTextRange(node.key.range, 'variant'); } else if (node.key.type === 'VDirectiveKey') { return fixer.replaceTextRange(node.key.argument.range, 'variant'); } }, }); } }, nativeType: function (node) { if (![ 'ncbutton', 'ncdialogbutton', ].includes(node.parent.parent.name)) { return; } if (!isVariantTypeValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useTypeInstead', fix: (fixer) => { if (node.key.type === 'VIdentifier') { return fixer.replaceTextRange(node.key.range, 'type'); } else if (node.key.type === 'VDirectiveKey') { return fixer.replaceTextRange(node.key.argument.range, 'type'); } }, }); }, ariaHidden: function (node) { if (node.parent.parent.name.startsWith('ncaction') || node.parent.parent.name === 'ncbutton') { if (!isAriaHiddenValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'removeAriaHidden', }); } }, allowSwipeNavigation: function (node) { if (node.parent.parent.name !== 'ncappcontent') { return; } if (!isDisableSwipeValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useDisableSwipeForNavInstead', }); }, showUserStatus: function (node) { if (node.parent.parent.name !== 'ncavatar') { return; } if (!isDefaultBooleanFalseValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useHideStatusInstead', }); }, showUserStatusCompact: function (node) { if (node.parent.parent.name !== 'ncavatar') { return; } if (!isDefaultBooleanFalseValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useVerboseStatusInstead', }); }, allowPlaceholder: function (node) { if (node.parent.parent.name !== 'ncavatar') { return; } if (!isDefaultBooleanFalseValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useNoPlaceholderInstead', }); }, formatter: function (node) { if (node.parent.parent.name !== 'ncdatetimepicker') { return; } if (!isDateTimePickerFormatValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useFormatInstead', }); }, lang: function (node) { if (node.parent.parent.name !== 'ncdatetimepicker') { return; } if (!isVue3Valid) { // Do not throw for v8.X.X return; } context.report({ node, messageId: 'useLocaleInstead', }); }, range: function (node) { if (node.parent.parent.name !== 'ncdatetimepicker') { return; } if (!isDateTimePickerFormatValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useTypeDateRangeInstead', }); }, canClose: function (node) { if (![ 'ncdialog', 'ncmodal', ].includes(node.parent.parent.name)) { return; } if (!isDefaultBooleanFalseValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useNoCloseInstead', }); }, closeOnClickOutside: function (node) { if (node.parent.parent.name !== 'ncpopover') { return; } if (!isVue3Valid) { // Do not throw for v8.X.X return; } context.report({ node, messageId: 'useNoCloseOnClickOutsideInstead', }); }, enableSwipe: function (node) { if (node.parent.parent.name !== 'ncmodal') { return; } if (!isDisableSwipeValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useDisableSwipeForModalInstead', }); }, closeButtonContained: function (node) { if (node.parent.parent.name !== 'ncmodal') { return; } if (!isCloseButtonOutsideValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useCloseButtonOutsideInstead', }); }, focusTrap: function (node) { if (node.parent.parent.name !== 'ncpopover') { return; } if (!isNcPopoverNoFocusTrapValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useNoFocusTrapInstead', }); }, closeOnSelect: function (node) { if (node.parent.parent.name !== 'ncselect') { return; } if (!isNcSelectKeepOpenValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useKeepOpenInstead', }); }, userSelect: function (node) { if (node.parent.parent.name !== 'ncselect') { return; } if (!isNcSelectUsersValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useNcSelectUsersInstead', }); }, trailingButtonIcon: function (node) { if (node.parent.parent.name !== 'nctextfield') { return; } if (!isNcTextFieldArrowEndValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } const isLiteral = node.value.type === 'VLiteral' && node.value.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 * if it is an expression with a defined deprecated value -> we migrate */ if (isLiteral || isExpression) { context.report({ node, messageId: 'useArrowEndInstead', fix: (fixer) => { if (node.key.type === 'VIdentifier') { return fixer.replaceTextRange(node.value.range, '"arrowEnd"'); } else if (node.key.type === 'VDirectiveKey') { const expression = node.value.expression; return expression.consequent.value === 'arrowRight' ? fixer.replaceTextRange(expression.consequent.range, '\'arrowEnd\'') : fixer.replaceTextRange(expression.alternate.range, '\'arrowEnd\''); } }, }); } }, limitWidth: function (node) { if (node.parent.parent.name !== 'ncsettingssection') { return; } // This was deprecated in 8.12.0 (Nextcloud 30+), before first supported version by plugin context.report({ node, messageId: 'removeLimitWidth', }); }, exact: function (node) { if (![ 'ncactionrouter', 'ncappnavigationitem', 'ncbreadcrumb', 'ncbutton', 'nclistitem', ].includes(node.parent.parent.name)) { return; } if (!isVue3Valid) { // Do not throw for v8.X.X return; } context.report({ node, messageId: 'removeExact', }); }, checked: function (node) { if (![ 'ncactioncheckbox', 'ncactionradio', 'nccheckboxradioswitch', ].includes(node.parent.parent.name)) { return; } if (!isModelValueValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useModelValueInsteadChecked', fix: (fixer) => { if (node.key.type === 'VIdentifier') { return fixer.replaceTextRange(node.key.range, 'model-value'); } else if (node.key.type === 'VDirectiveKey') { if (node.key.name.name === 'model') { return fixer.replaceTextRange(node.key.range, 'v-model'); } else if (node.key.modifiers.some((m) => m.name === 'sync')) { return fixer.replaceTextRange(node.key.range, 'v-model'); } else { return fixer.replaceTextRange(node.key.argument.range, 'model-value'); } } }, }); }, value: function (node) { if (![ 'ncactioninput', 'ncactiontexteditable', 'nccolorpicker', 'ncdatetimepicker', 'ncdatetimepickernative', 'ncinputfield', 'nctextfield', 'ncpasswordfield', 'ncrichcontenteditable', 'ncselecttags', 'ncselect', 'ncsettingsinputtext', 'ncsettingsselectgroup', 'nctextarea', 'nctimezonepicker', ].includes(node.parent.parent.name)) { return; } if (!isModelValueValid) { context.report({ node, messageId: 'outdatedVueLibrary' }); return; } context.report({ node, messageId: 'useModelValueInsteadValue', fix: (fixer) => { if (node.key.type === 'VIdentifier') { return fixer.replaceTextRange(node.key.range, 'model-value'); } else if (node.key.type === 'VDirectiveKey') { if (node.key.name.name === 'model') { return fixer.replaceTextRange(node.key.range, 'v-model'); } else if (node.key.modifiers.some((m) => m.name === 'sync')) { return fixer.replaceTextRange(node.key.range, 'v-model'); } else { return fixer.replaceTextRange(node.key.argument.range, 'model-value'); } } }, }); }, }; return defineTemplateBodyVisitor(context, { VAttribute(node) { const rawName = getRawName(node); if (rawName === undefined) { return; } handlers[toCamelCase(rawName)]?.(node); }, }); }, };