|
@@ -0,0 +1,454 @@
|
|
|
|
|
+module.exports = {
|
|
|
|
|
+ "multiline-import-specifiers": {
|
|
|
|
|
+ meta: {
|
|
|
|
|
+ type: "layout",
|
|
|
|
|
+ fixable: "whitespace",
|
|
|
|
|
+ schema: []
|
|
|
|
|
+ },
|
|
|
|
|
+ create(context) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ImportDeclaration(node) {
|
|
|
|
|
+ const specifiers = node.specifiers.filter(
|
|
|
|
|
+ s => s.type === "ImportSpecifier"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (specifiers.length <= 1) return;
|
|
|
|
|
+
|
|
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < specifiers.length - 1; i++) {
|
|
|
|
|
+ const current = specifiers[i];
|
|
|
|
|
+ const next = specifiers[i + 1];
|
|
|
|
|
+
|
|
|
|
|
+ if (current.loc.end.line === next.loc.start.line) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: next,
|
|
|
|
|
+ message: "Each import specifier should be on a new line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const comma = sourceCode.getTokenBefore(next);
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ comma.range[1],
|
|
|
|
|
+ next.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n "
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const lastSpecifier = specifiers[specifiers.length - 1];
|
|
|
|
|
+ const tokenAfter = sourceCode.getTokenAfter(lastSpecifier);
|
|
|
|
|
+
|
|
|
|
|
+ if (tokenAfter && tokenAfter.value === ",") {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: lastSpecifier,
|
|
|
|
|
+ message: "No trailing comma in imports",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ return fixer.remove(tokenAfter);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ "multiline-object-properties": {
|
|
|
|
|
+ meta: {
|
|
|
|
|
+ type: "layout",
|
|
|
|
|
+ fixable: "whitespace",
|
|
|
|
|
+ schema: []
|
|
|
|
|
+ },
|
|
|
|
|
+ create(context) {
|
|
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
|
|
+
|
|
|
|
|
+ function checkProperties(node, properties) {
|
|
|
|
|
+ if (properties.length <= 1) return;
|
|
|
|
|
+
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ 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 + " ";
|
|
|
|
|
+
|
|
|
|
|
+ const comma = sourceCode.getTokenBefore(next);
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ comma.range[1],
|
|
|
|
|
+ next.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + indent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const lastProperty = properties[properties.length - 1];
|
|
|
|
|
+ const tokenAfter = sourceCode.getTokenAfter(lastProperty);
|
|
|
|
|
+
|
|
|
|
|
+ if (tokenAfter && tokenAfter.value === ",") {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: lastProperty,
|
|
|
|
|
+ message: "No trailing comma in objects",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ return fixer.remove(tokenAfter);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ ObjectExpression(node) {
|
|
|
|
|
+ checkProperties(node, node.properties);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ ObjectPattern(node) {
|
|
|
|
|
+ checkProperties(node, node.properties);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ "multiline-jsx-attributes": {
|
|
|
|
|
+ meta: {
|
|
|
|
|
+ type: "layout",
|
|
|
|
|
+ fixable: "whitespace",
|
|
|
|
|
+ schema: []
|
|
|
|
|
+ },
|
|
|
|
|
+ create(context) {
|
|
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ JSXOpeningElement(node) {
|
|
|
|
|
+ if (node.attributes.length <= 1) return;
|
|
|
|
|
+
|
|
|
|
|
+ const firstToken = sourceCode.getFirstToken(node);
|
|
|
|
|
+ const tagNameToken = sourceCode.getTokenAfter(firstToken);
|
|
|
|
|
+ const firstAttr = node.attributes[0];
|
|
|
|
|
+
|
|
|
|
|
+ if (tagNameToken.loc.end.line === firstAttr.loc.start.line) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: firstAttr,
|
|
|
|
|
+ message: "First JSX attribute should be on a new line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
|
|
|
+ const indent = baseIndent + " ";
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ tagNameToken.range[1],
|
|
|
|
|
+ firstAttr.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + indent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < node.attributes.length - 1; i++) {
|
|
|
|
|
+ const current = node.attributes[i];
|
|
|
|
|
+ const next = node.attributes[i + 1];
|
|
|
|
|
+
|
|
|
|
|
+ if (current.loc.end.line === next.loc.start.line) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: next,
|
|
|
|
|
+ message: "Each JSX attribute should be on a new line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBraceLine = sourceCode.lines[firstToken.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBraceLine.match(/^\s*/)[0];
|
|
|
|
|
+ const indent = baseIndent + " ";
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ current.range[1],
|
|
|
|
|
+ next.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + indent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const lastAttr = node.attributes[node.attributes.length - 1];
|
|
|
|
|
+
|
|
|
|
|
+ const allTokens = [];
|
|
|
|
|
+ let currentToken = sourceCode.getTokenAfter(lastAttr);
|
|
|
|
|
+
|
|
|
|
|
+ while (currentToken && currentToken.range[1] <= node.range[1]) {
|
|
|
|
|
+ allTokens.push(currentToken);
|
|
|
|
|
+ currentToken = sourceCode.getTokenAfter(currentToken);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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];
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ lastAttr.range[1],
|
|
|
|
|
+ closingBracket.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + baseIndent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ "multiline-array-elements": {
|
|
|
|
|
+ meta: {
|
|
|
|
|
+ type: "layout",
|
|
|
|
|
+ fixable: "whitespace",
|
|
|
|
|
+ schema: []
|
|
|
|
|
+ },
|
|
|
|
|
+ create(context) {
|
|
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ ArrayExpression(node) {
|
|
|
|
|
+ if (node.elements.length <= 1) return;
|
|
|
|
|
+
|
|
|
|
|
+ const openBracket = sourceCode.getFirstToken(node);
|
|
|
|
|
+ const firstElement = node.elements[0];
|
|
|
|
|
+
|
|
|
|
|
+ if (openBracket.loc.end.line === firstElement.loc.start.line) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: firstElement,
|
|
|
|
|
+ message: "First array element should be on a new line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
|
|
|
+ const indent = baseIndent + " ";
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ openBracket.range[1],
|
|
|
|
|
+ firstElement.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + indent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < node.elements.length - 1; i++) {
|
|
|
|
|
+ const current = node.elements[i];
|
|
|
|
|
+ const next = node.elements[i + 1];
|
|
|
|
|
+
|
|
|
|
|
+ if (!current || !next) continue;
|
|
|
|
|
+
|
|
|
|
|
+ if (current.loc.end.line === next.loc.start.line) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: next,
|
|
|
|
|
+ message: "Each array element should be on a new line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
|
|
|
+ const indent = baseIndent + " ";
|
|
|
|
|
+
|
|
|
|
|
+ const comma = sourceCode.getTokenBefore(next);
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ comma.range[1],
|
|
|
|
|
+ next.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + indent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const lastElement = node.elements[node.elements.length - 1];
|
|
|
|
|
+ const closingBracket = sourceCode.getLastToken(node);
|
|
|
|
|
+
|
|
|
|
|
+ if (lastElement && lastElement.loc.end.line === closingBracket.loc.start.line) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: closingBracket,
|
|
|
|
|
+ message: "Array closing bracket should be on a new line",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ const openBracketLine = sourceCode.lines[openBracket.loc.start.line - 1];
|
|
|
|
|
+ const baseIndent = openBracketLine.match(/^\s*/)[0];
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.replaceTextRange(
|
|
|
|
|
+ [
|
|
|
|
|
+ lastElement.range[1],
|
|
|
|
|
+ closingBracket.range[0]
|
|
|
|
|
+ ],
|
|
|
|
|
+ "\n" + baseIndent
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const tokenAfter = sourceCode.getTokenAfter(lastElement);
|
|
|
|
|
+
|
|
|
|
|
+ if (tokenAfter && tokenAfter.value === "," && tokenAfter.range[1] <= closingBracket.range[0]) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: lastElement,
|
|
|
|
|
+ message: "No trailing comma in arrays",
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ return fixer.remove(tokenAfter);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ "custom-import-order": {
|
|
|
|
|
+ meta: {
|
|
|
|
|
+ type: "suggestion",
|
|
|
|
|
+ fixable: "code",
|
|
|
|
|
+ schema: []
|
|
|
|
|
+ },
|
|
|
|
|
+ create(context) {
|
|
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
|
|
+ const imports = [];
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ ImportDeclaration(node) {
|
|
|
|
|
+ imports.push(node);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ "Program:exit"() {
|
|
|
|
|
+ if (imports.length <= 1) return;
|
|
|
|
|
+
|
|
|
|
|
+ const categorized = categorizeImports(imports, sourceCode);
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < imports.length - 1; i++) {
|
|
|
|
|
+ const current = imports[i];
|
|
|
|
|
+ const next = imports[i + 1];
|
|
|
|
|
+
|
|
|
|
|
+ const currentCategory = getImportCategory(current, sourceCode);
|
|
|
|
|
+ const nextCategory = getImportCategory(next, sourceCode);
|
|
|
|
|
+
|
|
|
|
|
+ if (currentCategory > nextCategory) {
|
|
|
|
|
+ context.report({
|
|
|
|
|
+ node: next,
|
|
|
|
|
+ message: `Import from "${next.source.value}" should come before "${current.source.value}"`,
|
|
|
|
|
+ fix(fixer) {
|
|
|
|
|
+ return fixImportOrder(fixer, imports, categorized, sourceCode);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ function getImportCategory(node, sourceCode) {
|
|
|
|
|
+ const source = node.source.value;
|
|
|
|
|
+ const code = sourceCode.getText(node);
|
|
|
|
|
+
|
|
|
|
|
+ if (source === "react") return 1;
|
|
|
|
|
+
|
|
|
|
|
+ if (source === "react-native") return 2;
|
|
|
|
|
+
|
|
|
|
|
+ if (source.startsWith("./")) {
|
|
|
|
|
+ if (code.includes("import type") || source.includes(".types") || source.includes("/type")) {
|
|
|
|
|
+ return 3.1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (source.includes("style") || source.includes("Style")) {
|
|
|
|
|
+ return 3.2;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return 3.3;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const specifiers = node.specifiers
|
|
|
|
|
+ .filter(s => s.type === "ImportSpecifier")
|
|
|
|
|
+ .map(s => s.imported ? s.imported.name : "");
|
|
|
|
|
+
|
|
|
|
|
+ const hasNCoreHook = specifiers.some(name => name.startsWith("NCore"));
|
|
|
|
|
+ const hasUseHook = specifiers.some(name => name.startsWith("use"));
|
|
|
|
|
+
|
|
|
|
|
+ if (hasNCoreHook) return 4.1;
|
|
|
|
|
+ if (hasUseHook) return 4.2;
|
|
|
|
|
+
|
|
|
|
|
+ if (code.includes("import type") || source.includes("/types/") || source.includes("/type") || source.includes(".types")) {
|
|
|
|
|
+ return 5;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (source.startsWith("@/") || source.startsWith("~/")) return 6.3;
|
|
|
|
|
+ if (!source.startsWith(".")) return 6.1;
|
|
|
|
|
+ if (source.startsWith("../")) return 6.2;
|
|
|
|
|
+
|
|
|
|
|
+ return 6.4;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function categorizeImports(imports, sourceCode) {
|
|
|
|
|
+ return imports.map(imp => ({
|
|
|
|
|
+ node: imp,
|
|
|
|
|
+ category: getImportCategory(imp, sourceCode),
|
|
|
|
|
+ source: imp.source.value
|
|
|
|
|
+ })).sort((a, b) => {
|
|
|
|
|
+ if (a.category !== b.category) {
|
|
|
|
|
+ return a.category - b.category;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return a.source.localeCompare(b.source);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function fixImportOrder(fixer, imports, categorized, sourceCode) {
|
|
|
|
|
+ const firstImport = imports[0];
|
|
|
|
|
+ const lastImport = imports[imports.length - 1];
|
|
|
|
|
+
|
|
|
|
|
+ const rangeStart = firstImport.range[0];
|
|
|
|
|
+ const rangeEnd = lastImport.range[1];
|
|
|
|
|
+
|
|
|
|
|
+ let newImports = "";
|
|
|
|
|
+
|
|
|
|
|
+ categorized.forEach((imp, index) => {
|
|
|
|
|
+ newImports += sourceCode.getText(imp.node);
|
|
|
|
|
+
|
|
|
|
|
+ if (index < categorized.length - 1) {
|
|
|
|
|
+ newImports += "\n";
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return fixer.replaceTextRange([
|
|
|
|
|
+ rangeStart,
|
|
|
|
|
+ rangeEnd
|
|
|
|
|
+ ], newImports);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+};
|