|
|
@@ -12,18 +12,36 @@ module.exports = {
|
|
|
s => s.type === "ImportSpecifier"
|
|
|
);
|
|
|
|
|
|
- if (specifiers.length <= 1) return;
|
|
|
+ if (specifiers.length === 0) return;
|
|
|
|
|
|
const sourceCode = context.getSourceCode();
|
|
|
+ const openBrace = sourceCode.getFirstToken(node, t => t.value === "{");
|
|
|
+ const closeBrace = sourceCode.getLastToken(node, t => t.value === "}");
|
|
|
+
|
|
|
+ if (!openBrace || !closeBrace) return;
|
|
|
+
|
|
|
+ const firstSpecifier = specifiers[0];
|
|
|
+ if (firstSpecifier.loc.start.line - openBrace.loc.end.line !== 1) {
|
|
|
+ context.report({
|
|
|
+ node: firstSpecifier,
|
|
|
+ message: "There should be exactly one newline after '{'.",
|
|
|
+ fix(fixer) {
|
|
|
+ return fixer.replaceTextRange([
|
|
|
+ openBrace.range[1],
|
|
|
+ firstSpecifier.range[0]
|
|
|
+ ], "\n ");
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
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) {
|
|
|
+ if (next.loc.start.line - current.loc.end.line !== 1) {
|
|
|
context.report({
|
|
|
node: next,
|
|
|
- message: "Each import specifier should be on a new line",
|
|
|
+ message: "Each import specifier should be on a new line without blank lines between them.",
|
|
|
fix(fixer) {
|
|
|
const comma = sourceCode.getTokenBefore(next);
|
|
|
return fixer.replaceTextRange(
|
|
|
@@ -50,6 +68,20 @@ module.exports = {
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
+
|
|
|
+ const tokenBeforeClose = sourceCode.getTokenBefore(closeBrace);
|
|
|
+ if (closeBrace.loc.start.line - tokenBeforeClose.loc.end.line !== 1) {
|
|
|
+ context.report({
|
|
|
+ node: closeBrace,
|
|
|
+ message: "There should be exactly one newline before '}'.",
|
|
|
+ fix(fixer) {
|
|
|
+ return fixer.replaceTextRange([
|
|
|
+ tokenBeforeClose.range[1],
|
|
|
+ closeBrace.range[0]
|
|
|
+ ], "\n");
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
@@ -311,7 +343,7 @@ module.exports = {
|
|
|
const sourceCode = context.getSourceCode();
|
|
|
|
|
|
return {
|
|
|
- ArrayExpression(node) {
|
|
|
+ "ArrayExpression, ArrayPattern"(node) {
|
|
|
if (node.elements.length === 0) return;
|
|
|
|
|
|
const openBracket = sourceCode.getFirstToken(node);
|
|
|
@@ -429,24 +461,26 @@ module.exports = {
|
|
|
|
|
|
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);
|
|
|
+ let isSorted = true;
|
|
|
+ let errorNode = null;
|
|
|
|
|
|
- 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);
|
|
|
- }
|
|
|
- });
|
|
|
+ for (let i = 0; i < imports.length; i++) {
|
|
|
+ if (imports[i] !== categorized[i].node) {
|
|
|
+ isSorted = false;
|
|
|
+ errorNode = imports[i];
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ if (!isSorted) {
|
|
|
+ context.report({
|
|
|
+ node: errorNode,
|
|
|
+ message: "Imports are not sorted correctly according to custom rules.",
|
|
|
+ fix(fixer) {
|
|
|
+ return fixImportOrder(fixer, imports, categorized, sourceCode);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -492,15 +526,31 @@ module.exports = {
|
|
|
}
|
|
|
|
|
|
function categorizeImports(imports, sourceCode) {
|
|
|
- return imports.map(imp => ({
|
|
|
- node: imp,
|
|
|
- category: getImportCategory(imp, sourceCode),
|
|
|
- source: imp.source.value
|
|
|
- })).sort((a, b) => {
|
|
|
+ return imports.map(imp => {
|
|
|
+ const isDefaultImport = imp.specifiers.some(s => s.type === "ImportDefaultSpecifier");
|
|
|
+ const textLength = sourceCode.getText(imp).replace(/\s+/g, "").length;
|
|
|
+ return {
|
|
|
+ node: imp,
|
|
|
+ category: getImportCategory(imp, sourceCode),
|
|
|
+ source: imp.source.value,
|
|
|
+ isDefaultImport,
|
|
|
+ textLength
|
|
|
+ };
|
|
|
+ }).sort((a, b) => {
|
|
|
if (a.category !== b.category) {
|
|
|
return a.category - b.category;
|
|
|
}
|
|
|
|
|
|
+ if (a.category >= 6) {
|
|
|
+ if (a.isDefaultImport !== b.isDefaultImport) {
|
|
|
+ return a.isDefaultImport ? 1 : -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (a.textLength !== b.textLength) {
|
|
|
+ return b.textLength - a.textLength;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
return a.source.localeCompare(b.source);
|
|
|
});
|
|
|
}
|
|
|
@@ -993,5 +1043,639 @@ module.exports = {
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
+ },
|
|
|
+ "sort-jsx-props": {
|
|
|
+ meta: {
|
|
|
+ type: "layout",
|
|
|
+ fixable: "code",
|
|
|
+ schema: []
|
|
|
+ },
|
|
|
+ create(context) {
|
|
|
+ return {
|
|
|
+ JSXOpeningElement(node) {
|
|
|
+ const attributes = node.attributes;
|
|
|
+ if (!attributes || attributes.length <= 1) return;
|
|
|
+
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
+
|
|
|
+ const getAttrLength = (attr) => {
|
|
|
+ const text = sourceCode.getText(attr);
|
|
|
+ return text.replace(/\s+/g, "").length;
|
|
|
+ };
|
|
|
+
|
|
|
+ const groups = [];
|
|
|
+ let currentGroup = [];
|
|
|
+ for (const attr of attributes) {
|
|
|
+ if (attr.type === "JSXSpreadAttribute") {
|
|
|
+ if (currentGroup.length > 1) {
|
|
|
+ groups.push(currentGroup);
|
|
|
+ }
|
|
|
+ currentGroup = [];
|
|
|
+ } else {
|
|
|
+ currentGroup.push(attr);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (currentGroup.length > 1) {
|
|
|
+ groups.push(currentGroup);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const group of groups) {
|
|
|
+ const sortedGroup = [...group].sort((a, b) => {
|
|
|
+ return getAttrLength(b) - getAttrLength(a);
|
|
|
+ });
|
|
|
+
|
|
|
+ let isSorted = true;
|
|
|
+ for (let i = 0; i < group.length; i++) {
|
|
|
+ if (group[i] !== sortedGroup[i]) {
|
|
|
+ isSorted = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isSorted) {
|
|
|
+ context.report({
|
|
|
+ node: group[0],
|
|
|
+ message: "JSX attributes should be sorted from longest to shortest.",
|
|
|
+ fix(fixer) {
|
|
|
+ const firstAttr = group[0];
|
|
|
+ const startLoc = firstAttr.loc.start;
|
|
|
+ const indent = " ".repeat(startLoc.column);
|
|
|
+
|
|
|
+ const newText = sortedGroup.map(attr => sourceCode.getText(attr)).join("\n" + indent);
|
|
|
+
|
|
|
+ const start = group[0].range[0];
|
|
|
+ const end = group[group.length - 1].range[1];
|
|
|
+
|
|
|
+ return fixer.replaceTextRange([
|
|
|
+ start,
|
|
|
+ end
|
|
|
+ ], newText);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "sort-import-specifiers": {
|
|
|
+ meta: {
|
|
|
+ type: "layout",
|
|
|
+ fixable: "code",
|
|
|
+ schema: []
|
|
|
+ },
|
|
|
+ create(context) {
|
|
|
+ return {
|
|
|
+ ImportDeclaration(node) {
|
|
|
+ checkSpecifiers(node, "ImportSpecifier", "Import");
|
|
|
+ },
|
|
|
+ ExportNamedDeclaration(node) {
|
|
|
+ checkSpecifiers(node, "ExportSpecifier", "Export");
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ function checkSpecifiers(node, specifierType, typeName) {
|
|
|
+ if (!node.specifiers) return;
|
|
|
+ const specifiers = node.specifiers.filter(
|
|
|
+ s => s.type === specifierType
|
|
|
+ );
|
|
|
+
|
|
|
+ if (specifiers.length <= 1) return;
|
|
|
+
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
+
|
|
|
+ const getSpecLength = (spec) => {
|
|
|
+ return sourceCode.getText(spec).replace(/\s+/g, "").length;
|
|
|
+ };
|
|
|
+
|
|
|
+ const sortedSpecifiers = [...specifiers].sort((a, b) => {
|
|
|
+ return getSpecLength(b) - getSpecLength(a);
|
|
|
+ });
|
|
|
+
|
|
|
+ let isSorted = true;
|
|
|
+ for (let i = 0; i < specifiers.length; i++) {
|
|
|
+ if (specifiers[i] !== sortedSpecifiers[i]) {
|
|
|
+ isSorted = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isSorted) {
|
|
|
+ context.report({
|
|
|
+ node,
|
|
|
+ message: `${typeName} specifiers should be sorted from longest to shortest.`,
|
|
|
+ fix(fixer) {
|
|
|
+ const firstSpec = specifiers[0];
|
|
|
+ const lastSpec = specifiers[specifiers.length - 1];
|
|
|
+
|
|
|
+ const startLoc = firstSpec.loc.start;
|
|
|
+
|
|
|
+ let indent = " ";
|
|
|
+ if (firstSpec.loc.start.line !== node.loc.start.line) {
|
|
|
+ indent = " ".repeat(startLoc.column);
|
|
|
+ }
|
|
|
+
|
|
|
+ const newText = sortedSpecifiers.map(spec => sourceCode.getText(spec)).join(",\n" + indent);
|
|
|
+
|
|
|
+ return fixer.replaceTextRange([
|
|
|
+ firstSpec.range[0],
|
|
|
+ lastSpec.range[1]
|
|
|
+ ], newText);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "sort-translations": {
|
|
|
+ meta: {
|
|
|
+ type: "layout",
|
|
|
+ fixable: "code",
|
|
|
+ schema: []
|
|
|
+ },
|
|
|
+ create(context) {
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
+
|
|
|
+ function checkObject(node) {
|
|
|
+ if (!node.properties || node.properties.length <= 1) return;
|
|
|
+
|
|
|
+ let hasIsRTL = false;
|
|
|
+ let hasLocale = false;
|
|
|
+ let translationsProp = null;
|
|
|
+
|
|
|
+ for (const prop of node.properties) {
|
|
|
+ const keyNode = prop.key;
|
|
|
+ if (!keyNode) continue;
|
|
|
+
|
|
|
+ let keyName = "";
|
|
|
+ if (keyNode.type === "Identifier") keyName = keyNode.name;
|
|
|
+ else if (keyNode.type === "Literal" || keyNode.type === "JSONLiteral") keyName = keyNode.value;
|
|
|
+
|
|
|
+ if (keyName === "isRTL") hasIsRTL = true;
|
|
|
+ if (keyName === "locale") hasLocale = true;
|
|
|
+ if (keyName === "translations") translationsProp = prop;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hasIsRTL && hasLocale && translationsProp) {
|
|
|
+ const translationsObj = translationsProp.value;
|
|
|
+ if (!translationsObj || !translationsObj.properties || translationsObj.properties.length <= 1) return;
|
|
|
+
|
|
|
+ const props = translationsObj.properties;
|
|
|
+
|
|
|
+ const sortedProps = [...props].sort((a, b) => {
|
|
|
+ const lenA = sourceCode.getText(a.key).length + sourceCode.getText(a.value).length;
|
|
|
+ const lenB = sourceCode.getText(b.key).length + sourceCode.getText(b.value).length;
|
|
|
+
|
|
|
+ if (lenA !== lenB) {
|
|
|
+ return lenB - lenA;
|
|
|
+ }
|
|
|
+
|
|
|
+ const keyA = a.key.value || a.key.name || "";
|
|
|
+ const keyB = b.key.value || b.key.name || "";
|
|
|
+ return keyA.localeCompare(keyB);
|
|
|
+ });
|
|
|
+
|
|
|
+ let isSorted = true;
|
|
|
+ let errorNode = null;
|
|
|
+ for (let i = 0; i < props.length; i++) {
|
|
|
+ if (props[i] !== sortedProps[i]) {
|
|
|
+ isSorted = false;
|
|
|
+ errorNode = props[i];
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isSorted) {
|
|
|
+ context.report({
|
|
|
+ node: errorNode,
|
|
|
+ message: "Translations keys must be sorted by length (longest to shortest).",
|
|
|
+ fix(fixer) {
|
|
|
+ const firstProp = props[0];
|
|
|
+ const lastProp = props[props.length - 1];
|
|
|
+
|
|
|
+ const startLoc = firstProp.loc.start;
|
|
|
+ const indent = " ".repeat(startLoc.column);
|
|
|
+
|
|
|
+ let newText = "";
|
|
|
+ sortedProps.forEach((p, index) => {
|
|
|
+ newText += sourceCode.getText(p);
|
|
|
+ if (index < sortedProps.length - 1) {
|
|
|
+ newText += ",\n" + indent;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return fixer.replaceTextRange([
|
|
|
+ firstProp.range[0],
|
|
|
+ lastProp.range[1]
|
|
|
+ ], newText);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ ObjectExpression: checkObject,
|
|
|
+ JSONObjectExpression: checkObject
|
|
|
+ };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "ncore-sort-styles": {
|
|
|
+ meta: {
|
|
|
+ type: "layout",
|
|
|
+ fixable: "code",
|
|
|
+ schema: []
|
|
|
+ },
|
|
|
+ create(context) {
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
+
|
|
|
+ const shorthandDependencies = {
|
|
|
+ borderRadius: [
|
|
|
+ "borderTopLeftRadius",
|
|
|
+ "borderTopRightRadius",
|
|
|
+ "borderBottomLeftRadius",
|
|
|
+ "borderBottomRightRadius",
|
|
|
+ "borderTopStartRadius",
|
|
|
+ "borderTopEndRadius",
|
|
|
+ "borderBottomStartRadius",
|
|
|
+ "borderBottomEndRadius"
|
|
|
+ ],
|
|
|
+ padding: [
|
|
|
+ "paddingHorizontal",
|
|
|
+ "paddingVertical",
|
|
|
+ "paddingTop",
|
|
|
+ "paddingRight",
|
|
|
+ "paddingBottom",
|
|
|
+ "paddingLeft",
|
|
|
+ "paddingStart",
|
|
|
+ "paddingEnd"
|
|
|
+ ],
|
|
|
+ margin: [
|
|
|
+ "marginHorizontal",
|
|
|
+ "marginVertical",
|
|
|
+ "marginTop",
|
|
|
+ "marginRight",
|
|
|
+ "marginBottom",
|
|
|
+ "marginLeft",
|
|
|
+ "marginStart",
|
|
|
+ "marginEnd"
|
|
|
+ ],
|
|
|
+ borderWidth: [
|
|
|
+ "borderTopWidth",
|
|
|
+ "borderRightWidth",
|
|
|
+ "borderBottomWidth",
|
|
|
+ "borderLeftWidth",
|
|
|
+ "borderStartWidth",
|
|
|
+ "borderEndWidth"
|
|
|
+ ],
|
|
|
+ borderColor: [
|
|
|
+ "borderTopColor",
|
|
|
+ "borderRightColor",
|
|
|
+ "borderBottomColor",
|
|
|
+ "borderLeftColor",
|
|
|
+ "borderStartColor",
|
|
|
+ "borderEndColor"
|
|
|
+ ],
|
|
|
+ borderStyle: [
|
|
|
+ "borderTopStyle",
|
|
|
+ "borderRightStyle",
|
|
|
+ "borderBottomStyle",
|
|
|
+ "borderLeftStyle"
|
|
|
+ ],
|
|
|
+ paddingHorizontal: [
|
|
|
+ "paddingLeft",
|
|
|
+ "paddingRight",
|
|
|
+ "paddingStart",
|
|
|
+ "paddingEnd"
|
|
|
+ ],
|
|
|
+ marginHorizontal: [
|
|
|
+ "marginLeft",
|
|
|
+ "marginRight",
|
|
|
+ "marginStart",
|
|
|
+ "marginEnd"
|
|
|
+ ],
|
|
|
+ flex: [
|
|
|
+ "flexGrow",
|
|
|
+ "flexShrink",
|
|
|
+ "flexBasis"
|
|
|
+ ],
|
|
|
+ paddingVertical: [
|
|
|
+ "paddingTop",
|
|
|
+ "paddingBottom"
|
|
|
+ ],
|
|
|
+ marginVertical: [
|
|
|
+ "marginTop",
|
|
|
+ "marginBottom"
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ function isStyleObject(node) {
|
|
|
+ if (node.properties.length <= 1) return false;
|
|
|
+
|
|
|
+ if (node.parent.type === "CallExpression" &&
|
|
|
+ node.parent.callee.type === "MemberExpression" &&
|
|
|
+ node.parent.callee.object.name === "StyleSheet" &&
|
|
|
+ node.parent.callee.property.name === "create") {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (node.parent.type === "Property" &&
|
|
|
+ node.parent.parent.type === "ObjectExpression" &&
|
|
|
+ node.parent.parent.parent.type === "CallExpression" &&
|
|
|
+ node.parent.parent.parent.callee.type === "MemberExpression" &&
|
|
|
+ node.parent.parent.parent.callee.object.name === "StyleSheet") {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ const knownStyleKeys = new Set([
|
|
|
+ "alignItems",
|
|
|
+ "justifyContent",
|
|
|
+ "flexDirection",
|
|
|
+ "flexWrap",
|
|
|
+ "flex",
|
|
|
+ "padding",
|
|
|
+ "margin",
|
|
|
+ "paddingTop",
|
|
|
+ "marginTop",
|
|
|
+ "paddingBottom",
|
|
|
+ "marginBottom",
|
|
|
+ "paddingLeft",
|
|
|
+ "marginLeft",
|
|
|
+ "paddingRight",
|
|
|
+ "marginRight",
|
|
|
+ "paddingHorizontal",
|
|
|
+ "marginHorizontal",
|
|
|
+ "paddingVertical",
|
|
|
+ "marginVertical",
|
|
|
+ "backgroundColor",
|
|
|
+ "color",
|
|
|
+ "fontSize",
|
|
|
+ "fontWeight",
|
|
|
+ "lineHeight",
|
|
|
+ "textAlign",
|
|
|
+ "borderWidth",
|
|
|
+ "borderRadius",
|
|
|
+ "borderColor",
|
|
|
+ "borderStyle",
|
|
|
+ "position",
|
|
|
+ "top",
|
|
|
+ "bottom",
|
|
|
+ "left",
|
|
|
+ "right",
|
|
|
+ "display",
|
|
|
+ "width",
|
|
|
+ "height",
|
|
|
+ "minWidth",
|
|
|
+ "minHeight",
|
|
|
+ "maxWidth",
|
|
|
+ "maxHeight",
|
|
|
+ "alignSelf"
|
|
|
+ ]);
|
|
|
+
|
|
|
+ let hasStyleKey = false;
|
|
|
+ for (const prop of node.properties) {
|
|
|
+ if (prop.type === "Property" && prop.key.type === "Identifier") {
|
|
|
+ if (knownStyleKeys.has(prop.key.name)) {
|
|
|
+ hasStyleKey = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (hasStyleKey) return true;
|
|
|
+
|
|
|
+ if (node.parent.type === "VariableDeclarator" &&
|
|
|
+ (node.parent.id.name === "styles" || node.parent.id.name === "stylesheet" || node.parent.id.name === "style")) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (node.parent.type === "TSAsExpression" || node.parent.type === "TypeAssertion") {
|
|
|
+ const typeName = node.parent.typeAnnotation?.typeName?.name;
|
|
|
+ if (typeName && (typeName === "ViewStyle" || typeName === "TextStyle" || typeName === "ImageStyle")) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (node.parent.type === "Property" && node.parent.parent.type === "ObjectExpression") {
|
|
|
+ if (node.parent.parent.parent.type === "VariableDeclarator" &&
|
|
|
+ (node.parent.parent.parent.id.name === "styles" || node.parent.parent.parent.id.name === "stylesheet")) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function getSortedProperties(properties) {
|
|
|
+ const webStyles = [];
|
|
|
+ const others = [];
|
|
|
+ for (const p of properties) {
|
|
|
+ if (p.type === "SpreadElement" && p.argument.type === "CallExpression" && p.argument.callee.name === "webStyle") {
|
|
|
+ webStyles.push(p);
|
|
|
+ } else {
|
|
|
+ others.push(p);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const chunks = [];
|
|
|
+ let currentChunk = [];
|
|
|
+ const chunkSeparators = [];
|
|
|
+
|
|
|
+ for (const p of others) {
|
|
|
+ if (p.type === "SpreadElement") {
|
|
|
+ chunks.push(currentChunk);
|
|
|
+ chunkSeparators.push(p);
|
|
|
+ currentChunk = [];
|
|
|
+ } else {
|
|
|
+ currentChunk.push(p);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ chunks.push(currentChunk);
|
|
|
+
|
|
|
+ function sortChunk(chunk) {
|
|
|
+ if (chunk.length <= 1) return chunk;
|
|
|
+
|
|
|
+ const getLen = (node) => sourceCode.getText(node).length;
|
|
|
+
|
|
|
+ const initialSorted = chunk.map((node, i) => ({
|
|
|
+ node,
|
|
|
+ originalIndex: i,
|
|
|
+ length: getLen(node)
|
|
|
+ })).sort((a, b) => {
|
|
|
+ if (a.length !== b.length) return b.length - a.length;
|
|
|
+ return a.originalIndex - b.originalIndex;
|
|
|
+ });
|
|
|
+
|
|
|
+ const propNames = chunk.map(p => {
|
|
|
+ if (p.type === "Property") {
|
|
|
+ if (p.key.type === "Identifier") return p.key.name;
|
|
|
+ if (p.key.type === "Literal") return p.key.value;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
+ const prerequisites = new Map();
|
|
|
+ for (let i = 0; i < chunk.length; i++) {
|
|
|
+ prerequisites.set(i, new Set());
|
|
|
+ }
|
|
|
+ for (let i = 0; i < propNames.length; i++) {
|
|
|
+ const nameA = propNames[i];
|
|
|
+ if (!nameA) continue;
|
|
|
+ const deps = shorthandDependencies[nameA];
|
|
|
+ if (deps) {
|
|
|
+ for (let j = 0; j < propNames.length; j++) {
|
|
|
+ if (i === j) continue;
|
|
|
+ if (deps.includes(propNames[j])) {
|
|
|
+ prerequisites.get(j).add(i);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = [];
|
|
|
+ const placedIndices = new Set();
|
|
|
+ const pool = [...initialSorted];
|
|
|
+
|
|
|
+ while (pool.length > 0) {
|
|
|
+ let placedAny = false;
|
|
|
+ for (let i = 0; i < pool.length; i++) {
|
|
|
+ const item = pool[i];
|
|
|
+ const itemOriginalIndex = item.originalIndex;
|
|
|
+ const reqs = prerequisites.get(itemOriginalIndex);
|
|
|
+
|
|
|
+ let canPlace = true;
|
|
|
+ for (const req of reqs) {
|
|
|
+ if (!placedIndices.has(req)) {
|
|
|
+ canPlace = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (canPlace) {
|
|
|
+ result.push(item.node);
|
|
|
+ placedIndices.add(itemOriginalIndex);
|
|
|
+ pool.splice(i, 1);
|
|
|
+ placedAny = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!placedAny) {
|
|
|
+ const item = pool.shift();
|
|
|
+ result.push(item.node);
|
|
|
+ placedIndices.add(item.originalIndex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ const finalResult = [];
|
|
|
+ for (let i = 0; i < chunks.length; i++) {
|
|
|
+ finalResult.push(...sortChunk(chunks[i]));
|
|
|
+ if (i < chunkSeparators.length) {
|
|
|
+ finalResult.push(chunkSeparators[i]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ finalResult.push(...webStyles);
|
|
|
+ return finalResult;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ ObjectExpression(node) {
|
|
|
+ if (!isStyleObject(node)) return;
|
|
|
+
|
|
|
+ const oldOrder = node.properties;
|
|
|
+ if (oldOrder.length <= 1) return;
|
|
|
+
|
|
|
+ const newOrder = getSortedProperties(oldOrder);
|
|
|
+
|
|
|
+ let changed = false;
|
|
|
+ for (let i = 0; i < oldOrder.length; i++) {
|
|
|
+ if (oldOrder[i] !== newOrder[i]) {
|
|
|
+ changed = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (changed) {
|
|
|
+ context.report({
|
|
|
+ node: node,
|
|
|
+ message: "Style properties should be sorted by length (longest to shortest), with shorthand properties appropriately ordered and spread elements at the bottom.",
|
|
|
+ fix(fixer) {
|
|
|
+ const fixes = [];
|
|
|
+ for (let i = 0; i < oldOrder.length; i++) {
|
|
|
+ fixes.push(fixer.replaceText(oldOrder[i], sourceCode.getText(newOrder[i])));
|
|
|
+ }
|
|
|
+ return fixes;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ } },
|
|
|
+ "ncore-sort-types": {
|
|
|
+ meta: {
|
|
|
+ type: "layout",
|
|
|
+ fixable: "code",
|
|
|
+ schema: []
|
|
|
+ },
|
|
|
+ create(context) {
|
|
|
+ const sourceCode = context.getSourceCode();
|
|
|
+
|
|
|
+ function sortMembers(node, members) {
|
|
|
+ if (!members || members.length <= 1) return;
|
|
|
+
|
|
|
+ const getLen = (n) => {
|
|
|
+ const text = sourceCode.getText(n);
|
|
|
+ const lines = text.split(/\r?\n/);
|
|
|
+ let maxLen = 0;
|
|
|
+ for (const line of lines) {
|
|
|
+ const trimmed = line.trim();
|
|
|
+ if (trimmed.length > maxLen) {
|
|
|
+ maxLen = trimmed.length;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return maxLen;
|
|
|
+ };
|
|
|
+
|
|
|
+ const initialSorted = members.map((member, i) => ({
|
|
|
+ node: member,
|
|
|
+ originalIndex: i,
|
|
|
+ length: getLen(member)
|
|
|
+ })).sort((a, b) => {
|
|
|
+ if (a.length !== b.length) return b.length - a.length;
|
|
|
+ return a.originalIndex - b.originalIndex;
|
|
|
+ });
|
|
|
+
|
|
|
+ let changed = false;
|
|
|
+ for (let i = 0; i < members.length; i++) {
|
|
|
+ if (members[i] !== initialSorted[i].node) {
|
|
|
+ changed = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (changed) {
|
|
|
+ context.report({
|
|
|
+ node: node,
|
|
|
+ message: "Type properties should be sorted by length (longest to shortest).",
|
|
|
+ fix(fixer) {
|
|
|
+ const fixes = [];
|
|
|
+ for (let i = 0; i < members.length; i++) {
|
|
|
+ fixes.push(fixer.replaceText(members[i], sourceCode.getText(initialSorted[i].node)));
|
|
|
+ }
|
|
|
+ return fixes;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ TSInterfaceBody(node) {
|
|
|
+ sortMembers(node, node.body);
|
|
|
+ },
|
|
|
+ TSTypeLiteral(node) {
|
|
|
+ sortMembers(node, node.members);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
}
|
|
|
};
|