UNPKG

8.63 kBJavaScriptView Raw
1/**
2 * @fileoverview Validates whitespace in and around the JSX opening and closing brackets
3 * @author Diogo Franco (Kovensky)
4 */
5
6'use strict';
7
8const getTokenBeforeClosingBracket = require('../util/getTokenBeforeClosingBracket');
9const docsUrl = require('../util/docsUrl');
10
11// ------------------------------------------------------------------------------
12// Validators
13// ------------------------------------------------------------------------------
14
15function validateClosingSlash(context, node, option) {
16 const sourceCode = context.getSourceCode();
17
18 const SELF_CLOSING_NEVER_MESSAGE = 'Whitespace is forbidden between `/` and `>`; write `/>`';
19 const SELF_CLOSING_ALWAYS_MESSAGE = 'Whitespace is required between `/` and `>`; write `/ >`';
20 const NEVER_MESSAGE = 'Whitespace is forbidden between `<` and `/`; write `</`';
21 const ALWAYS_MESSAGE = 'Whitespace is required between `<` and `/`; write `< /`';
22
23 let adjacent;
24
25 if (node.selfClosing) {
26 const lastTokens = sourceCode.getLastTokens(node, 2);
27
28 adjacent = !sourceCode.isSpaceBetweenTokens(lastTokens[0], lastTokens[1]);
29
30 if (option === 'never') {
31 if (!adjacent) {
32 context.report({
33 node,
34 loc: {
35 start: lastTokens[0].loc.start,
36 end: lastTokens[1].loc.end
37 },
38 message: SELF_CLOSING_NEVER_MESSAGE,
39 fix(fixer) {
40 return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]);
41 }
42 });
43 }
44 } else if (option === 'always' && adjacent) {
45 context.report({
46 node,
47 loc: {
48 start: lastTokens[0].loc.start,
49 end: lastTokens[1].loc.end
50 },
51 message: SELF_CLOSING_ALWAYS_MESSAGE,
52 fix(fixer) {
53 return fixer.insertTextBefore(lastTokens[1], ' ');
54 }
55 });
56 }
57 } else {
58 const firstTokens = sourceCode.getFirstTokens(node, 2);
59
60 adjacent = !sourceCode.isSpaceBetweenTokens(firstTokens[0], firstTokens[1]);
61
62 if (option === 'never') {
63 if (!adjacent) {
64 context.report({
65 node,
66 loc: {
67 start: firstTokens[0].loc.start,
68 end: firstTokens[1].loc.end
69 },
70 message: NEVER_MESSAGE,
71 fix(fixer) {
72 return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]);
73 }
74 });
75 }
76 } else if (option === 'always' && adjacent) {
77 context.report({
78 node,
79 loc: {
80 start: firstTokens[0].loc.start,
81 end: firstTokens[1].loc.end
82 },
83 message: ALWAYS_MESSAGE,
84 fix(fixer) {
85 return fixer.insertTextBefore(firstTokens[1], ' ');
86 }
87 });
88 }
89 }
90}
91
92function validateBeforeSelfClosing(context, node, option) {
93 const sourceCode = context.getSourceCode();
94
95 const NEVER_MESSAGE = 'A space is forbidden before closing bracket';
96 const ALWAYS_MESSAGE = 'A space is required before closing bracket';
97
98 const leftToken = getTokenBeforeClosingBracket(node);
99 const closingSlash = sourceCode.getTokenAfter(leftToken);
100
101 if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
102 return;
103 }
104
105 if (option === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
106 context.report({
107 node,
108 loc: closingSlash.loc.start,
109 message: ALWAYS_MESSAGE,
110 fix(fixer) {
111 return fixer.insertTextBefore(closingSlash, ' ');
112 }
113 });
114 } else if (option === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
115 context.report({
116 node,
117 loc: closingSlash.loc.start,
118 message: NEVER_MESSAGE,
119 fix(fixer) {
120 const previousToken = sourceCode.getTokenBefore(closingSlash);
121 return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
122 }
123 });
124 }
125}
126
127function validateAfterOpening(context, node, option) {
128 const sourceCode = context.getSourceCode();
129
130 const NEVER_MESSAGE = 'A space is forbidden after opening bracket';
131 const ALWAYS_MESSAGE = 'A space is required after opening bracket';
132
133 const openingToken = sourceCode.getTokenBefore(node.name);
134
135 if (option === 'allow-multiline') {
136 if (openingToken.loc.start.line !== node.name.loc.start.line) {
137 return;
138 }
139 }
140
141 const adjacent = !sourceCode.isSpaceBetweenTokens(openingToken, node.name);
142
143 if (option === 'never' || option === 'allow-multiline') {
144 if (!adjacent) {
145 context.report({
146 node,
147 loc: {
148 start: openingToken.loc.start,
149 end: node.name.loc.start
150 },
151 message: NEVER_MESSAGE,
152 fix(fixer) {
153 return fixer.removeRange([openingToken.range[1], node.name.range[0]]);
154 }
155 });
156 }
157 } else if (option === 'always' && adjacent) {
158 context.report({
159 node,
160 loc: {
161 start: openingToken.loc.start,
162 end: node.name.loc.start
163 },
164 message: ALWAYS_MESSAGE,
165 fix(fixer) {
166 return fixer.insertTextBefore(node.name, ' ');
167 }
168 });
169 }
170}
171
172function validateBeforeClosing(context, node, option) {
173 // Don't enforce this rule for self closing tags
174 if (!node.selfClosing) {
175 const sourceCode = context.getSourceCode();
176
177 const NEVER_MESSAGE = 'A space is forbidden before closing bracket';
178 const ALWAYS_MESSAGE = 'Whitespace is required before closing bracket';
179
180 const lastTokens = sourceCode.getLastTokens(node, 2);
181 const closingToken = lastTokens[1];
182 const leftToken = lastTokens[0];
183
184 if (leftToken.loc.start.line !== closingToken.loc.start.line) {
185 return;
186 }
187
188 const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingToken);
189
190 if (option === 'never' && !adjacent) {
191 context.report({
192 node,
193 loc: {
194 start: leftToken.loc.end,
195 end: closingToken.loc.start
196 },
197 message: NEVER_MESSAGE,
198 fix(fixer) {
199 return fixer.removeRange([leftToken.range[1], closingToken.range[0]]);
200 }
201 });
202 } else if (option === 'always' && adjacent) {
203 context.report({
204 node,
205 loc: {
206 start: leftToken.loc.end,
207 end: closingToken.loc.start
208 },
209 message: ALWAYS_MESSAGE,
210 fix(fixer) {
211 return fixer.insertTextBefore(closingToken, ' ');
212 }
213 });
214 }
215 }
216}
217
218// ------------------------------------------------------------------------------
219// Rule Definition
220// ------------------------------------------------------------------------------
221
222const optionDefaults = {
223 closingSlash: 'never',
224 beforeSelfClosing: 'always',
225 afterOpening: 'never',
226 beforeClosing: 'allow'
227};
228
229module.exports = {
230 meta: {
231 docs: {
232 description: 'Validate whitespace in and around the JSX opening and closing brackets',
233 category: 'Stylistic Issues',
234 recommended: false,
235 url: docsUrl('jsx-tag-spacing')
236 },
237 fixable: 'whitespace',
238 schema: [
239 {
240 type: 'object',
241 properties: {
242 closingSlash: {
243 enum: ['always', 'never', 'allow']
244 },
245 beforeSelfClosing: {
246 enum: ['always', 'never', 'allow']
247 },
248 afterOpening: {
249 enum: ['always', 'allow-multiline', 'never', 'allow']
250 },
251 beforeClosing: {
252 enum: ['always', 'never', 'allow']
253 }
254 },
255 default: optionDefaults,
256 additionalProperties: false
257 }
258 ]
259 },
260 create(context) {
261 const options = Object.assign({}, optionDefaults, context.options[0]);
262
263 return {
264 JSXOpeningElement(node) {
265 if (options.closingSlash !== 'allow' && node.selfClosing) {
266 validateClosingSlash(context, node, options.closingSlash);
267 }
268 if (options.afterOpening !== 'allow') {
269 validateAfterOpening(context, node, options.afterOpening);
270 }
271 if (options.beforeSelfClosing !== 'allow' && node.selfClosing) {
272 validateBeforeSelfClosing(context, node, options.beforeSelfClosing);
273 }
274 if (options.beforeClosing !== 'allow') {
275 validateBeforeClosing(context, node, options.beforeClosing);
276 }
277 },
278 JSXClosingElement(node) {
279 if (options.afterOpening !== 'allow') {
280 validateAfterOpening(context, node, options.afterOpening);
281 }
282 if (options.closingSlash !== 'allow') {
283 validateClosingSlash(context, node, options.closingSlash);
284 }
285 if (options.beforeClosing !== 'allow') {
286 validateBeforeClosing(context, node, options.beforeClosing);
287 }
288 }
289 };
290 }
291};