|
@@ -64,39 +64,96 @@ module.exports = {
|
|
|
const sourceCode = context.getSourceCode();
|
|
const sourceCode = context.getSourceCode();
|
|
|
|
|
|
|
|
function checkProperties(node, properties) {
|
|
function checkProperties(node, properties) {
|
|
|
- if (properties.length <= 1) return;
|
|
|
|
|
|
|
+ if (properties.length === 0) return;
|
|
|
|
|
|
|
|
- for (let i = 0; i < properties.length - 1; i++) {
|
|
|
|
|
- const current = properties[i];
|
|
|
|
|
- const next = properties[i + 1];
|
|
|
|
|
|
|
+ const openBrace = sourceCode.getFirstToken(node);
|
|
|
|
|
|
|
|
- if (current.loc.end.line === next.loc.start.line) {
|
|
|
|
|
- context.report({
|
|
|
|
|
- node: next,
|
|
|
|
|
- message: "Each property should be on a new line",
|
|
|
|
|
- fix(fixer) {
|
|
|
|
|
- const openBrace = sourceCode.getFirstToken(node);
|
|
|
|
|
- const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
|
|
|
|
|
- const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
|
|
|
- const indent = baseIndent + " ";
|
|
|
|
|
|
|
+ let closingBraceToken = sourceCode.getTokenAfter(properties[properties.length - 1]);
|
|
|
|
|
+ while (closingBraceToken && closingBraceToken.value !== "}") {
|
|
|
|
|
+ closingBraceToken = sourceCode.getTokenAfter(closingBraceToken);
|
|
|
|
|
+ }
|
|
|
|
|
+ const closingBrace = closingBraceToken;
|
|
|
|
|
|
|
|
- const comma = sourceCode.getTokenBefore(next);
|
|
|
|
|
|
|
+ if (openBrace.loc.start.line === closingBrace.loc.end.line) return;
|
|
|
|
|
|
|
|
- return fixer.replaceTextRange(
|
|
|
|
|
- [
|
|
|
|
|
- comma.range[1],
|
|
|
|
|
- next.range[0]
|
|
|
|
|
- ],
|
|
|
|
|
- "\n" + indent
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ const firstProperty = properties[0];
|
|
|
|
|
+
|
|
|
|
|
+ if (openBrace.loc.end.line !== firstProperty.loc.start.line - 1) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: firstProperty,
|
|
|
|
|
+ message: "First property should be exactly on the next line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
|
|
|
+ const indent = baseIndent + " ";
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ openBrace.range[1],
|
|
|
|
|
+ firstProperty.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + indent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (properties.length > 1) {
|
|
|
|
|
+ for (let i = 0; i < properties.length - 1; i++) {
|
|
|
|
|
+ const current = properties[i];
|
|
|
|
|
+ const next = properties[i + 1];
|
|
|
|
|
+
|
|
|
|
|
+ if (current.loc.end.line !== next.loc.start.line - 1) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: next,
|
|
|
|
|
+ message: "Each property should be exactly on the next line (no empty lines)",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
|
|
|
+ const indent = baseIndent + " ";
|
|
|
|
|
+
|
|
|
|
|
+ const comma = sourceCode.getTokenBefore(next);
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ comma.range[1],
|
|
|
|
|
+ next.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + indent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const lastProperty = properties[properties.length - 1];
|
|
const lastProperty = properties[properties.length - 1];
|
|
|
- const tokenAfter = sourceCode.getTokenAfter(lastProperty);
|
|
|
|
|
|
|
|
|
|
|
|
+ if (lastProperty.loc.end.line !== closingBrace.loc.start.line - 1) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: closingBrace,
|
|
|
|
|
+ message: "Object closing brace should be exactly on the next line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
|
|
|
+
|
|
|
|
|
+ // Be careful about trailing comma
|
|
|
|
|
+ const tokenAfter = sourceCode.getTokenAfter(lastProperty);
|
|
|
|
|
+ const hasTrailingComma = tokenAfter && tokenAfter.value === ",";
|
|
|
|
|
+ const startRange = hasTrailingComma ? tokenAfter.range[1] : lastProperty.range[1];
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ startRange,
|
|
|
|
|
+ closingBrace.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + baseIndent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const tokenAfter = sourceCode.getTokenAfter(lastProperty);
|
|
|
if (tokenAfter && tokenAfter.value === ",") {
|
|
if (tokenAfter && tokenAfter.value === ",") {
|
|
|
context.report({
|
|
context.report({
|
|
|
node: lastProperty,
|
|
node: lastProperty,
|
|
@@ -112,7 +169,6 @@ module.exports = {
|
|
|
ObjectExpression(node) {
|
|
ObjectExpression(node) {
|
|
|
checkProperties(node, node.properties);
|
|
checkProperties(node, node.properties);
|
|
|
},
|
|
},
|
|
|
-
|
|
|
|
|
ObjectPattern(node) {
|
|
ObjectPattern(node) {
|
|
|
checkProperties(node, node.properties);
|
|
checkProperties(node, node.properties);
|
|
|
}
|
|
}
|
|
@@ -130,16 +186,16 @@ module.exports = {
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
JSXOpeningElement(node) {
|
|
JSXOpeningElement(node) {
|
|
|
- if (node.attributes.length <= 1) return;
|
|
|
|
|
|
|
+ if (node.attributes.length === 0) return;
|
|
|
|
|
|
|
|
const firstToken = sourceCode.getFirstToken(node);
|
|
const firstToken = sourceCode.getFirstToken(node);
|
|
|
const tagNameToken = sourceCode.getTokenAfter(firstToken);
|
|
const tagNameToken = sourceCode.getTokenAfter(firstToken);
|
|
|
const firstAttr = node.attributes[0];
|
|
const firstAttr = node.attributes[0];
|
|
|
|
|
|
|
|
- if (tagNameToken.loc.end.line === firstAttr.loc.start.line) {
|
|
|
|
|
|
|
+ if (tagNameToken.loc.end.line !== firstAttr.loc.start.line - 1) {
|
|
|
context.report({
|
|
context.report({
|
|
|
node: firstAttr,
|
|
node: firstAttr,
|
|
|
- message: "First JSX attribute should be on a new line",
|
|
|
|
|
|
|
+ message: "First JSX attribute should be exactly on the next line",
|
|
|
fix(fixer) {
|
|
fix(fixer) {
|
|
|
const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
|
|
const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
|
|
|
const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
@@ -160,10 +216,10 @@ module.exports = {
|
|
|
const current = node.attributes[i];
|
|
const current = node.attributes[i];
|
|
|
const next = node.attributes[i + 1];
|
|
const next = node.attributes[i + 1];
|
|
|
|
|
|
|
|
- if (current.loc.end.line === next.loc.start.line) {
|
|
|
|
|
|
|
+ if (current.loc.end.line !== next.loc.start.line - 1) {
|
|
|
context.report({
|
|
context.report({
|
|
|
node: next,
|
|
node: next,
|
|
|
- message: "Each JSX attribute should be on a new line",
|
|
|
|
|
|
|
+ message: "Each JSX attribute should be exactly on the next line (no empty lines)",
|
|
|
fix(fixer) {
|
|
fix(fixer) {
|
|
|
const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
|
|
const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
|
|
|
const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
@@ -193,37 +249,53 @@ module.exports = {
|
|
|
|
|
|
|
|
const closingBracket = allTokens[allTokens.length - 1];
|
|
const closingBracket = allTokens[allTokens.length - 1];
|
|
|
|
|
|
|
|
- if (lastAttr.loc.end.line === closingBracket.loc.start.line) {
|
|
|
|
|
- context.report({
|
|
|
|
|
- node: closingBracket,
|
|
|
|
|
- message: "JSX closing bracket should be on a new line",
|
|
|
|
|
- fix(fixer) {
|
|
|
|
|
- const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
|
|
|
|
|
- const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
|
|
|
|
|
+ // Check if self-closing slash is present
|
|
|
|
|
+ let hasSelfClosingSlash = false;
|
|
|
|
|
+ let slashToken = null;
|
|
|
|
|
|
|
|
- if (node.selfClosing && allTokens.length >= 2) {
|
|
|
|
|
- const slashToken = allTokens[allTokens.length - 2];
|
|
|
|
|
-
|
|
|
|
|
- if (slashToken && slashToken.value === "/") {
|
|
|
|
|
- return fixer.replaceTextRange(
|
|
|
|
|
- [
|
|
|
|
|
- lastAttr.range[1],
|
|
|
|
|
- slashToken.range[0]
|
|
|
|
|
- ],
|
|
|
|
|
- "\n" + baseIndent
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (node.selfClosing && allTokens.length >= 2) {
|
|
|
|
|
+ slashToken = allTokens[allTokens.length - 2];
|
|
|
|
|
+ if (slashToken && slashToken.value === "/") {
|
|
|
|
|
+ hasSelfClosingSlash = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return fixer.replaceTextRange(
|
|
|
|
|
- [
|
|
|
|
|
- lastAttr.range[1],
|
|
|
|
|
- closingBracket.range[0]
|
|
|
|
|
- ],
|
|
|
|
|
- "\n" + baseIndent
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ if (hasSelfClosingSlash) {
|
|
|
|
|
+ if (lastAttr.loc.end.line !== slashToken.loc.start.line - 1) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: slashToken,
|
|
|
|
|
+ message: "JSX closing slash should be exactly on the next line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ lastAttr.range[1],
|
|
|
|
|
+ slashToken.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + baseIndent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (lastAttr.loc.end.line !== closingBracket.loc.start.line - 1) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: closingBracket,
|
|
|
|
|
+ message: "JSX closing bracket should be exactly on the next line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ lastAttr.range[1],
|
|
|
|
|
+ closingBracket.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + baseIndent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
@@ -240,15 +312,19 @@ module.exports = {
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
ArrayExpression(node) {
|
|
ArrayExpression(node) {
|
|
|
- if (node.elements.length <= 1) return;
|
|
|
|
|
|
|
+ if (node.elements.length === 0) return;
|
|
|
|
|
|
|
|
const openBracket = sourceCode.getFirstToken(node);
|
|
const openBracket = sourceCode.getFirstToken(node);
|
|
|
|
|
+ const closingBracket = sourceCode.getLastToken(node);
|
|
|
|
|
+
|
|
|
|
|
+ if (openBracket.loc.start.line === closingBracket.loc.end.line) return;
|
|
|
|
|
+
|
|
|
const firstElement = node.elements[0];
|
|
const firstElement = node.elements[0];
|
|
|
|
|
|
|
|
- if (openBracket.loc.end.line === firstElement.loc.start.line) {
|
|
|
|
|
|
|
+ if (openBracket.loc.end.line !== firstElement.loc.start.line - 1) {
|
|
|
context.report({
|
|
context.report({
|
|
|
node: firstElement,
|
|
node: firstElement,
|
|
|
- message: "First array element should be on a new line",
|
|
|
|
|
|
|
+ message: "First array element should be exactly on the next line",
|
|
|
fix(fixer) {
|
|
fix(fixer) {
|
|
|
const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
|
|
const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
|
|
|
const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
@@ -271,10 +347,10 @@ module.exports = {
|
|
|
|
|
|
|
|
if (!current || !next) continue;
|
|
if (!current || !next) continue;
|
|
|
|
|
|
|
|
- if (current.loc.end.line === next.loc.start.line) {
|
|
|
|
|
|
|
+ if (current.loc.end.line !== next.loc.start.line - 1) {
|
|
|
context.report({
|
|
context.report({
|
|
|
node: next,
|
|
node: next,
|
|
|
- message: "Each array element should be on a new line",
|
|
|
|
|
|
|
+ message: "Each array element should be exactly on the next line (no empty lines)",
|
|
|
fix(fixer) {
|
|
fix(fixer) {
|
|
|
const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
|
|
const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
|
|
|
const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
@@ -295,19 +371,22 @@ module.exports = {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const lastElement = node.elements[node.elements.length - 1];
|
|
const lastElement = node.elements[node.elements.length - 1];
|
|
|
- const closingBracket = sourceCode.getLastToken(node);
|
|
|
|
|
|
|
|
|
|
- if (lastElement && lastElement.loc.end.line === closingBracket.loc.start.line) {
|
|
|
|
|
|
|
+ if (lastElement && lastElement.loc.end.line !== closingBracket.loc.start.line - 1) {
|
|
|
context.report({
|
|
context.report({
|
|
|
node: closingBracket,
|
|
node: closingBracket,
|
|
|
- message: "Array closing bracket should be on a new line",
|
|
|
|
|
|
|
+ message: "Array closing bracket should be exactly on the next line",
|
|
|
fix(fixer) {
|
|
fix(fixer) {
|
|
|
const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
|
|
const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
|
|
|
const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
|
|
|
|
|
|
|
|
+ const tokenAfter = sourceCode.getTokenAfter(lastElement);
|
|
|
|
|
+ const hasTrailingComma = tokenAfter && tokenAfter.value === ",";
|
|
|
|
|
+ const startRange = hasTrailingComma ? tokenAfter.range[1] : lastElement.range[1];
|
|
|
|
|
+
|
|
|
return fixer.replaceTextRange(
|
|
return fixer.replaceTextRange(
|
|
|
[
|
|
[
|
|
|
- lastElement.range[1],
|
|
|
|
|
|
|
+ startRange,
|
|
|
closingBracket.range[0]
|
|
closingBracket.range[0]
|
|
|
],
|
|
],
|
|
|
"\n" + baseIndent
|
|
"\n" + baseIndent
|
|
@@ -345,7 +424,6 @@ module.exports = {
|
|
|
ImportDeclaration(node) {
|
|
ImportDeclaration(node) {
|
|
|
imports.push(node);
|
|
imports.push(node);
|
|
|
},
|
|
},
|
|
|
-
|
|
|
|
|
"Program:exit"() {
|
|
"Program:exit"() {
|
|
|
if (imports.length <= 1) return;
|
|
if (imports.length <= 1) return;
|
|
|
|
|
|
|
@@ -450,5 +528,326 @@ module.exports = {
|
|
|
], newImports);
|
|
], newImports);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ },
|
|
|
|
|
+ "multiline-jsx-children": {
|
|
|
|
|
+ meta: {
|
|
|
|
|
+ type: "layout",
|
|
|
|
|
+ fixable: "whitespace",
|
|
|
|
|
+ schema: []
|
|
|
|
|
+ },
|
|
|
|
|
+ create(context) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ JSXElement(node) {
|
|
|
|
|
+ if (!node.closingElement) return;
|
|
|
|
|
+ if (node.children.length === 0) return;
|
|
|
|
|
+
|
|
|
|
|
+ const meaningfulChildren = node.children.filter(child => {
|
|
|
|
|
+ if (child.type === "JSXText" && child.value.trim() === "") return false;
|
|
|
|
|
+ return true;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (meaningfulChildren.length === 0) return;
|
|
|
|
|
+
|
|
|
|
|
+ const openingElement = node.openingElement;
|
|
|
|
|
+ const closingElement = node.closingElement;
|
|
|
|
|
+ const isMultilineOpening = openingElement.loc.start.line !== openingElement.loc.end.line;
|
|
|
|
|
+
|
|
|
|
|
+ if (!isMultilineOpening) return;
|
|
|
|
|
+
|
|
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
|
|
+
|
|
|
|
|
+ if (meaningfulChildren.length === 1 && meaningfulChildren[0].type === "JSXText") {
|
|
|
|
|
+ const child = meaningfulChildren[0];
|
|
|
|
|
+ const text = sourceCode.getText(child);
|
|
|
|
|
+ const hasLeadingNewline = /^\s*\n/.test(text);
|
|
|
|
|
+ const hasTrailingNewline = /\n\s*$/.test(text);
|
|
|
|
|
+
|
|
|
|
|
+ if (!hasLeadingNewline || !hasTrailingNewline) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: child,
|
|
|
|
|
+ message: "Multiline JSX element text content must be on its own line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
|
|
|
+ const indent = baseIndent + " ";
|
|
|
|
|
+ return fixer.replaceText(child, "\n" + indent + text.trim() + "\n" + baseIndent);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const firstChild = meaningfulChildren[0];
|
|
|
|
|
+ const openingEndLine = openingElement.loc.end.line;
|
|
|
|
|
+
|
|
|
|
|
+ let needsFirstChildFix = false;
|
|
|
|
|
+ if (firstChild.type === "JSXText") {
|
|
|
|
|
+ const text = sourceCode.getText(firstChild);
|
|
|
|
|
+ if (!/^\s*\n/.test(text)) needsFirstChildFix = true;
|
|
|
|
|
+ } else if (firstChild.loc.start.line === openingEndLine) {
|
|
|
|
|
+ needsFirstChildFix = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (needsFirstChildFix) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: firstChild,
|
|
|
|
|
+ message: "First child of a multiline JSX element must be on a new line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
|
|
|
+ const indent = baseIndent + " ";
|
|
|
|
|
+
|
|
|
|
|
+ if (firstChild.type === "JSXText") {
|
|
|
|
|
+ const text = sourceCode.getText(firstChild);
|
|
|
|
|
+ return fixer.replaceText(firstChild, "\n" + indent + text.replace(/^\s+/, ""));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.insertTextBefore(firstChild, "\n" + indent);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const lastChild = meaningfulChildren[meaningfulChildren.length - 1];
|
|
|
|
|
+ let needsLastChildFix = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (lastChild.type === "JSXText") {
|
|
|
|
|
+ const text = sourceCode.getText(lastChild);
|
|
|
|
|
+ if (!/\n\s*$/.test(text)) needsLastChildFix = true;
|
|
|
|
|
+ } else if (lastChild.loc.end.line === closingElement.loc.start.line) {
|
|
|
|
|
+ needsLastChildFix = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (needsLastChildFix) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: closingElement,
|
|
|
|
|
+ message: "Closing tag of a multiline JSX element must be on a new line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBracketLine = sourceCode.lines[openingElement.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
|
|
|
+
|
|
|
|
|
+ if (lastChild.type === "JSXText") {
|
|
|
|
|
+ const text = sourceCode.getText(lastChild);
|
|
|
|
|
+ return fixer.replaceText(lastChild, text.replace(/\s+$/, "") + "\n" + baseIndent);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.insertTextBefore(closingElement, "\n" + baseIndent);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ "no-jsx-parens-in-return": {
|
|
|
|
|
+ meta: {
|
|
|
|
|
+ type: "layout",
|
|
|
|
|
+ fixable: "code",
|
|
|
|
|
+ schema: []
|
|
|
|
|
+ },
|
|
|
|
|
+ create(context) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ReturnStatement(node) {
|
|
|
|
|
+ if (!node.argument) return;
|
|
|
|
|
+ if (node.argument.type === "JSXElement" || node.argument.type === "JSXFragment") {
|
|
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
|
|
+ const returnToken = sourceCode.getFirstToken(node);
|
|
|
|
|
+ const firstChildToken = sourceCode.getFirstToken(node.argument);
|
|
|
|
|
+
|
|
|
|
|
+ const tokensBetween = sourceCode.getTokensBetween(returnToken, firstChildToken);
|
|
|
|
|
+ const openParen = tokensBetween.find(t => t.value === "(");
|
|
|
|
|
+
|
|
|
|
|
+ if (openParen) {
|
|
|
|
|
+ const closeParen = sourceCode.getTokenAfter(node.argument);
|
|
|
|
|
+ if (closeParen && closeParen.value === ")") {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: node.argument,
|
|
|
|
|
+ message: "Returned JSX element should not be wrapped in parentheses",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const fixes = [
|
|
|
|
|
+ fixer.remove(closeParen)
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ if (returnToken.loc.end.line !== firstChildToken.loc.start.line) {
|
|
|
|
|
+ fixes.push(fixer.replaceTextRange([
|
|
|
|
|
+ returnToken.range[1],
|
|
|
|
|
+ firstChildToken.range[0]
|
|
|
|
|
+ ], " "));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ fixes.push(fixer.remove(openParen));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return fixes;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (returnToken.loc.end.line !== firstChildToken.loc.start.line) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: node.argument,
|
|
|
|
|
+ message: "Returned JSX element must start on the same line as 'return' to avoid Automatic Semicolon Insertion (ASI)",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ return fixer.replaceTextRange([
|
|
|
|
|
+ returnToken.range[1],
|
|
|
|
|
+ firstChildToken.range[0]
|
|
|
|
|
+ ], " ");
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ ArrowFunctionExpression(node) {
|
|
|
|
|
+ if (node.body.type === "JSXElement" || node.body.type === "JSXFragment") {
|
|
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
|
|
+ const arrowToken = sourceCode.getTokenBefore(node.body, t => t.value === "=>");
|
|
|
|
|
+ const firstChildToken = sourceCode.getFirstToken(node.body);
|
|
|
|
|
+
|
|
|
|
|
+ const tokensBetween = sourceCode.getTokensBetween(arrowToken, firstChildToken);
|
|
|
|
|
+ const openParen = tokensBetween.find(t => t.value === "(");
|
|
|
|
|
+
|
|
|
|
|
+ if (openParen) {
|
|
|
|
|
+ const closeParen = sourceCode.getTokenAfter(node.body);
|
|
|
|
|
+ if (closeParen && closeParen.value === ")") {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: node.body,
|
|
|
|
|
+ message: "Implicitly returned JSX element should not be wrapped in parentheses",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const fixes = [
|
|
|
|
|
+ fixer.remove(closeParen)
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ if (arrowToken.loc.end.line !== firstChildToken.loc.start.line) {
|
|
|
|
|
+ fixes.push(fixer.replaceTextRange([
|
|
|
|
|
+ arrowToken.range[1],
|
|
|
|
|
+ firstChildToken.range[0]
|
|
|
|
|
+ ], " "));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ fixes.push(fixer.remove(openParen));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return fixes;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (arrowToken.loc.end.line !== firstChildToken.loc.start.line) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: node.body,
|
|
|
|
|
+ message: "Implicitly returned JSX element must start on the same line as '=>'",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ return fixer.replaceTextRange([
|
|
|
|
|
+ arrowToken.range[1],
|
|
|
|
|
+ firstChildToken.range[0]
|
|
|
|
|
+ ], " ");
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ "no-newline-after-jsx": {
|
|
|
|
|
+ meta: {
|
|
|
|
|
+ type: "layout",
|
|
|
|
|
+ fixable: "whitespace",
|
|
|
|
|
+ schema: []
|
|
|
|
|
+ },
|
|
|
|
|
+ create(context) {
|
|
|
|
|
+ function checkPunctuation(node) {
|
|
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
|
|
+ const lastToken = sourceCode.getLastToken(node);
|
|
|
|
|
+ const nextToken = sourceCode.getTokenAfter(node);
|
|
|
|
|
+
|
|
|
|
|
+ if (nextToken && (nextToken.value === ";" || nextToken.value === "," || nextToken.value === ")")) {
|
|
|
|
|
+ if (lastToken.loc.end.line !== nextToken.loc.start.line) {
|
|
|
|
|
+ if (sourceCode.commentsExistBetween(node, nextToken)) return;
|
|
|
|
|
+
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: nextToken,
|
|
|
|
|
+ message: `Punctuation '${nextToken.value}' should be on the same line as the preceding JSX element`,
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ return fixer.replaceTextRange([
|
|
|
|
|
+ node.range[1],
|
|
|
|
|
+ nextToken.range[0]
|
|
|
|
|
+ ], "");
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ JSXElement: checkPunctuation,
|
|
|
|
|
+ JSXFragment: checkPunctuation
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ "no-static-inline-styles": {
|
|
|
|
|
+ meta: {
|
|
|
|
|
+ type: "suggestion",
|
|
|
|
|
+ messages: {
|
|
|
|
|
+ staticStyle: "Static inline styles are not allowed. Please move them to a stylesheet file."
|
|
|
|
|
+ },
|
|
|
|
|
+ schema: []
|
|
|
|
|
+ },
|
|
|
|
|
+ create(context) {
|
|
|
|
|
+ function checkProperty(propertyNode) {
|
|
|
|
|
+ if (!propertyNode || propertyNode.type !== "Property") return;
|
|
|
|
|
+
|
|
|
|
|
+ const value = propertyNode.value;
|
|
|
|
|
+ if (!value) return;
|
|
|
|
|
+
|
|
|
|
|
+ let isStatic = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (value.type === "Literal") {
|
|
|
|
|
+ if (typeof value.value === "string" || typeof value.value === "number") {
|
|
|
|
|
+ isStatic = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (value.type === "UnaryExpression" && value.operator === "-") {
|
|
|
|
|
+ if (value.argument && value.argument.type === "Literal" && typeof value.argument.value === "number") {
|
|
|
|
|
+ isStatic = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
|
|
|
|
|
+ isStatic = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (isStatic) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: propertyNode,
|
|
|
|
|
+ messageId: "staticStyle"
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function checkObjectExpression(node) {
|
|
|
|
|
+ if (node.type !== "ObjectExpression") return;
|
|
|
|
|
+ node.properties.forEach(checkProperty);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function checkStyleValue(node) {
|
|
|
|
|
+ if (!node) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (node.type === "ObjectExpression") {
|
|
|
|
|
+ checkObjectExpression(node);
|
|
|
|
|
+ } else if (node.type === "ArrayExpression") {
|
|
|
|
|
+ node.elements.forEach(element => {
|
|
|
|
|
+ if (element && element.type === "ObjectExpression") {
|
|
|
|
|
+ checkObjectExpression(element);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ JSXAttribute(node) {
|
|
|
|
|
+ const attrName = node.name.name;
|
|
|
|
|
+ if (typeof attrName !== "string") return;
|
|
|
|
|
+
|
|
|
|
|
+ if (attrName === "style" || attrName.endsWith("Style")) {
|
|
|
|
|
+ if (node.value && node.value.type === "JSXExpressionContainer") {
|
|
|
|
|
+ checkStyleValue(node.value.expression);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|