123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- /**
- * @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'],
- },
- },
- 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;
- }
- 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,
- };
- },
- };
|