Skip to content

Commit

Permalink
implement indent
Browse files Browse the repository at this point in the history
  • Loading branch information
patricklx committed Nov 2, 2023
1 parent 8543535 commit 0170dc8
Show file tree
Hide file tree
Showing 4 changed files with 614 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [no-ember-super-in-es-classes](docs/rules/no-ember-super-in-es-classes.md) | disallow use of `this._super` in ES class methods || 🔧 | |
| [no-empty-glimmer-component-classes](docs/rules/no-empty-glimmer-component-classes.md) | disallow empty backing classes for Glimmer components || | |
| [no-tracked-properties-from-args](docs/rules/no-tracked-properties-from-args.md) | disallow creating @tracked properties from this.args || | |
| [template-indent](docs/rules/template-indent.md) | enforce consistent indentation | | 🔧 | |

### jQuery

Expand Down
66 changes: 66 additions & 0 deletions docs/rules/template-indent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# ember/template-indent

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

This rule extends the base [eslint indent](https://eslint.org/docs/latest/rules/indent) rule, but only applies the indents to Glimmer Nodes.

Otherwise, it receives the same options as the original and can run together with the base rule.

## Configuration

<!-- begin auto-generated rule options list -->

| Name | Type | Default |
| :--------------- | :------- | :------ |
| `ignoreComments` | Boolean | `false` |
| `ignoredNodes` | String[] | |

<!-- end auto-generated rule options list -->

## Rule Details

Enforce consistent indentation for fcct templates

```js
const rules = {
'ember/template-indent': [
'error',
2, // or 'tab'
{
ignoreComments: false,
ignoredNodes: []
}
]
};
```

## Examples

Examples of **incorrect** code for this rule:

```gjs
// my-octane-component.gjs
<template>
<div>
</div>
</template>
}
```

Examples of **correct** code for this rule:

```gjs
// my-component.gjs
<template>
<div>
</div>
</template>
```

## References

- [eslint indent](https://eslint.org/docs/latest/rules/indent)
198 changes: 198 additions & 0 deletions lib/rules/template-indent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
const { builtinRules } = require('eslint/use-at-your-own-risk');

const baseRule = builtinRules.get('indent');
const IGNORED_ELEMENTS = new Set(['pre', 'script', 'style', 'textarea']);

const schema = baseRule.meta.schema.map((s) => ({ ...s }));
schema[1].properties = {
ignoredNodes: schema[1].properties.ignoredNodes,
ignoreComments: schema[1].properties.ignoreComments,
};

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
name: 'indent',
meta: {
type: 'layout',
docs: {
description: 'enforce consistent indentation for gts/gjs templates',
// too opinionated to be recommended
recommended: false,
category: 'Ember Octane',
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-indent.md',
},
fixable: 'whitespace',
hasSuggestions: baseRule.meta.hasSuggestions,
schema,
messages: baseRule.meta.messages,
},

create: (context) => {
const ctx = Object.create(context, {
report: {
writable: false,
configurable: false,
value: (info) => {
const node = context.sourceCode.getNodeByRangeIndex(info.node.range[0]);
if (!node.type.startsWith('Glimmer')) {
return;
}
context.report(info);
},
},
});
const rules = baseRule.create(ctx);
const sourceCode = context.sourceCode;

function JSXElement(node) {
let closingElement;
let openingElement;
if (node.type === 'GlimmerElementNode') {
const tokens = sourceCode.getTokens(node);
const openEnd = tokens.find((t) => t.value === '>');
const closeStart = tokens.findLast((t) => t.value === '<');
if (!node.selfClosing) {
closingElement = {
type: 'JSXClosingElement',
parent: node,
range: [closeStart.range[0], node.range[1]],
loc: {
start: Object.assign({}, node.loc.start),
end: Object.assign({}, node.loc.end),
},
};
closingElement.loc.start = sourceCode.getLocFromIndex(closeStart.range[0]);
closingElement.name = { ...closingElement, type: 'JSXIdentifier' };
closingElement.name.range = [
closingElement.name.range[0] + 1,
closingElement.name.range[1] - 1,
];
}

openingElement = {
type: 'JSXOpeningElement',
selfClosing: node.selfClosing,
attributes: node.attributes,
parent: node,
range: [node.range[0], openEnd.range[1]],
loc: {
start: Object.assign({}, node.loc.start),
end: Object.assign({}, node.loc.end),
},
};
openingElement.loc.end = sourceCode.getLocFromIndex(openEnd.range[1]);
openingElement.name = { ...openingElement, type: 'JSXIdentifier' };
openingElement.name.range = [
openingElement.name.range[0] + 1,
openingElement.name.range[1] - 1,
];
}
if (node.type === 'GlimmerBlockStatement') {
const tokens = sourceCode.getTokens(node);
let openEndIdx = tokens.findIndex((t) => t.value === '}');
while (tokens[openEndIdx + 1].value === '}') {
openEndIdx += 1;
}
const openEnd = tokens[openEndIdx];
let closeStartIdx = tokens.findLastIndex((t) => t.value === '{');
while (tokens[closeStartIdx - 1].value === '{') {
closeStartIdx -= 1;
}
const closeStart = tokens[closeStartIdx];
closingElement = {
type: 'JSXClosingElement',
parent: node,
range: [closeStart.range[0], node.range[1]],
loc: {
start: Object.assign({}, node.loc.start),
end: Object.assign({}, node.loc.end),
},
};
closingElement.loc.start = sourceCode.getLocFromIndex(closeStart.range[0]);

openingElement = {
type: 'JSXOpeningElement',
attributes: node.params,
parent: node,
range: [node.range[0], openEnd.range[1]],
loc: {
start: Object.assign({}, node.loc.start),
end: Object.assign({}, node.loc.end),
},
};
openingElement.loc.end = sourceCode.getLocFromIndex(openEnd.range[1]);
}
return {
type: 'JSXElement',
openingElement,
closingElement,
children: node.children || node.body,
parent: node.parent,
range: node.range,
loc: node.loc,
};
}

const ignoredStack = new Set();

return Object.assign({}, rules, {
// overwrite the base rule here so we can use our KNOWN_NODES list instead
'*:exit'(node) {
// For nodes we care about, skip the default handling, because it just marks the node as ignored...
if (
!node.type.startsWith('Glimmer') ||
(ignoredStack.size > 0 && !ignoredStack.has(node))
) {
rules['*:exit'](node);
}
if (ignoredStack.has(node)) {
ignoredStack.delete(node);
}
},
'GlimmerTemplate:exit'(node) {
if (!node.parent) {
rules['Program:exit'](node);
}
},
GlimmerElementNode(node) {
if (ignoredStack.size > 0) {
return;
}
if (IGNORED_ELEMENTS.has(node.tag)) {
ignoredStack.add(node);
}
const jsx = JSXElement(node);
rules['JSXElement'](jsx);
rules['JSXOpeningElement'](jsx.openingElement);
if (jsx.closingElement) {
rules['JSXClosingElement'](jsx.closingElement);
}
},
GlimmerAttrNode(node) {
if (ignoredStack.size > 0 || !node.value) {
return;
}
rules['JSXAttribute[value]']({
...node,
type: 'JSXAttribute',
name: {
type: 'JSXIdentifier',
name: node.name,
range: [node.range[0], node.range[0] + node.name.length - 1],
},
});
},
GlimmerTemplate(node) {
if (!node.parent) {
return;
}
const jsx = JSXElement({ ...node, tag: 'template', type: 'GlimmerElementNode' });
rules['JSXElement'](jsx);
},
GlimmerBlockStatement(node) {
const body = [...node.program.body, ...(node.inverse?.body || [])];
rules['JSXElement'](JSXElement({ ...node, body }));
},
});
},
};
Loading

0 comments on commit 0170dc8

Please sign in to comment.