Skip to content
This repository has been archived by the owner on Oct 10, 2018. It is now read-only.

Commit

Permalink
Feature/implement interface (#134)
Browse files Browse the repository at this point in the history
* add test workspace files

* tests need to be async

* adding code generation action

* changelog

* check if the found declaration is already imported

* different error message

* adding test classes

* test cases

* changing cases

* filter if a command for this class is already there

* comment removal

* bugfix

* fix tests

* prepare tests

* coding action provider tests

* coding fix extension tests

* parse method flag if its abstract

* only add abstract methods

* remove only

* adding nullcheck
  • Loading branch information
buehler committed Dec 3, 2016
1 parent c744970 commit e465005
Show file tree
Hide file tree
Showing 11 changed files with 508 additions and 66 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
#### Added
- Classmanager that can modify classes in a document ([#127](https://github.com/buehler/typescript-hero/issues/127))
- Support for light-bulb feature in tsx files ([#128](https://github.com/buehler/typescript-hero/issues/128))
- CodeFix can now implement missing methods and properties from interfaces and abstract classes ([#114](https://github.com/buehler/typescript-hero/issues/114))

## [0.10.1]
#### Added
Expand Down
55 changes: 44 additions & 11 deletions src/managers/ClassManager.ts
Expand Up @@ -94,26 +94,47 @@ export class ClassManager implements ObjectManager {
}

/**
* Add a property to the virtual class. Creates a Changeable<T> object with the .isNew flag set to true.
* Checks if a property with the given name exists on the virtual class.
*
* @param {string} name
* @param {PropertyVisibility} visibility
* @param {string} type
* @returns {boolean}
*
* @memberOf ClassManager
*/
public hasProperty(name: string): boolean {
return this.properties.some(o => o.object.name === name && !o.isDeleted);
}

/**
* Add a property to the virtual class. Creates a Changeable<T> object with the .isNew flag set to true.
*
* @param {(string | PropertyDeclaration)} nameOrDeclaration
* @param {DeclarationVisibility} [visibility]
* @param {string} [type]
* @returns {this}
*
* @memberOf ClassManager
*/
public addProperty(
name: string,
visibility: DeclarationVisibility,
type: string
nameOrDeclaration: string | PropertyDeclaration,
visibility?: DeclarationVisibility,
type?: string
): this {
if (this.properties.some(o => o.object.name === name && !o.isDeleted)) {
throw new PropertyDuplicated(name, this.managedClass.name);
let declaration: PropertyDeclaration;

if (nameOrDeclaration instanceof PropertyDeclaration) {
if (this.properties.some(o => o.object.name === nameOrDeclaration.name && !o.isDeleted)) {
throw new PropertyDuplicated(nameOrDeclaration.name, this.managedClass.name);
}
declaration = nameOrDeclaration;
} else {
if (this.properties.some(o => o.object.name === nameOrDeclaration && !o.isDeleted)) {
throw new PropertyDuplicated(nameOrDeclaration, this.managedClass.name);
}
declaration = new PropertyDeclaration(nameOrDeclaration, visibility, type);
}

let prop = new PropertyDeclaration(name, visibility, type);
this.properties.push(new Changeable(prop, true));
this.properties.push(new Changeable(declaration, true));

return this;
}
Expand All @@ -138,6 +159,18 @@ export class ClassManager implements ObjectManager {
return this;
}

/**
* Checks if a method with the given name does exist on the virtual class.
*
* @param {string} name
* @returns {boolean}
*
* @memberOf ClassManager
*/
public hasMethod(name: string): boolean {
return this.methods.some(o => o.object.name === name && !o.isDeleted);
}

/**
* Add a method to the virtual class.
*
Expand All @@ -163,7 +196,7 @@ export class ClassManager implements ObjectManager {
declaration = nameOrDeclaration;
} else {
if (this.methods.some(o => o.object.name === nameOrDeclaration && !o.isDeleted)) {
throw new MethodDeclaration(nameOrDeclaration, this.managedClass.name);
throw new MethodDuplicated(nameOrDeclaration, this.managedClass.name);
}
declaration = new MethodDeclaration(nameOrDeclaration, type, visibility);
declaration.parameters = parameters || [];
Expand Down
38 changes: 38 additions & 0 deletions src/models/CodeAction.ts
@@ -1,5 +1,7 @@
import { DeclarationInfo, ResolveIndex } from '../caches/ResolveIndex';
import { ClassManager } from '../managers/ClassManager';
import { ImportManager } from '../managers/ImportManager';
import { InterfaceDeclaration } from './TsDeclaration';
import { TextDocument } from 'vscode';

/**
Expand Down Expand Up @@ -86,3 +88,39 @@ export class NoopCodeAction implements CodeAction {
return Promise.resolve(true);
}
}

/**
* Code action that does implement missing properties and methods from interfaces or abstract classes.
*
* @export
* @class ImplementPolymorphElements
* @implements {CodeAction}
*/
export class ImplementPolymorphElements implements CodeAction {
constructor(
private document: TextDocument,
private managedClass: string,
private polymorphObject: InterfaceDeclaration
) { }

/**
* Executes the code action. Depending on the action, there are several actions performed.
*
* @returns {Promise<boolean>}
*
* @memberOf ImplementPolymorphElements
*/
public async execute(): Promise<boolean> {
let controller = await ClassManager.create(this.document, this.managedClass);

for (let property of this.polymorphObject.properties.filter(o => !controller.hasProperty(o.name))) {
controller.addProperty(property);
}

for (let method of this.polymorphObject.methods.filter(o => !controller.hasMethod(o.name) && o.isAbstract)) {
controller.addMethod(method);
}

return controller.commit();
}
}
9 changes: 8 additions & 1 deletion src/models/TsDeclaration.ts
Expand Up @@ -218,7 +218,14 @@ export class MethodDeclaration extends TsTypedExportableCallableDeclaration {
return CompletionItemKind.Method;
}

constructor(name: string, type?: string, public visibility?: DeclarationVisibility, start?: number, end?: number) {
constructor(
name: string,
type?: string,
public visibility?: DeclarationVisibility,
start?: number,
end?: number,
public isAbstract: boolean = false
) {
super(name, type, start, end, false);
}

Expand Down
10 changes: 8 additions & 2 deletions src/parser/TsResourceParser.ts
Expand Up @@ -580,7 +580,8 @@ export class TsResourceParser {
getNodeType(o.type),
DeclarationVisibility.Public,
o.getStart(),
o.getEnd()
o.getEnd(),
true
);
method.parameters = this.parseMethodParams(o);
interfaceDeclaration.methods.push(method);
Expand Down Expand Up @@ -642,7 +643,12 @@ export class TsResourceParser {
this.parseFunctionParts(tsResource, ctor, o);
} else if (isMethodDeclaration(o)) {
let method = new TshMethodDeclaration(
(o.name as Identifier).text, getNodeType(o.type), getNodeVisibility(o), o.getStart(), o.getEnd()
(o.name as Identifier).text,
getNodeType(o.type),
getNodeVisibility(o),
o.getStart(),
o.getEnd(),
o.modifiers && o.modifiers.some(m => m.kind === SyntaxKind.AbstractKeyword)
);
method.parameters = this.parseMethodParams(o);
classDeclaration.methods.push(method);
Expand Down
72 changes: 66 additions & 6 deletions src/provider/TypescriptCodeActionProvider.ts
@@ -1,4 +1,13 @@
import { AddImportCodeAction, AddMissingImportsCodeAction, CodeAction, NoopCodeAction } from '../models/CodeAction';
import { getAbsolutLibraryName } from '../utilities/ResolveIndexExtensions';
import { TsNamedImport } from '../models/TsImport';
import { TsResourceParser } from '../parser/TsResourceParser';
import {
AddImportCodeAction,
AddMissingImportsCodeAction,
CodeAction,
ImplementPolymorphElements,
NoopCodeAction
} from '../models/CodeAction';
import { ResolveIndex } from '../caches/ResolveIndex';
import { Logger, LoggerFactory } from '../utilities/Logger';
import { inject, injectable } from 'inversify';
Expand Down Expand Up @@ -27,7 +36,8 @@ export class TypescriptCodeActionProvider implements CodeActionProvider {

constructor(
@inject('LoggerFactory') loggerFactory: LoggerFactory,
private resolveIndex: ResolveIndex
private resolveIndex: ResolveIndex,
private parser: TsResourceParser
) {
this.logger = loggerFactory('TypescriptCodeActionProvider');
}
Expand All @@ -39,23 +49,22 @@ export class TypescriptCodeActionProvider implements CodeActionProvider {
* @param {Range} range
* @param {CodeActionContext} context
* @param {CancellationToken} token
* @returns {(Command[] | Thenable<Command[]>)}
* @returns {Promise<Command[]>}
*
* @memberOf TypescriptCodeActionProvider
*/
public provideCodeActions(
public async provideCodeActions(
document: TextDocument,
range: Range,
context: CodeActionContext,
token: CancellationToken
): Command[] {
): Promise<Command[]> {
let commands = [],
match: RegExpExecArray,
addAllMissingImportsAdded = false;

for (let diagnostic of context.diagnostics) {
switch (true) {
// When the problem is a missing import, add the import to the document.
case !!(match = isMissingImport(diagnostic)):
let infos = this.resolveIndex.declarationInfos.filter(o => o.declaration.name === match[1]);
if (infos.length > 0) {
Expand All @@ -78,6 +87,37 @@ export class TypescriptCodeActionProvider implements CodeActionProvider {
new NoopCodeAction()
));
}
break;
case !!(match = isIncorrectlyImplementingInterface(diagnostic)):
case !!(match = isIncorrectlyImplementingAbstract(diagnostic)):
let parsedDocument = await this.parser.parseSource(document.getText()),
alreadyImported = parsedDocument.imports.find(
o => o instanceof TsNamedImport && o.specifiers.some(s => s.specifier === match[2])
),
declaration = parsedDocument.declarations.find(o => o.name === match[2]) ||
(this.resolveIndex.declarationInfos.find(
o => o.declaration.name === match[2] &&
o.from === getAbsolutLibraryName(alreadyImported.libraryName, document.fileName)
) || { declaration: undefined }).declaration;

if (commands.some((o: Command) => o.title.indexOf(match[2]) >= 0)) {
// Do leave the method when a command with the found class is already added.
break;
}

if (!declaration) {
commands.push(this.createCommand(
`Cannot find "${match[2]}" in the index or the actual file.`,
new NoopCodeAction()
));
break;
}

commands.push(this.createCommand(
`Implement missing elements from "${match[2]}".`,
new ImplementPolymorphElements(document, match[1], declaration)
));

break;
default:
break;
Expand Down Expand Up @@ -115,3 +155,23 @@ export class TypescriptCodeActionProvider implements CodeActionProvider {
function isMissingImport(diagnostic: Diagnostic): RegExpExecArray {
return /cannot find name ['"](.*)['"]/ig.exec(diagnostic.message);
}

/**
* Determines if the problem is an incorrect implementation of an interface.
*
* @param {Diagnostic} diagnostic
* @returns {RegExpExecArray}
*/
function isIncorrectlyImplementingInterface(diagnostic: Diagnostic): RegExpExecArray {
return /class ['"](.*)['"] incorrectly implements.*['"](.*)['"]\./ig.exec(diagnostic.message);
}

/**
* Determines if the problem is missing implementations of an abstract class.
*
* @param {Diagnostic} diagnostic
* @returns {RegExpExecArray}
*/
function isIncorrectlyImplementingAbstract(diagnostic: Diagnostic): RegExpExecArray {
return /non-abstract class ['"](.*)['"].*implement inherited.*from class ['"](.*)['"]\./ig.exec(diagnostic.message);
}
13 changes: 13 additions & 0 deletions test/_workspace/codeFixExtension/exportedObjects.ts
@@ -0,0 +1,13 @@
export abstract class CodeFixImplementAbstract {
public pubProperty: string;

public abstract abstractMethod(): void;
public abstract abstractMethodWithParams(p1: string, p2): number;
}

export interface CodeFixImplementInterface {
property: number;

interfaceMethod(): string;
interfaceMethodWithParams(p1: string, p2): number;
}
24 changes: 24 additions & 0 deletions test/_workspace/codeFixExtension/implementInterfaceOrAbstract.ts
@@ -0,0 +1,24 @@
import { CodeFixImplementAbstract, CodeFixImplementInterface } from './exportedObjects';


class InterfaceImplement implements CodeFixImplementInterface {
}

class AbstractImplement extends CodeFixImplementAbstract {
}

abstract class InternalAbstract {
public method(): void{}
public abstract abstractMethod(): void;
}

interface InternalInterface {
method(p1: string): void;
methodTwo();
}

class InternalInterfaceImplement implements InternalInterface {
}

class InternalAbstractImplement extends InternalAbstract {
}

0 comments on commit e465005

Please sign in to comment.