[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Limit to one expression per line in JSX
|
---|
| 3 | * @author Mark Ivan Allen <Vydia.com>
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | 'use strict';
|
---|
| 7 |
|
---|
| 8 | const docsUrl = require('../util/docsUrl');
|
---|
| 9 | const eslintUtil = require('../util/eslint');
|
---|
| 10 | const jsxUtil = require('../util/jsx');
|
---|
| 11 | const report = require('../util/report');
|
---|
| 12 |
|
---|
| 13 | const getSourceCode = eslintUtil.getSourceCode;
|
---|
| 14 | const getText = eslintUtil.getText;
|
---|
| 15 |
|
---|
| 16 | // ------------------------------------------------------------------------------
|
---|
| 17 | // Rule Definition
|
---|
| 18 | // ------------------------------------------------------------------------------
|
---|
| 19 |
|
---|
| 20 | const optionDefaults = {
|
---|
| 21 | allow: 'none',
|
---|
| 22 | };
|
---|
| 23 |
|
---|
| 24 | const messages = {
|
---|
| 25 | moveToNewLine: '`{{descriptor}}` must be placed on a new line',
|
---|
| 26 | };
|
---|
| 27 |
|
---|
| 28 | /** @type {import('eslint').Rule.RuleModule} */
|
---|
| 29 | module.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 | };
|
---|