Skip to content

Commit

Permalink
feat: #469 - ClassDeclaration - Add "Extract interface"
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret committed Oct 27, 2018
1 parent 9804627 commit 8b19131
Show file tree
Hide file tree
Showing 4 changed files with 612 additions and 3 deletions.
15 changes: 15 additions & 0 deletions docs/details/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,21 @@ Get static members:
const staticMembers = classDeclaration.getStaticMembers();
```

### Extracting an Interface

An interface declaration structure can be extracted from a class by calling `classDeclaration.extractInterface()`. For example:

```ts
// the passed in name is optional and defaults to the class name
const interfaceStructure = classDeclaration.extractInterface(`I${classDeclaration.getName()}`);
```

Alternatively the static interface declaration structure of a class can be extracted by calling:

```ts
const interfaceStructure = classDeclaration.extractStaticInterface(`${classDeclaration.getName()}Static`);
```

## Abstract

Nodes on a class may be abstract.
Expand Down
10 changes: 10 additions & 0 deletions lib/ts-simple-ast.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3863,6 +3863,16 @@ export declare class ClassDeclaration extends ClassDeclarationBase<ts.ClassDecla
* Gets the structure equivalent to this node.
*/
getStructure(): ClassDeclarationStructure;
/**
* Extracts an interface declaration structure from the class.
* @param name - Name of the interface. Falls back to the same name as the class and then the filepath's base name.
*/
extractInterface(name?: string): InterfaceDeclarationStructure;
/**
* Extracts an interface declaration structure from the static part of the class.
* @param name - Name of the interface.
*/
extractStaticInterface(name: string): InterfaceDeclarationStructure;
}

declare const ClassExpressionBase: Constructor<ClassLikeDeclarationBase> & typeof PrimaryExpression;
Expand Down
137 changes: 136 additions & 1 deletion src/compiler/ast/class/ClassDeclaration.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { ClassDeclarationStructure, ConstructorDeclarationStructure, MethodDeclarationStructure, ClassDeclarationSpecificStructure } from "../../../structures";
import { ClassDeclarationStructure, ConstructorDeclarationStructure, MethodDeclarationStructure, ClassDeclarationSpecificStructure,
InterfaceDeclarationStructure, PropertyDeclarationStructure, ParameterDeclarationStructure } from "../../../structures";
import { ts } from "../../../typescript";
import { ArrayUtils, StringUtils, TypeGuards, KeyValueCache } from "../../../utils";
import { ChildOrderableNode, ExportableNode, AmbientableNode } from "../base";
import { callBaseSet } from "../callBaseSet";
import { NamespaceChildableNode } from "../module";
import { Statement } from "../statement";
import { callBaseGetStructure } from "../callBaseGetStructure";
import { ClassLikeDeclarationBase } from "./base";
import { Scope } from "../common";
import { ParameterDeclaration } from "../function";
import { PropertyDeclaration } from "./PropertyDeclaration";
import { ConstructorDeclaration } from "./ConstructorDeclaration";
import { GetAccessorDeclaration } from "./GetAccessorDeclaration";
import { MethodDeclaration } from "./MethodDeclaration";
import { SetAccessorDeclaration } from "./SetAccessorDeclaration";

export const ClassDeclarationBase = ChildOrderableNode(NamespaceChildableNode(AmbientableNode(ExportableNode(ClassLikeDeclarationBase(Statement)))));
export class ClassDeclaration extends ClassDeclarationBase<ts.ClassDeclaration> {
Expand Down Expand Up @@ -60,4 +69,130 @@ export class ClassDeclaration extends ClassDeclarationBase<ts.ClassDeclaration>
setAccessors: this.getSetAccessors().map(accessor => accessor.getStructure())
}) as any as ClassDeclarationStructure;
}

/**
* Extracts an interface declaration structure from the class.
* @param name - Name of the interface. Falls back to the same name as the class and then the filepath's base name.
*/
extractInterface(name?: string): InterfaceDeclarationStructure {
const { constructors, properties, methods, accessors } = getExtractedClassDetails(this, false);
const parameterProperties = ArrayUtils.flatten(constructors.map(c => c.getParameters().filter(p => p.isParameterProperty())))
.filter(p => p.getName() != null && p.getScope() === Scope.Public);

return {
name: getDefaultExtractedName(name, this),
docs: this.getJsDocs().map(d => d.getStructure()),
typeParameters: this.getTypeParameters().map(p => p.getStructure()),
properties: [
...parameterProperties.map(p => {
const jsDocComment = ArrayUtils.flatten((p.getParentOrThrow() as ConstructorDeclaration).getJsDocs().map(j => j.getTags()))
.filter(TypeGuards.isJSDocParameterTag)
.filter(t => t.getTagName() === "param" && t.getName() === p.getName() && t.getComment() != null)
.map(t => t.getComment()!.trim())[0];
return {
docs: jsDocComment == null ? [] : [{ description: jsDocComment }],
name: p.getName()!,
type: p.getType().getText(p),
hasQuestionToken: p.hasQuestionToken(),
isReadonly: p.isReadonly()
};
}),
...properties.map(getExtractedInterfacePropertyStructure),
...accessors.map(getExtractedInterfaceAccessorStructure)
],
methods: methods.map(getExtractedInterfaceMethodStructure)
};
}

/**
* Extracts an interface declaration structure from the static part of the class.
* @param name - Name of the interface.
*/
extractStaticInterface(name: string): InterfaceDeclarationStructure {
const { constructors, properties, methods, accessors } = getExtractedClassDetails(this, true);
const instanceName = getDefaultExtractedName(undefined, this);

return {
name,
properties: [
...properties.map(getExtractedInterfacePropertyStructure),
...accessors.map(getExtractedInterfaceAccessorStructure)
],
methods: methods.map(getExtractedInterfaceMethodStructure),
constructSignatures: constructors.map(c => ({
docs: c.getJsDocs().map(d => d.getStructure()),
parameters: c.getParameters().map(p => ({
...getExtractedInterfaceParameterStructure(p),
scope: undefined,
isReadonly: false
})),
returnType: instanceName
}))
};
}
}

function getExtractedClassDetails(classDec: ClassDeclaration, isStatic: boolean) {
const constructors = ArrayUtils.flatten(classDec.getConstructors().map(c => c.getOverloads().length > 0 ? c.getOverloads() : [c]));
const properties = classDec.getProperties().filter(p => p.isStatic() === isStatic && p.getScope() === Scope.Public);
const methods = ArrayUtils.flatten(classDec.getMethods()
.filter(p => p.isStatic() === isStatic && p.getScope() === Scope.Public)
.map(m => m.getOverloads().length > 0 ? m.getOverloads() : [m]));

return { constructors, properties, methods, accessors: getAccessors() };

function getAccessors() {
type GetOrSetArray = (GetAccessorDeclaration | SetAccessorDeclaration)[];
const result = new KeyValueCache<string, GetOrSetArray>();

for (const accessor of [...classDec.getGetAccessors(), ...classDec.getSetAccessors()]) {
if (accessor.isStatic() === isStatic && accessor.getScope() === Scope.Public)
result.getOrCreate<GetOrSetArray>(accessor.getName(), () => []).push(accessor);
}

return result.getValuesAsArray();
}
}

function getDefaultExtractedName(name: string | undefined, classDec: ClassDeclaration) {
name = StringUtils.isNullOrWhitespace(name) ? undefined : name;
return name || classDec.getName() || classDec.getSourceFile().getBaseNameWithoutExtension().replace(/[^a-zA-Z0-9_$]/g, "");
}

function getExtractedInterfacePropertyStructure(prop: PropertyDeclaration): PropertyDeclarationStructure {
return {
docs: prop.getJsDocs().map(d => d.getStructure()),
name: prop.getName()!,
type: prop.getType().getText(prop),
hasQuestionToken: prop.hasQuestionToken(),
isReadonly: prop.isReadonly()
};
}

function getExtractedInterfaceAccessorStructure(getAndSet: (GetAccessorDeclaration | SetAccessorDeclaration)[]): PropertyDeclarationStructure {
return {
docs: getAndSet[0].getJsDocs().map(d => d.getStructure()),
name: getAndSet[0].getName(),
type: getAndSet[0].getType().getText(getAndSet[0]),
hasQuestionToken: false,
isReadonly: getAndSet.every(TypeGuards.isGetAccessorDeclaration)
};
}

function getExtractedInterfaceMethodStructure(method: MethodDeclaration): MethodDeclarationStructure {
return {
docs: method.getJsDocs().map(d => d.getStructure()),
name: method.getName(),
hasQuestionToken: method.hasQuestionToken(),
returnType: method.getReturnType().getText(method),
parameters: method.getParameters().map(getExtractedInterfaceParameterStructure),
typeParameters: method.getTypeParameters().map(p => p.getStructure())
};
}

function getExtractedInterfaceParameterStructure(param: ParameterDeclaration): ParameterDeclarationStructure {
return {
...param.getStructure(),
decorators: []
};
}

0 comments on commit 8b19131

Please sign in to comment.