1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 | const docsUrl = require('../util/docsUrl');
|
9 | const jsxUtil = require('../util/jsx');
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | const optionDefaults = {
|
16 | allow: 'none'
|
17 | };
|
18 |
|
19 | module.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 |
|
144 |
|
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 | };
|