@@ -42,6 +42,10 @@ function hasNestedFixableHelper(node) {
4242 ) ;
4343}
4444
45+ function escapeRegExp ( string ) {
46+ return string . 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,125 @@ 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+ if ( inner . params . length > 1 ) {
100+ // (not (not c1 c2)) -> (or c1 c2)
101+ const paramsText = inner . params . map ( ( p ) => sourceCode . getText ( p ) ) . join ( ' ' ) ;
102+ return `(or ${ paramsText } )` ;
103+ }
104+ // (not (not x)) -> just x
105+ return sourceCode . getText ( inner . params [ 0 ] ) ;
106+ }
107+ // (not (eq a b)) -> (not-eq a b)
108+ const innerText = sourceCode . getText ( inner ) ;
109+ return innerText . replace ( `(${ helperName } ` , `(${ inverted } ` ) ;
110+ }
111+ }
112+ return sourceCode . getText ( inner ) ;
113+ }
114+
115+ /**
116+ * Build a fix function for block statements.
117+ */
118+ function buildBlockFix ( node , messageId ) {
119+ return function fix ( fixer ) {
120+ const fullText = sourceCode . getText ( node ) ;
121+ const keyword = node . path . original ;
122+ const notExpr = node . params [ 0 ] ;
123+
124+ if ( messageId === 'negatedHelper' ) {
125+ const conditionText = getUnwrappedConditionText ( notExpr , true ) ;
126+ const newText = fullText . replace ( sourceCode . getText ( notExpr ) , conditionText ) ;
127+ return fixer . replaceText ( node , newText ) ;
128+ }
129+
130+ if ( messageId === 'flipIf' ) {
131+ // {{#if (not x)}}A{{else}}B{{/if}} -> {{#if x}}B{{else}}A{{/if}}
132+ const conditionText = getUnwrappedConditionText ( notExpr , false ) ;
133+ const programBody = node . program . body . map ( ( n ) => sourceCode . getText ( n ) ) . join ( '' ) ;
134+ const inverseBody = node . inverse . body . map ( ( n ) => sourceCode . getText ( n ) ) . join ( '' ) ;
135+
136+ return fixer . replaceText (
137+ node ,
138+ `{{#${ keyword } ${ conditionText } }}${ inverseBody } {{else}}${ programBody } {{/${ keyword } }}`
139+ ) ;
140+ }
141+
142+ if ( messageId === 'useIf' || messageId === 'useUnless' ) {
143+ const newKeyword = keyword === 'unless' ? 'if' : 'unless' ;
144+ const conditionText = getUnwrappedConditionText ( notExpr , false ) ;
145+ const notExprText = escapeRegExp ( sourceCode . getText ( notExpr ) ) ;
146+ const newText = fullText
147+ . replace (
148+ new RegExp ( `^\\{\\{#${ keyword } ${ notExprText } ` ) ,
149+ `{{#${ newKeyword } ${ conditionText } `
150+ )
151+ . replace ( new RegExp ( `\\{\\{/${ keyword } \\}\\}$` ) , `{{/${ newKeyword } }}` ) ;
152+ return fixer . replaceText ( node , newText ) ;
153+ }
154+
155+ return null ;
156+ } ;
157+ }
158+
159+ /**
160+ * Build a fix function for inline (mustache/subexpression) statements.
161+ */
162+ function buildInlineFix ( node , messageId ) {
163+ return function fix ( fixer ) {
164+ const fullText = sourceCode . getText ( node ) ;
165+ const keyword = node . path . original ;
166+ const notExpr = node . params [ 0 ] ;
167+
168+ if ( messageId === 'negatedHelper' ) {
169+ const conditionText = getUnwrappedConditionText ( notExpr , true ) ;
170+ const newText = fullText . replace ( sourceCode . getText ( notExpr ) , conditionText ) ;
171+ return fixer . replaceText ( node , newText ) ;
172+ }
173+
174+ if ( messageId === 'flipIf' ) {
175+ const conditionText = getUnwrappedConditionText ( notExpr , false ) ;
176+ const param1Text = sourceCode . getText ( node . params [ 1 ] ) ;
177+ const param2Text = sourceCode . getText ( node . params [ 2 ] ) ;
178+ const isSubExpr = node . type === 'GlimmerSubExpression' ;
179+ const open = isSubExpr ? '(' : '{{' ;
180+ const close = isSubExpr ? ')' : '}}' ;
181+ return fixer . replaceText (
182+ node ,
183+ `${ open } ${ keyword } ${ conditionText } ${ param2Text } ${ param1Text } ${ close } `
184+ ) ;
185+ }
186+
187+ if ( messageId === 'useIf' || messageId === 'useUnless' ) {
188+ const newKeyword = keyword === 'unless' ? 'if' : 'unless' ;
189+ const conditionText = getUnwrappedConditionText ( notExpr , false ) ;
190+ const isSubExpr = node . type === 'GlimmerSubExpression' ;
191+ const open = isSubExpr ? '(' : '{{' ;
192+ const close = isSubExpr ? ')' : '}}' ;
193+ const remainingParams = node . params
194+ . slice ( 1 )
195+ . map ( ( p ) => sourceCode . getText ( p ) )
196+ . join ( ' ' ) ;
197+ return fixer . replaceText (
198+ node ,
199+ `${ open } ${ newKeyword } ${ conditionText } ${ remainingParams } ${ close } `
200+ ) ;
201+ }
202+
203+ return null ;
204+ } ;
205+ }
206+
83207 // eslint-disable-next-line complexity
84208 function checkNode ( node ) {
85209 const nodeIsIf = isIf ( node ) ;
@@ -97,7 +221,11 @@ module.exports = {
97221 if ( ! simplifyHelpers || ! hasNotHelper ( node ) || ! hasNestedFixableHelper ( node ) ) {
98222 return ;
99223 }
100- context . report ( { node : node . params [ 0 ] , messageId : 'negatedHelper' } ) ;
224+ context . report ( {
225+ node : node . params [ 0 ] ,
226+ messageId : 'negatedHelper' ,
227+ fix : buildBlockFix ( node , 'negatedHelper' ) ,
228+ } ) ;
101229 return ;
102230 }
103231
@@ -150,9 +278,11 @@ module.exports = {
150278 messageId = 'useUnless' ;
151279 }
152280
281+ const isBlock = node . type === 'GlimmerBlockStatement' ;
153282 context . report ( {
154283 node : notExpr ,
155284 messageId,
285+ fix : isBlock ? buildBlockFix ( node , messageId ) : buildInlineFix ( node , messageId ) ,
156286 } ) ;
157287 }
158288
0 commit comments