UNPKG

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