Skip to content

Commit 3b5063f

Browse files
author
Diogo Franco (Kovensky)
committed
Add jsx-tag-spacing rule (Fixes #693)
1 parent cad882b commit 3b5063f

File tree

3 files changed

+560
-1
lines changed

3 files changed

+560
-1
lines changed

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ var allRules = {
5555
'no-children-prop': require('./lib/rules/no-children-prop'),
5656
'no-comment-textnodes': require('./lib/rules/no-comment-textnodes'),
5757
'require-extension': require('./lib/rules/require-extension'),
58-
'wrap-multilines': require('./lib/rules/wrap-multilines')
58+
'wrap-multilines': require('./lib/rules/wrap-multilines'),
59+
'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing')
5960
};
6061

6162
function filterRules(rules, predicate) {

lib/rules/jsx-tag-spacing.js

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/**
2+
* @fileoverview Validates whitespace in and around the JSX opening and closing brackets
3+
* @author Diogo Franco (Kovensky)
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Helpers
9+
// ------------------------------------------------------------------------------
10+
11+
/**
12+
* Find the token before the closing bracket.
13+
* @param {ASTNode} node - The JSX element node.
14+
* @returns {Token} The token before the closing bracket.
15+
*/
16+
function getTokenBeforeClosingBracket(node) {
17+
var attributes = node.attributes;
18+
if (attributes.length === 0) {
19+
return node.name;
20+
}
21+
return attributes[ attributes.length - 1 ];
22+
}
23+
24+
// ------------------------------------------------------------------------------
25+
// Validators
26+
// ------------------------------------------------------------------------------
27+
28+
function validateClosingSlash(context, node, option) {
29+
var sourceCode = context.getSourceCode();
30+
31+
var SELF_CLOSING_NEVER_MESSAGE = 'Whitespace is forbidden between `/` and `>`';
32+
var SELF_CLOSING_ALWAYS_MESSAGE = 'Whitespace is required between `/` and `>`';
33+
var NEVER_MESSAGE = 'Whitespace is forbidden between `<` and `/`';
34+
var ALWAYS_MESSAGE = 'Whitespace is required between `<` and `/`';
35+
36+
var adjacent;
37+
38+
if (node.selfClosing) {
39+
var lastTokens = sourceCode.getLastTokens(node, 2);
40+
41+
adjacent = !sourceCode.isSpaceBetweenTokens(lastTokens[0], lastTokens[1]);
42+
43+
if (option === 'never') {
44+
if (!adjacent) {
45+
context.report({
46+
node: node,
47+
loc: {
48+
start: lastTokens[0].loc.start,
49+
end: lastTokens[1].loc.end
50+
},
51+
message: SELF_CLOSING_NEVER_MESSAGE,
52+
fix: function(fixer) {
53+
return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]);
54+
}
55+
});
56+
}
57+
} else if (option === 'always' && adjacent) {
58+
context.report({
59+
node: node,
60+
loc: {
61+
start: lastTokens[0].loc.start,
62+
end: lastTokens[1].loc.end
63+
},
64+
message: SELF_CLOSING_ALWAYS_MESSAGE,
65+
fix: function(fixer) {
66+
return fixer.insertTextBefore(lastTokens[1], ' ');
67+
}
68+
});
69+
}
70+
} else {
71+
var firstTokens = sourceCode.getFirstTokens(node, 2);
72+
73+
adjacent = !sourceCode.isSpaceBetweenTokens(firstTokens[0], firstTokens[1]);
74+
75+
if (option === 'never') {
76+
if (!adjacent) {
77+
context.report({
78+
node: node,
79+
loc: {
80+
start: firstTokens[0].loc.start,
81+
end: firstTokens[1].loc.end
82+
},
83+
message: NEVER_MESSAGE,
84+
fix: function(fixer) {
85+
return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]);
86+
}
87+
});
88+
}
89+
} else if (option === 'always' && adjacent) {
90+
context.report({
91+
node: node,
92+
loc: {
93+
start: firstTokens[0].loc.start,
94+
end: firstTokens[1].loc.end
95+
},
96+
message: ALWAYS_MESSAGE,
97+
fix: function(fixer) {
98+
return fixer.insertTextBefore(firstTokens[1], ' ');
99+
}
100+
});
101+
}
102+
}
103+
}
104+
105+
function validateBeforeSelfClosing(context, node, option) {
106+
var sourceCode = context.getSourceCode();
107+
108+
var NEVER_MESSAGE = 'A space is forbidden before closing bracket';
109+
var ALWAYS_MESSAGE = 'A space is required before closing bracket';
110+
111+
var leftToken = getTokenBeforeClosingBracket(node);
112+
var closingSlash = sourceCode.getTokenAfter(leftToken);
113+
114+
if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
115+
return;
116+
}
117+
118+
if (option === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
119+
context.report({
120+
node: node,
121+
loc: closingSlash.loc.start,
122+
message: ALWAYS_MESSAGE,
123+
fix: function(fixer) {
124+
return fixer.insertTextBefore(closingSlash, ' ');
125+
}
126+
});
127+
} else if (option === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
128+
context.report({
129+
node: node,
130+
loc: closingSlash.loc.start,
131+
message: NEVER_MESSAGE,
132+
fix: function(fixer) {
133+
var previousToken = sourceCode.getTokenBefore(closingSlash);
134+
return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
135+
}
136+
});
137+
}
138+
}
139+
140+
function validateAfterOpening(context, node, option) {
141+
var sourceCode = context.getSourceCode();
142+
143+
var NEVER_MESSAGE = 'A space is forbidden after opening bracket';
144+
var ALWAYS_MESSAGE = 'A space is required after opening bracket';
145+
146+
var openingToken = sourceCode.getTokenBefore(node.name);
147+
148+
if (option === 'allow-multiline') {
149+
if (openingToken.loc.start.line !== node.name.loc.start.line) {
150+
return;
151+
}
152+
}
153+
154+
var adjacent = !sourceCode.isSpaceBetweenTokens(openingToken, node.name);
155+
156+
if (option === 'never' || option === 'allow-multiline') {
157+
if (!adjacent) {
158+
context.report({
159+
node: node,
160+
loc: {
161+
start: openingToken.loc.start,
162+
end: node.name.loc.start
163+
},
164+
message: NEVER_MESSAGE,
165+
fix: function(fixer) {
166+
return fixer.removeRange([openingToken.range[1], node.name.range[0]]);
167+
}
168+
});
169+
}
170+
} else if (option === 'always' && adjacent) {
171+
context.report({
172+
node: node,
173+
loc: {
174+
start: openingToken.loc.start,
175+
end: node.name.loc.start
176+
},
177+
message: ALWAYS_MESSAGE,
178+
fix: function(fixer) {
179+
return fixer.insertTextBefore(node.name, ' ');
180+
}
181+
});
182+
}
183+
}
184+
185+
// ------------------------------------------------------------------------------
186+
// Rule Definition
187+
// ------------------------------------------------------------------------------
188+
189+
module.exports = {
190+
meta: {
191+
docs: {},
192+
fixable: 'whitespace',
193+
schema: [
194+
{
195+
type: 'object',
196+
properties: {
197+
closingSlash: {
198+
enum: ['always', 'never', 'allow']
199+
},
200+
beforeSelfClosing: {
201+
enum: ['always', 'never', 'allow']
202+
},
203+
afterOpening: {
204+
enum: ['always', 'allow-multiline', 'never', 'allow']
205+
}
206+
},
207+
default: {
208+
closingSlash: 'never',
209+
beforeSelfClosing: 'always',
210+
afterOpening: 'never'
211+
},
212+
additionalProperties: false
213+
}
214+
]
215+
},
216+
create: function (context) {
217+
var options = {
218+
closingSlash: 'never',
219+
beforeSelfClosing: 'always',
220+
afterOpening: 'never'
221+
};
222+
for (var key in options) {
223+
if (options.hasOwnProperty(key) && context.options[0].hasOwnProperty(key)) {
224+
options[key] = context.options[0][key];
225+
}
226+
}
227+
228+
return {
229+
JSXOpeningElement: function (node) {
230+
if (options.closingSlash !== 'allow' && node.selfClosing) {
231+
validateClosingSlash(context, node, options.closingSlash);
232+
}
233+
if (options.afterOpening !== 'allow') {
234+
validateAfterOpening(context, node, options.afterOpening);
235+
}
236+
if (options.beforeSelfClosing !== 'allow' && node.selfClosing) {
237+
validateBeforeSelfClosing(context, node, options.beforeSelfClosing);
238+
}
239+
},
240+
JSXClosingElement: function (node) {
241+
if (options.afterOpening !== 'allow') {
242+
validateAfterOpening(context, node, options.afterOpening);
243+
}
244+
if (options.closingSlash !== 'allow') {
245+
validateClosingSlash(context, node, options.closingSlash);
246+
}
247+
}
248+
};
249+
}
250+
};

0 commit comments

Comments
 (0)