You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
233 lines
7.4 KiB
233 lines
7.4 KiB
/** |
|
* @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: 'Limit to one expression per line in JSX', |
|
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, |
|
}; |
|
}, |
|
};
|
|
|