Skip to content

Commit 7fc34c6

Browse files
Merge pull request #2533 from johanrd/autofix/no-negated-condition
Restore autofix: `no-negated-condition`
2 parents 4003efd + 251dee7 commit 7fc34c6

4 files changed

Lines changed: 197 additions & 60 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ rules in templates can be disabled with eslint directives with mustache or html
237237
| [template-no-multiple-empty-lines](docs/rules/template-no-multiple-empty-lines.md) | disallow multiple consecutive empty lines in templates | | | |
238238
| [template-no-mut-helper](docs/rules/template-no-mut-helper.md) | disallow usage of (mut) helper | | | |
239239
| [template-no-negated-comparison](docs/rules/template-no-negated-comparison.md) | disallow negated comparisons in templates | | | |
240-
| [template-no-negated-condition](docs/rules/template-no-negated-condition.md) | disallow negated conditions in if/unless | | | |
240+
| [template-no-negated-condition](docs/rules/template-no-negated-condition.md) | disallow negated conditions in if/unless | | 🔧 | |
241241
| [template-no-nested-splattributes](docs/rules/template-no-nested-splattributes.md) | disallow nested ...attributes usage | | | |
242242
| [template-no-obscure-array-access](docs/rules/template-no-obscure-array-access.md) | disallow obscure array access patterns like `[email protected]` | | 🔧 | |
243243
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |

docs/rules/template-no-negated-condition.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# ember/template-no-negated-condition
22

3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
35
<!-- end auto-generated rule header -->
46

57
Disallow negated conditions in `{{#if}}` blocks. Use `{{#unless}}` instead or rewrite the condition.

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

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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} */
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,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

Comments
 (0)