Skip to content

Commit

Permalink
feat: Add printNode utility function and Node.print()
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret committed Jan 13, 2018
1 parent 9acb783 commit d6c2313
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 6 deletions.
1 change: 1 addition & 0 deletions code-generation/config/isAllowedMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function isAllowedMixin(mixin: MixinViewModel) {
case "UnwrappableNode":
case "ChildOrderableNode":
case "InitializerGetExpressionableNode":
case "ExpressionedNode":
return false;
default:
return true;
Expand Down
38 changes: 37 additions & 1 deletion docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,40 @@ import {TypeGuards} from "ts-simple-ast";
if (TypeGuards.isClassDeclaration(node)) {
// node is of type ClassDeclaration in here
}
```
```

### Printing a Node

Usually with the library, you can print any node by calling the `.print()` method:

```ts
node.print(); // returns: string
```

But sometimes you might want to print a compiler node. There's a `printNode` utility function for doing that:

```ts
import * as ts from "typescript";
import {printNode} from "ts-simple-ast";

// Source: https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
const tsFunctionDeclaration = ts.createFunctionDeclaration(
/*decorators*/ undefined,
/*modifiers*/[ts.createToken(ts.SyntaxKind.ExportKeyword)],
/*asteriskToken*/ undefined,
"myFunction",
/*typeParameters*/ undefined,
/*parameters*/ [],
/*returnType*/ ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
ts.createBlock([ts.createReturn(ts.createLiteral(5))], /*multiline*/ true),
);
// optionally provide a source file and there is some printing options on this
const functionText = printNode(tsFunctionDeclaration);

console.log(functionText);
// outputs:
// ========
// export function myFunction(): number {
// return 5;
// }
````
19 changes: 17 additions & 2 deletions src/compiler/common/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {GlobalContainer} from "./../../GlobalContainer";
import {IndentationText} from "./../../ManipulationSettings";
import {StructureToText} from "./../../structureToTexts";
import {insertIntoParent, getNextNonWhitespacePos, getPreviousMatchingPos} from "./../../manipulation";
import {TypeGuards, getTextFromStringOrWriter, ArrayUtils, isStringKind} from "./../../utils";
import {TypeGuards, getTextFromStringOrWriter, ArrayUtils, isStringKind, printNode, PrintNodeOptions} from "./../../utils";
import {SourceFile} from "./../file";
import * as base from "./../base";
import {ConstructorDeclaration, MethodDeclaration} from "./../class";
Expand Down Expand Up @@ -82,7 +82,8 @@ export class Node<NodeType extends ts.Node = ts.Node> {

/**
* Gets if the node was forgotten.
* @internal
*
* This will be true when the node was forgotten or removed.
*/
wasForgotten() {
return this._compilerNode == null;
Expand All @@ -102,6 +103,20 @@ export class Node<NodeType extends ts.Node = ts.Node> {
return ts.SyntaxKind[this.compilerNode.kind];
}

/**
* Prints the node using the compiler's printer.
* @param options - Options.
*/
print(options: PrintNodeOptions = {}): string {
if (options.newLineKind == null)
options.newLineKind = this.global.manipulationSettings.getNewLineKind();

if (this.getKind() === ts.SyntaxKind.SourceFile)
return printNode(this.compilerNode, options);
else
return printNode(this.compilerNode, this.sourceFile.compilerNode, options);
}

/**
* Gets the symbol or throws an error if it doesn't exist.
*/
Expand Down
7 changes: 4 additions & 3 deletions src/compiler/tools/LanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,11 @@ export class LanguageService {
throw new errors.FileNotFoundError(filePath);

let newText = getCompilerFormattedText(this);
if (settings.ensureNewLineAtEndOfFile && !StringUtils.endsWith(newText, settings.newLineCharacter!))
newText += settings.newLineCharacter;
const newLineChar = settings.newLineCharacter!; // this is filled in getFormattingEditsForDocument
if (settings.ensureNewLineAtEndOfFile && !StringUtils.endsWith(newText, newLineChar))
newText += newLineChar;

return newText.replace(/\r?\n/g, settings.newLineCharacter!);
return newText.replace(/\r?\n/g, newLineChar);

function getCompilerFormattedText(languageService: LanguageService) {
const formattingEditsInReverseOrder = languageService.getFormattingEditsForDocument(filePath, settings)
Expand Down
18 changes: 18 additions & 0 deletions src/tests/compiler/common/nodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import CodeBlockWriter from "code-block-writer";
import {expect} from "chai";
import {Node, EnumDeclaration, ClassDeclaration, FunctionDeclaration, InterfaceDeclaration, PropertySignature, PropertyAccessExpression} from "./../../../compiler";
import {TypeGuards} from "./../../../utils";
import {NewLineKind} from "./../../../ManipulationSettings";
import {getInfoFromText} from "./../testHelpers";

describe(nameof(Node), () => {
Expand Down Expand Up @@ -667,4 +668,21 @@ describe(nameof(Node), () => {
}).to.throw();
});
});

describe(nameof<Node>(n => n.print), () => {
const nodeText = "class MyClass {\n // comment\n prop: string;\n}";
const {sourceFile, firstChild} = getInfoFromText(nodeText);

it("should print the source file", () => {
expect(sourceFile.print()).to.equal(nodeText + "\n");
});

it("should print the node", () => {
expect(firstChild.print()).to.equal(nodeText);
});

it("should print the node with different newlines", () => {
expect(firstChild.print({ newLineKind: NewLineKind.CarriageReturnLineFeed })).to.equal(nodeText.replace(/\n/g, "\r\n"));
});
});
});
58 changes: 58 additions & 0 deletions src/tests/utils/printNodeTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as ts from "typescript";
import {expect} from "chai";
import {printNode, PrintNodeOptions} from "./../../utils";
import {NewLineKind} from "./../../ManipulationSettings";
import {getInfoFromText} from "./../compiler/testHelpers";

describe(nameof(printNode), () => {
const nodeText = "class MyClass {\n // comment\n prop: string;\n}";
const nodeTextNoComment = nodeText.replace(" // comment\n", "");
const {sourceFile, firstChild} = getInfoFromText(nodeText);
const tsSourceFile = ts.createSourceFile("file.tsx", nodeText, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX);
const tsClass = tsSourceFile.getChildren(tsSourceFile)[0].getChildren(tsSourceFile)[0];

it("should print the node when specifying a compiler node and options", () => {
expect(printNode(tsClass, { newLineKind: NewLineKind.CarriageReturnLineFeed })).to.equal(nodeTextNoComment.replace(/\n/g, "\r\n"));
});

it("should print with comments when specifying a source file", () => {
expect(printNode(tsSourceFile)).to.equal(nodeText + "\n");
});

it("should print the node when specifying a compiler node and source file", () => {
expect(printNode(tsClass, tsSourceFile)).to.equal(nodeText);
});

it("should print the node when specifying a compiler node, source file, and options", () => {
expect(printNode(tsClass, tsSourceFile, { newLineKind: NewLineKind.CarriageReturnLineFeed })).to.equal(nodeText.replace(/\n/g, "\r\n"));
});

it("should print the node when specifying a compiler node and source file", () => {
expect(printNode(tsClass)).to.equal(nodeTextNoComment);
});

it("general compiler api test", () => {
// https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
const tsFunctionDeclaration = ts.createFunctionDeclaration(
/*decorators*/ undefined,
/*modifiers*/[ts.createToken(ts.SyntaxKind.ExportKeyword)],
/*asteriskToken*/ undefined,
"myFunction",
/*typeParameters*/ undefined,
/*parameters*/ [],
/*returnType*/ ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
ts.createBlock([ts.createReturn(ts.createLiteral(5))], /*multiline*/ true)
);
expect(printNode(tsFunctionDeclaration)).to.equal("export function myFunction(): number {\n return 5;\n}");
});

it("should print the node when printing a jsx file", () => {
const node = ts.createJsxOpeningElement(ts.createIdentifier("Test"), ts.createJsxAttributes([]));
expect(printNode(node, { scriptKind: ts.ScriptKind.TSX })).to.equal("<Test>");
});

it("should print the node when printing a non-jsx file", () => {
const node = ts.createTypeAssertion(ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), ts.createIdentifier("test"));
expect(printNode(node, { scriptKind: ts.ScriptKind.TS })).to.equal("<string>test");
});
});
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./FileUtils";
export * from "./HashSet";
export * from "./KeyValueCache";
export * from "./Logger";
export * from "./printNode";
export * from "./StringUtils";
export * from "./TypeGuards";
export * from "./decorators";
Expand Down
89 changes: 89 additions & 0 deletions src/utils/printNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as ts from "typescript";
import {Node} from "./../compiler";
import {newLineKindToTs} from "./newLineKindToTs";
import {NewLineKind} from "./../ManipulationSettings";

/**
* Options for printing a node.
*/
export interface PrintNodeOptions {
/**
* Whether to remove comments or not.
*/
removeComments?: boolean;
/**
* New line kind.
*
* Defaults to line feed.
*/
newLineKind?: NewLineKind;
/**
* From the compiler api: "A value indicating the purpose of a node. This is primarily used to
* distinguish between an `Identifier` used in an expression position, versus an
* `Identifier` used as an `IdentifierName` as part of a declaration. For most nodes you
* should just pass `Unspecified`."
*
* Defaults to `Unspecified`.
*/
emitHint?: ts.EmitHint;
/**
* The script kind.
*
* Defaults to TSX. This is only useful when not using a wrapped node and not providing a source file.
*/
scriptKind?: ts.ScriptKind;
}

/**
* Prints the provided node using the compiler's printer.
* @param node - Compiler node.
* @param options - Options.
*/
export function printNode(node: ts.Node, options?: PrintNodeOptions): string;
/**
* Prints the provided node using the compiler's printer.
* @param node - Compiler node.
* @param sourceFile - Compiler source file.
* @param options - Options.
*/
export function printNode(node: ts.Node, sourceFile: ts.SourceFile, options?: PrintNodeOptions): string;
export function printNode(node: ts.Node, sourceFileOrOptions?: PrintNodeOptions | ts.SourceFile, secondOverloadOptions?: PrintNodeOptions) {
const isFirstOverload = sourceFileOrOptions == null || (sourceFileOrOptions as ts.SourceFile).kind !== ts.SyntaxKind.SourceFile;
const options = getOptions();
const sourceFile = getSourceFile();

const printer = ts.createPrinter({
newLine: newLineKindToTs(options.newLineKind || NewLineKind.LineFeed),
removeComments: options.removeComments || false
});

if (sourceFile == null)
return printer.printFile(node as ts.SourceFile);
else
return printer.printNode(options.emitHint || ts.EmitHint.Unspecified, node, sourceFile);

function getSourceFile() {
if (isFirstOverload) {
if (node.kind === ts.SyntaxKind.SourceFile)
return undefined;
const scriptKind = getScriptKind();
return ts.createSourceFile(`print.${getFileExt(scriptKind)}`, "", ts.ScriptTarget.Latest, false, scriptKind);
}

return sourceFileOrOptions as ts.SourceFile;

function getScriptKind() {
return options.scriptKind == null ? ts.ScriptKind.TSX : options.scriptKind;
}

function getFileExt(scriptKind: ts.ScriptKind) {
if (scriptKind === ts.ScriptKind.JSX || scriptKind === ts.ScriptKind.TSX)
return "tsx";
return "ts";
}
}

function getOptions() {
return (isFirstOverload ? sourceFileOrOptions as PrintNodeOptions : secondOverloadOptions) || {};
}
}

0 comments on commit d6c2313

Please sign in to comment.