Skip to content

Commit 3f1b905

Browse files
committed
Add autofix for template-no-negated-condition
1 parent 131cd12 commit 3f1b905

2 files changed

Lines changed: 194 additions & 66 deletions

File tree

lib/rules/template-no-negated-condition.js

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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} */
4650
module.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

Comments
 (0)