@@ -42,6 +42,10 @@ function hasNestedFixableHelper(node) {
4242 ) ;
4343}
4444
45+ function escapeRegExp ( str ) {
46+ return str . replaceAll ( / [ $ ( ) * + . ? [ \\ \] ^ { | } ] / g, '\\$&' ) ;
47+ }
48+
4549/** @type {import('eslint').Rule.RuleModule } */
4650module . exports = {
4751 meta : {
@@ -52,6 +56,7 @@ module.exports = {
5256 url : 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-negated-condition.md' ,
5357 templateMode : 'both' ,
5458 } ,
59+ fixable : 'code' ,
5560 schema : [
5661 {
5762 type : 'object' ,
@@ -80,6 +85,118 @@ module.exports = {
8085 const simplifyHelpers = options . simplifyHelpers === undefined ? true : options . simplifyHelpers ;
8186 const sourceCode = context . getSourceCode ( ) ;
8287
88+ /**
89+ * Get the source text for the inner condition of a (not ...) sub-expression,
90+ * with the nested helper optionally inverted.
91+ */
92+ function getUnwrappedConditionText ( notExpr , invertHelper ) {
93+ const inner = notExpr . params [ 0 ] ;
94+ if ( invertHelper && inner . path ?. type === 'GlimmerPathExpression' ) {
95+ const helperName = inner . path . original ;
96+ const inverted = HELPER_INVERSIONS [ helperName ] ;
97+ if ( inverted !== undefined ) {
98+ if ( inverted === null ) {
99+ // (not (not x)) -> just x
100+ return sourceCode . getText ( inner . params [ 0 ] ) ;
101+ }
102+ // (not (eq a b)) -> (not-eq a b)
103+ const innerText = sourceCode . getText ( inner ) ;
104+ return innerText . replace ( `(${ helperName } ` , `(${ inverted } ` ) ;
105+ }
106+ }
107+ return sourceCode . getText ( inner ) ;
108+ }
109+
110+ /**
111+ * Build a fix function for block statements.
112+ */
113+ function buildBlockFix ( node , msgId ) {
114+ return ( fixer ) => {
115+ const fullText = sourceCode . getText ( node ) ;
116+ const keyword = node . path . original ;
117+ const notExpr = node . params [ 0 ] ;
118+
119+ if ( msgId === 'negatedHelper' ) {
120+ const conditionText = getUnwrappedConditionText ( notExpr , true ) ;
121+ const newText = fullText . replace ( sourceCode . getText ( notExpr ) , conditionText ) ;
122+ return fixer . replaceText ( node , newText ) ;
123+ }
124+
125+ if ( msgId === 'flipIf' ) {
126+ const conditionText = getUnwrappedConditionText ( notExpr , false ) ;
127+ const programBody = node . program . body . map ( ( n ) => sourceCode . getText ( n ) ) . join ( '' ) ;
128+ const inverseBody = node . inverse . body . map ( ( n ) => sourceCode . getText ( n ) ) . join ( '' ) ;
129+ return fixer . replaceText (
130+ node ,
131+ `{{#${ keyword } ${ conditionText } }}${ inverseBody } {{else}}${ programBody } {{/${ keyword } }}`
132+ ) ;
133+ }
134+
135+ if ( msgId === 'useIf' || msgId === 'useUnless' ) {
136+ const newKeyword = keyword === 'unless' ? 'if' : 'unless' ;
137+ const conditionText = getUnwrappedConditionText ( notExpr , false ) ;
138+ const notExprText = escapeRegExp ( sourceCode . getText ( notExpr ) ) ;
139+ const newText = fullText
140+ . replace (
141+ new RegExp ( `^\\{\\{#${ keyword } ${ notExprText } ` ) ,
142+ `{{#${ newKeyword } ${ conditionText } `
143+ )
144+ . replace ( new RegExp ( `\\{\\{/${ keyword } \\}\\}$` ) , `{{/${ newKeyword } }}` ) ;
145+ return fixer . replaceText ( node , newText ) ;
146+ }
147+
148+ return null ;
149+ } ;
150+ }
151+
152+ /**
153+ * Build a fix function for inline (mustache/subexpression) statements.
154+ */
155+ function buildInlineFix ( node , msgId ) {
156+ return ( fixer ) => {
157+ const fullText = sourceCode . getText ( node ) ;
158+ const keyword = node . path . original ;
159+ const notExpr = node . params [ 0 ] ;
160+
161+ if ( msgId === 'negatedHelper' ) {
162+ const conditionText = getUnwrappedConditionText ( notExpr , true ) ;
163+ const newText = fullText . replace ( sourceCode . getText ( notExpr ) , conditionText ) ;
164+ return fixer . replaceText ( node , newText ) ;
165+ }
166+
167+ if ( msgId === 'flipIf' ) {
168+ const conditionText = getUnwrappedConditionText ( notExpr , false ) ;
169+ const param1Text = sourceCode . getText ( node . params [ 1 ] ) ;
170+ const param2Text = sourceCode . getText ( node . params [ 2 ] ) ;
171+ const isSubExpr = node . type === 'GlimmerSubExpression' ;
172+ const open = isSubExpr ? '(' : '{{' ;
173+ const close = isSubExpr ? ')' : '}}' ;
174+ return fixer . replaceText (
175+ node ,
176+ `${ open } ${ keyword } ${ conditionText } ${ param2Text } ${ param1Text } ${ close } `
177+ ) ;
178+ }
179+
180+ if ( msgId === 'useIf' || msgId === 'useUnless' ) {
181+ const newKeyword = keyword === 'unless' ? 'if' : 'unless' ;
182+ const conditionText = getUnwrappedConditionText ( notExpr , false ) ;
183+ const isSubExpr = node . type === 'GlimmerSubExpression' ;
184+ const open = isSubExpr ? '(' : '{{' ;
185+ const close = isSubExpr ? ')' : '}}' ;
186+ const remainingParams = node . params
187+ . slice ( 1 )
188+ . map ( ( p ) => sourceCode . getText ( p ) )
189+ . join ( ' ' ) ;
190+ return fixer . replaceText (
191+ node ,
192+ `${ open } ${ newKeyword } ${ conditionText } ${ remainingParams } ${ close } `
193+ ) ;
194+ }
195+
196+ return null ;
197+ } ;
198+ }
199+
83200 // eslint-disable-next-line complexity
84201 function checkNode ( node ) {
85202 const nodeIsIf = isIf ( node ) ;
@@ -97,7 +214,11 @@ module.exports = {
97214 if ( ! simplifyHelpers || ! hasNotHelper ( node ) || ! hasNestedFixableHelper ( node ) ) {
98215 return ;
99216 }
100- context . report ( { node : node . params [ 0 ] , messageId : 'negatedHelper' } ) ;
217+ context . report ( {
218+ node : node . params [ 0 ] ,
219+ messageId : 'negatedHelper' ,
220+ fix : buildBlockFix ( node , 'negatedHelper' ) ,
221+ } ) ;
101222 return ;
102223 }
103224
@@ -129,7 +250,7 @@ module.exports = {
129250 return ;
130251 }
131252
132- // (not a b c) with multiple params — can't simply remove negation
253+ // (not a b c) with multiple params - can't simply remove negation
133254 if ( notExpr . params ?. length > 1 ) {
134255 return ;
135256 }
@@ -150,9 +271,11 @@ module.exports = {
150271 messageId = 'useUnless' ;
151272 }
152273
274+ const isBlock = node . type === 'GlimmerBlockStatement' ;
153275 context . report ( {
154276 node : notExpr ,
155277 messageId,
278+ fix : isBlock ? buildBlockFix ( node , messageId ) : buildInlineFix ( node , messageId ) ,
156279 } ) ;
157280 }
158281
0 commit comments