1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 | const has = require('has');
|
9 | const docsUrl = require('../util/docsUrl');
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | module.exports = {
|
15 | meta: {
|
16 | docs: {
|
17 | description: 'Validate closing bracket location in JSX',
|
18 | category: 'Stylistic Issues',
|
19 | recommended: false,
|
20 | url: docsUrl('jsx-closing-bracket-location')
|
21 | },
|
22 | fixable: 'code',
|
23 |
|
24 | schema: [{
|
25 | oneOf: [
|
26 | {
|
27 | enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned']
|
28 | },
|
29 | {
|
30 | type: 'object',
|
31 | properties: {
|
32 | location: {
|
33 | enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned']
|
34 | }
|
35 | },
|
36 | additionalProperties: false
|
37 | }, {
|
38 | type: 'object',
|
39 | properties: {
|
40 | nonEmpty: {
|
41 | enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false]
|
42 | },
|
43 | selfClosing: {
|
44 | enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false]
|
45 | }
|
46 | },
|
47 | additionalProperties: false
|
48 | }
|
49 | ]
|
50 | }]
|
51 | },
|
52 |
|
53 | create(context) {
|
54 | const MESSAGE = 'The closing bracket must be {{location}}{{details}}';
|
55 | const MESSAGE_LOCATION = {
|
56 | 'after-props': 'placed after the last prop',
|
57 | 'after-tag': 'placed after the opening tag',
|
58 | 'props-aligned': 'aligned with the last prop',
|
59 | 'tag-aligned': 'aligned with the opening tag',
|
60 | 'line-aligned': 'aligned with the line containing the opening tag'
|
61 | };
|
62 | const DEFAULT_LOCATION = 'tag-aligned';
|
63 |
|
64 | const config = context.options[0];
|
65 | const options = {
|
66 | nonEmpty: DEFAULT_LOCATION,
|
67 | selfClosing: DEFAULT_LOCATION
|
68 | };
|
69 |
|
70 | if (typeof config === 'string') {
|
71 |
|
72 | options.nonEmpty = config;
|
73 | options.selfClosing = config;
|
74 | } else if (typeof config === 'object') {
|
75 |
|
76 | if (has(config, 'location')) {
|
77 | options.nonEmpty = config.location;
|
78 | options.selfClosing = config.location;
|
79 | }
|
80 |
|
81 | if (has(config, 'nonEmpty')) {
|
82 | options.nonEmpty = config.nonEmpty;
|
83 | }
|
84 |
|
85 | if (has(config, 'selfClosing')) {
|
86 | options.selfClosing = config.selfClosing;
|
87 | }
|
88 | }
|
89 |
|
90 | |
91 |
|
92 |
|
93 |
|
94 |
|
95 | function getExpectedLocation(tokens) {
|
96 | let location;
|
97 |
|
98 | if (typeof tokens.lastProp === 'undefined') {
|
99 | location = 'after-tag';
|
100 |
|
101 | } else if (tokens.opening.line === tokens.lastProp.lastLine) {
|
102 | location = 'after-props';
|
103 |
|
104 | } else {
|
105 | location = tokens.selfClosing ? options.selfClosing : options.nonEmpty;
|
106 | }
|
107 | return location;
|
108 | }
|
109 |
|
110 | |
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 | function getCorrectColumn(tokens, expectedLocation) {
|
118 | switch (expectedLocation) {
|
119 | case 'props-aligned':
|
120 | return tokens.lastProp.column;
|
121 | case 'tag-aligned':
|
122 | return tokens.opening.column;
|
123 | case 'line-aligned':
|
124 | return tokens.openingStartOfLine.column;
|
125 | default:
|
126 | return null;
|
127 | }
|
128 | }
|
129 |
|
130 | |
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 | function hasCorrectLocation(tokens, expectedLocation) {
|
137 | switch (expectedLocation) {
|
138 | case 'after-tag':
|
139 | return tokens.tag.line === tokens.closing.line;
|
140 | case 'after-props':
|
141 | return tokens.lastProp.lastLine === tokens.closing.line;
|
142 | case 'props-aligned':
|
143 | case 'tag-aligned':
|
144 | case 'line-aligned': {
|
145 | const correctColumn = getCorrectColumn(tokens, expectedLocation);
|
146 | return correctColumn === tokens.closing.column;
|
147 | }
|
148 | default:
|
149 | return true;
|
150 | }
|
151 | }
|
152 |
|
153 | |
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 | function getIndentation(tokens, expectedLocation, correctColumn) {
|
161 | correctColumn = correctColumn || 0;
|
162 | let indentation;
|
163 | let spaces = [];
|
164 | switch (expectedLocation) {
|
165 | case 'props-aligned':
|
166 | indentation = /^\s*/.exec(context.getSourceCode().lines[tokens.lastProp.firstLine - 1])[0];
|
167 | break;
|
168 | case 'tag-aligned':
|
169 | case 'line-aligned':
|
170 | indentation = /^\s*/.exec(context.getSourceCode().lines[tokens.opening.line - 1])[0];
|
171 | break;
|
172 | default:
|
173 | indentation = '';
|
174 | }
|
175 | if (indentation.length + 1 < correctColumn) {
|
176 |
|
177 | spaces = new Array(+correctColumn + 1 - indentation.length);
|
178 | }
|
179 | return indentation + spaces.join(' ');
|
180 | }
|
181 |
|
182 | |
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 | function getTokensLocations(node) {
|
190 | const sourceCode = context.getSourceCode();
|
191 | const opening = sourceCode.getFirstToken(node).loc.start;
|
192 | const closing = sourceCode.getLastTokens(node, node.selfClosing ? 2 : 1)[0].loc.start;
|
193 | const tag = sourceCode.getFirstToken(node.name).loc.start;
|
194 | let lastProp;
|
195 | if (node.attributes.length) {
|
196 | lastProp = node.attributes[node.attributes.length - 1];
|
197 | lastProp = {
|
198 | column: sourceCode.getFirstToken(lastProp).loc.start.column,
|
199 | firstLine: sourceCode.getFirstToken(lastProp).loc.start.line,
|
200 | lastLine: sourceCode.getLastToken(lastProp).loc.end.line
|
201 | };
|
202 | }
|
203 | const openingLine = sourceCode.lines[opening.line - 1];
|
204 | const openingStartOfLine = {
|
205 | column: /^\s*/.exec(openingLine)[0].length,
|
206 | line: opening.line
|
207 | };
|
208 | return {
|
209 | tag,
|
210 | opening,
|
211 | closing,
|
212 | lastProp,
|
213 | selfClosing: node.selfClosing,
|
214 | openingStartOfLine
|
215 | };
|
216 | }
|
217 |
|
218 | |
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 | function getOpeningElementId(node) {
|
225 | return node.range.join(':');
|
226 | }
|
227 |
|
228 | const lastAttributeNode = {};
|
229 |
|
230 | return {
|
231 | JSXAttribute(node) {
|
232 | lastAttributeNode[getOpeningElementId(node.parent)] = node;
|
233 | },
|
234 |
|
235 | JSXSpreadAttribute(node) {
|
236 | lastAttributeNode[getOpeningElementId(node.parent)] = node;
|
237 | },
|
238 |
|
239 | 'JSXOpeningElement:exit'(node) {
|
240 | const attributeNode = lastAttributeNode[getOpeningElementId(node)];
|
241 | const cachedLastAttributeEndPos = attributeNode ? attributeNode.range[1] : null;
|
242 | let expectedNextLine;
|
243 | const tokens = getTokensLocations(node);
|
244 | const expectedLocation = getExpectedLocation(tokens);
|
245 |
|
246 | if (hasCorrectLocation(tokens, expectedLocation)) {
|
247 | return;
|
248 | }
|
249 |
|
250 | const data = {location: MESSAGE_LOCATION[expectedLocation], details: ''};
|
251 | const correctColumn = getCorrectColumn(tokens, expectedLocation);
|
252 |
|
253 | if (correctColumn !== null) {
|
254 | expectedNextLine = tokens.lastProp
|
255 | && (tokens.lastProp.lastLine === tokens.closing.line);
|
256 | data.details = ` (expected column ${correctColumn + 1}${expectedNextLine ? ' on the next line)' : ')'}`;
|
257 | }
|
258 |
|
259 | context.report({
|
260 | node,
|
261 | loc: tokens.closing,
|
262 | message: MESSAGE,
|
263 | data,
|
264 | fix(fixer) {
|
265 | const closingTag = tokens.selfClosing ? '/>' : '>';
|
266 | switch (expectedLocation) {
|
267 | case 'after-tag':
|
268 | if (cachedLastAttributeEndPos) {
|
269 | return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
|
270 | (expectedNextLine ? '\n' : '') + closingTag);
|
271 | }
|
272 | return fixer.replaceTextRange([node.name.range[1], node.range[1]],
|
273 | (expectedNextLine ? '\n' : ' ') + closingTag);
|
274 | case 'after-props':
|
275 | return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
|
276 | (expectedNextLine ? '\n' : '') + closingTag);
|
277 | case 'props-aligned':
|
278 | case 'tag-aligned':
|
279 | case 'line-aligned':
|
280 | return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
|
281 | `\n${getIndentation(tokens, expectedLocation, correctColumn)}${closingTag}`);
|
282 | default:
|
283 | return true;
|
284 | }
|
285 | }
|
286 | });
|
287 | }
|
288 | };
|
289 | }
|
290 | };
|