Skip to content

Commit 2b2b24a

Browse files
patricklxbmish
andauthored
Add new rule template-indent (#1943)
Co-authored-by: Bryan Mishkin <[email protected]>
1 parent 8543535 commit 2b2b24a

File tree

4 files changed

+601
-0
lines changed

4 files changed

+601
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ rules in templates can be disabled with eslint directives with mustache or html
240240
| [no-ember-super-in-es-classes](docs/rules/no-ember-super-in-es-classes.md) | disallow use of `this._super` in ES class methods || 🔧 | |
241241
| [no-empty-glimmer-component-classes](docs/rules/no-empty-glimmer-component-classes.md) | disallow empty backing classes for Glimmer components || | |
242242
| [no-tracked-properties-from-args](docs/rules/no-tracked-properties-from-args.md) | disallow creating @tracked properties from this.args || | |
243+
| [template-indent](docs/rules/template-indent.md) | enforce consistent indentation for gts/gjs templates | | 🔧 | |
243244

244245
### jQuery
245246

docs/rules/template-indent.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# ember/template-indent
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
## Rule Details
8+
9+
Enforce consistent indentation for fcct templates.
10+
11+
This rule extends the base [eslint indent](https://eslint.org/docs/latest/rules/indent) rule, but only applies the indents to Glimmer Nodes.
12+
13+
Otherwise, it receives the same options as the original and can run together with the base rule.
14+
15+
## Configuration
16+
17+
<!-- begin auto-generated rule options list -->
18+
19+
| Name | Type | Default |
20+
| :--------------- | :------- | :------ |
21+
| `ignoreComments` | Boolean | `false` |
22+
| `ignoredNodes` | String[] | |
23+
24+
<!-- end auto-generated rule options list -->
25+
26+
## Examples
27+
28+
Examples of **incorrect** code for this rule:
29+
30+
```gjs
31+
// my-octane-component.gjs
32+
<template>
33+
<div>
34+
35+
</div>
36+
</template>
37+
}
38+
```
39+
40+
Examples of **correct** code for this rule:
41+
42+
```gjs
43+
// my-component.gjs
44+
<template>
45+
<div>
46+
47+
</div>
48+
</template>
49+
```
50+
51+
## References
52+
53+
- [eslint indent](https://eslint.org/docs/latest/rules/indent)

lib/rules/template-indent.js

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
const { builtinRules } = require('eslint/use-at-your-own-risk');
2+
3+
const baseRule = builtinRules.get('indent');
4+
const IGNORED_ELEMENTS = new Set(['pre', 'script', 'style', 'textarea']);
5+
6+
const schema = baseRule.meta.schema.map((s) => ({ ...s }));
7+
schema[1].properties = {
8+
ignoredNodes: schema[1].properties.ignoredNodes,
9+
ignoreComments: schema[1].properties.ignoreComments,
10+
};
11+
12+
/** @type {import('eslint').Rule.RuleModule} */
13+
module.exports = {
14+
name: 'indent',
15+
meta: {
16+
type: 'layout',
17+
docs: {
18+
description: 'enforce consistent indentation for gts/gjs templates',
19+
// too opinionated to be recommended
20+
recommended: false,
21+
category: 'Ember Octane',
22+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-indent.md',
23+
},
24+
fixable: 'whitespace',
25+
hasSuggestions: baseRule.meta.hasSuggestions,
26+
schema,
27+
messages: baseRule.meta.messages,
28+
},
29+
30+
create: (context) => {
31+
const ctx = Object.create(context, {
32+
report: {
33+
writable: false,
34+
configurable: false,
35+
value: (info) => {
36+
const node = context.sourceCode.getNodeByRangeIndex(info.node.range[0]);
37+
if (!node.type.startsWith('Glimmer')) {
38+
return;
39+
}
40+
context.report(info);
41+
},
42+
},
43+
});
44+
const rules = baseRule.create(ctx);
45+
const sourceCode = context.sourceCode;
46+
47+
function JSXElement(node) {
48+
let closingElement;
49+
let openingElement;
50+
if (node.type === 'GlimmerElementNode') {
51+
const tokens = sourceCode.getTokens(node);
52+
const openEnd = tokens.find((t) => t.value === '>');
53+
const closeStart = tokens.findLast((t) => t.value === '<');
54+
if (!node.selfClosing) {
55+
closingElement = {
56+
type: 'JSXClosingElement',
57+
parent: node,
58+
range: [closeStart.range[0], node.range[1]],
59+
loc: {
60+
start: Object.assign({}, node.loc.start),
61+
end: Object.assign({}, node.loc.end),
62+
},
63+
};
64+
closingElement.loc.start = sourceCode.getLocFromIndex(closeStart.range[0]);
65+
closingElement.name = { ...closingElement, type: 'JSXIdentifier' };
66+
closingElement.name.range = [
67+
closingElement.name.range[0] + 1,
68+
closingElement.name.range[1] - 1,
69+
];
70+
}
71+
72+
openingElement = {
73+
type: 'JSXOpeningElement',
74+
selfClosing: node.selfClosing,
75+
attributes: node.attributes,
76+
parent: node,
77+
range: [node.range[0], openEnd.range[1]],
78+
loc: {
79+
start: Object.assign({}, node.loc.start),
80+
end: Object.assign({}, node.loc.end),
81+
},
82+
};
83+
openingElement.loc.end = sourceCode.getLocFromIndex(openEnd.range[1]);
84+
openingElement.name = { ...openingElement, type: 'JSXIdentifier' };
85+
openingElement.name.range = [
86+
openingElement.name.range[0] + 1,
87+
openingElement.name.range[1] - 1,
88+
];
89+
}
90+
if (node.type === 'GlimmerBlockStatement') {
91+
const tokens = sourceCode.getTokens(node);
92+
let openEndIdx = tokens.findIndex((t) => t.value === '}');
93+
while (tokens[openEndIdx + 1].value === '}') {
94+
openEndIdx += 1;
95+
}
96+
const openEnd = tokens[openEndIdx];
97+
let closeStartIdx = tokens.findLastIndex((t) => t.value === '{');
98+
while (tokens[closeStartIdx - 1].value === '{') {
99+
closeStartIdx -= 1;
100+
}
101+
const closeStart = tokens[closeStartIdx];
102+
closingElement = {
103+
type: 'JSXClosingElement',
104+
parent: node,
105+
range: [closeStart.range[0], node.range[1]],
106+
loc: {
107+
start: Object.assign({}, node.loc.start),
108+
end: Object.assign({}, node.loc.end),
109+
},
110+
};
111+
closingElement.loc.start = sourceCode.getLocFromIndex(closeStart.range[0]);
112+
113+
openingElement = {
114+
type: 'JSXOpeningElement',
115+
attributes: node.params,
116+
parent: node,
117+
range: [node.range[0], openEnd.range[1]],
118+
loc: {
119+
start: Object.assign({}, node.loc.start),
120+
end: Object.assign({}, node.loc.end),
121+
},
122+
};
123+
openingElement.loc.end = sourceCode.getLocFromIndex(openEnd.range[1]);
124+
}
125+
return {
126+
type: 'JSXElement',
127+
openingElement,
128+
closingElement,
129+
children: node.children || node.body,
130+
parent: node.parent,
131+
range: node.range,
132+
loc: node.loc,
133+
};
134+
}
135+
136+
const ignoredStack = new Set();
137+
138+
return Object.assign({}, rules, {
139+
// overwrite the base rule here so we can use our KNOWN_NODES list instead
140+
'*:exit'(node) {
141+
// For nodes we care about, skip the default handling, because it just marks the node as ignored...
142+
if (
143+
!node.type.startsWith('Glimmer') ||
144+
(ignoredStack.size > 0 && !ignoredStack.has(node))
145+
) {
146+
rules['*:exit'](node);
147+
}
148+
if (ignoredStack.has(node)) {
149+
ignoredStack.delete(node);
150+
}
151+
},
152+
'GlimmerTemplate:exit'(node) {
153+
if (!node.parent) {
154+
rules['Program:exit'](node);
155+
}
156+
},
157+
GlimmerElementNode(node) {
158+
if (ignoredStack.size > 0) {
159+
return;
160+
}
161+
if (IGNORED_ELEMENTS.has(node.tag)) {
162+
ignoredStack.add(node);
163+
}
164+
const jsx = JSXElement(node);
165+
rules['JSXElement'](jsx);
166+
rules['JSXOpeningElement'](jsx.openingElement);
167+
if (jsx.closingElement) {
168+
rules['JSXClosingElement'](jsx.closingElement);
169+
}
170+
},
171+
GlimmerAttrNode(node) {
172+
if (ignoredStack.size > 0 || !node.value) {
173+
return;
174+
}
175+
rules['JSXAttribute[value]']({
176+
...node,
177+
type: 'JSXAttribute',
178+
name: {
179+
type: 'JSXIdentifier',
180+
name: node.name,
181+
range: [node.range[0], node.range[0] + node.name.length - 1],
182+
},
183+
});
184+
},
185+
GlimmerTemplate(node) {
186+
if (!node.parent) {
187+
return;
188+
}
189+
const jsx = JSXElement({ ...node, tag: 'template', type: 'GlimmerElementNode' });
190+
rules['JSXElement'](jsx);
191+
},
192+
GlimmerBlockStatement(node) {
193+
const body = [...node.program.body, ...(node.inverse?.body || [])];
194+
rules['JSXElement'](JSXElement({ ...node, body }));
195+
},
196+
});
197+
},
198+
};

0 commit comments

Comments
 (0)