66'use strict'
77
88const utils = require ( '../utils' )
9+ const casing = require ( '../utils/casing' )
10+
11+ /**
12+ * @typedef { VDirectiveKey & { name: VIdentifier & { name: 'bind' }, argument: VExpressionContainer | VIdentifier } } VBindDirectiveKey
13+ * @typedef { VDirective & { key: VBindDirectiveKey } } VBindDirective
14+ */
15+
16+ /**
17+ * @param {VBindDirective } node
18+ * @returns {boolean }
19+ */
20+ function isSameName ( node ) {
21+ const attrName =
22+ node . key . argument . type === 'VIdentifier' ? node . key . argument . rawName : null
23+ const valueName =
24+ node . value ?. expression ?. type === 'Identifier'
25+ ? node . value . expression . name
26+ : null
27+ return Boolean (
28+ attrName &&
29+ valueName &&
30+ casing . camelCase ( attrName ) === casing . camelCase ( valueName )
31+ )
32+ }
33+
34+ /**
35+ * @param {VBindDirectiveKey } key
36+ * @returns {number }
37+ */
38+ function getCutStart ( key ) {
39+ const modifiers = key . modifiers
40+ return modifiers . length > 0
41+ ? modifiers [ modifiers . length - 1 ] . range [ 1 ]
42+ : key . argument . range [ 1 ]
43+ }
944
1045module . exports = {
1146 meta : {
@@ -16,60 +51,113 @@ module.exports = {
1651 url : 'https://eslint.vuejs.org/rules/v-bind-style.html'
1752 } ,
1853 fixable : 'code' ,
19- schema : [ { enum : [ 'shorthand' , 'longform' ] } ] ,
54+ schema : [
55+ { enum : [ 'shorthand' , 'longform' ] } ,
56+ {
57+ type : 'object' ,
58+ properties : {
59+ sameNameShorthand : { enum : [ 'always' , 'never' , 'ignore' ] }
60+ } ,
61+ additionalProperties : false
62+ }
63+ ] ,
2064 messages : {
2165 expectedLonghand : "Expected 'v-bind' before ':'." ,
2266 unexpectedLonghand : "Unexpected 'v-bind' before ':'." ,
23- expectedLonghandForProp : "Expected 'v-bind:' instead of '.'."
67+ expectedLonghandForProp : "Expected 'v-bind:' instead of '.'." ,
68+ expectedShorthand : 'Expected same-name shorthand.' ,
69+ unexpectedShorthand : 'Unexpected same-name shorthand.'
2470 }
2571 } ,
2672 /** @param {RuleContext } context */
2773 create ( context ) {
2874 const preferShorthand = context . options [ 0 ] !== 'longform'
75+ /** @type {"always" | "never" | "ignore" } */
76+ const sameNameShorthand = context . options [ 1 ] ?. sameNameShorthand || 'ignore'
2977
30- return utils . defineTemplateBodyVisitor ( context , {
31- /** @param {VDirective } node */
32- "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]" (
33- node
34- ) {
35- const shorthandProp = node . key . name . rawName === '.'
36- const shorthand = node . key . name . rawName === ':' || shorthandProp
37- if ( shorthand === preferShorthand ) {
38- return
39- }
78+ /** @param {VBindDirective } node */
79+ function checkAttributeStyle ( node ) {
80+ const shorthandProp = node . key . name . rawName === '.'
81+ const shorthand = node . key . name . rawName === ':' || shorthandProp
82+ if ( shorthand === preferShorthand ) {
83+ return
84+ }
4085
41- let messageId = 'expectedLonghand'
42- if ( preferShorthand ) {
43- messageId = 'unexpectedLonghand'
44- } else if ( shorthandProp ) {
45- messageId = 'expectedLonghandForProp'
46- }
86+ let messageId = 'expectedLonghand'
87+ if ( preferShorthand ) {
88+ messageId = 'unexpectedLonghand'
89+ } else if ( shorthandProp ) {
90+ messageId = 'expectedLonghandForProp'
91+ }
92+
93+ context . report ( {
94+ node,
95+ loc : node . loc ,
96+ messageId,
97+ * fix ( fixer ) {
98+ if ( preferShorthand ) {
99+ yield fixer . remove ( node . key . name )
100+ } else {
101+ yield fixer . insertTextBefore ( node , 'v-bind' )
102+
103+ if ( shorthandProp ) {
104+ // Replace `.` by `:`.
105+ yield fixer . replaceText ( node . key . name , ':' )
47106
48- context . report ( {
49- node,
50- loc : node . loc ,
51- messageId,
52- * fix ( fixer ) {
53- if ( preferShorthand ) {
54- yield fixer . remove ( node . key . name )
55- } else {
56- yield fixer . insertTextBefore ( node , 'v-bind' )
57-
58- if ( shorthandProp ) {
59- // Replace `.` by `:`.
60- yield fixer . replaceText ( node . key . name , ':' )
61-
62- // Insert `.prop` modifier if it doesn't exist.
63- const modifier = node . key . modifiers [ 0 ]
64- const isAutoGeneratedPropModifier =
65- modifier . name === 'prop' && modifier . rawName === ''
66- if ( isAutoGeneratedPropModifier ) {
67- yield fixer . insertTextBefore ( modifier , '.prop' )
68- }
107+ // Insert `.prop` modifier if it doesn't exist.
108+ const modifier = node . key . modifiers [ 0 ]
109+ const isAutoGeneratedPropModifier =
110+ modifier . name === 'prop' && modifier . rawName === ''
111+ if ( isAutoGeneratedPropModifier ) {
112+ yield fixer . insertTextBefore ( modifier , '.prop' )
69113 }
70114 }
71115 }
72- } )
116+ }
117+ } )
118+ }
119+
120+ /** @param {VBindDirective } node */
121+ function checkAttributeSameName ( node ) {
122+ if ( sameNameShorthand === 'ignore' || ! isSameName ( node ) ) return
123+
124+ const preferShorthand = sameNameShorthand === 'always'
125+ const isShorthand = utils . isVBindSameNameShorthand ( node )
126+ if ( isShorthand === preferShorthand ) {
127+ return
128+ }
129+
130+ const messageId = preferShorthand
131+ ? 'expectedShorthand'
132+ : 'unexpectedShorthand'
133+
134+ context . report ( {
135+ node,
136+ loc : node . loc ,
137+ messageId,
138+ * fix ( fixer ) {
139+ if ( preferShorthand ) {
140+ /** @type {Range } */
141+ const valueRange = [ getCutStart ( node . key ) , node . range [ 1 ] ]
142+
143+ yield fixer . removeRange ( valueRange )
144+ } else if ( node . key . argument . type === 'VIdentifier' ) {
145+ yield fixer . insertTextAfter (
146+ node ,
147+ `="${ casing . camelCase ( node . key . argument . rawName ) } "`
148+ )
149+ }
150+ }
151+ } )
152+ }
153+
154+ return utils . defineTemplateBodyVisitor ( context , {
155+ /** @param {VBindDirective } node */
156+ "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]" (
157+ node
158+ ) {
159+ checkAttributeSameName ( node )
160+ checkAttributeStyle ( node )
73161 }
74162 } )
75163 }
0 commit comments