recast/test/lines.ts
Brian Donovan 7dee9e08ec
refactor: replace any type annotations in Lines
Many of these could just be left inferred.
2024-06-18 09:07:22 -07:00

550 lines
15 KiB
TypeScript

import assert from "assert";
import fs from "fs";
import path from "path";
import { fromString, concat, countSpaces, Lines } from "../lib/lines";
import { EOL as eol } from "os";
import { namedTypes } from "ast-types";
type LinesInput = string | Lines;
function toString(s: LinesInput): string {
if (typeof s === "string") {
return s;
}
return s.toString({ lineTerminator: eol });
}
function check(a: LinesInput, b: LinesInput) {
assert.strictEqual(toString(a), toString(b));
}
describe("lines", function () {
describe("line terminators", function () {
const source = ["foo;", "bar;"];
const terminators = [
"\u000A",
"\u000D",
"\u2028",
"\u2029",
"\u000D\u000A",
];
terminators.forEach(function (t) {
it("can handle " + escape(t) + " as line terminator", function () {
const lines = fromString(source.join(t));
assert.strictEqual(lines.length, 2);
assert.strictEqual(lines.getLineLength(1), 4);
});
});
});
it("FromString", function () {
function checkIsCached(s: LinesInput) {
assert.strictEqual(fromString(s), fromString(s));
check(fromString(s), s);
}
checkIsCached("");
checkIsCached(",");
checkIsCached(eol);
checkIsCached("this");
checkIsCached(", ");
checkIsCached(": ");
const longer =
"This is a somewhat longer string that we do not want to cache.";
assert.notStrictEqual(fromString(longer), fromString(longer));
// Since Lines objects are immutable, if one is passed to fromString,
// we can return it as-is without having to make a defensive copy.
const longerLines = fromString(longer);
assert.strictEqual(fromString(longerLines), longerLines);
});
it("ToString", function ToStringTest() {
const code = String(ToStringTest);
const lines = fromString(code);
check(lines, code);
check(lines.indentTail(5).indentTail(-7).indentTail(2), code);
});
function testEachPosHelper(lines: Lines, code: string) {
check(lines, code);
const chars: string[] = [];
let emptyCount = 0;
function iterator(pos: namedTypes.Position) {
const ch = lines.charAt(pos);
if (ch === "") emptyCount += 1;
chars.push(ch);
}
lines.eachPos(iterator, undefined);
// The character at the position just past the end (as returned by
// lastPos) should be the only empty string.
assert.strictEqual(emptyCount, 1);
// Function.prototype.toString uses \r\n line endings on non-*NIX
// systems, so normalize those to \n characters.
code = code.replace(/\r\n/g, "\n");
let joined = chars.join("");
assert.strictEqual(joined.length, code.length);
assert.strictEqual(joined, code);
const withoutSpaces = code.replace(/\s+/g, "");
chars.length = emptyCount = 0;
lines.eachPos(iterator, undefined, true); // Skip spaces this time.
assert.strictEqual(emptyCount, 0);
joined = chars.join("");
assert.strictEqual(joined.length, withoutSpaces.length);
assert.strictEqual(joined, withoutSpaces);
}
it("EachPos", function EachPosTest() {
const code = String(EachPosTest);
let lines = fromString(code);
testEachPosHelper(lines, code);
lines = lines.indentTail(5);
testEachPosHelper(lines, lines.toString());
lines = lines.indentTail(-9);
testEachPosHelper(lines, lines.toString());
lines = lines.indentTail(4);
testEachPosHelper(lines, code);
});
it("CharAt", function CharAtTest() {
// Function.prototype.toString uses \r\n line endings on non-*NIX
// systems, so normalize those to \n characters.
const code = String(CharAtTest).replace(/\r\n/g, "\n");
const lines = fromString(code);
function compare(pos: namedTypes.Position) {
assert.strictEqual(lines.charAt(pos), lines.bootstrapCharAt(pos));
}
lines.eachPos(compare);
let original = fromString(" ab" + eol + " c"),
indented = original.indentTail(4),
reference = fromString(" ab" + eol + " c");
function compareIndented(pos: namedTypes.Position) {
const c = indented.charAt(pos);
check(c, reference.charAt(pos));
check(c, indented.bootstrapCharAt(pos));
check(c, reference.bootstrapCharAt(pos));
}
indented.eachPos(compareIndented);
indented = indented.indentTail(-4);
reference = original;
indented.eachPos(compareIndented);
});
it("Concat", function () {
const strings = ["asdf", "zcxv", "qwer"],
lines = fromString(strings.join(eol)),
indented = lines.indentTail(4);
assert.strictEqual(lines.length, 3);
check(indented, strings.join(eol + " "));
assert.strictEqual(5, concat([lines, indented]).length);
assert.strictEqual(5, concat([indented, lines]).length);
check(concat([lines, indented]), lines.toString() + indented.toString());
check(
concat([lines, indented]).indentTail(4),
strings.join(eol + " ") + strings.join(eol + " "),
);
check(
concat([indented, lines]),
strings.join(eol + " ") + lines.toString(),
);
check(concat([lines, indented]), lines.concat(indented));
check(concat([indented, lines]), indented.concat(lines));
check(concat([]), fromString(""));
assert.strictEqual(concat([]), fromString(""));
check(
fromString(" ").join([fromString("var"), fromString("foo")]),
fromString("var foo"),
);
check(fromString(" ").join(["var", "foo"]), fromString("var foo"));
check(
concat([fromString("var"), fromString(" "), fromString("foo")]),
fromString("var foo"),
);
check(concat(["var", " ", "foo"]), fromString("var foo"));
check(concat([fromString("debugger"), ";"]), fromString("debugger;"));
});
it("Empty", function () {
function c(s: LinesInput) {
const lines = fromString(s);
check(lines, s);
assert.strictEqual(lines.isEmpty(), s.length === 0);
assert.ok(lines.trimLeft().isEmpty());
assert.ok(lines.trimRight().isEmpty());
assert.ok(lines.trim().isEmpty());
}
c("");
c(" ");
c(" ");
c(" " + eol);
c(eol + " ");
c(" " + eol + " ");
c(eol + " " + eol + " ");
c(" " + eol + eol + " ");
c(" " + eol + " " + eol + " ");
c(" " + eol + " " + eol + eol);
});
it("SingleLine", function () {
const string = "asdf",
line = fromString(string);
check(line, string);
check(line.indentTail(4), string);
check(line.indentTail(-4), string);
// Single-line Lines objects are completely unchanged by indentTail.
assert.strictEqual(line.indentTail(10), line);
// Multi-line Lines objects are altered by indentTail, but only if the
// amount of the indentation is non-zero.
const twice = line.concat(eol, line);
assert.notStrictEqual(twice.indentTail(10), twice);
assert.strictEqual(twice.indentTail(0), twice);
check(line.concat(line), string + string);
check(line.indentTail(4).concat(line), string + string);
check(line.concat(line.indentTail(4)), string + string);
check(line.indentTail(8).concat(line.indentTail(4)), string + string);
line.eachPos(function (start) {
line.eachPos(function (end) {
check(line.slice(start, end), string.slice(start.column, end.column));
}, start);
});
});
it("Slice", function SliceTest() {
const code = String(SliceTest),
lines = fromString(code);
checkAllSlices(lines);
});
function checkAllSlices(lines: Lines) {
lines.eachPos(function (start) {
lines.eachPos(function (end) {
check(lines.slice(start, end), lines.bootstrapSlice(start, end));
check(
lines.sliceString(start, end),
lines.bootstrapSliceString(start, end),
);
}, start);
});
}
function getSourceLocation(lines: Lines) {
return { start: lines.firstPos(), end: lines.lastPos() };
}
it("GetSourceLocation", function GetSourceLocationTest() {
const code = String(GetSourceLocationTest),
lines = fromString(code);
function verify(indent: number) {
const indented = lines.indentTail(indent),
loc = getSourceLocation(indented),
string = indented.toString(),
strings = string.split(eol),
lastLine = strings[strings.length - 1];
assert.strictEqual(loc.end.line, strings.length);
assert.strictEqual(loc.end.column, lastLine.length);
assert.deepEqual(
loc,
getSourceLocation(indented.slice(loc.start, loc.end)),
);
}
verify(0);
verify(4);
verify(-4);
});
it("Trim", function () {
const string = " xxx " + eol + " ";
const options = { tabWidth: 4 };
fromString(string);
function test(string: string) {
const lines = fromString(string, options);
check(lines.trimLeft(), fromString(string.replace(/^\s+/, ""), options));
check(lines.trimRight(), fromString(string.replace(/\s+$/, ""), options));
check(
lines.trim(),
fromString(string.replace(/^\s+|\s+$/g, ""), options),
);
}
test("");
test(" ");
test(" xxx " + eol + " ");
test(" xxx");
test("xxx ");
test(eol + "x" + eol + "x" + eol + "x" + eol);
test("\t" + eol + "x" + eol + "x" + eol + "x" + eol + "\t" + eol);
test("xxx");
});
it("NoIndentEmptyLines", function () {
const lines = fromString("a" + eol + eol + "b"),
indented = lines.indent(4),
tailIndented = lines.indentTail(5);
check(indented, fromString(" a" + eol + eol + " b"));
check(tailIndented, fromString("a" + eol + eol + " b"));
check(indented.indent(-4), lines);
check(tailIndented.indent(-5), lines);
});
it("CountSpaces", function () {
const count = countSpaces;
assert.strictEqual(count(""), 0);
assert.strictEqual(count(" "), 1);
assert.strictEqual(count(" "), 2);
assert.strictEqual(count(" "), 3);
function check(s: string, tabWidth: number, result: number) {
assert.strictEqual(count(s, tabWidth), result);
}
check("", 2, 0);
check("", 3, 0);
check("", 4, 0);
check(" ", 2, 1);
check("\t", 2, 2);
check("\t\t", 2, 4);
check(" \t\t", 2, 4);
check(" \t \t", 2, 4);
check(" \t \t", 2, 6);
check(" \t \t", 2, 8);
check(" \t \t", 2, 6);
check(" \t \t", 2, 6);
check(" ", 3, 1);
check("\t", 3, 3);
check("\t\t", 3, 6);
check(" \t\t", 3, 6);
check(" \t \t", 3, 6);
check(" \t \t", 3, 6);
check(" \t \t", 3, 6);
check(" \t \t", 3, 9);
check(" \t \t", 3, 9);
check("\t\t\t ", 2, 9);
check("\t\t\t ", 3, 12);
check("\t\t\t ", 4, 15);
check("\r", 4, 0);
check("\r ", 4, 1);
check(" \r ", 4, 2);
check(" \r\r ", 4, 2);
});
it("IndentWithTabs", function () {
const tabWidth = 4;
const tabOpts = { tabWidth: tabWidth, useTabs: true };
const noTabOpts = { tabWidth: tabWidth, useTabs: false };
let code = ["function f() {", "\treturn this;", "}"].join(eol);
function checkUnchanged(lines: Lines, code: string) {
check(lines.toString(tabOpts), code);
check(lines.toString(noTabOpts), code);
check(lines.indent(3).indent(-5).indent(2).toString(tabOpts), code);
check(lines.indent(-3).indent(4).indent(-1).toString(noTabOpts), code);
}
let lines = fromString(code, tabOpts);
checkUnchanged(lines, code);
check(
lines.indent(1).toString(tabOpts),
[" function f() {", "\t return this;", " }"].join(eol),
);
check(
lines.indent(tabWidth).toString(tabOpts),
["\tfunction f() {", "\t\treturn this;", "\t}"].join(eol),
);
check(
lines.indent(1).toString(noTabOpts),
[" function f() {", " return this;", " }"].join(eol),
);
check(
lines.indent(tabWidth).toString(noTabOpts),
[" function f() {", " return this;", " }"].join(eol),
);
const funkyCode = [
" function g() { \t ",
" \t\t return this; ",
"\t} ",
].join(eol);
const funky = fromString(funkyCode, tabOpts);
checkUnchanged(funky, funkyCode);
check(
funky.indent(1).toString(tabOpts),
[" function g() { \t ", "\t\t return this; ", "\t } "].join(eol),
);
check(
funky.indent(2).toString(tabOpts),
[" function g() { \t ", "\t\t\treturn this; ", "\t } "].join(eol),
);
check(
funky.indent(1).toString(noTabOpts),
[" function g() { \t ", " return this; ", " } "].join(
eol,
),
);
check(
funky.indent(2).toString(noTabOpts),
[" function g() { \t ", " return this; ", " } "].join(
eol,
),
);
// Test that '\v' characters are ignored for the purposes of indentation,
// but preserved when printing untouched lines.
code = ["\vfunction f() {\v", " \v return \vthis;\v", "\v} \v "].join(
eol,
);
lines = fromString(code, tabOpts);
checkUnchanged(lines, code);
check(
lines.indent(4).toString(noTabOpts),
[" function f() {\v", " return \vthis;\v", " } \v "].join(
eol,
),
);
check(
lines.indent(5).toString(tabOpts),
["\t function f() {\v", "\t\t return \vthis;\v", "\t } \v "].join(eol),
);
});
it("GuessTabWidth", function GuessTabWidthTest(done) {
let lines;
lines = fromString(
["function identity(x) {", " return x;", "}"].join(eol),
);
assert.strictEqual(lines.guessTabWidth(), 4);
lines = fromString(
["function identity(x) {", " return x;", "}"].join(eol),
);
assert.strictEqual(lines.guessTabWidth(), 2);
assert.strictEqual(lines.indent(5).guessTabWidth(), 2);
assert.strictEqual(lines.indent(-4).guessTabWidth(), 2);
fs.readFile(__filename, "utf-8", function (err, source) {
assert.equal(err, null);
assert.strictEqual(fromString(source).guessTabWidth(), 4);
fs.readFile(
path.join(__dirname, "..", "package.json"),
"utf-8",
function (err, source) {
assert.equal(err, null);
assert.strictEqual(fromString(source).guessTabWidth(), 2);
done();
},
);
});
});
it("ExoticWhitespace", function () {
let source = "";
const spacePattern = /^\s+$/;
for (let i = 0; i < 0xffff; ++i) {
const ch = String.fromCharCode(i);
if (spacePattern.test(ch)) {
source += ch;
}
}
source += "x";
const options = { tabWidth: 4 };
const lines = fromString(source, options);
assert.strictEqual(lines.length, 5);
assert.strictEqual(lines.getLineLength(1), options.tabWidth);
assert.strictEqual(lines.getIndentAt(1), options.tabWidth);
assert.strictEqual(
lines
.slice({
line: 5,
column: lines.getLineLength(5) - 1,
})
.toString(options),
"x",
);
assert.ok(
spacePattern.test(
lines
.slice(lines.firstPos(), {
line: 5,
column: lines.getLineLength(5) - 1,
})
.toString(options),
),
);
});
});