source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/jsx-closing-bracket-location.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 10.7 KB
Line 
1/**
2 * @fileoverview Validate closing bracket location in JSX
3 * @author Yannick Croissant
4 */
5
6'use strict';
7
8const has = require('hasown');
9const repeat = require('string.prototype.repeat');
10
11const docsUrl = require('../util/docsUrl');
12const getSourceCode = require('../util/eslint').getSourceCode;
13const report = require('../util/report');
14
15// ------------------------------------------------------------------------------
16// Rule Definition
17// ------------------------------------------------------------------------------
18
19const messages = {
20 bracketLocation: 'The closing bracket must be {{location}}{{details}}',
21};
22
23module.exports = {
24 meta: {
25 docs: {
26 description: 'Enforce closing bracket location in JSX',
27 category: 'Stylistic Issues',
28 recommended: false,
29 url: docsUrl('jsx-closing-bracket-location'),
30 },
31 fixable: 'code',
32
33 messages,
34
35 schema: [{
36 anyOf: [
37 {
38 enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'],
39 },
40 {
41 type: 'object',
42 properties: {
43 location: {
44 enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'],
45 },
46 },
47 additionalProperties: false,
48 }, {
49 type: 'object',
50 properties: {
51 nonEmpty: {
52 enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false],
53 },
54 selfClosing: {
55 enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false],
56 },
57 },
58 additionalProperties: false,
59 },
60 ],
61 }],
62 },
63
64 create(context) {
65 const MESSAGE_LOCATION = {
66 'after-props': 'placed after the last prop',
67 'after-tag': 'placed after the opening tag',
68 'props-aligned': 'aligned with the last prop',
69 'tag-aligned': 'aligned with the opening tag',
70 'line-aligned': 'aligned with the line containing the opening tag',
71 };
72 const DEFAULT_LOCATION = 'tag-aligned';
73
74 const config = context.options[0];
75 const options = {
76 nonEmpty: DEFAULT_LOCATION,
77 selfClosing: DEFAULT_LOCATION,
78 };
79
80 if (typeof config === 'string') {
81 // simple shorthand [1, 'something']
82 options.nonEmpty = config;
83 options.selfClosing = config;
84 } else if (typeof config === 'object') {
85 // [1, {location: 'something'}] (back-compat)
86 if (has(config, 'location')) {
87 options.nonEmpty = config.location;
88 options.selfClosing = config.location;
89 }
90 // [1, {nonEmpty: 'something'}]
91 if (has(config, 'nonEmpty')) {
92 options.nonEmpty = config.nonEmpty;
93 }
94 // [1, {selfClosing: 'something'}]
95 if (has(config, 'selfClosing')) {
96 options.selfClosing = config.selfClosing;
97 }
98 }
99
100 /**
101 * Get expected location for the closing bracket
102 * @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
103 * @return {String} Expected location for the closing bracket
104 */
105 function getExpectedLocation(tokens) {
106 let location;
107 // Is always after the opening tag if there is no props
108 if (typeof tokens.lastProp === 'undefined') {
109 location = 'after-tag';
110 // Is always after the last prop if this one is on the same line as the opening bracket
111 } else if (tokens.opening.line === tokens.lastProp.lastLine) {
112 location = 'after-props';
113 // Else use configuration dependent on selfClosing property
114 } else {
115 location = tokens.selfClosing ? options.selfClosing : options.nonEmpty;
116 }
117 return location;
118 }
119
120 /**
121 * Get the correct 0-indexed column for the closing bracket, given the
122 * expected location.
123 * @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
124 * @param {String} expectedLocation Expected location for the closing bracket
125 * @return {?Number} The correct column for the closing bracket, or null
126 */
127 function getCorrectColumn(tokens, expectedLocation) {
128 switch (expectedLocation) {
129 case 'props-aligned':
130 return tokens.lastProp.column;
131 case 'tag-aligned':
132 return tokens.opening.column;
133 case 'line-aligned':
134 return tokens.openingStartOfLine.column;
135 default:
136 return null;
137 }
138 }
139
140 /**
141 * Check if the closing bracket is correctly located
142 * @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
143 * @param {String} expectedLocation Expected location for the closing bracket
144 * @return {Boolean} True if the closing bracket is correctly located, false if not
145 */
146 function hasCorrectLocation(tokens, expectedLocation) {
147 switch (expectedLocation) {
148 case 'after-tag':
149 return tokens.tag.line === tokens.closing.line;
150 case 'after-props':
151 return tokens.lastProp.lastLine === tokens.closing.line;
152 case 'props-aligned':
153 case 'tag-aligned':
154 case 'line-aligned': {
155 const correctColumn = getCorrectColumn(tokens, expectedLocation);
156 return correctColumn === tokens.closing.column;
157 }
158 default:
159 return true;
160 }
161 }
162
163 /**
164 * Get the characters used for indentation on the line to be matched
165 * @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
166 * @param {String} expectedLocation Expected location for the closing bracket
167 * @param {Number} [correctColumn] Expected column for the closing bracket. Default to 0
168 * @return {String} The characters used for indentation
169 */
170 function getIndentation(tokens, expectedLocation, correctColumn) {
171 const newColumn = correctColumn || 0;
172 let indentation;
173 let spaces = '';
174 switch (expectedLocation) {
175 case 'props-aligned':
176 indentation = /^\s*/.exec(getSourceCode(context).lines[tokens.lastProp.firstLine - 1])[0];
177 break;
178 case 'tag-aligned':
179 case 'line-aligned':
180 indentation = /^\s*/.exec(getSourceCode(context).lines[tokens.opening.line - 1])[0];
181 break;
182 default:
183 indentation = '';
184 }
185 if (indentation.length + 1 < newColumn) {
186 // Non-whitespace characters were included in the column offset
187 spaces = repeat(' ', +correctColumn - indentation.length);
188 }
189 return indentation + spaces;
190 }
191
192 /**
193 * Get the locations of the opening bracket, closing bracket, last prop, and
194 * start of opening line.
195 * @param {ASTNode} node The node to check
196 * @return {Object} Locations of the opening bracket, closing bracket, last
197 * prop and start of opening line.
198 */
199 function getTokensLocations(node) {
200 const sourceCode = getSourceCode(context);
201 const opening = sourceCode.getFirstToken(node).loc.start;
202 const closing = sourceCode.getLastTokens(node, node.selfClosing ? 2 : 1)[0].loc.start;
203 const tag = sourceCode.getFirstToken(node.name).loc.start;
204 let lastProp;
205 if (node.attributes.length) {
206 lastProp = node.attributes[node.attributes.length - 1];
207 lastProp = {
208 column: sourceCode.getFirstToken(lastProp).loc.start.column,
209 firstLine: sourceCode.getFirstToken(lastProp).loc.start.line,
210 lastLine: sourceCode.getLastToken(lastProp).loc.end.line,
211 };
212 }
213 const openingLine = sourceCode.lines[opening.line - 1];
214 const closingLine = sourceCode.lines[closing.line - 1];
215 const isTab = {
216 openTab: /^\t/.test(openingLine),
217 closeTab: /^\t/.test(closingLine),
218 };
219 const openingStartOfLine = {
220 column: /^\s*/.exec(openingLine)[0].length,
221 line: opening.line,
222 };
223 return {
224 isTab,
225 tag,
226 opening,
227 closing,
228 lastProp,
229 selfClosing: node.selfClosing,
230 openingStartOfLine,
231 };
232 }
233
234 /**
235 * Get an unique ID for a given JSXOpeningElement
236 *
237 * @param {ASTNode} node The AST node being checked.
238 * @returns {String} Unique ID (based on its range)
239 */
240 function getOpeningElementId(node) {
241 return node.range.join(':');
242 }
243
244 const lastAttributeNode = {};
245
246 return {
247 JSXAttribute(node) {
248 lastAttributeNode[getOpeningElementId(node.parent)] = node;
249 },
250
251 JSXSpreadAttribute(node) {
252 lastAttributeNode[getOpeningElementId(node.parent)] = node;
253 },
254
255 'JSXOpeningElement:exit'(node) {
256 const attributeNode = lastAttributeNode[getOpeningElementId(node)];
257 const cachedLastAttributeEndPos = attributeNode ? attributeNode.range[1] : null;
258
259 let expectedNextLine;
260 const tokens = getTokensLocations(node);
261 const expectedLocation = getExpectedLocation(tokens);
262 let usingSameIndentation = true;
263
264 if (expectedLocation === 'tag-aligned') {
265 usingSameIndentation = tokens.isTab.openTab === tokens.isTab.closeTab;
266 }
267
268 if (hasCorrectLocation(tokens, expectedLocation) && usingSameIndentation) {
269 return;
270 }
271
272 const data = {
273 location: MESSAGE_LOCATION[expectedLocation],
274 details: '',
275 };
276 const correctColumn = getCorrectColumn(tokens, expectedLocation);
277
278 if (correctColumn !== null) {
279 expectedNextLine = tokens.lastProp
280 && (tokens.lastProp.lastLine === tokens.closing.line);
281 data.details = ` (expected column ${correctColumn + 1}${expectedNextLine ? ' on the next line)' : ')'}`;
282 }
283
284 report(context, messages.bracketLocation, 'bracketLocation', {
285 node,
286 loc: tokens.closing,
287 data,
288 fix(fixer) {
289 const closingTag = tokens.selfClosing ? '/>' : '>';
290 switch (expectedLocation) {
291 case 'after-tag':
292 if (cachedLastAttributeEndPos) {
293 return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
294 (expectedNextLine ? '\n' : '') + closingTag);
295 }
296 return fixer.replaceTextRange([node.name.range[1], node.range[1]],
297 (expectedNextLine ? '\n' : ' ') + closingTag);
298 case 'after-props':
299 return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
300 (expectedNextLine ? '\n' : '') + closingTag);
301 case 'props-aligned':
302 case 'tag-aligned':
303 case 'line-aligned':
304 return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
305 `\n${getIndentation(tokens, expectedLocation, correctColumn)}${closingTag}`);
306 default:
307 return true;
308 }
309 },
310 });
311 },
312 };
313 },
314};
Note: See TracBrowser for help on using the repository browser.