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

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

Pred finalna verzija

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