Skip to content

Commit fb820ec

Browse files
feat: add test for tabs in no-extra-spacing-attrs (#202)
* Add disallowTabs * Add unexpectedBeforeClose * Update docs * prettier * De-nesting some if statements * Fix tests for multiline cases * Prettier * Fix using col instead of range * Add another multiline test case
1 parent 7a0ae82 commit fb820ec

File tree

3 files changed

+283
-89
lines changed

3 files changed

+283
-89
lines changed

docs/rules/no-extra-spacing-attrs.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# no-extra-spacing-attrs
22

3-
This rule disallows extra spaces around attributes.
3+
This rule disallows extra spaces around attributes, and/or between the tag start and end
44

55
## How to use
66

@@ -26,13 +26,17 @@ Examples of **incorrect** code for this rule:
2626
2727
<!-- an extra space between tag end and attribute -->
2828
<div foo="foo" ></div>
29+
30+
<!-- an extra space between tag start and end -->
31+
<div ></div>
2932
```
3033

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

3336
```html,correct
3437
<div foo="foo" bar="bar"></div>
3538
<div foo="foo"></div>
39+
<div></div>
3640
```
3741

3842
## Options
@@ -84,3 +88,27 @@ Example(s) of **correct** code for this rule with the `{ "disallowMissing": true
8488
```
8589

8690
<!-- prettier-ignore-end -->
91+
92+
- `disallowTabs` (default: false): Enforce using spaces instead of tabs between attributes
93+
94+
Example(s) of **incorrect** code for this rule with the `{ "disallowTabs": true }` option:
95+
96+
<!-- prettier-ignore-start -->
97+
98+
```html
99+
<div id="foo" class="bar">
100+
</div>
101+
```
102+
103+
<!-- prettier-ignore-end -->
104+
105+
Example(s) of **correct** code for this rule with the `{ "disallowTabs": true }` option:
106+
107+
<!-- prettier-ignore-start -->
108+
109+
```html
110+
<div id="foo" class="bar">
111+
</div>
112+
```
113+
114+
<!-- prettier-ignore-end -->

packages/eslint-plugin/lib/rules/no-extra-spacing-attrs.js

Lines changed: 121 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ const MESSAGE_IDS = {
2020
EXTRA_BETWEEN: "unexpectedBetween",
2121
EXTRA_AFTER: "unexpectedAfter",
2222
EXTRA_BEFORE: "unexpectedBefore",
23+
EXTRA_BEFORE_CLOSE: "unexpectedBeforeClose",
2324
MISSING_BEFORE: "missingBefore",
2425
MISSING_BEFORE_SELF_CLOSE: "missingBeforeSelfClose",
2526
EXTRA_BEFORE_SELF_CLOSE: "unexpectedBeforeSelfClose",
27+
EXTRA_TAB_BEFORE: "unexpectedTabBefore",
28+
EXTRA_TAB_BEFORE_SELF_CLOSE: "unexpectedTabBeforeSelfClose",
29+
EXTRA_TAB_BETWEEN: "unexpectedTabBetween",
2630
};
2731

2832
/**
@@ -46,6 +50,9 @@ module.exports = {
4650
disallowMissing: {
4751
type: "boolean",
4852
},
53+
disallowTabs: {
54+
type: "boolean",
55+
},
4956
enforceBeforeSelfClose: {
5057
type: "boolean",
5158
},
@@ -56,17 +63,27 @@ module.exports = {
5663
[MESSAGE_IDS.EXTRA_BETWEEN]: "Unexpected space between attributes",
5764
[MESSAGE_IDS.EXTRA_AFTER]: "Unexpected space after attribute",
5865
[MESSAGE_IDS.EXTRA_BEFORE]: "Unexpected space before attribute",
66+
[MESSAGE_IDS.EXTRA_BEFORE_CLOSE]: "Unexpected space before closing",
5967
[MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE]:
6068
"Missing space before self closing",
6169
[MESSAGE_IDS.EXTRA_BEFORE_SELF_CLOSE]:
6270
"Unexpected extra spaces before self closing",
6371
[MESSAGE_IDS.MISSING_BEFORE]: "Missing space before attribute",
72+
[MESSAGE_IDS.EXTRA_TAB_BEFORE]:
73+
"Unexpected tab before attribute; use space instead",
74+
[MESSAGE_IDS.EXTRA_TAB_BEFORE_SELF_CLOSE]:
75+
"Unexpected tab before self closing; use space instead",
76+
[MESSAGE_IDS.EXTRA_TAB_BETWEEN]:
77+
"Unexpected tab between attributes; use space instead",
6478
},
6579
},
6680
create(context) {
6781
const enforceBeforeSelfClose = !!(context.options[0] || {})
6882
.enforceBeforeSelfClose;
6983
const disallowMissing = !!(context.options[0] || {}).disallowMissing;
84+
const disallowTabs = !!(context.options[0] || {}).disallowTabs;
85+
86+
const sourceCode = context.getSourceCode().text;
7087

7188
/**
7289
* @param {AttributeNode[]} attrs
@@ -98,48 +115,23 @@ module.exports = {
98115
return fixer.insertTextAfter(current, " ");
99116
},
100117
});
118+
} else if (disallowTabs) {
119+
if (sourceCode[current.range[1]] === `\t`) {
120+
context.report({
121+
loc: getLocBetween(current, after),
122+
messageId: MESSAGE_IDS.EXTRA_TAB_BETWEEN,
123+
fix(fixer) {
124+
return fixer.replaceTextRange(
125+
[current.range[1], current.range[1] + 1],
126+
` `
127+
);
128+
},
129+
});
130+
}
101131
}
102132
});
103133
}
104134

105-
/**
106-
* @param {OpenTagEndNode | OpenScriptTagEndNode | OpenStyleTagEndNode} openEnd
107-
* @param {AttributeNode} lastAttr
108-
* @param {boolean} isSelfClosed
109-
* @returns {void}
110-
*/
111-
function checkExtraSpaceAfter(openEnd, lastAttr, isSelfClosed) {
112-
if (openEnd.loc.end.line !== lastAttr.loc.end.line) {
113-
// skip the attribute on the different line with the start tag
114-
return;
115-
}
116-
const limit = isSelfClosed && enforceBeforeSelfClose ? 1 : 0;
117-
const spacesBetween = openEnd.loc.start.column - lastAttr.loc.end.column;
118-
119-
if (spacesBetween > limit) {
120-
context.report({
121-
loc: getLocBetween(lastAttr, openEnd),
122-
messageId: MESSAGE_IDS.EXTRA_AFTER,
123-
fix(fixer) {
124-
return fixer.removeRange([
125-
lastAttr.range[1],
126-
lastAttr.range[1] + spacesBetween - limit,
127-
]);
128-
},
129-
});
130-
}
131-
132-
if (isSelfClosed && enforceBeforeSelfClose && spacesBetween < 1) {
133-
context.report({
134-
loc: getLocBetween(lastAttr, openEnd),
135-
messageId: MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE,
136-
fix(fixer) {
137-
return fixer.insertTextAfter(lastAttr, " ");
138-
},
139-
});
140-
}
141-
}
142-
143135
/**
144136
* @param {OpenScriptTagStartNode | OpenTagStartNode | OpenStyleTagStartNode} node
145137
* @param {AttributeNode} firstAttr
@@ -164,42 +156,19 @@ module.exports = {
164156
]);
165157
},
166158
});
167-
}
168-
}
169-
170-
/**
171-
* @param {AnyNode} beforeSelfClosing
172-
* @param {OpenTagEndNode | OpenScriptTagEndNode | OpenStyleTagEndNode} openEnd
173-
* @returns
174-
*/
175-
function checkSpaceBeforeSelfClosing(beforeSelfClosing, openEnd) {
176-
if (beforeSelfClosing.loc.start.line !== openEnd.loc.start.line) {
177-
// skip the attribute on the different line with the start tag
178-
return;
179-
}
180-
const spacesBetween =
181-
openEnd.loc.start.column - beforeSelfClosing.loc.end.column;
182-
const locBetween = getLocBetween(beforeSelfClosing, openEnd);
183-
184-
if (spacesBetween > 1) {
185-
context.report({
186-
loc: locBetween,
187-
messageId: MESSAGE_IDS.EXTRA_BEFORE_SELF_CLOSE,
188-
fix(fixer) {
189-
return fixer.removeRange([
190-
beforeSelfClosing.range[1] + 1,
191-
openEnd.range[0],
192-
]);
193-
},
194-
});
195-
} else if (spacesBetween < 1) {
196-
context.report({
197-
loc: locBetween,
198-
messageId: MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE,
199-
fix(fixer) {
200-
return fixer.insertTextAfter(beforeSelfClosing, " ");
201-
},
202-
});
159+
} else if (disallowTabs) {
160+
if (sourceCode[firstAttr.range[0] - 1] === `\t`) {
161+
context.report({
162+
loc: firstAttr.loc,
163+
messageId: MESSAGE_IDS.EXTRA_TAB_BEFORE,
164+
fix(fixer) {
165+
return fixer.replaceTextRange(
166+
[firstAttr.range[0] - 1, firstAttr.range[0]],
167+
` `
168+
);
169+
},
170+
});
171+
}
203172
}
204173
}
205174

@@ -216,24 +185,89 @@ module.exports = {
216185
if (node.attributes.length) {
217186
checkExtraSpaceBefore(node.openStart, node.attributes[0]);
218187
}
188+
219189
if (node.openEnd) {
190+
checkExtraSpacesBetweenAttrs(node.attributes);
191+
192+
const lastAttr = node.attributes[node.attributes.length - 1];
193+
const nodeBeforeEnd =
194+
node.attributes.length === 0 ? node.openStart : lastAttr;
195+
196+
if (nodeBeforeEnd.loc.end.line !== node.openEnd.loc.start.line) {
197+
return;
198+
}
199+
220200
const isSelfClosing = node.openEnd.value === "/>";
221201

222-
if (node.attributes && node.attributes.length > 0) {
223-
checkExtraSpaceAfter(
224-
node.openEnd,
225-
node.attributes[node.attributes.length - 1],
226-
isSelfClosing
227-
);
202+
const spacesBetween =
203+
node.openEnd.loc.start.column - nodeBeforeEnd.loc.end.column;
204+
const locBetween = getLocBetween(nodeBeforeEnd, node.openEnd);
205+
206+
if (isSelfClosing && enforceBeforeSelfClose) {
207+
if (spacesBetween < 1) {
208+
context.report({
209+
loc: locBetween,
210+
messageId: MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE,
211+
fix(fixer) {
212+
return fixer.insertTextAfter(nodeBeforeEnd, " ");
213+
},
214+
});
215+
} else if (spacesBetween === 1) {
216+
if (
217+
disallowTabs &&
218+
sourceCode[node.openEnd.range[0] - 1] === `\t`
219+
) {
220+
context.report({
221+
loc: node.openEnd.loc,
222+
messageId: MESSAGE_IDS.EXTRA_TAB_BEFORE_SELF_CLOSE,
223+
fix(fixer) {
224+
return fixer.replaceTextRange(
225+
[node.openEnd.range[0] - 1, node.openEnd.range[0]],
226+
` `
227+
);
228+
},
229+
});
230+
}
231+
} else {
232+
context.report({
233+
loc: locBetween,
234+
messageId: MESSAGE_IDS.EXTRA_BEFORE_SELF_CLOSE,
235+
fix(fixer) {
236+
return fixer.removeRange([
237+
nodeBeforeEnd.range[1] + 1,
238+
node.openEnd.range[0],
239+
]);
240+
},
241+
});
242+
}
243+
244+
return;
228245
}
229246

230-
checkExtraSpacesBetweenAttrs(node.attributes);
231-
if (
232-
node.attributes.length === 0 &&
233-
isSelfClosing &&
234-
enforceBeforeSelfClose
235-
) {
236-
checkSpaceBeforeSelfClosing(node.openStart, node.openEnd);
247+
if (spacesBetween > 0) {
248+
if (node.attributes.length > 0) {
249+
context.report({
250+
loc: locBetween,
251+
messageId: MESSAGE_IDS.EXTRA_AFTER,
252+
fix(fixer) {
253+
return fixer.removeRange([
254+
lastAttr.range[1],
255+
node.openEnd.range[0],
256+
]);
257+
},
258+
});
259+
} else {
260+
context.report({
261+
loc: locBetween,
262+
messageId: MESSAGE_IDS.EXTRA_BEFORE_CLOSE,
263+
fix(fixer) {
264+
return fixer.removeRange([
265+
node.openStart.range[1],
266+
node.openEnd.range[0],
267+
]);
268+
},
269+
});
270+
}
237271
}
238272
}
239273
},

0 commit comments

Comments
 (0)