Skip to content

Commit

Permalink
feat: #765 - Add QuestionDotTokenableNode.
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret committed Nov 30, 2019
1 parent b28ed73 commit b620848
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 11 deletions.
35 changes: 32 additions & 3 deletions packages/ts-morph/lib/ts-morph.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2006,6 +2006,31 @@ export interface ParameteredNode {
declare type ParameteredNodeExtensionType = Node<ts.Node & {
parameters: ts.NodeArray<ts.ParameterDeclaration>;
}>;
export declare function QuestionDotTokenableNode<T extends Constructor<QuestionDotTokenableNodeExtensionType>>(Base: T): Constructor<QuestionDotTokenableNode> & T;

export interface QuestionDotTokenableNode {
/**
* If it has a question dot token.
*/
hasQuestionDotToken(): boolean;
/**
* Gets the question dot token node or returns undefined if it doesn't exist.
*/
getQuestionDotTokenNode(): Node<ts.QuestionDotToken> | undefined;
/**
* Gets the question dot token node or throws.
*/
getQuestionDotTokenNodeOrThrow(): Node<ts.QuestionDotToken>;
/**
* Sets if this node has a question dot token.
* @param value - If it should have a question dot token or not.
*/
setHasQuestionDotToken(value: boolean): this;
}

declare type QuestionDotTokenableNodeExtensionType = Node<ts.Node & {
questionDotToken?: ts.QuestionDotToken;
}>;
export declare function QuestionTokenableNode<T extends Constructor<QuestionTokenableNodeExtensionType>>(Base: T): Constructor<QuestionTokenableNode> & T;

export interface QuestionTokenableNode {
Expand Down Expand Up @@ -5809,7 +5834,7 @@ export declare class BinaryExpression<T extends ts.BinaryExpression = ts.BinaryE
getRight(): Expression;
}

declare const CallExpressionBase: Constructor<TypeArgumentedNode> & Constructor<ArgumentedNode> & Constructor<LeftHandSideExpressionedNode> & typeof LeftHandSideExpression;
declare const CallExpressionBase: Constructor<TypeArgumentedNode> & Constructor<ArgumentedNode> & Constructor<QuestionDotTokenableNode> & Constructor<LeftHandSideExpressionedNode> & typeof LeftHandSideExpression;

export declare class CallExpression<T extends ts.CallExpression = ts.CallExpression> extends CallExpressionBase<T> {
/**
Expand Down Expand Up @@ -5869,7 +5894,7 @@ export declare class DeleteExpression extends DeleteExpressionBase<ts.DeleteExpr
getParentOrThrow(): NonNullable<NodeParentType<ts.DeleteExpression>>;
}

declare const ElementAccessExpressionBase: Constructor<LeftHandSideExpressionedNode> & typeof MemberExpression;
declare const ElementAccessExpressionBase: Constructor<QuestionDotTokenableNode> & Constructor<LeftHandSideExpressionedNode> & typeof MemberExpression;

export declare class ElementAccessExpression<T extends ts.ElementAccessExpression = ts.ElementAccessExpression> extends ElementAccessExpressionBase<T> {
/**
Expand Down Expand Up @@ -6399,7 +6424,7 @@ export declare class PrefixUnaryExpression extends PrefixUnaryExpressionBase<ts.
export declare class PrimaryExpression<T extends ts.PrimaryExpression = ts.PrimaryExpression> extends MemberExpression<T> {
}

declare const PropertyAccessExpressionBase: Constructor<NamedNode> & Constructor<LeftHandSideExpressionedNode> & typeof MemberExpression;
declare const PropertyAccessExpressionBase: Constructor<NamedNode> & Constructor<QuestionDotTokenableNode> & Constructor<LeftHandSideExpressionedNode> & typeof MemberExpression;

export declare class PropertyAccessExpression<T extends ts.PropertyAccessExpression = ts.PropertyAccessExpression> extends PropertyAccessExpressionBase<T> {
}
Expand Down Expand Up @@ -11501,6 +11526,10 @@ export interface ParameteredNodeStructure {
parameters?: OptionalKind<ParameterDeclarationStructure>[];
}

export interface QuestionDotTokenableNodeStructure {
hasQuestionDotToken?: boolean;
}

export interface QuestionTokenableNodeStructure {
hasQuestionToken?: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { errors, SyntaxKind, ts } from "@ts-morph/common";
import { insertIntoParentTextRange, removeChildren } from "../../../manipulation";
import { QuestionDotTokenableNodeStructure } from "../../../structures";
import { Constructor } from "../../../types";
import { callBaseSet } from "../callBaseSet";
import { Node } from "../common";
import { callBaseGetStructure } from "../callBaseGetStructure";

export type QuestionDotTokenableNodeExtensionType = Node<ts.Node & { questionDotToken?: ts.QuestionDotToken; }>;

export interface QuestionDotTokenableNode {
/**
* If it has a question dot token.
*/
hasQuestionDotToken(): boolean;
/**
* Gets the question dot token node or returns undefined if it doesn't exist.
*/
getQuestionDotTokenNode(): Node<ts.QuestionDotToken> | undefined;
/**
* Gets the question dot token node or throws.
*/
getQuestionDotTokenNodeOrThrow(): Node<ts.QuestionDotToken>;
/**
* Sets if this node has a question dot token.
* @param value - If it should have a question dot token or not.
*/
setHasQuestionDotToken(value: boolean): this;
}

export function QuestionDotTokenableNode<T extends Constructor<QuestionDotTokenableNodeExtensionType>>(Base: T): Constructor<QuestionDotTokenableNode> & T {
return class extends Base implements QuestionDotTokenableNode {
hasQuestionDotToken() {
return this.compilerNode.questionDotToken != null;
}

getQuestionDotTokenNode(): Node<ts.QuestionDotToken> | undefined {
return this._getNodeFromCompilerNodeIfExists(this.compilerNode.questionDotToken);
}

getQuestionDotTokenNodeOrThrow(): Node<ts.QuestionDotToken> {
return errors.throwIfNullOrUndefined(this.getQuestionDotTokenNode(), "Expected to find a question dot token.");
}

setHasQuestionDotToken(value: boolean) {
const questionDotTokenNode = this.getQuestionDotTokenNode();
const hasQuestionDotToken = questionDotTokenNode != null;

if (value === hasQuestionDotToken)
return this;

if (value) {
if (Node.isPropertyAccessExpression(this))
this.getFirstChildByKindOrThrow(SyntaxKind.DotToken).replaceWithText("?.");
else {
insertIntoParentTextRange({
insertPos: getInsertPos.call(this),
parent: this,
newText: "?."
});
}
}
else {
if (Node.isPropertyAccessExpression(this))
questionDotTokenNode!.replaceWithText(".");
else
removeChildren({ children: [questionDotTokenNode!] });
}

return this;

function getInsertPos(this: QuestionDotTokenableNode & Node) {
if (Node.isCallExpression(this))
return this.getFirstChildByKindOrThrow(SyntaxKind.OpenParenToken).getStart();
if (Node.isElementAccessExpression(this))
return this.getFirstChildByKindOrThrow(SyntaxKind.OpenBracketToken).getStart();
errors.throwNotImplementedForSyntaxKindError(this.compilerNode.kind);
}
}

set(structure: Partial<QuestionDotTokenableNodeStructure>) {
callBaseSet(Base.prototype, this, structure);

if (structure.hasQuestionDotToken != null)
this.setHasQuestionDotToken(structure.hasQuestionDotToken);

return this;
}

getStructure() {
return callBaseGetStructure<QuestionDotTokenableNodeStructure>(Base.prototype, this, {
hasQuestionDotToken: this.hasQuestionDotToken()
});
}
};
}
1 change: 1 addition & 0 deletions packages/ts-morph/src/compiler/ast/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from "./ModifierableNode";
export * from "./ModuledNode";
export * from "./name";
export * from "./ParameteredNode";
export * from "./QuestionDotTokenableNode";
export * from "./QuestionTokenableNode";
export * from "./ReadonlyableNode";
export * from "./ReturnTypedNode";
Expand Down
15 changes: 15 additions & 0 deletions packages/ts-morph/src/compiler/ast/common/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3683,6 +3683,21 @@ export class Node<NodeType extends ts.Node = ts.Node> {
*/
static readonly isQualifiedName: (node: compiler.Node) => node is compiler.QualifiedName = Node.is(SyntaxKind.QualifiedName);

/**
* Gets if the node is a QuestionDotTokenableNode.
* @param node - Node to check.
*/
static isQuestionDotTokenableNode<T extends compiler.Node>(node: T): node is compiler.QuestionDotTokenableNode & compiler.QuestionDotTokenableNodeExtensionType & T {
switch (node.getKind()) {
case SyntaxKind.CallExpression:
case SyntaxKind.ElementAccessExpression:
case SyntaxKind.PropertyAccessExpression:
return true;
default:
return false;
}
}

/**
* Gets if the node is a QuestionTokenableNode.
* @param node - Node to check.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ts } from "@ts-morph/common";
import { Type } from "../../types";
import { ArgumentedNode, TypeArgumentedNode } from "../base";
import { ArgumentedNode, QuestionDotTokenableNode, TypeArgumentedNode } from "../base";
import { LeftHandSideExpressionedNode } from "./expressioned";
import { LeftHandSideExpression } from "./LeftHandSideExpression";

const createBase = <T extends typeof LeftHandSideExpression>(ctor: T) => TypeArgumentedNode(ArgumentedNode(
LeftHandSideExpressionedNode(ctor)
QuestionDotTokenableNode(LeftHandSideExpressionedNode(ctor))
));
export const CallExpressionBase = createBase(LeftHandSideExpression);
export class CallExpression<T extends ts.CallExpression = ts.CallExpression> extends CallExpressionBase<T> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { errors, ts } from "@ts-morph/common";
import { QuestionDotTokenableNode } from "../base";
import { Expression } from "./Expression";
import { LeftHandSideExpressionedNode } from "./expressioned";
import { MemberExpression } from "./MemberExpression";

export const ElementAccessExpressionBase = LeftHandSideExpressionedNode(MemberExpression);
const createBase = <T extends typeof MemberExpression>(ctor: T) => QuestionDotTokenableNode(LeftHandSideExpressionedNode(ctor));
export const ElementAccessExpressionBase = createBase(MemberExpression);
export class ElementAccessExpression<T extends ts.ElementAccessExpression = ts.ElementAccessExpression> extends ElementAccessExpressionBase<T> {
/**
* Gets this element access expression's argument expression or undefined if none exists.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ts } from "@ts-morph/common";
import { NamedNode } from "../base";
import { NamedNode, QuestionDotTokenableNode } from "../base";
import { LeftHandSideExpressionedNode } from "./expressioned";
import { MemberExpression } from "./MemberExpression";

const createBase = <T extends typeof MemberExpression>(ctor: T) => NamedNode(LeftHandSideExpressionedNode(ctor));
const createBase = <T extends typeof MemberExpression>(ctor: T) => NamedNode(QuestionDotTokenableNode(LeftHandSideExpressionedNode(ctor)));
export const PropertyAccessExpressionBase = createBase(MemberExpression);
export class PropertyAccessExpression<T extends ts.PropertyAccessExpression = ts.PropertyAccessExpression> extends PropertyAccessExpressionBase<T> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface QuestionDotTokenableNodeStructure {
hasQuestionDotToken?: boolean;
}
1 change: 1 addition & 0 deletions packages/ts-morph/src/structures/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from "./InitializerExpressionableNodeStructure";
export * from "./JSDocableNodeStructure";
export * from "./name";
export * from "./ParameteredNodeStructure";
export * from "./QuestionDotTokenableNodeStructure";
export * from "./QuestionTokenableNodeStructure";
export * from "./ReadonlyableNodeStructure";
export * from "./ReturnTypedNodeStructure";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { expect } from "chai";
import { QuestionDotTokenableNode, ExpressionStatement } from "../../../../compiler";
import { QuestionDotTokenableNodeStructure } from "../../../../structures";
import { getInfoFromText } from "../../testHelpers";

describe(nameof(QuestionDotTokenableNode), () => {
function getInfoWithFirstExpr(text: string) {
const result = getInfoFromText<ExpressionStatement>(text);
return { ...result, expr: result.firstChild.getExpression() as any as QuestionDotTokenableNode };
}

describe(nameof<QuestionDotTokenableNode>(d => d.hasQuestionDotToken), () => {
function doTest(text: string, value: boolean) {
const { expr: firstMember } = getInfoWithFirstExpr(text);
expect(firstMember.hasQuestionDotToken()).to.equal(value);
}

it("should have when has with call expression", () => {
doTest("callExpr?.()", true);
});

it("should have when has with element access expression", () => {
doTest("elementAccess?.[0]", true);
});

it("should have when has with prop access expression", () => {
doTest("propAccess?.test", true);
});

it("should not have when not has one", () => {
doTest("callExpr()", false);
});
});

describe(nameof<QuestionDotTokenableNode>(d => d.getQuestionDotTokenNode), () => {
it("should get it", () => {
const { expr: firstMember } = getInfoWithFirstExpr("call?.()");
expect(firstMember.getQuestionDotTokenNode()!.getText()).to.equal("?.");
});

it("should be undefined when not has one", () => {
const { expr: firstMember } = getInfoWithFirstExpr("call()");
expect(firstMember.getQuestionDotTokenNode()).to.be.undefined;
});
});

describe(nameof<QuestionDotTokenableNode>(d => d.getQuestionDotTokenNodeOrThrow), () => {
it("should get it", () => {
const { expr: firstMember } = getInfoWithFirstExpr("call?.()");
expect(firstMember.getQuestionDotTokenNodeOrThrow()!.getText()).to.equal("?.");
});

it("should throw when not has one", () => {
const { expr: firstMember } = getInfoWithFirstExpr("call()");
expect(() => firstMember.getQuestionDotTokenNodeOrThrow()).to.throw();
});
});

describe(nameof<QuestionDotTokenableNode>(d => d.setHasQuestionDotToken), () => {
function doTest(startText: string, value: boolean, expected: string) {
const { expr: firstMember, sourceFile } = getInfoWithFirstExpr(startText);
firstMember.setHasQuestionDotToken(value);
expect(sourceFile.getFullText()).to.be.equal(expected);
}

it("should be set when not", () => {
doTest("call()", true, "call?.()");
});

it("should be set as not when is", () => {
doTest("call?.()", false, "call()");
});

it("should do nothing when setting to same value", () => {
doTest("call?.()", true, "call?.()");
});

it("should be set when element access", () => {
doTest("element[0]", true, "element?.[0]");
});

it("should remove when element access", () => {
doTest("element?.[0]", false, "element[0]");
});

it("should be set when prop access", () => {
doTest("prop.test", true, "prop?.test");
});

it("should be set when prop access on multiple lines", () => {
doTest("prop\n .test", true, "prop\n ?.test");
});

it("should remove when prop access", () => {
doTest("prop?.test", false, "prop.test");
});

it("should remove when prop access on multiple lines", () => {
doTest("prop\n ?.test", false, "prop\n .test");
});
});

// todo: getStructure and set tests (not used right now)
});
6 changes: 3 additions & 3 deletions packages/ts-morph/wrapped-nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The disadvantage to a node not being wrapped is that it won't have helper method
* :heavy_check_mark: label
* [CallExpression](src/compiler/ast/expression/CallExpression.ts)
* :heavy_check_mark: expression
* :x: questionDotToken
* :heavy_check_mark: questionDotToken
* :heavy_check_mark: typeArguments
* :heavy_check_mark: arguments
* [CallSignatureDeclaration](src/compiler/ast/interface/CallSignatureDeclaration.ts)
Expand Down Expand Up @@ -94,7 +94,7 @@ The disadvantage to a node not being wrapped is that it won't have helper method
* :heavy_check_mark: expression
* [ElementAccessExpression](src/compiler/ast/expression/ElementAccessExpression.ts)
* :heavy_check_mark: expression
* :x: questionDotToken
* :heavy_check_mark: questionDotToken
* :heavy_check_mark: argumentExpression
* [EmptyStatement](src/compiler/ast/statement/EmptyStatement.ts)
* [EnumDeclaration](src/compiler/ast/enum/EnumDeclaration.ts)
Expand Down Expand Up @@ -311,7 +311,7 @@ The disadvantage to a node not being wrapped is that it won't have helper method
* [PrimaryExpression](src/compiler/ast/expression/PrimaryExpression.ts)
* [PropertyAccessExpression](src/compiler/ast/expression/PropertyAccessExpression.ts)
* :heavy_check_mark: expression
* :x: questionDotToken
* :heavy_check_mark: questionDotToken
* :heavy_check_mark: name
* [PropertyAssignment](src/compiler/ast/expression/object/PropertyAssignment.ts)
* :heavy_check_mark: name
Expand Down

0 comments on commit b620848

Please sign in to comment.