diff --git a/CHANGELOG.md b/CHANGELOG.md index 35c8d2f..a09aed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/managers/ClassManager.ts b/src/managers/ClassManager.ts index 697e5ac..1055a08 100644 --- a/src/managers/ClassManager.ts +++ b/src/managers/ClassManager.ts @@ -94,26 +94,47 @@ export class ClassManager implements ObjectManager { } /** - * Add a property to the virtual class. Creates a Changeable 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 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; } @@ -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. * @@ -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 || []; diff --git a/src/models/CodeAction.ts b/src/models/CodeAction.ts index 7d5b3bf..825ec26 100644 --- a/src/models/CodeAction.ts +++ b/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'; /** @@ -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} + * + * @memberOf ImplementPolymorphElements + */ + public async execute(): Promise { + 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(); + } +} diff --git a/src/models/TsDeclaration.ts b/src/models/TsDeclaration.ts index 00bd59a..52fb3ff 100644 --- a/src/models/TsDeclaration.ts +++ b/src/models/TsDeclaration.ts @@ -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); } diff --git a/src/parser/TsResourceParser.ts b/src/parser/TsResourceParser.ts index 86e87f1..08679ec 100644 --- a/src/parser/TsResourceParser.ts +++ b/src/parser/TsResourceParser.ts @@ -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); @@ -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); diff --git a/src/provider/TypescriptCodeActionProvider.ts b/src/provider/TypescriptCodeActionProvider.ts index 3395a23..fbe450c 100644 --- a/src/provider/TypescriptCodeActionProvider.ts +++ b/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'; @@ -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'); } @@ -39,23 +49,22 @@ export class TypescriptCodeActionProvider implements CodeActionProvider { * @param {Range} range * @param {CodeActionContext} context * @param {CancellationToken} token - * @returns {(Command[] | Thenable)} + * @returns {Promise} * * @memberOf TypescriptCodeActionProvider */ - public provideCodeActions( + public async provideCodeActions( document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken - ): Command[] { + ): Promise { 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) { @@ -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; @@ -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); +} diff --git a/test/_workspace/codeFixExtension/exportedObjects.ts b/test/_workspace/codeFixExtension/exportedObjects.ts new file mode 100644 index 0000000..7bbfce0 --- /dev/null +++ b/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; +} diff --git a/test/_workspace/codeFixExtension/implementInterfaceOrAbstract.ts b/test/_workspace/codeFixExtension/implementInterfaceOrAbstract.ts new file mode 100644 index 0000000..d78ba60 --- /dev/null +++ b/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 { +} diff --git a/test/extensions/CodeFixExtension.test.ts b/test/extensions/CodeFixExtension.test.ts index 85d2d0f..0e83ed8 100644 --- a/test/extensions/CodeFixExtension.test.ts +++ b/test/extensions/CodeFixExtension.test.ts @@ -60,9 +60,48 @@ describe('CodeFixExtension', () => { } }); + it('should warn the user if the result is false', async done => { + let stub = sinon.stub(window, 'showWarningMessage', (text) => { + return Promise.resolve(); + }); + + try { + await extension.executeCodeAction(new SpyCodeAction(sinon.spy(), false)); + stub.should.be.calledWith('The provided code action could not complete. Please see the logs.'); + done(); + } catch (e) { + done(e); + } finally { + stub.restore(); + } + }); + + }); + + describe('missingImport', () => { + + const file = join(workspace.rootPath, 'codeFixExtension/empty.ts'); + let document: TextDocument; + + before(async done => { + document = await workspace.openTextDocument(file); + await window.showTextDocument(document); + done(); + }); + + afterEach(async done => { + await window.activeTextEditor.edit(builder => { + builder.delete(new Range( + new Position(0, 0), + document.lineAt(document.lineCount - 1).rangeIncludingLineBreak.end + )); + }); + done(); + }); + it('should add a missing import to a document', async done => { try { - let cmds = actionProvider.provideCodeActions( + let cmds = await actionProvider.provideCodeActions( document, new Range(new Position(0, 0), new Position(0, 0)), { diagnostics: [{ message: `Cannot find name 'Class1'.` }] }, @@ -76,19 +115,120 @@ describe('CodeFixExtension', () => { } }); - it('should warn the user if the result is false', async done => { - let stub = sinon.stub(window, 'showWarningMessage', (text) => { - return Promise.resolve(); + }); + + describe('missingPolymorphicElements', () => { + + const file = join(workspace.rootPath, 'codeFixExtension/implementInterfaceOrAbstract.ts'); + let document: TextDocument, + documentText: string; + + before(async done => { + document = await workspace.openTextDocument(file); + await window.showTextDocument(document); + documentText = document.getText(); + done(); + }); + + afterEach(async done => { + await window.activeTextEditor.edit(builder => { + builder.delete(new Range( + new Position(0, 0), + document.lineAt(document.lineCount - 1).rangeIncludingLineBreak.end + )); + builder.insert(new Position(0, 0), documentText); }); + done(); + }); + it('should add interface elements to a class', async done => { try { - await extension.executeCodeAction(new SpyCodeAction(sinon.spy(), false)); - stub.should.be.calledWith('The provided code action could not complete. Please see the logs.'); + let cmds = await actionProvider.provideCodeActions( + document, + new Range(new Position(0, 0), new Position(0, 0)), + { diagnostics: [{ message: `class 'InterfaceImplement' incorrectly implements 'CodeFixImplementInterface'.` }] }, + null + ); + await extension.executeCodeAction(cmds[0].arguments[0]); + document.lineAt(3).text.should.equal(`class InterfaceImplement implements CodeFixImplementInterface {`); + document.lineAt(4).text.should.equal(` public property: number;`); + document.lineAt(6).text.should.equal(` public interfaceMethod(): string {`); + document.lineAt(7).text.should.equal(` throw new Error('Not implemented yet.');`); + document.lineAt(8).text.should.equal(` }`); + document.lineAt(10).text.should.equal(` public interfaceMethodWithParams(p1: string, p2): number {`); + document.lineAt(11).text.should.equal(` throw new Error('Not implemented yet.');`); + document.lineAt(12).text.should.equal(` }`); + document.lineAt(13).text.should.equal(`}`); + done(); + } catch (e) { + done(e); + } + }); + + it('should add abstract class elements to a class', async done => { + try { + let cmds = await actionProvider.provideCodeActions( + document, + new Range(new Position(0, 0), new Position(0, 0)), + { diagnostics: [{ message: `non-abstract class 'AbstractImplement' implement inherited from class 'CodeFixImplementAbstract'.` }] }, + null + ); + await extension.executeCodeAction(cmds[0].arguments[0]); + document.lineAt(6).text.should.equal(`class AbstractImplement extends CodeFixImplementAbstract {`); + document.lineAt(7).text.should.equal(` public pubProperty: string;`); + document.lineAt(9).text.should.equal(` public abstractMethod(): void {`); + document.lineAt(10).text.should.equal(` throw new Error('Not implemented yet.');`); + document.lineAt(11).text.should.equal(` }`); + document.lineAt(13).text.should.equal(` public abstractMethodWithParams(p1: string, p2): number {`); + document.lineAt(14).text.should.equal(` throw new Error('Not implemented yet.');`); + document.lineAt(15).text.should.equal(` }`); + document.lineAt(16).text.should.equal(`}`); + done(); + } catch (e) { + done(e); + } + }); + + it('should add local interface elements to a class', async done => { + try { + let cmds = await actionProvider.provideCodeActions( + document, + new Range(new Position(0, 0), new Position(0, 0)), + { diagnostics: [{ message: `class 'InternalInterfaceImplement' incorrectly implements 'InternalInterface'.` }] }, + null + ); + await extension.executeCodeAction(cmds[0].arguments[0]); + document.lineAt(19).text.should.equal(`class InternalInterfaceImplement implements InternalInterface {`); + document.lineAt(21).text.should.equal(` public method(p1: string): void {`); + document.lineAt(22).text.should.equal(` throw new Error('Not implemented yet.');`); + document.lineAt(23).text.should.equal(` }`); + document.lineAt(25).text.should.equal(` public methodTwo() {`); + document.lineAt(26).text.should.equal(` throw new Error('Not implemented yet.');`); + document.lineAt(27).text.should.equal(` }`); + document.lineAt(28).text.should.equal(`}`); + done(); + } catch (e) { + done(e); + } + }); + + it('should add local abstract class elements to a class', async done => { + try { + let cmds = await actionProvider.provideCodeActions( + document, + new Range(new Position(0, 0), new Position(0, 0)), + { diagnostics: [{ message: `non-abstract class 'InternalAbstractImplement' implement inherited from class 'InternalAbstract'.` }] }, + null + ); + await extension.executeCodeAction(cmds[0].arguments[0]); + document.lineAt(22).text.should.equal(`class InternalAbstractImplement extends InternalAbstract {`); + document.lineAt(24).text.should.equal(` public abstractMethod(): void {`); + document.lineAt(25).text.should.equal(` throw new Error('Not implemented yet.');`); + document.lineAt(26).text.should.equal(` }`); + document.lineAt(27).text.should.equal(`}`); done(); } catch (e) { done(e); - } finally { - stub.restore(); } }); diff --git a/test/parser/TsResourceParser.test.ts b/test/parser/TsResourceParser.test.ts index 5587259..99caa6c 100644 --- a/test/parser/TsResourceParser.test.ts +++ b/test/parser/TsResourceParser.test.ts @@ -478,7 +478,10 @@ describe('TsResourceParser', () => { parsedClass.properties[0].type.should.equal('string'); }); - it('should parse a methods visibility'); + it('should parse a methods visibility', () => { + let parsedClass = parsed.declarations[1] as ClassDeclaration; + parsedClass.methods[0].visibility.should.equal(DeclarationVisibility.Public); + }); }); diff --git a/test/provider/TypescriptCodeActionProvider.test.ts b/test/provider/TypescriptCodeActionProvider.test.ts index cc6a36e..99c2774 100644 --- a/test/provider/TypescriptCodeActionProvider.test.ts +++ b/test/provider/TypescriptCodeActionProvider.test.ts @@ -1,8 +1,9 @@ import { Injector } from '../../src/IoC'; -import { AddImportCodeAction, CodeAction } from '../../src/models/CodeAction'; +import { AddImportCodeAction, CodeAction, ImplementPolymorphElements } from '../../src/models/CodeAction'; import { TypescriptCodeActionProvider } from '../../src/provider/TypescriptCodeActionProvider'; import * as chai from 'chai'; import { join } from 'path'; +import { TextDocument } from 'vscode'; import * as vscode from 'vscode'; chai.should(); @@ -15,76 +16,192 @@ class NoopCodeAction implements CodeAction { describe('TypescriptCodeActionProvider', () => { - let provider: any; + const file = join(vscode.workspace.rootPath, 'codeFixExtension/empty.ts'); + let provider: any, + document: TextDocument; - before(() => { + before(async done => { provider = Injector.get(TypescriptCodeActionProvider); + document = await vscode.workspace.openTextDocument(file); + await vscode.window.showTextDocument(document); + done(); }); describe('provideCodeActions()', () => { + it('should not resolve to a code action if the problem is not recognized', async done => { + try { + let cmds = await provider.provideCodeActions( + document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), + { diagnostics: [{ message: `I really do have a problem mate..` }] }, + null + ); + cmds.should.have.lengthOf(0); + done(); + } catch (e) { + done(e); + } + }); + describe('missing import actions', () => { - const file = join(vscode.workspace.rootPath, 'codeFixExtension/empty.ts'); - let document: vscode.TextDocument; + it('should resolve a missing import problem to a code action', async done => { + try { + let cmds = await provider.provideCodeActions( + document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), + { diagnostics: [{ message: `Cannot find name 'Class1'.` }] }, + null + ); + cmds.should.have.lengthOf(2); + let action = cmds[0]; + action.title.should.equal('Import "Class1" from "/resourceIndex".'); + action.arguments[0].should.be.an.instanceof(AddImportCodeAction); + done(); + } catch (e) { + done(e); + } + }); - before(async done => { - document = await vscode.workspace.openTextDocument(file); - await vscode.window.showTextDocument(document); - done(); + it('should resolve to a NOOP code action if the missing import is not found in the index', async done => { + try { + let cmds = await provider.provideCodeActions( + document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), + { diagnostics: [{ message: `Cannot find name 'FOOOOBAR'.` }] }, + null + ); + cmds.should.have.lengthOf(1); + cmds[0].title.should.equal('Cannot find "FOOOOBAR" in the index.'); + done(); + } catch (e) { + done(e); + } + }); + + it('should add multiple code actions for multiple declarations found', async done => { + try { + let cmds = await provider.provideCodeActions( + document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), + { diagnostics: [{ message: `Cannot find name 'FancierLibraryClass'.` }] }, + null + ); + cmds.should.have.lengthOf(3); + let action = cmds[0]; + action.title.should.equal('Import "FancierLibraryClass" from "/resourceIndex".'); + action.arguments[0].should.be.an.instanceof(AddImportCodeAction); + + action = cmds[1]; + action.title.should.equal('Import "FancierLibraryClass" from "fancy-library/FancierLibraryClass".'); + action.arguments[0].should.be.an.instanceof(AddImportCodeAction); + done(); + } catch (e) { + done(e); + } }); - it('should resolve a missing import problem to a code action', () => { - let cmds = provider.provideCodeActions( + }); + + }); + + describe('missing polymorphic elements actions', () => { + + const implFile = join(vscode.workspace.rootPath, 'codeFixExtension/implementInterfaceOrAbstract.ts'); + + before(async done => { + document = await vscode.workspace.openTextDocument(implFile); + await vscode.window.showTextDocument(document); + done(); + }); + + it('should resolve missing implementations of an interface to a code action', async done => { + try { + let cmds = await provider.provideCodeActions( document, new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), - { diagnostics: [{ message: `Cannot find name 'Class1'.` }] }, + { diagnostics: [{ message: `class 'Foobar' incorrectly implements 'CodeFixImplementInterface'.` }] }, null ); - cmds.should.have.lengthOf(2); + cmds.should.have.lengthOf(1); let action = cmds[0]; - action.title.should.equal('Import "Class1" from "/resourceIndex".'); - action.arguments[0].should.be.an.instanceof(AddImportCodeAction); - }); + action.title.should.equal('Implement missing elements from "CodeFixImplementInterface".'); + action.arguments[0].should.be.an.instanceof(ImplementPolymorphElements); + done(); + } catch (e) { + done(e); + } + }); - it('should resolve to a NOOP code action if the missing import is not found in the index', () => { - let cmds = provider.provideCodeActions( + it('should resolve missing implementations of an abstract class to a code action', async done => { + try { + let cmds = await provider.provideCodeActions( document, new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), - { diagnostics: [{ message: `Cannot find name 'FOOOOBAR'.` }] }, + { diagnostics: [{ message: `non-abstract class 'Foobar' implement inherited from class 'CodeFixImplementAbstract'.` }] }, null ); cmds.should.have.lengthOf(1); - cmds[0].title.should.equal('Cannot find "FOOOOBAR" in the index.'); - }); + let action = cmds[0]; + action.title.should.equal('Implement missing elements from "CodeFixImplementAbstract".'); + action.arguments[0].should.be.an.instanceof(ImplementPolymorphElements); + done(); + } catch (e) { + done(e); + } + }); - it('should not resolve to a code action if the problem is not recognized', () => { - let cmds = provider.provideCodeActions( + it('should resolve missing implementations of a local interface to a code action', async done => { + try { + let cmds = await provider.provideCodeActions( document, new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), - { diagnostics: [{ message: `I really do have a problem mate..` }] }, + { diagnostics: [{ message: `class 'Foobar' incorrectly implements 'InternalInterface'.` }] }, null ); - cmds.should.have.lengthOf(0); - }); + cmds.should.have.lengthOf(1); + let action = cmds[0]; + action.title.should.equal('Implement missing elements from "InternalInterface".'); + action.arguments[0].should.be.an.instanceof(ImplementPolymorphElements); + done(); + } catch (e) { + done(e); + } + }); - it('should add multiple code actions for multiple declarations found', () => { - let cmds = provider.provideCodeActions( + it('should resolve missing implementations of a local abstract class to a code action', async done => { + try { + let cmds = await provider.provideCodeActions( document, new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), - { diagnostics: [{ message: `Cannot find name 'FancierLibraryClass'.` }] }, + { diagnostics: [{ message: `non-abstract class 'Foobar' implement inherited from class 'InternalAbstract'.` }] }, null ); - cmds.should.have.lengthOf(3); + cmds.should.have.lengthOf(1); let action = cmds[0]; - action.title.should.equal('Import "FancierLibraryClass" from "/resourceIndex".'); - action.arguments[0].should.be.an.instanceof(AddImportCodeAction); - - action = cmds[1]; - action.title.should.equal('Import "FancierLibraryClass" from "fancy-library/FancierLibraryClass".'); - action.arguments[0].should.be.an.instanceof(AddImportCodeAction); - }); + action.title.should.equal('Implement missing elements from "InternalAbstract".'); + action.arguments[0].should.be.an.instanceof(ImplementPolymorphElements); + done(); + } catch (e) { + done(e); + } + }); + it('should resolve missing to a NOOP if the interface / class is not found', async done => { + try { + let cmds = await provider.provideCodeActions( + document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), + { diagnostics: [{ message: `non-abstract class 'Foobar' implement inherited from class 'FOOOOBAR'.` }] }, + null + ); + cmds.should.have.lengthOf(1); + cmds[0].title.should.equal('Cannot find "FOOOOBAR" in the index or the actual file.'); + done(); + } catch (e) { + done(e); + } }); });