source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/jsx-one-expression-per-line.js

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

Update repo after prototype presentation

  • Property mode set to 100644
File size: 7.8 KB
Line 
1/**
2 * @fileoverview Limit to one expression per line in JSX
3 * @author Mark Ivan Allen <Vydia.com>
4 */
5
6'use strict';
7
8const docsUrl = require('../util/docsUrl');
9const eslintUtil = require('../util/eslint');
10const jsxUtil = require('../util/jsx');
11const report = require('../util/report');
12
13const getSourceCode = eslintUtil.getSourceCode;
14const getText = eslintUtil.getText;
15
16// ------------------------------------------------------------------------------
17// Rule Definition
18// ------------------------------------------------------------------------------
19
20const optionDefaults = {
21 allow: 'none',
22};
23
24const messages = {
25 moveToNewLine: '`{{descriptor}}` must be placed on a new line',
26};
27
28/** @type {import('eslint').Rule.RuleModule} */
29module.exports = {
30 meta: {
31 docs: {
32 description: 'Require one JSX element per line',
33 category: 'Stylistic Issues',
34 recommended: false,
35 url: docsUrl('jsx-one-expression-per-line'),
36 },
37 fixable: 'whitespace',
38
39 messages,
40
41 schema: [
42 {
43 type: 'object',
44 properties: {
45 allow: {
46 enum: ['none', 'literal', 'single-child', 'non-jsx'],
47 },
48 },
49 default: optionDefaults,
50 additionalProperties: false,
51 },
52 ],
53 },
54
55 create(context) {
56 const options = Object.assign({}, optionDefaults, context.options[0]);
57
58 function nodeKey(node) {
59 return `${node.loc.start.line},${node.loc.start.column}`;
60 }
61
62 /**
63 * @param {ASTNode} n
64 * @returns {string}
65 */
66 function nodeDescriptor(n) {
67 return n.openingElement ? n.openingElement.name.name : getText(context, n).replace(/\n/g, '');
68 }
69
70 function handleJSX(node) {
71 const children = node.children;
72
73 if (!children || !children.length) {
74 return;
75 }
76
77 if (
78 options.allow === 'non-jsx'
79 && !children.find((child) => (child.type === 'JSXFragment' || child.type === 'JSXElement'))
80 ) {
81 return;
82 }
83
84 const openingElement = node.openingElement || node.openingFragment;
85 const closingElement = node.closingElement || node.closingFragment;
86 const openingElementStartLine = openingElement.loc.start.line;
87 const openingElementEndLine = openingElement.loc.end.line;
88 const closingElementStartLine = closingElement.loc.start.line;
89 const closingElementEndLine = closingElement.loc.end.line;
90
91 if (children.length === 1) {
92 const child = children[0];
93 if (
94 openingElementStartLine === openingElementEndLine
95 && openingElementEndLine === closingElementStartLine
96 && closingElementStartLine === closingElementEndLine
97 && closingElementEndLine === child.loc.start.line
98 && child.loc.start.line === child.loc.end.line
99 ) {
100 if (
101 options.allow === 'single-child'
102 || (options.allow === 'literal' && (child.type === 'Literal' || child.type === 'JSXText'))
103 ) {
104 return;
105 }
106 }
107 }
108
109 const childrenGroupedByLine = {};
110 const fixDetailsByNode = {};
111
112 children.forEach((child) => {
113 let countNewLinesBeforeContent = 0;
114 let countNewLinesAfterContent = 0;
115
116 if (child.type === 'Literal' || child.type === 'JSXText') {
117 if (jsxUtil.isWhiteSpaces(child.raw)) {
118 return;
119 }
120
121 countNewLinesBeforeContent = (child.raw.match(/^\s*\n/g) || []).length;
122 countNewLinesAfterContent = (child.raw.match(/\n\s*$/g) || []).length;
123 }
124
125 const startLine = child.loc.start.line + countNewLinesBeforeContent;
126 const endLine = child.loc.end.line - countNewLinesAfterContent;
127
128 if (startLine === endLine) {
129 if (!childrenGroupedByLine[startLine]) {
130 childrenGroupedByLine[startLine] = [];
131 }
132 childrenGroupedByLine[startLine].push(child);
133 } else {
134 if (!childrenGroupedByLine[startLine]) {
135 childrenGroupedByLine[startLine] = [];
136 }
137 childrenGroupedByLine[startLine].push(child);
138 if (!childrenGroupedByLine[endLine]) {
139 childrenGroupedByLine[endLine] = [];
140 }
141 childrenGroupedByLine[endLine].push(child);
142 }
143 });
144
145 Object.keys(childrenGroupedByLine).forEach((_line) => {
146 const line = parseInt(_line, 10);
147 const firstIndex = 0;
148 const lastIndex = childrenGroupedByLine[line].length - 1;
149
150 childrenGroupedByLine[line].forEach((child, i) => {
151 let prevChild;
152 let nextChild;
153
154 if (i === firstIndex) {
155 if (line === openingElementEndLine) {
156 prevChild = openingElement;
157 }
158 } else {
159 prevChild = childrenGroupedByLine[line][i - 1];
160 }
161
162 if (i === lastIndex) {
163 if (line === closingElementStartLine) {
164 nextChild = closingElement;
165 }
166 } else {
167 // We don't need to append a trailing because the next child will prepend a leading.
168 // nextChild = childrenGroupedByLine[line][i + 1];
169 }
170
171 function spaceBetweenPrev() {
172 return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && / $/.test(prevChild.raw))
173 || ((child.type === 'Literal' || child.type === 'JSXText') && /^ /.test(child.raw))
174 || getSourceCode(context).isSpaceBetweenTokens(prevChild, child);
175 }
176
177 function spaceBetweenNext() {
178 return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw))
179 || ((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw))
180 || getSourceCode(context).isSpaceBetweenTokens(child, nextChild);
181 }
182
183 if (!prevChild && !nextChild) {
184 return;
185 }
186
187 const source = getText(context, child);
188 const leadingSpace = !!(prevChild && spaceBetweenPrev());
189 const trailingSpace = !!(nextChild && spaceBetweenNext());
190 const leadingNewLine = !!prevChild;
191 const trailingNewLine = !!nextChild;
192
193 const key = nodeKey(child);
194
195 if (!fixDetailsByNode[key]) {
196 fixDetailsByNode[key] = {
197 node: child,
198 source,
199 descriptor: nodeDescriptor(child),
200 };
201 }
202
203 if (leadingSpace) {
204 fixDetailsByNode[key].leadingSpace = true;
205 }
206 if (leadingNewLine) {
207 fixDetailsByNode[key].leadingNewLine = true;
208 }
209 if (trailingNewLine) {
210 fixDetailsByNode[key].trailingNewLine = true;
211 }
212 if (trailingSpace) {
213 fixDetailsByNode[key].trailingSpace = true;
214 }
215 });
216 });
217
218 Object.keys(fixDetailsByNode).forEach((key) => {
219 const details = fixDetailsByNode[key];
220
221 const nodeToReport = details.node;
222 const descriptor = details.descriptor;
223 const source = details.source.replace(/(^ +| +(?=\n)*$)/g, '');
224
225 const leadingSpaceString = details.leadingSpace ? '\n{\' \'}' : '';
226 const trailingSpaceString = details.trailingSpace ? '{\' \'}\n' : '';
227 const leadingNewLineString = details.leadingNewLine ? '\n' : '';
228 const trailingNewLineString = details.trailingNewLine ? '\n' : '';
229
230 const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`;
231
232 report(context, messages.moveToNewLine, 'moveToNewLine', {
233 node: nodeToReport,
234 data: {
235 descriptor,
236 },
237 fix(fixer) {
238 return fixer.replaceText(nodeToReport, replaceText);
239 },
240 });
241 });
242 }
243
244 return {
245 JSXElement: handleJSX,
246 JSXFragment: handleJSX,
247 };
248 },
249};
Note: See TracBrowser for help on using the repository browser.