Skip to content

Commit 10548c5

Browse files
committed
Port autofix for template-simple-unless
ember-template-lint's simple-unless fixes two of the four violation types; the autofix was lost when the rule was extracted. Restores it using text-based replacement (this rule does not have access to ember-template-recast): - withHelper: `{{unless cond}}` -> `{{if (not cond)}}`, with two De Morgan special-cases: - `{{unless (not x)}}` -> `{{if x}}` (unwrap the not) - `{{unless (not x y)}}` -> `{{if (or x y)}}` (multi-arg `not` is true only when all args are falsy) - followingElseBlock: `{{#unless cond}}A{{else}}B{{/unless}}` -> `{{#if cond}}B{{else}}A{{/if}}` (body/inverse swap) The followingElseIfBlock (`{{#unless}}...{{else if}}`) and asElseUnlessBlock (`{{#if}}...{{else unless}}`) cases remain unfixable, also matching upstream. The body-swap fixer slices sourceCode.text between node.program.range and node.inverse.range and reassembles with a hardcoded `{{else}}` separator, which normalizes whitespace around `{{else}}`.
1 parent c97528a commit 10548c5

4 files changed

Lines changed: 166 additions & 39 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ rules in templates can be disabled with eslint directives with mustache or html
283283
| [template-require-valid-named-block-naming-format](docs/rules/template-require-valid-named-block-naming-format.md) | require valid named block naming format | | 🔧 | |
284284
| [template-self-closing-void-elements](docs/rules/template-self-closing-void-elements.md) | require self-closing on void elements | | 🔧 | |
285285
| [template-simple-modifiers](docs/rules/template-simple-modifiers.md) | require simple modifier syntax | | | |
286-
| [template-simple-unless](docs/rules/template-simple-unless.md) | require simple conditions in unless blocks | | | |
286+
| [template-simple-unless](docs/rules/template-simple-unless.md) | require simple conditions in unless blocks | | 🔧 | |
287287
| [template-sort-invocations](docs/rules/template-sort-invocations.md) | require sorted attributes and modifiers | | | |
288288
| [template-splat-attributes-only](docs/rules/template-splat-attributes-only.md) | disallow ...spread other than ...attributes | | | |
289289
| [template-style-concatenation](docs/rules/template-style-concatenation.md) | disallow string concatenation in inline styles | | | |

docs/rules/template-simple-unless.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# ember/template-simple-unless
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
Require simple conditions in `{{#unless}}` blocks. Complex expressions should use `{{#if}}` with negation instead.

lib/rules/template-simple-unless.js

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module.exports = {
1616
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-simple-unless.md',
1717
templateMode: 'both',
1818
},
19+
fixable: 'code',
1920
schema: [
2021
{
2122
type: 'object',
@@ -58,6 +59,64 @@ module.exports = {
5859
return false;
5960
}
6061

62+
/**
63+
* Build a fixer for the _withHelper case.
64+
* Converts `unless` to `if` and wraps the first param with `(not ...)`.
65+
* Special-cases when the first param is already `(not ...)`:
66+
* - single arg: `unless (not x)` → `if x`
67+
* - multiple args: `unless (not x y)` → `if (or x y)`
68+
*/
69+
function buildWithHelperFix(node) {
70+
return function fix(fixer) {
71+
const nodeText = sourceCode.getText(node);
72+
const firstParam = node.params[0];
73+
const firstParamText = sourceCode.getText(firstParam);
74+
75+
// Replace 'unless' with 'if' in the keyword
76+
let newText = nodeText.replace(/^({{#?)unless/, '$1if');
77+
78+
if (firstParam.type === 'GlimmerSubExpression' && firstParam.path?.original === 'not') {
79+
// Special case: first param is (not ...)
80+
if (firstParam.params.length > 1) {
81+
// (not x y) → (or x y)
82+
const innerParamsText = firstParam.params.map((p) => sourceCode.getText(p)).join(' ');
83+
newText = newText.replace(firstParamText, `(or ${innerParamsText})`);
84+
} else {
85+
// (not x) → x — unwrap the not
86+
const innerText = sourceCode.getText(firstParam.params[0]);
87+
newText = newText.replace(firstParamText, innerText);
88+
}
89+
} else {
90+
// Wrap with (not ...)
91+
newText = newText.replace(firstParamText, `(not ${firstParamText})`);
92+
}
93+
94+
// Also fix the closing tag for block statements
95+
if (node.type === 'GlimmerBlockStatement') {
96+
newText = newText.replace(/{{\/unless}}$/, '{{/if}}');
97+
}
98+
99+
return fixer.replaceText(node, newText);
100+
};
101+
}
102+
103+
/**
104+
* Build a fixer for the _followingElseBlock case (simple else, no else-if).
105+
* Swaps body/inverse and changes unless→if.
106+
*/
107+
function buildFollowingElseBlockFix(node) {
108+
return function fix(fixer) {
109+
const programStart = node.program.range[0];
110+
const bodyText = sourceCode.text.slice(programStart, node.program.range[1]);
111+
const inverseText = sourceCode.text.slice(node.inverse.range[0], node.inverse.range[1]);
112+
const openingTag = sourceCode.text
113+
.slice(node.range[0], programStart)
114+
.replace(/^({{#)unless/, '$1if');
115+
// Result shape: {{#if cond}}inverse{{else}}body{{/if}}
116+
return fixer.replaceText(node, `${openingTag}${inverseText}{{else}}${bodyText}{{/if}}`);
117+
};
118+
}
119+
61120
function checkWithHelper(node) {
62121
let helperCount = 0;
63122
let nextParams = node.params || [];
@@ -78,6 +137,7 @@ module.exports = {
78137
data: {
79138
message: `Using {{unless}} in combination with other helpers should be avoided. MaxHelpers: ${maxHelpers}`,
80139
},
140+
fix: buildWithHelperFix(node),
81141
});
82142
return;
83143
}
@@ -89,6 +149,7 @@ module.exports = {
89149
data: {
90150
message: `Using {{unless}} in combination with other helpers should be avoided. Allowed helper${allowlist.length > 1 ? 's' : ''}: ${allowlist}`,
91151
},
152+
fix: buildWithHelperFix(node),
92153
});
93154
return;
94155
}
@@ -100,6 +161,7 @@ module.exports = {
100161
data: {
101162
message: `Using {{unless}} in combination with other helpers should be avoided. Restricted helper${denylist.length > 1 ? 's' : ''}: ${denylist}`,
102163
},
164+
fix: buildWithHelperFix(node),
103165
});
104166
return;
105167
}
@@ -128,19 +190,21 @@ module.exports = {
128190
if (isUnless(node)) {
129191
// Check for {{#unless}}...{{else if}}
130192
if (nodeInverse.body[0] && isIf(nodeInverse.body[0])) {
193+
// Not fixable (ETL: _followingElseIfBlock, isFixable=false)
131194
context.report({
132195
node: node.program || node,
133196
messageId: 'followingElseBlock',
134197
});
135198
} else {
136-
// {{#unless}}...{{else}}
199+
// {{#unless}}...{{else}} — fixable
137200
context.report({
138201
node: node.program || node,
139202
messageId: 'followingElseBlock',
203+
fix: buildFollowingElseBlockFix(node),
140204
});
141205
}
142206
} else if (isElseUnlessBlock(nodeInverse.body[0])) {
143-
// {{#if}}...{{else unless}}
207+
// {{#if}}...{{else unless}} — not fixable (ETL: isFixable=false)
144208
context.report({
145209
node: nodeInverse.body[0],
146210
messageId: 'asElseUnlessBlock',

0 commit comments

Comments
 (0)