"use strict"; import * as recast from "../main"; const n = recast.types.namedTypes; const b = recast.types.builders; import { Printer } from "../lib/printer"; import { fromString } from "../lib/lines"; import assert from "assert"; import { EOL as eol } from "os"; const annotated = [ "function dup(/* string */ s,", " /* int */ n) /* string */", "{", " // Use an array full of holes.", " return Array(n + /*", " * off-by-*/ 1).join(s);", "}", ]; const nodeMajorVersion = parseInt(process.versions.node, 10); describe("comments", function () { [ "../parsers/acorn", "../parsers/babel", "../parsers/esprima", "../parsers/flow", "../parsers/typescript", ].forEach(runTestsForParser); }); function runTestsForParser(parserId: any) { if (nodeMajorVersion < 6) { const parser = parserId.split("/").pop(); if (parser === "babel" || parser === "flow" || parser === "typescript") { // Babel 7 no longer supports Node 4 and 5. return; } } const parserName = parserId.split("/").pop(); const parser = require(parserId); function pit(message: any, callback: any) { it("[" + parserName + "] " + message, callback); } pit("attachment and reprinting", function () { const code = annotated.join(eol); const ast = recast.parse(code, { parser }); const dup = ast.program.body[0]; n.FunctionDeclaration.assert(dup); assert.strictEqual(dup.id.name, "dup"); // More of a basic sanity test than a comment test. assert.strictEqual(recast.print(ast).code, code); assert.strictEqual(recast.print(ast.program).code, code); assert.strictEqual(recast.print(dup).code, code); assert.strictEqual(recast.print(dup.params[0]).code, "/* string */ s"); assert.strictEqual(recast.print(dup.params[1]).code, "/* int */ n"); assert.strictEqual( recast.print(dup.body).code, ["/* string */"].concat(annotated.slice(2)).join(eol), ); const retStmt = dup.body.body[0]; n.ReturnStatement.assert(retStmt); const indented = annotated.slice(3, 6).join(eol); const flush = fromString(indented).indent(-2); assert.strictEqual(recast.print(retStmt).code, flush.toString()); const join = retStmt.argument; n.CallExpression.assert(join); const one = join.callee.object.arguments[0].right; n.Literal.assert(one); assert.strictEqual(one.value, 1); assert.strictEqual( recast.print(one).code, ["/*", " * off-by-*/ 1"].join(eol), ); }); const trailing = [ "Foo.prototype = {", "// Copyright (c) 2013 Ben Newman ", "", " /**", " * Leading comment.", " */", " constructor: Foo, // Important for instanceof", " // to work in all browsers.", ' bar: "baz", // Just in case we need it.', " qux: { // Here is an object literal.", " zxcv: 42", " // Put more properties here when you think of them.", " } // There was an object literal...", " // ... and here I am continuing this comment.", "", "};", ]; const trailingExpected = [ "Foo.prototype = {", " // Copyright (c) 2013 Ben Newman ", "", " /**", " * Leading comment.", " */", " // Important for instanceof", " // to work in all browsers.", " constructor: Foo,", "", " // Just in case we need it.", ' bar: "baz",', "", " // There was an object literal...", " // ... and here I am continuing this comment.", " qux: {", " // Here is an object literal.", " // Put more properties here when you think of them.", " zxcv: 42,", "", " asdf: 43", " },", "", ' extra: "property"', "};", ]; pit("TrailingComments", function () { const code = trailing.join(eol); const ast = recast.parse(code, { parser }); assert.strictEqual(recast.print(ast).code, code); // Drop all original source information to force reprinting. recast.visit(ast, { visitNode: function (path) { this.traverse(path); path.value.original = null; }, }); const assign = ast.program.body[0].expression; n.AssignmentExpression.assert(assign); const esprimaInfo = { Property: n.Property, propBuilder(key: any, value: any) { return b.property("init", key, value); }, literalBuilder(value: any) { return b.literal(value); }, }; const babelInfo = { Property: n.ObjectProperty, propBuilder(key: any, value: any) { return b.objectProperty(key, value); }, literalBuilder(value: any) { if (typeof value === "string") { return b.stringLiteral(value); } if (typeof value === "number") { return b.numericLiteral(value); } throw new Error("unexpected literal: " + value); }, }; const info = ( { acorn: esprimaInfo, babel: babelInfo, esprima: esprimaInfo, flow: babelInfo, typescript: babelInfo, } as any )[parserName]; const props = assign.right.properties; info.Property.arrayOf().assert(props); props.push( info.propBuilder(b.identifier("extra"), info.literalBuilder("property")), ); const quxVal = props[2].value; n.ObjectExpression.assert(quxVal); quxVal.properties.push( info.propBuilder(b.identifier("asdf"), info.literalBuilder(43)), ); const actual = recast.print(ast, { tabWidth: 2 }).code; const expected = trailingExpected.join(eol); // Check semantic equivalence: recast.types.astNodesAreEquivalent.assert( ast, recast.parse(actual, { parser }), ); assert.strictEqual(actual, expected); }); const bodyTrailing = [ "module.exports = {};", "/**", " * Trailing comment.", " */", ]; const bodyTrailingExpected = [ "module.exports = {};", "/**", " * Trailing comment.", " */", ]; pit("BodyTrailingComments", function () { const code = bodyTrailing.join(eol); const ast = recast.parse(code, { parser }); // Drop all original source information to force reprinting. recast.visit(ast, { visitNode: function (path) { this.traverse(path); path.value.original = null; }, }); const actual = recast.print(ast, { tabWidth: 2 }).code; const expected = bodyTrailingExpected.join(eol); assert.strictEqual(actual, expected); }); const paramTrailing = [ "function foo(bar, baz /* = null */) {", " assert.strictEqual(baz, null);", "}", ]; const paramTrailingExpected = [ "function foo(zxcv, bar, baz /* = null */) {", " assert.strictEqual(baz, null);", "}", ]; pit("ParamTrailingComments", function () { const code = paramTrailing.join(eol); const ast = recast.parse(code, { parser }); const func = ast.program.body[0]; n.FunctionDeclaration.assert(func); func.params.unshift(b.identifier("zxcv")); const actual = recast.print(ast, { tabWidth: 2 }).code; const expected = paramTrailingExpected.join(eol); assert.strictEqual(actual, expected); }); const statementTrailing = [ "if (true) {", " f();", " // trailing 1", " /* trailing 2 */", " // trailing 3", " /* trailing 4 */", "}", ]; const statementTrailingExpected = [ "if (true) {", " e();", " f();", " // trailing 1", " /* trailing 2 */", " // trailing 3", " /* trailing 4 */", "}", ]; pit("StatementTrailingComments", function () { const code = statementTrailing.join(eol); const ast = recast.parse(code, { parser }); const block = ast.program.body[0].consequent; n.BlockStatement.assert(block); block.body.unshift( b.expressionStatement(b.callExpression(b.identifier("e"), [])), ); const actual = recast.print(ast, { tabWidth: 2 }).code; const expected = statementTrailingExpected.join(eol); assert.strictEqual(actual, expected); }); const protoAssign = [ "A.prototype.foo = function() {", " return this.bar();", "}", // Lack of semicolon screws up location info. "", "// Comment about the bar method.", "A.prototype.bar = function() {", " return this.foo();", "}", ]; pit("ProtoAssignComment", function () { const code = protoAssign.join(eol); const ast = recast.parse(code, { parser }); const foo = ast.program.body[0]; const bar = ast.program.body[1]; n.ExpressionStatement.assert(foo); n.ExpressionStatement.assert(bar); assert.strictEqual(foo.expression.left.property.name, "foo"); assert.strictEqual(bar.expression.left.property.name, "bar"); assert.ok(!foo.comments); assert.ok(bar.comments); assert.strictEqual(bar.comments.length, 1); const barComment = bar.comments[0]; assert.strictEqual(barComment.leading, true); assert.strictEqual(barComment.trailing, false); assert.strictEqual(barComment.value, " Comment about the bar method."); }); const conciseMethods = [ "var obj = {", " a(/*before*/ param) {},", " b(param /*after*/) {},", " c(param) /*body*/ {}", "};", ]; pit("should correctly attach to concise methods", function () { const code = conciseMethods.join(eol); const ast = recast.parse(code, { parser }); const objExpr = ast.program.body[0].declarations[0].init; n.ObjectExpression.assert(objExpr); const a = objExpr.properties[0]; n.Identifier.assert(a.key); assert.strictEqual(a.key.name, "a"); const aComments = (a.value || a).params[0].comments; assert.strictEqual(aComments.length, 1); const aComment = aComments[0]; assert.strictEqual(aComment.leading, true); assert.strictEqual(aComment.trailing, false); assert.ok(aComment.type.endsWith("Block")); assert.strictEqual(aComment.value, "before"); assert.strictEqual(recast.print(a).code, "a(/*before*/ param) {}"); const b = objExpr.properties[1]; n.Identifier.assert(b.key); assert.strictEqual(b.key.name, "b"); const bComments = (b.value || b).params[0].comments; assert.strictEqual(bComments.length, 1); const bComment = bComments[0]; assert.strictEqual(bComment.leading, false); assert.strictEqual(bComment.trailing, true); assert.ok(bComment.type.endsWith("Block")); assert.strictEqual(bComment.value, "after"); assert.strictEqual(recast.print(b).code, "b(param /*after*/) {}"); const c = objExpr.properties[2]; n.Identifier.assert(c.key); assert.strictEqual(c.key.name, "c"); const cComments = (c.value || c).body.comments; assert.strictEqual(cComments.length, 1); const cComment = cComments[0]; assert.strictEqual(cComment.leading, true); assert.strictEqual(cComment.trailing, false); assert.ok(cComment.type.endsWith("Block")); assert.strictEqual(cComment.value, "body"); assert.strictEqual(recast.print(c).code, "c(param) /*body*/ {}"); }); pit("should attach comments as configurable", function () { // Given const simpleCommentedCode = ["// A comment", "var obj = {", "};"]; const code = simpleCommentedCode.join(eol); const ast = recast.parse(code, { parser }); // When Object.defineProperty(ast.program, "comments", { value: undefined, enumerable: false, }); // Then // An exception will be thrown if `comments` aren't configurable. }); pit("should be reprinted when modified", function () { const code = ["foo;", "// bar", "bar;"].join(eol); const ast = recast.parse(code, { parser }); const comments = ast.program.body[1].comments; assert.strictEqual(comments.length, 1); let comment = comments[0]; assert.ok(comment.type.endsWith("Line")); assert.strictEqual(comment.value, " bar"); comment.value = " barbara"; assert.strictEqual( recast.print(ast).code, ["foo;", "// barbara", "bar;"].join(eol), ); ast.program.body[0].comments = comments; delete ast.program.body[1].comments; assert.strictEqual( recast.print(ast).code, ["// barbara", "foo;", "bar;"].join(eol), ); ast.program.body[0] = b.blockStatement([ast.program.body[0]]); assert.strictEqual( recast.print(ast).code, ["{", " // barbara", " foo;", "}", "", "bar;"].join(eol), ); comment = ast.program.body[0].body[0].comments[0]; comment.type = "Block"; assert.strictEqual( recast.print(ast).code, ["{", " /* barbara*/", " foo;", "}", "", "bar;"].join(eol), ); comment.value += "\n * babar\n "; assert.strictEqual( recast.print(ast).code, [ "{", " /* barbara", " * babar", " */", " foo;", "}", "", "bar;", ].join(eol), ); ast.program.body[1].comments = [comment]; assert.strictEqual( recast.print(ast).code, [ "{", " /* barbara", " * babar", " */", " foo;", "}", "", "/* barbara", " * babar", " */", "bar;", ].join(eol), ); delete ast.program.body[0].body[0].comments; ast.program.comments = [b.line(" program comment")]; assert.strictEqual( recast.print(ast).code, [ "// program comment", "{", " foo;", "}", "", "/* barbara", " * babar", " */", "bar;", ].join(eol), ); ast.program.body.push(ast.program.body.shift()); assert.strictEqual( recast.print(ast).code, [ "// program comment", "/* barbara", " * babar", " */", "bar;", "", "{", " foo;", "}", ].join(eol), ); recast.visit(ast, { visitNode: function (path) { delete path.value.comments; this.traverse(path); }, }); assert.strictEqual( recast.print(ast).code, ["bar;", "", "{", " foo;", "}"].join(eol), ); ast.program.body[1] = ast.program.body[1].body[0]; assert.strictEqual(recast.print(ast).code, ["bar;", "foo;"].join(eol)); }); pit("should preserve stray non-comment syntax", function () { const code = [ "[", " foo", " , /* comma */", " /* hole */", " , /* comma */", " bar", "]", ].join(eol); const ast = recast.parse(code, { parser }); assert.strictEqual(recast.print(ast).code, code); const elems = ast.program.body[0].expression.elements; elems[0].comments.push(b.line(" line comment", true, false)); assert.strictEqual( recast.print(ast).code, [ "[", " // line comment", " foo /* comma */", " /* hole */", " ,", " , /* comma */", " bar", "]", ].join(eol), ); }); pit("should be reprinted even if dangling", function () { const code = "[/*dangling*/] // array literal"; const ast = recast.parse(code, { parser }); const array = ast.program.body[0].expression; const stmt = ast.program.body[0]; let danglingComment: any; let trailingComment: any; function handleComment(comment: any) { if (comment.trailing) { trailingComment = comment; } else if (!comment.leading) { danglingComment = comment; } } (stmt.comments || []).forEach(handleComment); (stmt.expression.comments || []).forEach(handleComment); assert.strictEqual(danglingComment.leading, false); assert.strictEqual(danglingComment.trailing, false); assert.strictEqual(trailingComment.leading, false); assert.strictEqual(trailingComment.trailing, true); danglingComment.value = " neither leading nor trailing "; assert.strictEqual( recast.print(ast).code, ["[/* neither leading nor trailing */] // array literal"].join(eol), ); trailingComment.value = " trailing"; assert.strictEqual( recast.print(ast).code, ["[/* neither leading nor trailing */] // trailing"].join(eol), ); // Unfortuantely altering the elements of the array leads to // reprinting which blows away the dangling comment. array.elements.push(b.literal(1)); assert.strictEqual(recast.print(ast).code, "[1] // trailing"); }); pit("should attach to program.body[0] instead of program", function () { const code = [ "// comment 1", "var a;", "// comment 2", "var b;", "if (true) {", " // comment 3", " var c;", "}", ].join("\n"); const ast = recast.parse(code, { parser }); assert.ok(!ast.program.comments); const aDecl = ast.program.body[0]; n.VariableDeclaration.assert(aDecl); assert.strictEqual(aDecl.comments.length, 1); assert.strictEqual(aDecl.comments[0].leading, true); assert.strictEqual(aDecl.comments[0].trailing, false); assert.strictEqual(aDecl.comments[0].value, " comment 1"); const bDecl = ast.program.body[1]; n.VariableDeclaration.assert(bDecl); assert.strictEqual(bDecl.comments.length, 1); assert.strictEqual(bDecl.comments[0].leading, true); assert.strictEqual(bDecl.comments[0].trailing, false); assert.strictEqual(bDecl.comments[0].value, " comment 2"); const cDecl = ast.program.body[2].consequent.body[0]; n.VariableDeclaration.assert(cDecl); assert.strictEqual(cDecl.comments.length, 1); assert.strictEqual(cDecl.comments[0].leading, true); assert.strictEqual(cDecl.comments[0].trailing, false); assert.strictEqual(cDecl.comments[0].value, " comment 3"); }); pit("should not collapse multi line function definitions", function () { const code = [ "var obj = {", " a(", " /*before*/ param", " ) /*after*/ {", " },", "};", ].join(eol); const ast = recast.parse(code, { parser }); const printer = new Printer({ tabWidth: 2, }); assert.strictEqual(printer.print(ast).code, code); }); pit("should be pretty-printable in illegal positions", function () { const code = [ "var sum = function /*anonymous*/(/*...args*/) /*int*/ {", " // TODO", "};", ].join(eol); const ast = recast.parse(code, { parser }); const funExp = ast.program.body[0].declarations[0].init; n.FunctionExpression.assert(funExp); funExp.original = null; const comments = funExp.body.comments; assert.strictEqual(comments.length, 4); funExp.id = comments.shift(); funExp.params.push(comments.shift()); funExp.body.body.push(comments.pop()); assert.strictEqual(recast.print(ast).code, code); }); pit( "should preserve correctness when a return expression has a comment", function () { const code = ["function f() {", " return 3;", "}"].join(eol); const ast = recast.parse(code, { parser }); ast.program.body[0].body.body[0].argument.comments = [b.line("Foo")]; assert.strictEqual( recast.print(ast).code, [ "function f() {", " return (", " //Foo", " 3", " );", "}", ].join(eol), ); }, ); pit( "should wrap in parens when the return expression has nested leftmost comment", function () { const code = ["function f() {", " return 1 + 2;", "}"].join(eol); const ast = recast.parse(code, { parser }); ast.program.body[0].body.body[0].argument.left.comments = [b.line("Foo")]; assert.strictEqual( recast.print(ast).code, [ "function f() {", " return (", " //Foo", " 1 + 2", " );", "}", ].join(eol), ); }, ); pit( "should not wrap in parens when the return expression has an interior comment", function () { const code = ["function f() {", " return 1 + 2;", "}"].join(eol); const ast = recast.parse(code, { parser }); ast.program.body[0].body.body[0].argument.right.comments = [ b.line("Foo"), ]; assert.strictEqual( recast.print(ast).code, ["function f() {", " return 1 + //Foo", " 2;", "}"].join(eol), ); }, ); pit("should correctly handle a lonesome comment (alt 1)", function () { const code = ["", "// boo", ""].join(eol); const ast = recast.parse(code); assert.strictEqual(recast.print(ast).code, ["", "// boo", ""].join(eol)); }); pit( "should correctly handle a not-so-lonesome comment (alt 2 - trailing whitespace)", function () { const code = ["", "// boo ", ";"].join(eol); const ast = recast.parse(code); assert.strictEqual( recast.print(ast).code, ["", "// boo ", ";"].join(eol), ); }, ); pit( "should correctly handle a lonesome comment (alt 3 - trailing whitespace)", function () { const code = ["", "// boo ", ""].join(eol); const ast = recast.parse(code); assert.strictEqual(recast.print(ast).code, ["", "// boo ", ""].join(eol)); }, ); pit( "should not reformat a return statement that is not modified", function () { const code = [ "function f() {", " return {", " a: 1,", " b: 2,", " };", "}", ].join(eol); const ast = recast.parse(code, { parser }); assert.strictEqual(recast.print(ast).code, code); }, ); pit( "should correctly handle a removing the argument from a return", function () { const code = ["function f() {", " return 'foo';", "}"].join(eol); const ast = recast.parse(code, { parser }); ast.program.body[0].body.body[0].argument = null; assert.strictEqual( recast.print(ast).code, ["function f() {", " return;", "}"].join(eol), ); }, ); pit("should preserve comments attached to EmptyStatement", function () { const code = [ "removeThisStatement;", "// comment", ";(function() {})();", ].join(eol); const ast = recast.parse(code, { parser }); ast.program.body.shift(); assert.strictEqual( recast.print(ast).code, ["// comment", ";(function() {})();"].join(eol), ); }); pit( "should preserve comments attached to CallExpression argument", function () { const code = "testFunc (/** @type {string} */ (a), b);"; const ast = recast.parse(code, { parser }); const args = ast.program.body[0].expression.arguments; args.unshift(args[1]); const expected = "testFunc(b, /** @type {string} */ a, b);"; assert.strictEqual(recast.print(ast).code, expected); }, ); pit("should preserve comments attached: no enclosed", function () { const code = [ "hello.world; // has computed value", 'hello["world"]; // has computed value', ].join(eol); const ast = recast.parse(code, { parser }); const expected = [ "hello.world; // has computed value", 'hello["world"]; // has computed value', ].join(eol); assert.strictEqual(recast.print(ast).code, expected); }); }