prettier-plugin-imports
Version:
A prettier plugins to sort imports in provided RegEx order
203 lines (185 loc) • 6.9 kB
text/typescript
import type {
CommentBlock,
CommentLine,
Directive,
ImportDeclaration,
InterpreterDirective,
Statement,
} from '@babel/types';
/** A range between a start position (inclusive) and an end position (exclusive). */
type Range = readonly [start: number, end: number];
/**
* An optional range between a start position (inclusive) and an end position
* (exclusive).
*/
type OptionalRange = readonly [
start: number | null | undefined,
end: number | null | undefined,
];
/** Compares two range by their start position. */
function compareRangesByStart(range1: Range, range2: Range): number {
return range1[0] < range2[0] ? -1 : range1[0] > range2[0] ? 1 : 0;
}
/** Type predicate that checks whether a range has a defined start and end. */
function hasRange(range: OptionalRange): range is Range {
return (
range[0] !== null &&
range[1] !== null &&
Number.isSafeInteger(range[0]) &&
Number.isSafeInteger(range[1])
);
}
/**
* @param range1 One range to check.
* @param range2 Another range to check.
* @param `true` If both ranges have some overlap. This overlap may consist of a
* single point, i.e. `[2, 5)` and `[4, 8)` are considered overlapping.
*/
function hasOverlap(range1: Range, range2: Range): boolean {
return range1[1] > range2[0] && range2[1] > range1[0];
}
/**
* Given two ranges that are known to overlap, constructs a new range
* representing the single range enclosing both ranges.
*
* @param range1 One range to process.
* @param range2 Another range to process.
* @returns A single range representing the union of both ranges.
*/
function mergeOverlappingRanges(range1: Range, range2: Range): Range {
return [Math.min(range1[0], range2[0]), Math.max(range1[1], range2[1])];
}
/**
* Given a list of ordered, possibly overlapping (non-disjoint) ranges,
* constructs a new list of ranges that consists entirely of disjoint ranges.
* The new list is also ordered.
*
* @param A List of ranges that may be overlapping, but are ordered by their
* start position.
* @returns A list of disjoint ranges that are also ordered by their start
* position.
*/
function mergeRanges(ranges: Range[]): Range[] {
// Start with a result list initialized to the empty list
// Iterate over all given ranges. If a range overlaps the last item in
// the result list, replace the last item with the merger between that item
// and the range. Otherwise, just add the item to the result list.
// For comparison, see also
// https://www.geeksforgeeks.org/merging-intervals/
const merged: Range[] = [];
for (const range of ranges) {
const currRange = merged[merged.length - 1];
if (currRange !== undefined && hasOverlap(currRange, range)) {
merged[merged.length - 1] = mergeOverlappingRanges(currRange, range);
} else {
merged.push(range);
}
}
return merged;
}
/**
* Takes a list of ordered, disjoint, non-overlapping ranges and a range `[0,
* totalLength)` that encloses all those ranges.
*
* Constructs a new list of ranges representing the negation of the ranges with
* respect to the enclosing range `[0, totalLength)`. Put in other words,
* subtracts the given ranges from the range `[0, totalLength)`.
*
* More formally, let `r_1`, `r_2`, ..., `r_n` denote the sets represented by
* the given ranges; and let `r` be the set `[0, totalLength)`. Then this
* function returns a list of ranges representing the set
*
* > R \ r_1 \ r_2 \ ... \ r_n
*
* (where `\` is the set negation operator)
*
* @param ranges A list of disjoint (non-overlapping) ranges ordered by their
* start position.
* @param totalLength The end of the enclosing range from which to subtract the
* given ranges.
* @returns A list of ranges representing the inverse of the given ranges with
* respect to the enclosing range.
*/
function invertRanges(ranges: Range[], totalLength: number): Range[] {
// We'd run into out-of-bounds checks if we performed the rest of the
// algorithm with an empty array, and would have to insert additional
// checks. So just return immediately to keep the code simpler.
if (ranges.length === 0) {
return ranges;
}
const resultRanges: Range[] = [];
const isValidRange = (start: number, end: number) => end > start;
// Add the part from the start of the enclosing range to the start of the
// first range to exclude.
//
// |-----------xxxxx-----xxxx-----xxxx-----------|
// ^---------^
// This part
const firstRange = ranges[0];
if (isValidRange(0, firstRange[0])) {
resultRanges.push([0, firstRange[0]]);
}
// Iterate over the parts between the ranges to remove and add those parts.
//
// |----------xxxxx-----xxxx------xxxx-----------|
// ^---^ ^----^
// These parts
for (let index = 0; index < ranges.length - 1; index += 1) {
const prevRange = ranges[index];
const nextRange = ranges[index + 1];
const start = prevRange[1];
const end = nextRange[0];
if (isValidRange(start, end)) {
resultRanges.push([start, end]);
}
}
// Add the part from the end of the last range to exclude to the end of the
// enclosing range.
//
// |----------xxxxx-----xxxx-----xxxx------------|
// ^----------^
// This part
const lastRange = ranges[ranges.length - 1];
if (isValidRange(lastRange[1], totalLength)) {
resultRanges.push([lastRange[1], totalLength]);
}
return resultRanges;
}
/**
* Given a piece of code and a list of nodes that appear in that code, removes
* all those nodes from the code.
*
* @param code The whole file as text
* @param nodes List of nodes to be removed from the code.
* @returns The given code with all parts of the code removed that correspond to
* one of the given nodes.
*/
export const removeNodesFromOriginalCode = (
code: string,
nodes: (
| Statement
| CommentBlock
| CommentLine
| ImportDeclaration
| InterpreterDirective
| Directive
)[],
): string => {
// A list of ranges we should remove from the code
// Each range [start, end] consists of a start position in the code
// (inclusive) and an end position in the code (exclusive)
const excludes = nodes
.map((node) => [node.start, node.end] as const)
.filter(hasRange)
.sort(compareRangesByStart);
// In case there are overlapping ranges, merge all overlapping ranges into
// a single range.
const mergedExcludes = mergeRanges(excludes);
// Find the code ranges we want to keep by inverting the excludes with
// respect to the entire range [0, code.length]
const includes = invertRanges(mergedExcludes, code.length);
// Extract all code parts we want to keep and join them together again
return includes
.map((include) => code.substring(include[0], include[1]))
.join('');
};