UNPKG

eslint-plugin-mui-sx-order

Version:

Auto-sorts MUI sx/style props for clean, consistent code with ESLint

283 lines 15.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.checkAndReport = checkAndReport; /** * Checks if properties in an object expression are in the correct order * and reports violations with auto-fix capability */ function checkAndReport(context, node, getOrder) { const properties = node.properties; if (properties.length < 2) { return; } // First, recursively check nested objects except responsive keys (handled inline) const responsiveKeys = new Set(['xs', 'sm', 'md', 'lg', 'xl']); properties.forEach((prop) => { if (prop.type === 'Property' && prop.key && prop.key.type && (prop.key.type === 'Identifier' || prop.key.type === 'Literal') && prop.value && prop.value.type === 'ObjectExpression') { const keyName = prop.key.type === 'Identifier' ? prop.key.name : String(prop.key.value); if (!responsiveKeys.has(keyName)) { checkAndReport(context, prop.value, getOrder); } } }); // Get property names and their order indices const makeOrderItem = (prop, index) => { const keyName = prop.key.type === 'Identifier' ? prop.key.name : String(prop.key.value); return { name: keyName, order: getOrder(keyName), originalIndex: index, property: prop, }; }; // Build contiguous Property segments, split by non-Property (e.g., SpreadElement) const segments = []; const indexToSegment = Array(properties.length).fill(-1); let currentItems = []; let currentIndices = []; properties.forEach((prop, index) => { if (prop.type === 'Property' && (prop.key.type === 'Identifier' || prop.key.type === 'Literal')) { currentItems.push(makeOrderItem(prop, index)); currentIndices.push(index); } else { if (currentItems.length) { const segIdx = segments.push({ items: currentItems, indices: currentIndices }) - 1; currentIndices.forEach((i) => (indexToSegment[i] = segIdx)); } currentItems = []; currentIndices = []; } }); if (currentItems.length) { const segIdx = segments.push({ items: currentItems, indices: currentIndices }) - 1; currentIndices.forEach((i) => (indexToSegment[i] = segIdx)); } // Check sortedness per segment const allSegmentsSorted = segments.every((seg) => seg.items.every((item, idx) => idx === 0 ? true : item.order >= seg.items[idx - 1].order)); if (allSegmentsSorted) { return; } // (no global sorted list; sorting will be applied per segment only) const sourceCode = context.sourceCode; // Analyze for simple safe case const objectText = sourceCode.getText(node); const hasInlineComments = /\/\*|\/\//.test(objectText.slice(objectText.indexOf('{') + 1, objectText.lastIndexOf('}'))); const hasOnlyPlainProperties = properties.every((prop) => prop && prop.type === 'Property'); const isSimpleWholeObject = !hasInlineComments && hasOnlyPlainProperties && segments.length === 1; // We allow fixing even when comments are present by using segment-based rebuild context.report({ node, messageId: 'incorrectOrder', fix(fixer) { // Simple whole-object replacement: preserve previous behavior and expected formatting if (isSimpleWholeObject) { // flatten items of the only segment and sort const seg = segments[0]; const sorted = [...seg.items].sort((a, b) => a.order === b.order ? a.originalIndex - b.originalIndex : a.order - b.order); const parts = sorted.map((item, index) => { const property = item.property; let propertyText = sourceCode.getText(property); // If value is an ObjectExpression, render its content in multiline sorted form if (property.value && property.value.type === 'ObjectExpression') { const inner = property.value; const innerProps = inner.properties.filter((p) => p.type === 'Property' && (p.key.type === 'Identifier' || p.key.type === 'Literal')); if (innerProps.length >= 2) { const innerItems = innerProps.map((p) => { const name = p.key.type === 'Identifier' ? p.key.name : String(p.key.value); return { name, order: getOrder(name), property: p }; }); const innerSorted = innerItems .sort((a, b) => a.order === b.order ? 0 : a.order - b.order) .map((it, idx) => { const t = sourceCode.getText(it.property); return idx < innerItems.length - 1 ? `${t},` : t; }) .join('\n'); const innerText = `{$\n${innerSorted}\n}`.replace('$', ''); const innerStart = inner.range[0]; const innerEnd = inner.range[1]; const relStart = innerStart - property.range[0]; const relEnd = relStart + (innerEnd - innerStart); propertyText = propertyText.slice(0, relStart) + innerText + propertyText.slice(relEnd); } } return index < sorted.length - 1 ? `${propertyText},` : propertyText; }); const sortedSource = parts.join('\n'); return fixer.replaceText(node, `{\n${sortedSource}\n}`); } // For segmented case, rebuild only the segment ranges preserving trivia const fixes = []; segments.forEach((seg) => { if (seg.items.length < 2) return; // nothing to reorder // Determine segment bounds const firstIdx = seg.indices[0]; const lastIdx = seg.indices[seg.indices.length - 1]; const firstProp = properties[firstIdx]; const lastProp = properties[lastIdx]; const segStart = firstProp.range[0]; const segEnd = lastProp.range[1]; const pieces = []; // Segment leading trivia (before the first property in the segment) const firstStart = firstProp.range[0]; // Compute text between the opening '{' of the object and the first property's start const nodeStart = node.range[0]; const nodeEnd = node.range[1]; const nodeText = sourceCode.text.slice(nodeStart, nodeEnd); const braceIdx = nodeText.indexOf('{'); const contentStart = braceIdx >= 0 ? nodeStart + braceIdx + 1 : segStart; let segmentLeading = sourceCode.text.slice(contentStart, firstStart); const isMultilineObject = /\n/.test(nodeText); if (!isMultilineObject) { // For single-line, normalize to single space after '{' segmentLeading = ' '; } else { // Collapse multiple blank lines to a single newline segmentLeading = segmentLeading.replace(/\n\s*\n+/g, '\n'); // Ensure at least one newline + two spaces indentation if (!/^\s*\n\s{2,}$/.test(segmentLeading)) { segmentLeading = '\n '; } } // Build pieces in original order with leading trivia for (let k = 0; k < seg.indices.length; k++) { const i = seg.indices[k]; const prop = properties[i]; const start = prop.range[0]; const end = prop.range[1]; let text = sourceCode.text.slice(start, end); // If this property was not the last in original segment, strip trailing comma from its own text. // The comma will be preserved as part of the NEXT property's leading trivia. if (k + 1 < seg.indices.length) { text = text.replace(/,\s*$/, ''); } // leading is text after previous property's end up to this property's start (or segment start) const prevIdx = k - 1 >= 0 ? seg.indices[k - 1] : undefined; const prevEnd = typeof prevIdx === 'number' ? properties[prevIdx].range[1] : firstStart; const leading = sourceCode.text.slice(prevEnd, start); const keyName = prop.key.type === 'Identifier' ? prop.key.name : String(prop.key.value); // Inline-fix nested object expressions (e.g., responsive blocks) without separate reports if (prop.value && prop.value.type === 'ObjectExpression') { const inner = prop.value; const innerProps = inner.properties.filter((p) => p.type === 'Property' && (p.key.type === 'Identifier' || p.key.type === 'Literal')); if (innerProps.length >= 2) { const innerItems = innerProps.map((p) => { const name = p.key.type === 'Identifier' ? p.key.name : String(p.key.value); return { name, order: getOrder(name), property: p }; }); const innerSorted = innerItems .sort((a, b) => a.order === b.order ? 0 : a.order - b.order) .map((it, idx) => { const t = sourceCode.getText(it.property); return idx < innerItems.length - 1 ? `${t},` : t; }) .join('\n'); const innerText = `{$\n${innerSorted}\n}`.replace('$', ''); // Replace inner object text within property text by using ranges const innerStart = inner.range[0]; const innerEnd = inner.range[1]; const relStart = innerStart - start; const relEnd = relStart + (innerEnd - innerStart); text = text.slice(0, relStart) + innerText + text.slice(relEnd); } } pieces.push({ key: keyName, originalIndex: i, text, leading, wasFirstInSegment: k === 0, }); } // Compute sorted order for this segment const sorted = [...seg.items].sort((a, b) => a.order === b.order ? a.originalIndex - b.originalIndex : a.order - b.order); // Map from originalIndex to piece const byIndex = new Map(pieces.map((p) => [p.originalIndex, p])); // Derive a default separator (gap) from the first leading if available, else comma+space const firstGap = pieces.length > 1 ? pieces[1].leading : ''; let defaultSep = /,/.test(firstGap) ? firstGap : ', '; if (isMultilineObject) { defaultSep = ',\n '; } // Rebuild: ensure moved items that were last get a separator when no postfix and not last now const reordered = sorted.map((item) => byIndex.get(item.originalIndex)); // If the new first item had leading that contains comments (e.g., from previous neighbor), // preserve that leading by prepending it to segmentLeading, but strip any starting commas/spaces. let rebuilt = segmentLeading; if (reordered.length > 0) { const firstPiece = reordered[0]; if (firstPiece.leading && firstPiece.leading.trim().length > 0) { const cleanedFirstLeading = firstPiece.leading.replace(/^[\s,]+/, (m) => { // remove leading commas and spaces entirely return m .replace(/[,]+/g, '') .replace(/\s+/g, () => (isMultilineObject ? '\n ' : ' ')) .trimStart(); }); if (cleanedFirstLeading.length > 0) { rebuilt += cleanedFirstLeading; } } } for (let i = 0; i < reordered.length; i++) { const p = reordered[i]; // If originally-first piece moves away from first position, drop its original leading (which may include top-level comments) const effectiveLeading = p.wasFirstInSegment && i > 0 ? defaultSep : p.leading; // use existing leading if present; otherwise synthesize default sep (for non-first) let leading = i === 0 ? '' : effectiveLeading.length > 0 ? effectiveLeading : defaultSep; if (i > 0 && !isMultilineObject && leading.trim().length === 0) { leading = defaultSep; // ensure at least ', ' on single-line } rebuilt += leading + p.text; } fixes.push(fixer.replaceTextRange([segStart, segEnd], rebuilt)); // If this is the last segment (ends at last property), and there is a trailing comma before '}', remove it const isLastSegment = lastIdx === properties.length - 1; if (isLastSegment) { const after = sourceCode.text.slice(segEnd, node.range[1]); const m = after.match(/^(\s*),(?=\s*\})/); if (m && typeof m[1] === 'string') { const commaStart = segEnd + m[1].length; const commaEnd = commaStart + 1; fixes.push(fixer.replaceTextRange([commaStart, commaEnd], '')); } } }); return fixes; }, }); } //# sourceMappingURL=checkAndReport.js.map