Skip to content

Commit 1f89759

Browse files
committed
Extract rule: template-simple-unless
1 parent a1378e5 commit 1f89759

4 files changed

Lines changed: 669 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ rules in templates can be disabled with eslint directives with mustache or html
244244
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
245245
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
246246
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
247+
| [template-simple-unless](docs/rules/template-simple-unless.md) | require simple conditions in unless blocks | | | |
247248

248249
### Components
249250

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# ember/template-simple-unless
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Require simple conditions in `{{#unless}}` blocks. Complex expressions should use `{{#if}}` with negation instead.
6+
7+
## Rule Details
8+
9+
This rule enforces using simple property paths in `{{#unless}}` blocks rather than complex helper expressions.
10+
11+
## Examples
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```gjs
16+
<template>
17+
{{#unless (or (eq a 1) (gt b 2))}}
18+
Complex condition
19+
{{/unless}}
20+
</template>
21+
```
22+
23+
```gjs
24+
<template>
25+
{{#unless (and isAdmin (not isBanned))}}
26+
Not allowed
27+
{{/unless}}
28+
</template>
29+
```
30+
31+
Examples of **correct** code for this rule:
32+
33+
```gjs
34+
<template>
35+
{{#unless isHidden}}
36+
Visible
37+
{{/unless}}
38+
</template>
39+
```
40+
41+
```gjs
42+
<template>
43+
{{#unless (eq value 1)}}
44+
Not one
45+
{{/unless}}
46+
</template>
47+
```
48+
49+
```gjs
50+
<template>
51+
{{#if (not (or a b))}}
52+
Neither
53+
{{/if}}
54+
</template>
55+
```
56+
57+
## Options
58+
59+
| Name | Type | Default | Description |
60+
| ------------ | ---------- | ------- | --------------------------------------------------------------------------- |
61+
| `allowlist` | `string[]` | `[]` | Helper names allowed inside `{{unless}}`. |
62+
| `denylist` | `string[]` | `[]` | Helper names explicitly denied inside `{{unless}}`. |
63+
| `maxHelpers` | `integer` | `1` | Maximum number of helpers allowed inside `{{unless}}` (`-1` for unlimited). |
64+
65+
## Related Rules
66+
67+
- [no-negated-condition](template-no-negated-condition.md)
68+
69+
## References
70+
71+
- [Wikipedia/boolean algebra](https://en.wikipedia.org/wiki/Boolean_algebra)
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
function isUnless(node) {
2+
return node.path?.type === 'GlimmerPathExpression' && node.path.original === 'unless';
3+
}
4+
5+
function isIf(node) {
6+
return node.path?.type === 'GlimmerPathExpression' && node.path.original === 'if';
7+
}
8+
9+
/** @type {import('eslint').Rule.RuleModule} */
10+
module.exports = {
11+
meta: {
12+
type: 'suggestion',
13+
docs: {
14+
description: 'require simple conditions in unless blocks',
15+
category: 'Best Practices',
16+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-simple-unless.md',
17+
templateMode: 'both',
18+
},
19+
schema: [
20+
{
21+
type: 'object',
22+
properties: {
23+
allowlist: { type: 'array', items: { type: 'string' } },
24+
denylist: { type: 'array', items: { type: 'string' } },
25+
maxHelpers: { type: 'integer' },
26+
},
27+
additionalProperties: false,
28+
},
29+
],
30+
messages: {
31+
followingElseBlock: 'Using an `else` block with `unless` should be avoided.',
32+
asElseUnlessBlock: 'Using an `else unless` block should be avoided.',
33+
withHelper: '{{message}}',
34+
},
35+
originallyFrom: {
36+
name: 'ember-template-lint',
37+
rule: 'lib/rules/simple-unless.js',
38+
docs: 'docs/rule/simple-unless.md',
39+
tests: 'test/unit/rules/simple-unless-test.js',
40+
},
41+
},
42+
43+
create(context) {
44+
const options = context.options[0] || {};
45+
const allowlist = options.allowlist || [];
46+
const denylist = options.denylist || [];
47+
const maxHelpers = options.maxHelpers === undefined ? 1 : options.maxHelpers;
48+
const sourceCode = context.getSourceCode();
49+
50+
function isElseUnlessBlock(node) {
51+
if (!node) {
52+
return false;
53+
}
54+
if (node.path?.type === 'GlimmerPathExpression' && node.path.original === 'unless') {
55+
const text = sourceCode.getText(node);
56+
return text.startsWith('{{else ');
57+
}
58+
return false;
59+
}
60+
61+
function checkWithHelper(node) {
62+
let helperCount = 0;
63+
let nextParams = node.params || [];
64+
65+
do {
66+
const currentParams = nextParams;
67+
nextParams = [];
68+
69+
for (const param of currentParams) {
70+
if (param.type === 'GlimmerSubExpression') {
71+
helperCount++;
72+
const helperName = param.path?.original || '';
73+
74+
if (maxHelpers > -1 && helperCount > maxHelpers) {
75+
context.report({
76+
node: param,
77+
messageId: 'withHelper',
78+
data: {
79+
message: `Using {{unless}} in combination with other helpers should be avoided. MaxHelpers: ${maxHelpers}`,
80+
},
81+
});
82+
return;
83+
}
84+
85+
if (allowlist.length > 0 && !allowlist.includes(helperName)) {
86+
context.report({
87+
node: param,
88+
messageId: 'withHelper',
89+
data: {
90+
message: `Using {{unless}} in combination with other helpers should be avoided. Allowed helper${allowlist.length > 1 ? 's' : ''}: ${allowlist}`,
91+
},
92+
});
93+
return;
94+
}
95+
96+
if (denylist.length > 0 && denylist.includes(helperName)) {
97+
context.report({
98+
node: param,
99+
messageId: 'withHelper',
100+
data: {
101+
message: `Using {{unless}} in combination with other helpers should be avoided. Restricted helper${denylist.length > 1 ? 's' : ''}: ${denylist}`,
102+
},
103+
});
104+
return;
105+
}
106+
107+
if (param.params) {
108+
nextParams.push(...param.params);
109+
}
110+
}
111+
}
112+
} while (nextParams.some((p) => p.type === 'GlimmerSubExpression'));
113+
}
114+
115+
return {
116+
GlimmerMustacheStatement(node) {
117+
if (node.path?.type === 'GlimmerPathExpression' && node.path.original === 'unless') {
118+
if (node.params?.[0]?.path) {
119+
checkWithHelper(node);
120+
}
121+
}
122+
},
123+
124+
GlimmerBlockStatement(node) {
125+
const nodeInverse = node.inverse;
126+
127+
if (nodeInverse && nodeInverse.body?.length > 0) {
128+
if (isUnless(node)) {
129+
// Check for {{#unless}}...{{else if}}
130+
if (nodeInverse.body[0] && isIf(nodeInverse.body[0])) {
131+
context.report({
132+
node: node.program || node,
133+
messageId: 'followingElseBlock',
134+
});
135+
} else {
136+
// {{#unless}}...{{else}}
137+
context.report({
138+
node: node.program || node,
139+
messageId: 'followingElseBlock',
140+
});
141+
}
142+
} else if (isElseUnlessBlock(nodeInverse.body[0])) {
143+
// {{#if}}...{{else unless}}
144+
context.report({
145+
node: nodeInverse.body[0],
146+
messageId: 'asElseUnlessBlock',
147+
});
148+
}
149+
} else if (isUnless(node) && node.params?.[0]?.path) {
150+
checkWithHelper(node);
151+
}
152+
},
153+
};
154+
},
155+
};

0 commit comments

Comments
 (0)