import type { AST , Rule } from 'eslint' ; import type { Loc } from 'pug-lexer' ; import { processRule } from '../utils' ; import { getExactConverter } from '../utils/casing' ; import { isHtmlWellKnownElementName } from '../utils/html-element' ; import { isMathMlWellKnownElementName } from '../utils/math-ml-element' ; import { previousTagToken } from '../utils/pug-utils' ; import { SVG_ATTRIBUTES_WEIRD_CASE } from '../utils/svg-attributes-weird-case' ; import { isSvgWellKnownElementName } from '../utils/svg-element' ; export default { meta : { type : 'suggestion' , docs : { description : 'enforce attribute naming style on custom components in template' , categories : [ 'vue3-strongly-recommended' , 'strongly-recommended' ] , url : 'https://eslint.vuejs.org/rules/attribute-hyphenation.html' } , fixable : 'code' , schema : [ { enum : [ 'always' , 'never' ] } , { type : 'object' , properties : { ignore : { type : 'array' , items : { allOf : [ { type : 'string' } , { not : { type : 'string' , pattern : ':exit$' } } , { not : { type : 'string' , pattern : '^\\s*$' } } ] } , uniqueItems : true , additionalItems : false } } , additionalProperties : false } ] } , create ( context ) { return processRule ( context , ( ) => { const useHyphenated = context . options [ 0 ] !== 'never' ; const { ignore = [ ] } = context . options [ 1 ] ?? { } ; const ignoredAttributes = [ 'data-' , 'aria-' , 'slot-scope' , ... SVG_ATTRIBUTES_WEIRD_CASE , ... ignore ] ; const caseConverter = getExactConverter ( useHyphenated ? 'kebab-case' : 'camelCase' ) ; function isIgnoredAttribute ( value ) { const isIgnored = ignoredAttributes . some ( ( attr ) => value . includes ( attr ) ) ; if ( isIgnored ) { return true ; } if ( /^(v-bind)?:\[.+\]$/ . test ( value ) ) { return true ; } if ( useHyphenated ) { return value . toLowerCase ( ) === value ; } return ! value . includes ( '-' ) ; } return { attribute ( token , { index , tokens } ) { const attributeName = token . name ; const tagName = previousTagToken ( tokens , index ) ?. val ?? '' ; if ( tagName && ( isHtmlWellKnownElementName ( tagName ) || isSvgWellKnownElementName ( tagName ) || isMathMlWellKnownElementName ( tagName ) ) ) { return ; } if ( ! attributeName || isIgnoredAttribute ( attributeName ) ) { return ; } const loc = token . loc ; const range = token . range ; const columnStart = loc . start . column - 1 ; const columnEnd = columnStart + attributeName . length ; context . report ( { node : { type : /^(v-bind)?:/ . test ( attributeName ) ? 'VDirectiveKey' : 'VIdentifier' } as unknown as Rule . Node , loc : { line : loc . start . line , column : loc . start . column - 1 , start : { line : loc . start . line , column : columnStart } , end : { line : loc . end . line , column : columnEnd } } , message : useHyphenated ? "Attribute '{{text}}' must be hyphenated." : "Attribute '{{text}}' can't be hyphenated." , data : { text : attributeName } , fix ( fixer ) { let converted ; const parts = attributeName . split ( ':' ) ; if ( parts . length > 1 && ( parts [ 0 ] === '' || parts [ 0 ] === 'v-bind' ) ) { const name = parts . slice ( 1 ) . join ( '' ) ; converted = `${ parts [ 0 ] }:${ caseConverter ( name ) }` ; } else { converted = caseConverter ( attributeName ) ; } return fixer . replaceTextRange ( [ range [ 0 ] , range [ 0 ] + attributeName . length ] , converted ) ; } } ) ; } } ; } ) ; } } as Rule . RuleModule ;