Skip to content

Commit 2cf8ef7

Browse files
Merge pull request #2465 from NullVoxPopuli/nvp/template-lint-extract-rule-template-linebreak-style
Extract rule: template-linebreak-style
2 parents e20ba32 + 50713d8 commit 2cf8ef7

4 files changed

Lines changed: 335 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ rules in templates can be disabled with eslint directives with mustache or html
387387
| [order-in-models](docs/rules/order-in-models.md) | enforce proper order of properties in models | | 🔧 | |
388388
| [order-in-routes](docs/rules/order-in-routes.md) | enforce proper order of properties in routes | | 🔧 | |
389389
| [template-attribute-order](docs/rules/template-attribute-order.md) | enforce consistent ordering of attributes in template elements | | | |
390+
| [template-linebreak-style](docs/rules/template-linebreak-style.md) | enforce consistent linebreaks in templates | | 🔧 | |
390391
| [template-no-only-default-slot](docs/rules/template-no-only-default-slot.md) | disallow using only the default slot | | 🔧 | |
391392

392393
### Testing
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# ember/template-linebreak-style
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+
Enforce consistent linebreaks in templates.
8+
9+
Having consistent linebreaks is important to make sure that the source code is rendered correctly in editors.
10+
11+
## Rule Details
12+
13+
This rule enforces consistent line endings in templates, independent of the operating system.
14+
15+
## Config
16+
17+
This rule accepts a single string option:
18+
19+
- `"unix"` (default) — enforces the usage of Unix line endings: `\n` for LF
20+
- `"windows"` — enforces the usage of Windows line endings: `\r\n` for CRLF
21+
- `"system"` — enforces the usage of the current platform's line ending
22+
23+
## Examples
24+
25+
Examples of **incorrect** code with the default `"unix"` config:
26+
27+
```hbs
28+
<div>test</div>\r\n
29+
```
30+
31+
Examples of **correct** code with the default `"unix"` config:
32+
33+
```hbs
34+
<div>test</div>\n
35+
```
36+
37+
Examples of **incorrect** code with the `"windows"` config:
38+
39+
```hbs
40+
<div>test</div>\n
41+
```
42+
43+
Examples of **correct** code with the `"windows"` config:
44+
45+
```hbs
46+
<div>test</div>\r\n
47+
```
48+
49+
## Related Rules
50+
51+
- [linebreak-style](https://eslint.org/docs/rules/linebreak-style) from eslint
52+
53+
## References
54+
55+
- [ember-template-lint linebreak-style](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/linebreak-style.md)
56+
- [Git/line endings](https://docs.github.com/en/github/using-git/configuring-git-to-handle-line-endings)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use strict';
2+
3+
const os = require('node:os');
4+
const editorConfigUtil = require('../utils/editorconfig');
5+
6+
function toDisplay(value) {
7+
return value.replaceAll('\r', 'CR').replaceAll('\n', 'LF');
8+
}
9+
10+
const EOL_MAP = { lf: '\n', cr: '\r', crlf: '\r\n' };
11+
12+
/** @type {import('eslint').Rule.RuleModule} */
13+
module.exports = {
14+
meta: {
15+
type: 'layout',
16+
docs: {
17+
description: 'enforce consistent linebreaks in templates',
18+
category: 'Stylistic Issues',
19+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-linebreak-style.md',
20+
templateMode: 'both',
21+
},
22+
fixable: 'whitespace',
23+
schema: [
24+
{
25+
enum: ['unix', 'windows', 'system'],
26+
},
27+
],
28+
messages: {
29+
wrongLinebreak: 'Wrong linebreak used. Expected {{expected}} but found {{found}}.',
30+
},
31+
originallyFrom: {
32+
name: 'ember-template-lint',
33+
rule: 'lib/rules/linebreak-style.js',
34+
docs: 'docs/rule/linebreak-style.md',
35+
tests: 'test/unit/rules/linebreak-style-test.js',
36+
},
37+
},
38+
39+
create(context) {
40+
const option = context.options[0] || 'unix';
41+
42+
// editorconfig end_of_line takes precedence over the rule option
43+
const editorConfig = editorConfigUtil.resolveEditorConfig(context.getFilename());
44+
const editorConfigEol = editorConfig['end_of_line'];
45+
46+
let expectedLinebreak;
47+
if (editorConfigEol && EOL_MAP[editorConfigEol]) {
48+
expectedLinebreak = EOL_MAP[editorConfigEol];
49+
} else if (option === 'system') {
50+
expectedLinebreak = os.EOL;
51+
} else if (option === 'windows') {
52+
expectedLinebreak = '\r\n';
53+
} else {
54+
expectedLinebreak = '\n';
55+
}
56+
57+
const sourceCode = context.getSourceCode();
58+
59+
return {
60+
'GlimmerTemplate:exit'(node) {
61+
const text = sourceCode.getText(node);
62+
const re = /\r\n?|\n/g;
63+
let match;
64+
65+
while ((match = re.exec(text)) !== null) {
66+
const found = match[0];
67+
if (found !== expectedLinebreak) {
68+
const startIndex = node.range[0] + match.index;
69+
context.report({
70+
loc: {
71+
start: sourceCode.getLocFromIndex(startIndex),
72+
end: sourceCode.getLocFromIndex(startIndex + found.length),
73+
},
74+
messageId: 'wrongLinebreak',
75+
data: {
76+
expected: toDisplay(expectedLinebreak),
77+
found: toDisplay(found),
78+
},
79+
fix(fixer) {
80+
return fixer.replaceTextRange(
81+
[startIndex, startIndex + found.length],
82+
expectedLinebreak
83+
);
84+
},
85+
});
86+
}
87+
}
88+
},
89+
};
90+
},
91+
};
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const editorConfigUtil = require('../../../lib/utils/editorconfig');
6+
const rule = require('../../../lib/rules/template-linebreak-style');
7+
const RuleTester = require('eslint').RuleTester;
8+
9+
//------------------------------------------------------------------------------
10+
// Tests
11+
//
12+
// All tests are wrapped in a describe so beforeAll/afterAll can install a spy
13+
// on editorConfigUtil.resolveEditorConfig before any rule invocation. This
14+
// prevents the project's own .editorconfig from influencing test outcomes.
15+
//------------------------------------------------------------------------------
16+
17+
describe('template-linebreak-style', () => {
18+
beforeAll(() => {
19+
vi.spyOn(editorConfigUtil, 'resolveEditorConfig').mockReturnValue({});
20+
});
21+
22+
afterAll(() => {
23+
vi.restoreAllMocks();
24+
});
25+
26+
const ruleTester = new RuleTester({
27+
parser: require.resolve('ember-eslint-parser'),
28+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
29+
});
30+
31+
ruleTester.run('template-linebreak-style', rule, {
32+
valid: [
33+
// default 'unix' — LF only
34+
'<template>\n<div>test</div>\n</template>',
35+
],
36+
37+
invalid: [
38+
{
39+
code: '<template>\r\n<div>test</div>\r\n</template>',
40+
output: '<template>\n<div>test</div>\n</template>',
41+
options: ['unix'],
42+
errors: [{ messageId: 'wrongLinebreak' }, { messageId: 'wrongLinebreak' }],
43+
},
44+
],
45+
});
46+
47+
const hbsRuleTester = new RuleTester({
48+
parser: require.resolve('ember-eslint-parser/hbs'),
49+
parserOptions: {
50+
ecmaVersion: 2022,
51+
sourceType: 'module',
52+
},
53+
});
54+
55+
hbsRuleTester.run('template-linebreak-style', rule, {
56+
valid: [
57+
// default 'unix' — all LF
58+
'testing this',
59+
'testing \n this',
60+
{ code: 'testing\nthis', options: ['unix'] },
61+
// windows — all CRLF
62+
{ code: 'testing\r\nthis', options: ['windows'] },
63+
// no linebreaks at all
64+
'single line no linebreaks',
65+
],
66+
67+
invalid: [
68+
// default 'unix' — mixed linebreaks, first is LF so CRLF is wrong
69+
{
70+
code: 'something\ngoes\r\n',
71+
output: 'something\ngoes\n',
72+
options: ['unix'],
73+
errors: [{ messageId: 'wrongLinebreak' }],
74+
},
75+
// unix — CRLF standalone
76+
{
77+
code: '\r\n',
78+
output: '\n',
79+
options: ['unix'],
80+
errors: [{ messageId: 'wrongLinebreak' }],
81+
},
82+
// unix — CRLF in block mustache
83+
{
84+
code: '{{#if test}}\r\n{{/if}}',
85+
output: '{{#if test}}\n{{/if}}',
86+
options: ['unix'],
87+
errors: [{ messageId: 'wrongLinebreak' }],
88+
},
89+
// unix — CRLF between mustaches
90+
{
91+
code: '{{blah}}\r\n{{blah}}',
92+
output: '{{blah}}\n{{blah}}',
93+
options: ['unix'],
94+
errors: [{ messageId: 'wrongLinebreak' }],
95+
},
96+
// unix — CRLF trailing
97+
{
98+
code: '{{blah}}\r\n',
99+
output: '{{blah}}\n',
100+
options: ['unix'],
101+
errors: [{ messageId: 'wrongLinebreak' }],
102+
},
103+
// unix — CRLF in attribute value
104+
{
105+
code: '{{blah arg="\r\n"}}',
106+
output: '{{blah arg="\n"}}',
107+
options: ['unix'],
108+
errors: [{ messageId: 'wrongLinebreak' }],
109+
},
110+
// unix — CRLF in element attribute
111+
{
112+
code: '<blah arg="\r\n" />',
113+
output: '<blah arg="\n" />',
114+
options: ['unix'],
115+
errors: [{ messageId: 'wrongLinebreak' }],
116+
},
117+
// windows — LF is wrong
118+
{
119+
code: '\n',
120+
output: '\r\n',
121+
options: ['windows'],
122+
errors: [{ messageId: 'wrongLinebreak' }],
123+
},
124+
],
125+
});
126+
127+
//------------------------------------------------------------------------------
128+
// EditorConfig integration tests
129+
//------------------------------------------------------------------------------
130+
131+
describe('editorconfig integration', () => {
132+
const hbsRuleTesterEditorConfig = new RuleTester({
133+
parser: require.resolve('ember-eslint-parser/hbs'),
134+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
135+
});
136+
137+
afterEach(() => {
138+
editorConfigUtil.resolveEditorConfig.mockReturnValue({});
139+
});
140+
141+
describe('end_of_line: crlf overrides rule option', () => {
142+
beforeEach(() => {
143+
editorConfigUtil.resolveEditorConfig.mockReturnValue({ end_of_line: 'crlf' });
144+
});
145+
146+
hbsRuleTesterEditorConfig.run('template-linebreak-style', rule, {
147+
valid: [
148+
// editorconfig crlf wins even when rule option says unix
149+
{ code: 'testing\r\nthis', options: ['unix'] },
150+
// editorconfig crlf matches windows option too
151+
{ code: 'testing\r\nthis', options: ['windows'] },
152+
],
153+
invalid: [
154+
// LF is wrong when editorconfig says crlf
155+
{
156+
code: 'testing\nthis',
157+
output: 'testing\r\nthis',
158+
options: ['unix'],
159+
errors: [{ messageId: 'wrongLinebreak' }],
160+
},
161+
],
162+
});
163+
});
164+
165+
describe('end_of_line: lf overrides rule option', () => {
166+
beforeEach(() => {
167+
editorConfigUtil.resolveEditorConfig.mockReturnValue({ end_of_line: 'lf' });
168+
});
169+
170+
hbsRuleTesterEditorConfig.run('template-linebreak-style', rule, {
171+
valid: [
172+
// editorconfig lf wins even when rule option says windows
173+
{ code: 'testing\nthis', options: ['windows'] },
174+
],
175+
invalid: [
176+
// CRLF is wrong when editorconfig says lf
177+
{
178+
code: 'testing\r\nthis',
179+
output: 'testing\nthis',
180+
options: ['windows'],
181+
errors: [{ messageId: 'wrongLinebreak' }],
182+
},
183+
],
184+
});
185+
});
186+
});
187+
});

0 commit comments

Comments
 (0)