Skip to content

Commit

Permalink
feat: #363 - Add type guard overloads to methods with a condition for…
Browse files Browse the repository at this point in the history
… a node.
  • Loading branch information
dsherret committed Jul 15, 2018
1 parent 72a1305 commit 21da2fc
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 3 deletions.
80 changes: 77 additions & 3 deletions src/compiler/common/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Gets the first child by a condition or throws.
* @param condition - Condition.
*/
getFirstChildOrThrow<T extends Node>(condition?: (node: Node) => node is T): T;
/**
* Gets the first child by a condition or throws.
* @param condition - Condition.
*/
getFirstChildOrThrow(condition?: (node: Node) => boolean): Node;
getFirstChildOrThrow(condition?: (node: Node) => boolean) {
return errors.throwIfNullOrUndefined(this.getFirstChild(condition), "Could not find a child that matched the specified condition.");
}
Expand All @@ -241,7 +247,13 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Gets the first child by a condition.
* @param condition - Condition.
*/
getFirstChild(condition?: (node: Node) => boolean): Node | undefined {
getFirstChild<T extends Node>(condition?: (node: Node) => node is T): T | undefined;
/**
* Gets the first child by a condition.
* @param condition - Condition.
*/
getFirstChild(condition?: (node: Node) => boolean): Node | undefined;
getFirstChild(condition?: (node: Node) => boolean) {
const firstChild = this.getCompilerFirstChild(getWrappedCondition(this, condition));
return this.getNodeFromCompilerNodeIfExists(firstChild);
}
Expand All @@ -250,6 +262,12 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Gets the last child by a condition or throws.
* @param condition - Condition.
*/
getLastChildOrThrow<T extends Node>(condition?: (node: Node) => node is T): T;
/**
* Gets the last child by a condition or throws.
* @param condition - Condition.
*/
getLastChildOrThrow(condition?: (node: Node) => boolean): Node;
getLastChildOrThrow(condition?: (node: Node) => boolean) {
return errors.throwIfNullOrUndefined(this.getLastChild(condition), "Could not find a child that matched the specified condition.");
}
Expand All @@ -258,6 +276,12 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Gets the last child by a condition.
* @param condition - Condition.
*/
getLastChild<T extends Node>(condition?: (node: Node) => node is T): T | undefined;
/**
* Gets the last child by a condition.
* @param condition - Condition.
*/
getLastChild(condition?: (node: Node) => boolean): Node | undefined;
getLastChild(condition?: (node: Node) => boolean): Node | undefined {
const lastChild = this.getCompilerLastChild(getWrappedCondition(this, condition));
return this.getNodeFromCompilerNodeIfExists(lastChild);
Expand All @@ -267,6 +291,12 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Gets the first descendant by a condition or throws.
* @param condition - Condition.
*/
getFirstDescendantOrThrow<T extends Node>(condition?: (node: Node) => node is T): T;
/**
* Gets the first descendant by a condition or throws.
* @param condition - Condition.
*/
getFirstDescendantOrThrow(condition?: (node: Node) => boolean): Node;
getFirstDescendantOrThrow(condition?: (node: Node) => boolean) {
return errors.throwIfNullOrUndefined(this.getFirstDescendant(condition), "Could not find a descendant that matched the specified condition.");
}
Expand All @@ -275,6 +305,12 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Gets the first descendant by a condition.
* @param condition - Condition.
*/
getFirstDescendant<T extends Node>(condition?: (node: Node) => node is T): T | undefined;
/**
* Gets the first descendant by a condition.
* @param condition - Condition.
*/
getFirstDescendant(condition?: (node: Node) => boolean): Node | undefined;
getFirstDescendant(condition?: (node: Node) => boolean) {
for (const descendant of this.getDescendantsIterator()) {
if (condition == null || condition(descendant))
Expand All @@ -301,6 +337,12 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Gets the previous sibling or throws.
* @param condition - Optional condition for getting the previous sibling.
*/
getPreviousSiblingOrThrow<T extends Node>(condition?: (node: Node) => node is T): T;
/**
* Gets the previous sibling or throws.
* @param condition - Optional condition for getting the previous sibling.
*/
getPreviousSiblingOrThrow(condition?: (node: Node) => boolean): Node;
getPreviousSiblingOrThrow(condition?: (node: Node) => boolean) {
return errors.throwIfNullOrUndefined(this.getPreviousSibling(condition), "Could not find the previous sibling.");
}
Expand All @@ -309,6 +351,12 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Gets the previous sibling.
* @param condition - Optional condition for getting the previous sibling.
*/
getPreviousSibling<T extends Node>(condition?: (node: Node) => node is T): T | undefined;
/**
* Gets the previous sibling.
* @param condition - Optional condition for getting the previous sibling.
*/
getPreviousSibling(condition?: (node: Node) => boolean): Node | undefined;
getPreviousSibling(condition?: (node: Node) => boolean): Node | undefined {
const previousSibling = this.getCompilerPreviousSibling(getWrappedCondition(this, condition));
return this.getNodeFromCompilerNodeIfExists(previousSibling);
Expand All @@ -318,6 +366,12 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Gets the next sibling or throws.
* @param condition - Optional condition for getting the next sibling.
*/
getNextSiblingOrThrow<T extends Node>(condition?: (node: Node) => node is T): T;
/**
* Gets the next sibling or throws.
* @param condition - Optional condition for getting the next sibling.
*/
getNextSiblingOrThrow(condition?: (node: Node) => boolean): Node;
getNextSiblingOrThrow(condition?: (node: Node) => boolean) {
return errors.throwIfNullOrUndefined(this.getNextSibling(condition), "Could not find the next sibling.");
}
Expand All @@ -326,6 +380,12 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Gets the next sibling.
* @param condition - Optional condition for getting the next sibling.
*/
getNextSibling<T extends Node>(condition?: (node: Node) => node is T): T | undefined;
/**
* Gets the next sibling.
* @param condition - Optional condition for getting the next sibling.
*/
getNextSibling(condition?: (node: Node) => boolean): Node | undefined;
getNextSibling(condition?: (node: Node) => boolean): Node | undefined {
const nextSibling = this.getCompilerNextSibling(getWrappedCondition(this, condition));
return this.getNodeFromCompilerNodeIfExists(nextSibling);
Expand Down Expand Up @@ -807,6 +867,13 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Throws if the initial parent doesn't match the condition.
* @param condition - Condition that tests the parent to see if the expression is true.
*/
getParentWhileOrThrow<T extends Node>(condition: (node: Node) => node is T): T;
/**
* Goes up the parents (ancestors) of the node while a condition is true.
* Throws if the initial parent doesn't match the condition.
* @param condition - Condition that tests the parent to see if the expression is true.
*/
getParentWhileOrThrow(condition: (node: Node) => boolean): Node;
getParentWhileOrThrow(condition: (node: Node) => boolean) {
return errors.throwIfNullOrUndefined(this.getParentWhile(condition), "The initial parent did not match the provided condition.");
}
Expand All @@ -816,6 +883,13 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Returns undefined if the initial parent doesn't match the condition.
* @param condition - Condition that tests the parent to see if the expression is true.
*/
getParentWhile<T extends Node>(condition: (node: Node) => node is T): T | undefined;
/**
* Goes up the parents (ancestors) of the node while a condition is true.
* Returns undefined if the initial parent doesn't match the condition.
* @param condition - Condition that tests the parent to see if the expression is true.
*/
getParentWhile(condition: (node: Node) => boolean): Node | undefined;
getParentWhile(condition: (node: Node) => boolean) {
let node: Node | undefined = undefined;
let nextParent = this.getParent();
Expand All @@ -841,8 +915,8 @@ export class Node<NodeType extends ts.Node = ts.Node> {
* Returns undefined if the initial parent is not the specified syntax kind.
* @param kind - Syntax kind to check for.
*/
getParentWhileKind<TKind extends SyntaxKind>(kind: TKind) {
return this.getParentWhile(n => n.getKind() === kind) as KindToNodeMappings[TKind];
getParentWhileKind<TKind extends SyntaxKind>(kind: TKind): KindToNodeMappings[TKind] | undefined {
return this.getParentWhile(n => n.getKind() === kind);
}

/**
Expand Down
105 changes: 105 additions & 0 deletions src/tests/compiler/common/nodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,15 @@ class MyClass {
const child = sourceFile.getVariableStatements()[0];
expect(child.getParentWhile(() => false)).to.be.undefined;
});

it("should get by a type guard", () => {
const {sourceFile} = getInfoFromText("const t = Test.Test2.Test3.Test4;");
const node = sourceFile.getFirstDescendantOrThrow(n => n.getText() === "Test4");
const result = node.getParentWhile(TypeGuards.isPropertyAccessExpression);
assert<IsExactType<typeof result, PropertyAccessExpression | undefined>>(true);
expect(result).to.be.instanceOf(PropertyAccessExpression);
expect(result!.getText()).to.equal("Test.Test2.Test3.Test4");
});
});

describe(nameof<Node>(n => n.getParentWhileOrThrow), () => {
Expand All @@ -451,6 +460,15 @@ class MyClass {
const child = sourceFile.getVariableStatements()[0];
expect(() => child.getParentWhileOrThrow(() => false)).to.throw();
});

it("should get by a type guard", () => {
const {sourceFile} = getInfoFromText("const t = Test.Test2.Test3.Test4;");
const node = sourceFile.getFirstDescendantOrThrow(n => n.getText() === "Test4");
const result = node.getParentWhileOrThrow(TypeGuards.isPropertyAccessExpression);
assert<IsExactType<typeof result, PropertyAccessExpression>>(true);
expect(result).to.be.instanceOf(PropertyAccessExpression);
expect(result.getText()).to.equal("Test.Test2.Test3.Test4");
});
});

describe(nameof<Node>(n => n.getParentWhileKind), () => {
Expand Down Expand Up @@ -483,6 +501,25 @@ class MyClass {
});
});

describe(nameof<Node>(n => n.getFirstChild), () => {
const {sourceFile, firstChild} = getInfoFromText<ClassDeclaration>("class Identifier { prop: string; }\ninterface MyInterface {}");
const syntaxList = sourceFile.getChildSyntaxListOrThrow();

it("should get the first child by a condition", () => {
expect(syntaxList.getFirstChild(n => n.getKind() === SyntaxKind.InterfaceDeclaration)).to.be.instanceOf(InterfaceDeclaration);
});

it("should get the first child by a type guard", () => {
const result = syntaxList.getFirstChild(TypeGuards.isInterfaceDeclaration);
assert<IsExactType<typeof result, InterfaceDeclaration | undefined>>(true);
expect(result).to.be.instanceOf(InterfaceDeclaration);
});

it("should return undefined when it can't find the child", () => {
expect(syntaxList.getFirstChild(n => n.getKind() === SyntaxKind.IsKeyword)).to.be.undefined;
});
});

describe(nameof<Node>(n => n.getFirstChildOrThrow), () => {
const {sourceFile, firstChild} = getInfoFromText<ClassDeclaration>("class Identifier { prop: string; }\ninterface MyInterface {}");
const syntaxList = sourceFile.getChildSyntaxListOrThrow();
Expand All @@ -491,11 +528,37 @@ class MyClass {
expect(syntaxList.getFirstChildOrThrow(n => n.getKind() === SyntaxKind.InterfaceDeclaration)).to.be.instanceOf(InterfaceDeclaration);
});

it("should get the first child by a type guard", () => {
const result = syntaxList.getFirstChildOrThrow(TypeGuards.isInterfaceDeclaration);
assert<IsExactType<typeof result, InterfaceDeclaration>>(true);
expect(result).to.be.instanceOf(InterfaceDeclaration);
});

it("should throw when it can't find the child", () => {
expect(() => syntaxList.getFirstChildOrThrow(n => n.getKind() === SyntaxKind.IsKeyword)).to.throw();
});
});

describe(nameof<Node>(n => n.getLastChild), () => {
const {sourceFile} = getInfoFromText<ClassDeclaration>("interface Identifier { prop: string; }\ninterface MyInterface {}");
const syntaxList = sourceFile.getChildSyntaxListOrThrow();

it("should get the last child by a condition", () => {
const interfaceDec = syntaxList.getLastChild(n => n.getKind() === SyntaxKind.InterfaceDeclaration) as InterfaceDeclaration;
expect(interfaceDec.getName()).to.equal("MyInterface");
});

it("should get the last child by a type guard", () => {
const result = syntaxList.getLastChild(TypeGuards.isInterfaceDeclaration);
assert<IsExactType<typeof result, InterfaceDeclaration | undefined>>(true);
expect(result).to.be.instanceOf(InterfaceDeclaration);
});

it("should return undefined when it can't find the child", () => {
expect(syntaxList.getLastChild(n => n.getKind() === SyntaxKind.IsKeyword)).to.be.undefined;
});
});

describe(nameof<Node>(n => n.getLastChildOrThrow), () => {
const {sourceFile} = getInfoFromText<ClassDeclaration>("interface Identifier { prop: string; }\ninterface MyInterface {}");
const syntaxList = sourceFile.getChildSyntaxListOrThrow();
Expand All @@ -505,6 +568,12 @@ class MyClass {
expect(interfaceDec.getName()).to.equal("MyInterface");
});

it("should get the last child by a type guard", () => {
const result = syntaxList.getLastChildOrThrow(TypeGuards.isInterfaceDeclaration);
assert<IsExactType<typeof result, InterfaceDeclaration>>(true);
expect(result).to.be.instanceOf(InterfaceDeclaration);
});

it("should throw when it can't find the child", () => {
expect(() => syntaxList.getLastChildOrThrow(n => n.getKind() === SyntaxKind.IsKeyword)).to.throw();
});
Expand All @@ -519,6 +588,12 @@ class MyClass {
expect((prop as PropertySignature).getName()).to.equal("prop");
});

it("should get by a type guard", () => {
const result = sourceFile.getFirstDescendant(TypeGuards.isPropertySignature);
assert<IsExactType<typeof result, PropertySignature | undefined>>(true);
expect(result).to.be.instanceOf(PropertySignature);
});

it("should return undefined when it can't find it", () => {
const privateKeyword = sourceFile.getFirstDescendant(n => n.getKind() === SyntaxKind.PrivateKeyword);
expect(privateKeyword).to.be.undefined;
Expand All @@ -534,6 +609,12 @@ class MyClass {
expect((prop as PropertySignature).getName()).to.equal("prop");
});

it("should get by a type guard", () => {
const result = sourceFile.getFirstDescendantOrThrow(TypeGuards.isPropertySignature);
assert<IsExactType<typeof result, PropertySignature>>(true);
expect(result).to.be.instanceOf(PropertySignature);
});

it("should return undefined when it can't find it", () => {
expect(() => sourceFile.getFirstDescendantOrThrow(n => n.getKind() === SyntaxKind.PrivateKeyword)).to.throw();
});
Expand Down Expand Up @@ -598,6 +679,12 @@ class MyClass {
it("should return undefined when there isn't a previous sibling", () => {
expect(sourceFile.getInterfaces()[0].getPreviousSibling()).to.be.undefined;
});

it("should get by a type guard", () => {
const result = sourceFile.getInterfaces()[1].getPreviousSibling(TypeGuards.isInterfaceDeclaration);
assert<IsExactType<typeof result, InterfaceDeclaration | undefined>>(true);
expect(result).to.be.instanceOf(InterfaceDeclaration);
});
});

describe(nameof<Node>(n => n.getPreviousSiblingOrThrow), () => {
Expand All @@ -619,6 +706,12 @@ class MyClass {
it("should throw when there isn't a previous sibling", () => {
expect(() => sourceFile.getInterfaces()[0].getPreviousSiblingOrThrow()).to.throw();
});

it("should get by a type guard", () => {
const result = sourceFile.getInterfaces()[1].getPreviousSiblingOrThrow(TypeGuards.isInterfaceDeclaration);
assert<IsExactType<typeof result, InterfaceDeclaration>>(true);
expect(result).to.be.instanceOf(InterfaceDeclaration);
});
});

describe(nameof<Node>(n => n.getNextSibling), () => {
Expand All @@ -640,6 +733,12 @@ class MyClass {
it("should return undefined when there isn't a next sibling", () => {
expect(sourceFile.getInterfaces()[2].getNextSibling()).to.be.undefined;
});

it("should get by a type guard", () => {
const result = sourceFile.getInterfaces()[1].getNextSibling(TypeGuards.isInterfaceDeclaration);
assert<IsExactType<typeof result, InterfaceDeclaration | undefined>>(true);
expect(result).to.be.instanceOf(InterfaceDeclaration);
});
});

describe(nameof<Node>(n => n.getNextSiblingOrThrow), () => {
Expand All @@ -661,6 +760,12 @@ class MyClass {
it("should throw when there isn't a next sibling", () => {
expect(() => sourceFile.getInterfaces()[2].getNextSiblingOrThrow()).to.throw();
});

it("should get by a type guard", () => {
const result = sourceFile.getInterfaces()[1].getNextSiblingOrThrow(TypeGuards.isInterfaceDeclaration);
assert<IsExactType<typeof result, InterfaceDeclaration>>(true);
expect(result).to.be.instanceOf(InterfaceDeclaration);
});
});

describe(nameof<Node>(n => n.getPreviousSiblings), () => {
Expand Down

0 comments on commit 21da2fc

Please sign in to comment.