Skip to content

Commit 055f60b

Browse files
Merge pull request #2483 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-negated-condition
Extract rule: template-no-negated-condition
2 parents 1270a69 + 90246c7 commit 055f60b

4 files changed

Lines changed: 701 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ rules in templates can be disabled with eslint directives with mustache or html
210210
| [template-no-input-placeholder](docs/rules/template-no-input-placeholder.md) | disallow placeholder attribute on input elements | | | |
211211
| [template-no-input-tagname](docs/rules/template-no-input-tagname.md) | disallow tagName attribute on {{input}} helper | | | |
212212
| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
213+
| [template-no-negated-condition](docs/rules/template-no-negated-condition.md) | disallow negated conditions in if/unless | | | |
213214
| [template-no-nested-splattributes](docs/rules/template-no-nested-splattributes.md) | disallow nested ...attributes usage | | | |
214215
| [template-no-obscure-array-access](docs/rules/template-no-obscure-array-access.md) | disallow obscure array access patterns like `objectPath.@each.property` | | | |
215216
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# ember/template-no-negated-condition
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallow negated conditions in `{{#if}}` blocks. Use `{{#unless}}` instead or rewrite the condition.
6+
7+
## Rule Details
8+
9+
This rule discourages the use of `{{#if (not condition)}}` in favor of `{{#unless condition}}` for better readability.
10+
11+
## Examples
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```gjs
16+
<template>
17+
{{#if (not isValid)}}
18+
Invalid
19+
{{/if}}
20+
</template>
21+
```
22+
23+
Examples of **correct** code for this rule:
24+
25+
```gjs
26+
<template>
27+
{{#unless isValid}}
28+
Invalid
29+
{{/unless}}
30+
</template>
31+
32+
<template>
33+
{{#if isValid}}
34+
Valid
35+
{{/if}}
36+
</template>
37+
```
38+
39+
## Options
40+
41+
| Name | Type | Default | Description |
42+
| ----------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |
43+
| `simplifyHelpers` | `boolean` | `true` | When `true`, also reports negated comparison helpers (e.g. `(not (eq ...))`) and suggests using `(not-eq ...)` instead. |
44+
45+
## Related Rules
46+
47+
- [simple-unless](template-simple-unless.md)
48+
49+
## References
50+
51+
- [no-negated-condition](https://eslint.org/docs/rules/no-negated-condition) from eslint
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
const ERROR_MESSAGE_FLIP_IF =
2+
'Change `{{if (not condition)}} ... {{else}} ... {{/if}}` to `{{if condition}} ... {{else}} ... {{/if}}`.';
3+
const ERROR_MESSAGE_USE_IF = 'Change `unless (not condition)` to `if condition`.';
4+
const ERROR_MESSAGE_USE_UNLESS = 'Change `if (not condition)` to `unless condition`.';
5+
const ERROR_MESSAGE_NEGATED_HELPER = 'Simplify unnecessary negation of helper.';
6+
7+
const INVERTIBLE_HELPERS = new Set(['not', 'eq', 'not-eq', 'gt', 'gte', 'lt', 'lte']);
8+
9+
const HELPER_INVERSIONS = {
10+
not: null, // special case
11+
eq: 'not-eq',
12+
'not-eq': 'eq',
13+
gt: 'lte',
14+
gte: 'lt',
15+
lt: 'gte',
16+
lte: 'gt',
17+
};
18+
19+
function isIf(node) {
20+
return node.path?.type === 'GlimmerPathExpression' && node.path.original === 'if';
21+
}
22+
23+
function isUnless(node) {
24+
return node.path?.type === 'GlimmerPathExpression' && node.path.original === 'unless';
25+
}
26+
27+
function hasNotHelper(node) {
28+
return (
29+
node.params?.length > 0 &&
30+
node.params[0].type === 'GlimmerSubExpression' &&
31+
node.params[0].path?.type === 'GlimmerPathExpression' &&
32+
node.params[0].path.original === 'not'
33+
);
34+
}
35+
36+
function hasNestedFixableHelper(node) {
37+
const inner = node.params[0]?.params?.[0];
38+
return (
39+
inner &&
40+
inner.path?.type === 'GlimmerPathExpression' &&
41+
INVERTIBLE_HELPERS.has(inner.path.original)
42+
);
43+
}
44+
45+
/** @type {import('eslint').Rule.RuleModule} */
46+
module.exports = {
47+
meta: {
48+
type: 'suggestion',
49+
docs: {
50+
description: 'disallow negated conditions in if/unless',
51+
category: 'Best Practices',
52+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-negated-condition.md',
53+
templateMode: 'both',
54+
},
55+
schema: [
56+
{
57+
type: 'object',
58+
properties: {
59+
simplifyHelpers: { type: 'boolean' },
60+
},
61+
additionalProperties: false,
62+
},
63+
],
64+
messages: {
65+
flipIf: ERROR_MESSAGE_FLIP_IF,
66+
useIf: ERROR_MESSAGE_USE_IF,
67+
useUnless: ERROR_MESSAGE_USE_UNLESS,
68+
negatedHelper: ERROR_MESSAGE_NEGATED_HELPER,
69+
},
70+
originallyFrom: {
71+
name: 'ember-template-lint',
72+
rule: 'lib/rules/no-negated-condition.js',
73+
docs: 'docs/rule/no-negated-condition.md',
74+
tests: 'test/unit/rules/no-negated-condition-test.js',
75+
},
76+
},
77+
78+
create(context) {
79+
const options = context.options[0] || {};
80+
const simplifyHelpers = options.simplifyHelpers === undefined ? true : options.simplifyHelpers;
81+
const sourceCode = context.getSourceCode();
82+
83+
// eslint-disable-next-line complexity
84+
function checkNode(node) {
85+
const nodeIsIf = isIf(node);
86+
const nodeIsUnless = isUnless(node);
87+
88+
if (!nodeIsIf && !nodeIsUnless) {
89+
return;
90+
}
91+
92+
// Skip `{{else if ...}}` / `{{else unless ...}}` chains for the outer check,
93+
// unless they have a fixable negated helper inside
94+
if (node.type === 'GlimmerBlockStatement') {
95+
const text = sourceCode.getText(node);
96+
if (text.startsWith('{{else ')) {
97+
if (!simplifyHelpers || !hasNotHelper(node) || !hasNestedFixableHelper(node)) {
98+
return;
99+
}
100+
context.report({ node: node.params[0], messageId: 'negatedHelper' });
101+
return;
102+
}
103+
104+
// Ignore `if ... else if ...` (chained) to avoid forcing negation
105+
if (
106+
node.inverse?.body?.length > 0 &&
107+
node.inverse.body[0].type === 'GlimmerBlockStatement' &&
108+
nodeIsIf &&
109+
isIf(node.inverse.body[0])
110+
) {
111+
return;
112+
}
113+
}
114+
115+
if (!hasNotHelper(node)) {
116+
return;
117+
}
118+
119+
const notExpr = node.params[0];
120+
const hasFixableHelper = hasNestedFixableHelper(node);
121+
122+
// If it's `if (not (someHelper ...))` and we can't simplify the helper,
123+
// don't suggest converting to `unless` (simple-unless rule would reject it)
124+
if (
125+
nodeIsIf &&
126+
notExpr.params?.[0]?.type === 'GlimmerSubExpression' &&
127+
(!simplifyHelpers || !hasFixableHelper)
128+
) {
129+
return;
130+
}
131+
132+
// (not a b c) with multiple params — can't simply remove negation
133+
if (notExpr.params?.length > 1) {
134+
return;
135+
}
136+
137+
// Determine message
138+
const isIfElseBlock = node.type === 'GlimmerBlockStatement' && node.inverse?.body?.length > 0;
139+
const isIfElseInline = node.type !== 'GlimmerBlockStatement' && node.params?.length === 3;
140+
const shouldFlip = isIfElseBlock || isIfElseInline;
141+
142+
let messageId;
143+
if (hasFixableHelper && simplifyHelpers) {
144+
messageId = 'negatedHelper';
145+
} else if (shouldFlip && nodeIsIf) {
146+
messageId = 'flipIf';
147+
} else if (nodeIsUnless) {
148+
messageId = 'useIf';
149+
} else {
150+
messageId = 'useUnless';
151+
}
152+
153+
context.report({
154+
node: notExpr,
155+
messageId,
156+
});
157+
}
158+
159+
return {
160+
GlimmerBlockStatement: checkNode,
161+
GlimmerMustacheStatement: checkNode,
162+
GlimmerSubExpression: checkNode,
163+
};
164+
},
165+
};

0 commit comments

Comments
 (0)