UNPKG

sortier

Version:
452 lines (451 loc) 22.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.compare = compare; exports.getContextGroups = getContextGroups; exports.reorderValues = reorderValues; exports.isIgnored = isIgnored; const string_utils_js_1 = require("./string-utils.js"); function compare(a, b) { const aType = typeof a; const bType = typeof b; if (aType !== bType) { return compare(aType, bType); } return a < b ? -1 : a > b ? 1 : 0; } // Blank lines between cases are considered Context breakers... we don't sort through them. function getContextGroups(nodes, comments, fileContents, additionalContextBarrierIndexes = []) { comments = comments.filter((comment) => { // There seems to be bugs with the parsers regarding certain comments // https://github.com/eslint/typescript-eslint-parser/issues/450 return isValidComment(fileContents, comment); }); if (nodes.length === 0) { return [ { comments: comments, nodes: nodes, }, ]; } // Determine the start and end of the nodes and comments provided const firstNodeLoc = nodes[0].range; const lastNodeLoc = nodes[nodes.length - 1].range; if (firstNodeLoc == null || lastNodeLoc == null) { throw new Error("Node location is null?"); } let rangeStart = firstNodeLoc[0]; let rangeEnd = lastNodeLoc[1]; let firstNodeComments = getPrecedingCommentsForSpecifier(fileContents, comments, nodes[0]); if (firstNodeComments.length > 0) { const firstNodeCommentsRange = firstNodeComments[0].range; if (firstNodeCommentsRange != null) { rangeStart = firstNodeCommentsRange[0]; } } const lastNodeComments = getSucceedingCommentsForSpecifier(fileContents, comments, nodes[nodes.length - 1]); if (lastNodeComments.length > 0) { const lastNodeCommentsRange = lastNodeComments[lastNodeComments.length - 1].range; if (lastNodeCommentsRange != null) { rangeEnd = lastNodeCommentsRange[1]; } } // For performance, shorten the comments array to only the comments that are between what is provided comments = comments.filter((comment) => { if (comment.range != null) { return rangeStart <= comment.range[0] && comment.range[0] <= rangeEnd; } return true; }); // Now figure out all the indexes of any whitespace surrounded by two new lines (e.g. context separator) const blankLines = string_utils_js_1.StringUtils.getBlankLineLocations(fileContents, rangeStart, rangeEnd); const contextBarrierIndices = [...blankLines, ...additionalContextBarrierIndexes].sort((a, b) => a - b); // Now that we have the indices of all context breaks, anything that is between those breaks will be a single context const groupings = []; let nodeIndex = 0; const commentIndex = 0; while (contextBarrierIndices.length > 0) { const partialNodes = []; let partialComments = []; let contextBarrierIndex = contextBarrierIndices.shift(); if (contextBarrierIndex == null) { throw new Error("Context barrier index is null?"); } // Nodes while (nodeIndex < nodes.length) { const node = nodes[nodeIndex]; const range = node.range; if (range == null) { continue; } // Deal with context barriers if its between the last node and the current if (contextBarrierIndex != null && nodeIndex > 0) { const lastNode = nodes[nodeIndex - 1]; const lastRange = lastNode.range; if (lastRange != null) { while (contextBarrierIndex != null && contextBarrierIndex < lastRange[1]) { contextBarrierIndex = contextBarrierIndices.shift(); } if (contextBarrierIndex != null && contextBarrierIndex >= lastRange[1] && contextBarrierIndex <= range[0]) { break; } } } partialNodes.push(node); const precedingComments = getPrecedingCommentsForSpecifier(fileContents, comments, node); const succeedingComments = getSucceedingCommentsForSpecifier(fileContents, comments, node); partialComments.push(...precedingComments, ...succeedingComments); nodeIndex++; } // If the only comments for the whole group are above the first node, it's contextual firstNodeComments = []; if (partialNodes.length > 0) { firstNodeComments = getPrecedingCommentsForSpecifier(fileContents, comments, partialNodes[0]); } if (partialComments.length === firstNodeComments.length) { partialComments = []; } groupings.push({ comments: partialComments, nodes: partialNodes, }); } const partialNodes = nodes.slice(nodeIndex); let partialComments = []; partialNodes.forEach((node) => { const precedingComments = getPrecedingCommentsForSpecifier(fileContents, comments, node); const succeedingComments = getSucceedingCommentsForSpecifier(fileContents, comments, node); partialComments.push(...precedingComments, ...succeedingComments); }); // If the only comments for the whole group are above the first node, it's contextual firstNodeComments = []; if (partialNodes.length > 0) { firstNodeComments = getPrecedingCommentsForSpecifier(fileContents, comments, partialNodes[0]); } if (partialComments.length === firstNodeComments.length) { partialComments = []; } if (commentIndex < comments.length || nodeIndex < nodes.length) { groupings.push({ comments: partialComments, nodes: nodes.slice(nodeIndex), }); } return groupings; } function reorderValues(fileContents, comments, unsortedTypes, sortedTypes) { if (unsortedTypes.length !== sortedTypes.length) { throw new Error("Sortier ran into a problem - Expected the same number of unsorted types and sorted types to be provided"); } if (unsortedTypes.length === 1) { return fileContents; } let newFileContents = fileContents.slice(); let newFileContentIndexCorrection = 0; // Now go through the original specifiers again and if any have moved, switch them for (let x = 0; x < unsortedTypes.length; x++) { const specifier = unsortedTypes[x]; const newSpecifier = sortedTypes[x]; // Checks to see if the two specifiers are actually the same thing if (specifier === newSpecifier) { continue; } const specifierRange = specifier.range; const newSpecifierRange = newSpecifier.range; if (specifierRange == null || newSpecifierRange == null) { throw new Error("Range cannot be null"); } if (specifierRange[0] === newSpecifierRange[0] && specifierRange[1] === newSpecifierRange[1]) { continue; } // As long as the comment isn't the same comment and one of the specifiers has a comment // Swap the specifier comments (as they will be before the specifier) let specifierCommentRange = getPrecedingCommentRangeForSpecifier(fileContents, comments, specifier); let newSpecifierCommentRange = getPrecedingCommentRangeForSpecifier(fileContents, comments, newSpecifier); let noCommentsExist = specifierCommentRange[0] === specifierCommentRange[1] && newSpecifierCommentRange[0] === newSpecifierCommentRange[1]; if (!noCommentsExist && specifierCommentRange[0] !== newSpecifierCommentRange[0] && specifierCommentRange[1] !== newSpecifierCommentRange[1]) { const spliceRemoveIndexStart = specifierCommentRange[0] + newFileContentIndexCorrection; const spliceRemoveIndexEnd = specifierCommentRange[1] + newFileContentIndexCorrection; const untouchedBeginning = newFileContents.slice(0, spliceRemoveIndexStart); const untouchedEnd = newFileContents.slice(spliceRemoveIndexEnd); const spliceAddIndexStart = newSpecifierCommentRange[0]; const spliceAddIndexEnd = newSpecifierCommentRange[1]; const stringToInsert = fileContents.substring(spliceAddIndexStart, spliceAddIndexEnd); newFileContents = untouchedBeginning + stringToInsert + untouchedEnd; newFileContentIndexCorrection += spliceAddIndexEnd - spliceAddIndexStart - (spliceRemoveIndexEnd - spliceRemoveIndexStart); } // Swap the specifier { const spliceRemoveIndexStart = specifierRange[0] + newFileContentIndexCorrection; const spliceRemoveIndexEnd = specifierRange[1] + newFileContentIndexCorrection; const untouchedBeginning = newFileContents.slice(0, spliceRemoveIndexStart); const untouchedEnd = newFileContents.slice(spliceRemoveIndexEnd); const spliceAddIndexStart = newSpecifierRange[0]; const spliceAddIndexEnd = newSpecifierRange[1]; const stringToInsert = fileContents.substring(spliceAddIndexStart, spliceAddIndexEnd); newFileContents = untouchedBeginning + stringToInsert + untouchedEnd; newFileContentIndexCorrection += spliceAddIndexEnd - spliceAddIndexStart - (spliceRemoveIndexEnd - spliceRemoveIndexStart); } // As long as the comment isn't the same comment and one of the specifiers has a comment // Swap the specifier comments (as they will be before the specifier) specifierCommentRange = getSucceedingCommentRangeForSpecifier(fileContents, comments, specifier); newSpecifierCommentRange = getSucceedingCommentRangeForSpecifier(fileContents, comments, newSpecifier); noCommentsExist = specifierCommentRange[0] === specifierCommentRange[1] && newSpecifierCommentRange[0] === newSpecifierCommentRange[1]; if (!noCommentsExist && specifierCommentRange[0] !== newSpecifierCommentRange[0] && specifierCommentRange[1] !== newSpecifierCommentRange[1]) { const spliceRemoveIndexStart = specifierCommentRange[0] + newFileContentIndexCorrection; const spliceRemoveIndexEnd = specifierCommentRange[1] + newFileContentIndexCorrection; const untouchedBeginning = newFileContents.slice(0, spliceRemoveIndexStart); const untouchedEnd = newFileContents.slice(spliceRemoveIndexEnd); const spliceAddIndexStart = newSpecifierCommentRange[0]; const spliceAddIndexEnd = newSpecifierCommentRange[1]; const stringToInsert = fileContents.substring(spliceAddIndexStart, spliceAddIndexEnd); newFileContents = untouchedBeginning + stringToInsert + untouchedEnd; newFileContentIndexCorrection += spliceAddIndexEnd - spliceAddIndexStart - (spliceRemoveIndexEnd - spliceRemoveIndexStart); } } return newFileContents; } function getPrecedingCommentRangeForSpecifier(fileContents, comments, specifier) { // Determine where the specifier line starts const range = specifier.range; if (range == null) { throw new Error("Specifier range cannot be null"); } const specifierComments = getPrecedingCommentsForSpecifier(fileContents, comments, specifier); // If the specifier comments are block comments infront of the specifier if (specifierComments.length >= 1 && specifierComments[0].type === "Block") { const firstCommentRange = specifierComments[0].range; const lastCommentRange = specifierComments[specifierComments.length - 1].range; if (firstCommentRange == null || lastCommentRange == null) { throw new Error("Comment cannot have a null range"); } const textBetweenCommentAndSpecifier = fileContents.substring(lastCommentRange[1], range[0]); if (textBetweenCommentAndSpecifier.indexOf("\n") === -1) { return [firstCommentRange[0], lastCommentRange[1] + textBetweenCommentAndSpecifier.length]; } } const indexOfNewLineBeforeSpecifier = Math.max(0, fileContents.substring(0, range[0]).lastIndexOf("\n")); const textBetweenLineAndSpecifier = fileContents.substring(indexOfNewLineBeforeSpecifier, range[0]); let firstIndexOfNonWhitespace = textBetweenLineAndSpecifier.search(/[^(\s)]/gim); if (firstIndexOfNonWhitespace === -1) { firstIndexOfNonWhitespace = textBetweenLineAndSpecifier.length; } const specifierLineStart = indexOfNewLineBeforeSpecifier + firstIndexOfNonWhitespace; // If we got a comment for the specifier, lets set up it's range and use it if (specifierComments.length !== 0) { const firstComment = specifierComments[0]; if (firstComment.range != null && specifier.range != null) { return [firstComment.range[0], specifierLineStart]; } } return [specifierLineStart, specifierLineStart]; } function getSucceedingCommentRangeForSpecifier(fileContents, comments, specifier) { // Determine where the specifier line starts const range = specifier.range; if (range == null) { throw new Error("Specifier range cannot be null"); } let specifierEndOfLine = fileContents.indexOf("\r", range[1]); if (specifierEndOfLine === -1) { specifierEndOfLine = fileContents.indexOf("\n", range[1]); } if (specifierEndOfLine === -1) { specifierEndOfLine = fileContents.length; } const specifierComments = getSucceedingCommentsForSpecifier(fileContents, comments, specifier); // Null and empty checks if (specifierComments.length === 0) { return [specifierEndOfLine, specifierEndOfLine]; } const firstCommentRange = specifierComments[0].range; const lastCommentRange = specifierComments[specifierComments.length - 1].range; if (firstCommentRange == null || lastCommentRange == null) { return [specifierEndOfLine, specifierEndOfLine]; } // Determine where we need to copy paste from const textBetweenSpecifierAndComment = fileContents.substring(range[1], firstCommentRange[0]); const nonWhiteSpaceMatches = textBetweenSpecifierAndComment.match(/[^(\s)]/gim); let lastIndexOfNonWhitespace = 0; if (nonWhiteSpaceMatches != null && nonWhiteSpaceMatches.length !== 0) { lastIndexOfNonWhitespace = textBetweenSpecifierAndComment.lastIndexOf(nonWhiteSpaceMatches[nonWhiteSpaceMatches.length - 1]) + 1; } const specifierLineStart = range[1] + lastIndexOfNonWhitespace; return [specifierLineStart, lastCommentRange[1]]; } // Currently we only accept comments before the specifier. function getPrecedingCommentsForSpecifier(fileContents, comments, specifier) { comments = comments.filter((comment) => { // There seems to be bugs with the parsers regarding certain comments // https://github.com/eslint/typescript-eslint-parser/issues/450 return isValidComment(fileContents, comment); }); const specifierRange = specifier.range; if (specifierRange == null) { return []; } // Determine the comment next to the specifier let latestCommentIndex = -1; let firstIndex = 0; let lastIndex = Math.max(0, comments.length - 1); let middleIndex = Math.floor((lastIndex + firstIndex) / 2); while (Math.abs(firstIndex - lastIndex) > 1) { const commentRange = comments[middleIndex].range; if (commentRange == null) { continue; } if (commentRange[0] < specifierRange[0]) { firstIndex = middleIndex; middleIndex = Math.floor((lastIndex + middleIndex) / 2); } if (commentRange[0] > specifierRange[0]) { lastIndex = middleIndex; middleIndex = Math.floor((firstIndex + middleIndex) / 2); } } for (let index = middleIndex; index < comments.length; index++) { const commentRange = comments[index].range; if (commentRange == null) { continue; } if (commentRange[0] > specifierRange[0]) { break; } const textBetweenStartOfLineAndComment = fileContents.substring(fileContents.lastIndexOf("\n", commentRange[0]) + 1, commentRange[0]); const textBetweenCommentAndSpecifier = fileContents.substring(commentRange[1], specifierRange[0]); const isTextBetweenStartOfLineAndCommentWhitespace = textBetweenStartOfLineAndComment.match(/[^\s]/gim) == null; const isCommentOwnedByPreviousLine = !isTextBetweenStartOfLineAndCommentWhitespace && textBetweenCommentAndSpecifier.indexOf("\n") !== -1; const isTextBetweenCommentAndSpecifierWhitespace = textBetweenCommentAndSpecifier.match(/[^\s]/gim) == null; const newLineCount = textBetweenCommentAndSpecifier.match(/\n/gim)?.length || 0; if (newLineCount <= 1 && isTextBetweenCommentAndSpecifierWhitespace && !isCommentOwnedByPreviousLine) { latestCommentIndex = index; } } // If there are multiple comments all stacked on one another on separate lines let earliestCommentIndex = latestCommentIndex; if (latestCommentIndex !== -1) { while (earliestCommentIndex > 0) { const previousComment = comments[earliestCommentIndex - 1].range; const thisComment = comments[earliestCommentIndex].range; if (previousComment == null || thisComment == null) { throw new Error("Comment cannot have a null range"); } const textBetweenCommentAndSpecifier = fileContents.substring(previousComment[1], thisComment[0]); const textBetweenStartOfLineAndComment = fileContents.substring(fileContents.lastIndexOf("\n", previousComment[0]), previousComment[0]); const isTextBetweenStartOfLineAndCommentWhitespace = textBetweenStartOfLineAndComment.match(/[^\s]/gim) == null; const isCommentOwnedByPreviousLine = !isTextBetweenStartOfLineAndCommentWhitespace && textBetweenCommentAndSpecifier.indexOf("\n") !== -1; // Ignore opeators and whitespace const newLineCount = textBetweenCommentAndSpecifier.match(/\n/gim); if (textBetweenCommentAndSpecifier.match(/[^(|&+\-*/\s)]/gim) || (newLineCount != null && 1 < newLineCount.length) || isCommentOwnedByPreviousLine) { break; } else { earliestCommentIndex--; } } } if (latestCommentIndex === -1) { return []; } if (earliestCommentIndex === -1) { earliestCommentIndex = latestCommentIndex; } return comments.slice(earliestCommentIndex, latestCommentIndex + 1); } function getSucceedingCommentsForSpecifier(fileContents, comments, specifier) { comments = comments.filter((comment) => { // There seems to be bugs with the parsers regarding certain comments // https://github.com/eslint/typescript-eslint-parser/issues/450 return isValidComment(fileContents, comment); }); const lastRange = specifier.range; if (lastRange == null) { return []; } let firstIndex = 0; let lastIndex = Math.max(0, comments.length - 1); let middleIndex = Math.floor((lastIndex + firstIndex) / 2); while (Math.abs(firstIndex - lastIndex) > 1) { const commentRange = comments[middleIndex].range; if (commentRange == null) { continue; } if (commentRange[0] < lastRange[0]) { firstIndex = middleIndex; middleIndex = Math.floor((lastIndex + middleIndex) / 2); } if (commentRange[0] > lastRange[0]) { lastIndex = middleIndex; middleIndex = Math.floor((firstIndex + middleIndex) / 2); } } for (let index = middleIndex; index < comments.length; index++) { const comment = comments[index]; const commentRange = comment.range; if (commentRange == null) { continue; } // Comment is before the specifier if (commentRange[0] < lastRange[0]) { continue; } const textBetweenCommentAndSpecifier = fileContents.substring(lastRange[1], commentRange[0]); const nextNewLine = fileContents.indexOf("\n", lastRange[1]); const textBetweenCommentAndEndOfLine = fileContents.substring(commentRange[1], nextNewLine === -1 ? undefined : nextNewLine); const isTextBetweenCommentAndSpecifierWhitespaceButNotNewline = textBetweenCommentAndSpecifier.match(/[\w\n]/gim) == null; const isTextBetweenCommentAndEndOfLineWhitespaceButNotNewline = textBetweenCommentAndEndOfLine.match(/[\w\n]/gim) == null; if (isTextBetweenCommentAndSpecifierWhitespaceButNotNewline && isTextBetweenCommentAndEndOfLineWhitespaceButNotNewline) { // TODO test multiple block comments at the end of the line return comments.slice(index, index + 1); } break; } return []; } function isValidComment(fileContents, comment) { const commentRange = comment.range; if (commentRange == null) { return false; } if (comment.type === "Line" && !fileContents.substring(commentRange[0], commentRange[1]).startsWith("//")) { return false; } if (comment.type === "Block" && !fileContents.substring(commentRange[0], commentRange[1]).startsWith("/*")) { return false; } return true; } function isIgnored(fileContents, comments, node) { if (node.range == null) { return false; } const newLineBeforeRange = fileContents.lastIndexOf("\n", node.range[0]); if (newLineBeforeRange === -1) { return false; } let beginningOfLine = fileContents.lastIndexOf("\n", newLineBeforeRange - 1); beginningOfLine = beginningOfLine === -1 ? 0 : beginningOfLine; const commentText = fileContents.substring(beginningOfLine, newLineBeforeRange); if (commentText.indexOf("sortier-ignore-nodes") !== -1) { return true; } if (commentText.indexOf("sortier-ignore-next-line") === -1) { return false; } const nodeText = fileContents.substring(node.range[0], node.range[1]); return nodeText.indexOf("\n") === -1; }