241 lines
7.6 KiB
JavaScript
241 lines
7.6 KiB
JavaScript
|
/**
|
||
|
* @fileoverview Limit to one expression per line in JSX
|
||
|
* @author Mark Ivan Allen <Vydia.com>
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
const docsUrl = require('../util/docsUrl');
|
||
|
const jsxUtil = require('../util/jsx');
|
||
|
const report = require('../util/report');
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// Rule Definition
|
||
|
// ------------------------------------------------------------------------------
|
||
|
|
||
|
const optionDefaults = {
|
||
|
allow: 'none',
|
||
|
};
|
||
|
|
||
|
const messages = {
|
||
|
moveToNewLine: '`{{descriptor}}` must be placed on a new line',
|
||
|
};
|
||
|
|
||
|
module.exports = {
|
||
|
meta: {
|
||
|
docs: {
|
||
|
description: 'Require one JSX element per line',
|
||
|
category: 'Stylistic Issues',
|
||
|
recommended: false,
|
||
|
url: docsUrl('jsx-one-expression-per-line'),
|
||
|
},
|
||
|
fixable: 'whitespace',
|
||
|
|
||
|
messages,
|
||
|
|
||
|
schema: [
|
||
|
{
|
||
|
type: 'object',
|
||
|
properties: {
|
||
|
allow: {
|
||
|
enum: ['none', 'literal', 'single-child', 'non-jsx'],
|
||
|
},
|
||
|
},
|
||
|
default: optionDefaults,
|
||
|
additionalProperties: false,
|
||
|
},
|
||
|
],
|
||
|
},
|
||
|
|
||
|
create(context) {
|
||
|
const options = Object.assign({}, optionDefaults, context.options[0]);
|
||
|
|
||
|
function nodeKey(node) {
|
||
|
return `${node.loc.start.line},${node.loc.start.column}`;
|
||
|
}
|
||
|
|
||
|
function nodeDescriptor(n) {
|
||
|
return n.openingElement ? n.openingElement.name.name : context.getSourceCode().getText(n).replace(/\n/g, '');
|
||
|
}
|
||
|
|
||
|
function handleJSX(node) {
|
||
|
const children = node.children;
|
||
|
|
||
|
if (!children || !children.length) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
options.allow === 'non-jsx'
|
||
|
&& !children.find((child) => (child.type === 'JSXFragment' || child.type === 'JSXElement'))
|
||
|
) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const openingElement = node.openingElement || node.openingFragment;
|
||
|
const closingElement = node.closingElement || node.closingFragment;
|
||
|
const openingElementStartLine = openingElement.loc.start.line;
|
||
|
const openingElementEndLine = openingElement.loc.end.line;
|
||
|
const closingElementStartLine = closingElement.loc.start.line;
|
||
|
const closingElementEndLine = closingElement.loc.end.line;
|
||
|
|
||
|
if (children.length === 1) {
|
||
|
const child = children[0];
|
||
|
if (
|
||
|
openingElementStartLine === openingElementEndLine
|
||
|
&& openingElementEndLine === closingElementStartLine
|
||
|
&& closingElementStartLine === closingElementEndLine
|
||
|
&& closingElementEndLine === child.loc.start.line
|
||
|
&& child.loc.start.line === child.loc.end.line
|
||
|
) {
|
||
|
if (
|
||
|
options.allow === 'single-child'
|
||
|
|| (options.allow === 'literal' && (child.type === 'Literal' || child.type === 'JSXText'))
|
||
|
) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const childrenGroupedByLine = {};
|
||
|
const fixDetailsByNode = {};
|
||
|
|
||
|
children.forEach((child) => {
|
||
|
let countNewLinesBeforeContent = 0;
|
||
|
let countNewLinesAfterContent = 0;
|
||
|
|
||
|
if (child.type === 'Literal' || child.type === 'JSXText') {
|
||
|
if (jsxUtil.isWhiteSpaces(child.raw)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
countNewLinesBeforeContent = (child.raw.match(/^\s*\n/g) || []).length;
|
||
|
countNewLinesAfterContent = (child.raw.match(/\n\s*$/g) || []).length;
|
||
|
}
|
||
|
|
||
|
const startLine = child.loc.start.line + countNewLinesBeforeContent;
|
||
|
const endLine = child.loc.end.line - countNewLinesAfterContent;
|
||
|
|
||
|
if (startLine === endLine) {
|
||
|
if (!childrenGroupedByLine[startLine]) {
|
||
|
childrenGroupedByLine[startLine] = [];
|
||
|
}
|
||
|
childrenGroupedByLine[startLine].push(child);
|
||
|
} else {
|
||
|
if (!childrenGroupedByLine[startLine]) {
|
||
|
childrenGroupedByLine[startLine] = [];
|
||
|
}
|
||
|
childrenGroupedByLine[startLine].push(child);
|
||
|
if (!childrenGroupedByLine[endLine]) {
|
||
|
childrenGroupedByLine[endLine] = [];
|
||
|
}
|
||
|
childrenGroupedByLine[endLine].push(child);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
Object.keys(childrenGroupedByLine).forEach((_line) => {
|
||
|
const line = parseInt(_line, 10);
|
||
|
const firstIndex = 0;
|
||
|
const lastIndex = childrenGroupedByLine[line].length - 1;
|
||
|
|
||
|
childrenGroupedByLine[line].forEach((child, i) => {
|
||
|
let prevChild;
|
||
|
let nextChild;
|
||
|
|
||
|
if (i === firstIndex) {
|
||
|
if (line === openingElementEndLine) {
|
||
|
prevChild = openingElement;
|
||
|
}
|
||
|
} else {
|
||
|
prevChild = childrenGroupedByLine[line][i - 1];
|
||
|
}
|
||
|
|
||
|
if (i === lastIndex) {
|
||
|
if (line === closingElementStartLine) {
|
||
|
nextChild = closingElement;
|
||
|
}
|
||
|
} else {
|
||
|
// We don't need to append a trailing because the next child will prepend a leading.
|
||
|
// nextChild = childrenGroupedByLine[line][i + 1];
|
||
|
}
|
||
|
|
||
|
function spaceBetweenPrev() {
|
||
|
return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && / $/.test(prevChild.raw))
|
||
|
|| ((child.type === 'Literal' || child.type === 'JSXText') && /^ /.test(child.raw))
|
||
|
|| context.getSourceCode().isSpaceBetweenTokens(prevChild, child);
|
||
|
}
|
||
|
|
||
|
function spaceBetweenNext() {
|
||
|
return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw))
|
||
|
|| ((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw))
|
||
|
|| context.getSourceCode().isSpaceBetweenTokens(child, nextChild);
|
||
|
}
|
||
|
|
||
|
if (!prevChild && !nextChild) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const source = context.getSourceCode().getText(child);
|
||
|
const leadingSpace = !!(prevChild && spaceBetweenPrev());
|
||
|
const trailingSpace = !!(nextChild && spaceBetweenNext());
|
||
|
const leadingNewLine = !!prevChild;
|
||
|
const trailingNewLine = !!nextChild;
|
||
|
|
||
|
const key = nodeKey(child);
|
||
|
|
||
|
if (!fixDetailsByNode[key]) {
|
||
|
fixDetailsByNode[key] = {
|
||
|
node: child,
|
||
|
source,
|
||
|
descriptor: nodeDescriptor(child),
|
||
|
};
|
||
|
}
|
||
|
|
||
|
if (leadingSpace) {
|
||
|
fixDetailsByNode[key].leadingSpace = true;
|
||
|
}
|
||
|
if (leadingNewLine) {
|
||
|
fixDetailsByNode[key].leadingNewLine = true;
|
||
|
}
|
||
|
if (trailingNewLine) {
|
||
|
fixDetailsByNode[key].trailingNewLine = true;
|
||
|
}
|
||
|
if (trailingSpace) {
|
||
|
fixDetailsByNode[key].trailingSpace = true;
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
Object.keys(fixDetailsByNode).forEach((key) => {
|
||
|
const details = fixDetailsByNode[key];
|
||
|
|
||
|
const nodeToReport = details.node;
|
||
|
const descriptor = details.descriptor;
|
||
|
const source = details.source.replace(/(^ +| +(?=\n)*$)/g, '');
|
||
|
|
||
|
const leadingSpaceString = details.leadingSpace ? '\n{\' \'}' : '';
|
||
|
const trailingSpaceString = details.trailingSpace ? '{\' \'}\n' : '';
|
||
|
const leadingNewLineString = details.leadingNewLine ? '\n' : '';
|
||
|
const trailingNewLineString = details.trailingNewLine ? '\n' : '';
|
||
|
|
||
|
const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`;
|
||
|
|
||
|
report(context, messages.moveToNewLine, 'moveToNewLine', {
|
||
|
node: nodeToReport,
|
||
|
data: {
|
||
|
descriptor,
|
||
|
},
|
||
|
fix(fixer) {
|
||
|
return fixer.replaceText(nodeToReport, replaceText);
|
||
|
},
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
JSXElement: handleJSX,
|
||
|
JSXFragment: handleJSX,
|
||
|
};
|
||
|
},
|
||
|
};
|