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), ), ); }); });