@fimbul/wotan
Version:
Pluggable TypeScript and JavaScript linter
114 lines • 4.79 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyFixes = void 0;
const stableSort = require("stable");
/**
* Tries to apply all fixes. The replacements of all fixes are sorted by index ascending.
* They are then applied in order. If a replacement overlaps (or touches) the range of the previous replacement,
* the process rolls back to the state before the first replacement of the offending fix was applied. The replacements
* of this fix are not applied again.
* At least one fix will be applied.
*/
function applyFixes(source, fixes) {
let fixed = fixes.length;
const replacements = [];
for (const fix of fixes) {
const state = { replacements: combineReplacements(fix.replacements), skip: false, state: undefined };
for (const replacement of state.replacements)
replacements.push({ fix: state, ...replacement });
}
const range = {
span: {
start: 0,
length: 0,
},
newLength: 0,
};
let output = '';
let position = -1;
replacements.sort(compareReplacements);
for (let i = 0; i < replacements.length; ++i) {
const replacement = replacements[i];
const { fix } = replacement;
if (fix.skip)
continue; // there was a conflict, don't use replacements of this fix
if (replacement.start <= position) {
// ranges overlap (or have touching boundaries) -> don't fix to prevent unspecified behavior
if (fix.state !== undefined) {
// rollback to state before the first replacement of the fix was applied
const rollbackToIndex = fix.state.index;
for (--i; i !== rollbackToIndex; --i) { // this automatically resets `i` to the correct position
const f = replacements[i].fix;
// we need to reset all states of fixes that applied their first replacement after
// the first replacement of the just rolled back fix
if (f.state !== undefined && f.state.index === i)
f.state = undefined;
// retry all rolled back fixes, that applied their first replacement after the just rolled back fix
// unfortunately this doesn't take conflicting fixes into account, so we may roll it back later
if (f.state === undefined && f.skip) {
++fixed;
f.skip = false;
}
}
output = output.substring(0, fix.state.length);
position = fix.state.position;
}
fix.skip = true;
--fixed;
continue;
}
// save the current state to jump back if necessary
if (fix.state === undefined && fix.replacements.length !== 1)
fix.state = { position, index: i, length: output.length };
if (position === -1) {
// we are about to apply the first fix
range.span.start = replacement.start;
output = source.substring(0, replacement.start);
}
else {
output += source.substring(position, replacement.start);
}
output += replacement.text;
position = replacement.end;
}
output += source.substring(position);
range.span.length = position - range.span.start;
range.newLength = range.span.length + output.length - source.length;
return {
fixed,
range,
result: output,
};
}
exports.applyFixes = applyFixes;
function compareReplacements(a, b) {
return a.start - b.start || a.end - b.end;
}
/** Combine adjacent replacements to avoid sorting replacements of other fixes between them. */
function combineReplacements(replacements) {
if (replacements.length === 1)
return replacements;
// use a stable sorting algorithm to avoid shuffling insertions at the same position as these have the same start and end values
replacements = stableSort.inplace(replacements.slice(), compareReplacements);
const result = [];
let current = replacements[0];
for (let i = 1; i < replacements.length; ++i) {
const replacement = replacements[i];
if (current.end > replacement.start)
throw new Error('Replacements of fix overlap.');
if (current.end === replacement.start) {
current = {
start: current.start,
end: replacement.end,
text: current.text + replacement.text,
};
}
else {
result.push(current);
current = replacement;
}
}
result.push(current);
return result;
}
//# sourceMappingURL=fix.js.map
;