diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index d2c15b308ec4f..92cd4b2a5c62c 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -222,6 +222,20 @@ declare namespace ts.server.protocol { file: string; } + export interface SimpleTextSpan { + start: number; + length: number; + } + + export class TextChange { + span: SimpleTextSpan; + newText: string; + } + + export interface Refactoring extends TextChange { + filePath: string; + } + /** * Definition response message. Gives text range for definition. */ @@ -343,6 +357,21 @@ declare namespace ts.server.protocol { findInStrings?: boolean; } + export interface QuickFixRequest extends FileLocationRequest { + } + + export interface QuickFixResponse extends Response { + body?: QuickFixResponseBody; + } + + export interface QuickFix { + display: string; + refactorings: Refactoring[]; + } + + export interface QuickFixResponseBody { + quickFixes?: QuickFix[]; + } /** * Rename request; value of command field is "rename". Return diff --git a/src/server/session.ts b/src/server/session.ts index 0ec3d87e58c9a..44fcc5f802ac3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2,6 +2,7 @@ /// /// /// +/// namespace ts.server { const spaceCache: string[] = []; @@ -125,6 +126,7 @@ namespace ts.server { export const TypeDefinition = "typeDefinition"; export const ProjectInfo = "projectInfo"; export const ReloadProjects = "reloadProjects"; + export const QuickFixes = "quickfix"; export const Unknown = "unknown"; } @@ -435,6 +437,44 @@ namespace ts.server { return projectInfo; } + private getQuickFixes(fileName: string, line: number, offset: number): protocol.QuickFixResponseBody { + fileName = ts.normalizePath(fileName); + + const possibleFixes: protocol.QuickFix[] = []; + const project = this.projectService.getProjectForFile(fileName); + const syntacticDiags = project.compilerService.languageService.getSyntacticDiagnostics(fileName); + const semanticDiags = project.compilerService.languageService.getSemanticDiagnostics(fileName); + const sourceFile = project.compilerService.languageService.getProgram().getSourceFile(fileName); + + const positionNode = getTokenAtPosition(sourceFile, project.compilerService.host.lineOffsetToPosition(fileName, line, offset)); + const diagnostics: Diagnostic[] = syntacticDiags.concat(semanticDiags).filter(diag => { + const isSameNode = diag.start == positionNode.getStart(); + return isSameNode; + }); + + const param: ts.quickFix.QuickFixQueryInformation = { + project: project, + service: project.compilerService.languageService, + program: project.program, + typeChecker: project.program.getTypeChecker(), + sourceFile: sourceFile, + positionErrors: diagnostics + }; + + quickFixRegistry.QuickFixes.allQuickFixes.forEach(possibleQuickfix => { + try { + possibleQuickfix.provideFix(param).forEach(fix => { + if (fix && fix.display && fix.refactorings && fix.refactorings.length > 0) { + possibleFixes.push({ display: fix.display, refactorings: fix.refactorings }); + } + }); + } + catch (error) { + } + }); + return { quickFixes: possibleFixes }; + } + private getRenameLocations(line: number, offset: number, fileName: string, findInComments: boolean, findInStrings: boolean): protocol.RenameResponseBody { const file = ts.normalizePath(fileName); const info = this.projectService.getScriptInfo(file); @@ -1043,6 +1083,10 @@ namespace ts.server { const renameArgs = request.arguments; return { response: this.getRenameLocations(renameArgs.line, renameArgs.offset, renameArgs.file, renameArgs.findInComments, renameArgs.findInStrings), responseRequired: true }; }, + [CommandNames.QuickFixes]: (request: protocol.Request) => { + const quickFixArgs = request.arguments; + return { response: this.getQuickFixes(quickFixArgs.file, quickFixArgs.line, quickFixArgs.offset), responseRequired: true }; + }, [CommandNames.Open]: (request: protocol.Request) => { const openArgs = request.arguments; let scriptKind: ScriptKind; diff --git a/src/services/quickfixes/quickFix.ts b/src/services/quickfixes/quickFix.ts new file mode 100644 index 0000000000000..52d6847c9f974 --- /dev/null +++ b/src/services/quickfixes/quickFix.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// +/// + +namespace ts.quickFix { + + export interface QuickFixQueryInformation { + project: server.Project; + service: LanguageService; + program: Program; + typeChecker: TypeChecker; + sourceFile: SourceFile; + positionErrors: Diagnostic[]; + } + + export interface QuickFix { + provideFix(info: QuickFixQueryInformation): ts.server.protocol.QuickFix[]; + } + +} \ No newline at end of file diff --git a/src/services/quickfixes/quickFixRegistry.ts b/src/services/quickfixes/quickFixRegistry.ts new file mode 100644 index 0000000000000..f00e26eca1cd7 --- /dev/null +++ b/src/services/quickfixes/quickFixRegistry.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// +/// +/// +/// +/// +/// +/// +/// + +namespace ts.quickFixRegistry { + export class QuickFixes{ + public static allQuickFixes: quickFix.QuickFix[] = [ + new quickFix.semantic.AddClassMethod(), + new quickFix.semantic.AddClassMember(), + new quickFix.semantic.AddImportFromStatement(), + new quickFix.semantic.AddImportStatement(), + new quickFix.semantic.TypeAssertPropertyAccessToAny(), + new quickFix.semantic.ImplementInterface(), + new quickFix.semantic.ImplementAbstractClass() + ]; + } +} \ No newline at end of file diff --git a/src/services/quickfixes/semantic/addClassMember.ts b/src/services/quickfixes/semantic/addClassMember.ts new file mode 100644 index 0000000000000..d8e1eeb0a19a5 --- /dev/null +++ b/src/services/quickfixes/semantic/addClassMember.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// +namespace ts.quickFix.semantic { + export class AddClassMember extends MessageAndCodeBasedQuickFixBase { + protected getPattern(): RegExp { + return /Property \'(\w+)\' does not exist on type \'(\w+)\'./; + } + protected getSupportedErrorCode(): number { + return Diagnostics.Property_0_does_not_exist_on_type_1.code; + } + protected getIdentifierAndClassNameFromMatch(info: QuickFixQueryInformation, error: Diagnostic, match: RegExpMatchArray): IdentifierAndClassName[] { + return [{ identifierName: match[1], className: match[2] }]; + } + protected getDisplay(context: QuickFixContext): string { + var {identifierName, className} = context.identifierAndClassName; + return `Add ${identifierName} to ${className}` + } + + private getLastNameAfterDot(text: string) { + return text.substr(text.lastIndexOf('.') + 1); + } + + private getTypeStringForNode(node: Node, typeChecker: TypeChecker) { + var type = typeChecker.getTypeAtLocation(node); + return displayPartsToString(ts.typeToDisplayParts(typeChecker, type)).replace(/\s+/g, ' '); + } + protected getRefactorings(context: QuickFixContext) { + var {identifierName, className} = context.identifierAndClassName; + var {info} = context; + + // Get the type of the stuff on the right if its an assignment + var typeString = 'any'; + var parentOfParent = context.identifier.parent.parent; + if (parentOfParent.kind === SyntaxKind.BinaryExpression + && (parentOfParent).operatorToken.getText().trim() === '=') { + + let binaryExpression = parentOfParent; + typeString = this.getTypeStringForNode(binaryExpression.right, info.typeChecker); + } + else if (parentOfParent.kind === SyntaxKind.CallExpression) { + let callExp = parentOfParent; + let typeStringParts = ['(']; + + // Find the number of arguments + let args: string[] = []; + callExp.arguments.forEach(arg => { + var argName = (this.getLastNameAfterDot(arg.getText())); + var argType = this.getTypeStringForNode(arg, info.typeChecker); + + args.push(`${argName}: ${argType}`); + }); + typeStringParts.push(args.join(', ')); + + // TODO: infer the return type as well if the next parent is an assignment + // Currently its `any` + typeStringParts.push(') => any'); + typeString = typeStringParts.join(''); + } + + // Find the containing class declaration + var memberTarget = getNodeByKindAndName([info.sourceFile], SyntaxKind.ClassDeclaration, className) || + getNodeByKindAndName(info.program.getSourceFiles(), SyntaxKind.ClassDeclaration, className); + if (!memberTarget) { + // Find the containing interface declaration + memberTarget = getNodeByKindAndName([info.sourceFile], SyntaxKind.InterfaceDeclaration, className) || + getNodeByKindAndName(info.program.getSourceFiles(), SyntaxKind.InterfaceDeclaration, className); + } + if (!memberTarget) { + return []; + } + + // The following code will be same (and typesafe) for either class or interface + let targetDeclaration = memberTarget; + + // Then the first brace + let firstBrace = targetDeclaration.getChildren().filter(x => x.kind === SyntaxKind.OpenBraceToken)[0]; + + let formatCodeOptions = info.project.projectService.getFormatCodeOptions(); + var newLine = formatCodeOptions.NewLineCharacter; + // And the correct indent + var indentLength = info.service.getIndentationAtPosition(memberTarget.getSourceFile().fileName, firstBrace.end, formatCodeOptions); + var indent = formatting.getIndentationString(indentLength + formatCodeOptions.IndentSize, formatCodeOptions); + + // And add stuff after the first brace + let refactoring: server.protocol.Refactoring = { + span: { + start: firstBrace.end, + length: 0 + }, + newText: `${newLine}${indent}${identifierName}: ${typeString};`, + filePath: targetDeclaration.getSourceFile().fileName + }; + + return [refactoring]; + } + } +} \ No newline at end of file diff --git a/src/services/quickfixes/semantic/addClassMethod.ts b/src/services/quickfixes/semantic/addClassMethod.ts new file mode 100644 index 0000000000000..9c802e4a30cc8 --- /dev/null +++ b/src/services/quickfixes/semantic/addClassMethod.ts @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// +namespace ts.quickFix.semantic { + export class AddClassMethod extends MessageAndCodeBasedQuickFixBase { + protected getPattern(): RegExp { + return /Property \'(\w+)\' does not exist on type \'(\w+)\'./; + } + protected getSupportedErrorCode(): number { + return Diagnostics.Property_0_does_not_exist_on_type_1.code; + } + protected getIdentifierAndClassNameFromMatch(info: QuickFixQueryInformation, error: Diagnostic, match: RegExpMatchArray): IdentifierAndClassName[] { + return [{ identifierName: match[1], className: match[2] }]; + } + protected getDisplay(context: QuickFixContext): string { + var {identifierName, className} = context.identifierAndClassName; + return `Add method '${identifierName}' to current class ${className}` + } + + private getTypeStringForNode(node: Node, typeChecker: TypeChecker) { + var type = typeChecker.getTypeAtLocation(node); + return displayPartsToString(ts.typeToDisplayParts(typeChecker, type)).replace(/\s+/g, ' '); + } + protected getRefactorings(context: QuickFixContext) { + var {identifierName, className} = context.identifierAndClassName; + var {info} = context; + // Get the type of the stuff on the right if its an assignment + var typeString = 'any'; + var parentOfParent = context.identifier.parent.parent; + if (parentOfParent.kind === SyntaxKind.BinaryExpression + && (parentOfParent).operatorToken.getText().trim() === '=') { + + let binaryExpression = parentOfParent; + typeString = this.getTypeStringForNode(binaryExpression.right, info.typeChecker); + + } + else if (parentOfParent.kind === SyntaxKind.CallExpression) { + + let nativeTypes = ['string', 'number', 'boolean', 'object', 'null', 'undefined', 'RegExp']; + let abc = 'abcdefghijklmnopqrstuvwxyz'; + let argsAlphabet = abc.split(''); + let argsAlphabetPosition = 0; + let argName = ''; + let argCount = 0; + + let callExp = parentOfParent; + let typeStringParts = ['(']; + + // Find the number of arguments + let args: string[] = []; + callExp.arguments.forEach(arg => { + var argType = this.getTypeStringForNode(arg, info.typeChecker); + + // determine argument output type + // use consecutive letters for native types + // or use decapitalized Class name + counter as argument name + if (nativeTypes.indexOf(argType) !== -1 //native types + || argType.indexOf('{') !== -1 //Casted inline argument declarations + || argType.indexOf('=>') !== -1 //Method references + || argType.indexOf('[]') !== -1 //Array references + ) { + + var type: Type = info.typeChecker.getTypeAtLocation(arg); + var typeName: string = 'type'; + if (type && + type.symbol && + type.symbol.name) { + typeName = type.symbol.name.replace(/[\[\]]/g, ''); + }; + var hasAnonymous = typeName.indexOf('__') === 0; + var isAnonymousTypedArgument = hasAnonymous && typeName.substring(2) === 'type'; + var isAnonymousMethod = hasAnonymous && typeName.substring(2) === 'function'; + var isAnonymousObject = hasAnonymous && typeName.substring(2) === 'object'; + + if (argType.indexOf('=>') !== -1 && + !isAnonymousTypedArgument && + !isAnonymousMethod && + !isAnonymousObject) { + if (typeName === 'Array') { typeName = 'array'; }; + argName = `${typeName}${argCount++}`; + } + else if (argType.indexOf('[]') !== -1) { + argName = `array${argCount++}`; + } + else { + if (isAnonymousMethod) { + typeName = 'function'; + argName = `${typeName}${argCount++}`; + } + else if (isAnonymousObject) { + typeName = 'object'; + argName = `${typeName}${argCount++}`; + } + else { + argName = argsAlphabet[argsAlphabetPosition]; + argsAlphabet[argsAlphabetPosition] += argsAlphabet[argsAlphabetPosition].substring(1); + argsAlphabetPosition++; + argsAlphabetPosition %= abc.length; + } + } + } + else { + // replace 'typeof ' from name + argName = argType.replace('typeof ', ''); + // decapitalize and concat + if (argType.indexOf('typeof ') === -1) { + var firstLower = argName[0].toLowerCase(); + + if (argName.length === 1) { + argName = firstLower; + } + else { + argName = firstLower + argName.substring(1); + } + } + // add counter value and increment it + argName += argCount.toString(); + argCount++; + } + + // cast null and undefined to any type + if (argType.indexOf('null') !== -1 || argType.indexOf('undefined') !== -1) { + argType = argType.replace(/null|undefined/g, 'any'); + } + args.push(`${argName}: ${argType}`); + + }); + typeStringParts.push(args.join(', ')); + + // TODO: infer the return type as well if the next parent is an assignment + // Currently its `any` + typeStringParts.push(`): any { }`); + typeString = typeStringParts.join(''); + } + + // Find the containing class declaration + var memberTarget = getNodeByKindAndName([info.sourceFile], SyntaxKind.ClassDeclaration, className) || + getNodeByKindAndName(info.program.getSourceFiles(), SyntaxKind.ClassDeclaration, className); + if (!memberTarget) { + // Find the containing interface declaration + memberTarget = getNodeByKindAndName([info.sourceFile], SyntaxKind.InterfaceDeclaration, className) || + getNodeByKindAndName(info.program.getSourceFiles(), SyntaxKind.InterfaceDeclaration, className); + } + if (!memberTarget) { + return []; + } + + // The following code will be same (and typesafe) for either class or interface + let targetDeclaration = memberTarget; + + // Then the first brace + let firstBrace = targetDeclaration.getChildren().filter(x => x.kind === SyntaxKind.OpenBraceToken)[0]; + + let formatCodeOptions = info.project.projectService.getFormatCodeOptions(); + var newLine = formatCodeOptions.NewLineCharacter; + // And the correct indent + var indentLength = info.service.getIndentationAtPosition( + memberTarget.getSourceFile().fileName, firstBrace.end, formatCodeOptions); + var indent = formatting.getIndentationString(indentLength + formatCodeOptions.IndentSize, formatCodeOptions); + + // And add stuff after the first brace + let refactoring: server.protocol.Refactoring = { + span: { + start: firstBrace.end, + length: 0 + }, + newText: `${newLine}${indent}public ${identifierName}${typeString}`, + filePath: targetDeclaration.getSourceFile().fileName + }; + + return [refactoring]; + } + } +} \ No newline at end of file diff --git a/src/services/quickfixes/semantic/addImportFromStatement.ts b/src/services/quickfixes/semantic/addImportFromStatement.ts new file mode 100644 index 0000000000000..ad8ec90a8517e --- /dev/null +++ b/src/services/quickfixes/semantic/addImportFromStatement.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// +namespace ts.quickFix.semantic { + let path = require('path'); + export class AddImportFromStatement extends MessageAndCodeBasedQuickFixBase { + protected getPattern(): RegExp { + return /Cannot find name \'(\w+)\'./; + } + protected getSupportedErrorCode(): number { + return Diagnostics.Cannot_find_name_0.code; + } + protected getIdentifierAndClassNameFromMatch(info: QuickFixQueryInformation, error: Diagnostic, match: RegExpMatchArray): IdentifierAndClassName[] { + var [, identifierName] = match; + let importCandidates = getClassesOrInterfacesBasedOnName(info.program.getSourceFiles(), identifierName); + return importCandidates.map(importCandidate => { + if (importCandidate) { + let file = removeExt(makeRelativePath(path.dirname(error.file.path), importCandidate.getSourceFile().path)); + return { identifierName, file }; + } + }); + } + protected getDisplay(context: QuickFixContext): string { + var {identifierName, file} = context.identifierAndClassName; + return file ? `import {${identifierName}} from \"${file}\"` : undefined; + } + + protected getRefactorings(context: QuickFixContext) { + var {identifierName, file} = context.identifierAndClassName; + let formatCodeOptions = context.info.project.projectService.getFormatCodeOptions(); + var newLine = formatCodeOptions.NewLineCharacter; + let refactorings: ts.server.protocol.Refactoring[] = [{ + span: { + start: 0, + length: 0 + }, + newText: `import {${identifierName}} from \"${file}\";${newLine}`, + filePath: context.info.sourceFile.fileName + }]; + + return refactorings; + } + } +} \ No newline at end of file diff --git a/src/services/quickfixes/semantic/addImportStatement.ts b/src/services/quickfixes/semantic/addImportStatement.ts new file mode 100644 index 0000000000000..c569a38b75ae4 --- /dev/null +++ b/src/services/quickfixes/semantic/addImportStatement.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// +namespace ts.quickFix.semantic { + let path = require('path'); + export class AddImportStatement extends MessageAndCodeBasedQuickFixBase { + protected getPattern(): RegExp { + return /Cannot find name \'(\w+)\'./; + } + protected getSupportedErrorCode(): number { + return Diagnostics.Cannot_find_name_0.code; + } + protected getIdentifierAndClassNameFromMatch(info: QuickFixQueryInformation, error: Diagnostic, match: RegExpMatchArray): IdentifierAndClassName[] { + var [, identifierName] = match; + let importCandidates = getClassesOrInterfacesBasedOnName(info.program.getSourceFiles(), identifierName); + return importCandidates.map(importCandidate => { + if (importCandidate) { + let file = removeExt(makeRelativePath(path.dirname(error.file.path), importCandidate.getSourceFile().path)); + return { identifierName, file }; + } + }); + } + protected getDisplay(context: QuickFixContext): string { + var {identifierName, file} = context.identifierAndClassName; + return file ? `import ${identifierName} = require(\"${file}\")` : undefined; + } + + protected getRefactorings(context: QuickFixContext) { + var {identifierName, file} = context.identifierAndClassName; + let formatCodeOptions = context.info.project.projectService.getFormatCodeOptions(); + var newLine = formatCodeOptions.NewLineCharacter; + let refactorings: ts.server.protocol.Refactoring[] = [{ + span: { + start: 0, + length: 0 + }, + newText: `import ${identifierName} = require(\"${file}\");${newLine}`, + filePath: context.info.sourceFile.fileName + }]; + + return refactorings; + } + } +} \ No newline at end of file diff --git a/src/services/quickfixes/semantic/implementAbstractClass.ts b/src/services/quickfixes/semantic/implementAbstractClass.ts new file mode 100644 index 0000000000000..e9c053406af31 --- /dev/null +++ b/src/services/quickfixes/semantic/implementAbstractClass.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// +namespace ts.quickFix.semantic { + export class ImplementAbstractClass extends MessageAndCodeBasedQuickFixBase { + protected getPattern(): RegExp { + return /Non-abstract class \'(\w+)\' does not implement inherited abstract member \'(\w+)\' from class \'(\w+)\'./; + } + protected getSupportedErrorCode(): number { + return Diagnostics.Non_abstract_class_0_does_not_implement_inherited_abstract_member_1_from_class_2.code; + } + protected getIdentifierAndClassNameFromMatch(info: QuickFixQueryInformation, error: Diagnostic, match: RegExpMatchArray): IdentifierAndClassName[] { + var [, className, identifierName, abstractClassName] = match; + return [{ className, abstractClassName, identifierName }]; + } + protected getDisplay(context: QuickFixContext): string { + var {abstractClassName, className, identifierName} = context.identifierAndClassName; + return `Implement ${identifierName} from ${abstractClassName} in ${className}`; + } + + protected getRefactorings(context: QuickFixContext) { + var {className, abstractClassName, identifierName} = context.identifierAndClassName; + var {info} = context; + + let abstractClassTarget = ts.quickFix.getNodeByKindAndName(info.program.getSourceFiles(), ts.SyntaxKind.ClassDeclaration, abstractClassName); + + let classTarget = ts.quickFix.getNodeByKindAndName([info.sourceFile], ts.SyntaxKind.ClassDeclaration, className); + + let firstBrace = classTarget.getChildren().filter(x => x.kind === ts.SyntaxKind.OpenBraceToken)[0]; + + let formatCodeOptions = info.project.projectService.getFormatCodeOptions(); + var newLine = formatCodeOptions.NewLineCharacter; + + var indentLength = info.service.getIndentationAtPosition( + classTarget.getSourceFile().fileName, firstBrace.end, formatCodeOptions); + var indent = formatting.getIndentationString(indentLength + formatCodeOptions.IndentSize, formatCodeOptions); + var indentForContent = formatting.getIndentationString(indentLength + 2 * formatCodeOptions.IndentSize, formatCodeOptions); + + let refactorings: ts.server.protocol.Refactoring[] = []; + + abstractClassTarget.members.forEach(function (member) { + let name = member.name && (member.name).text; + if (name && identifierName === name) { + var memberAsText = member.getFullText(); + if (memberAsText.indexOf('abstract') > -1) { + var content = memberAsText.replace('abstract', ''); + if (content.lastIndexOf(';') === content.length - 1) { + content = content.substring(0, content.length - 1); + } + + content += ' {' + newLine + indentForContent + 'return null;' + newLine + indent + '}'; + var refactoring = { + span: { + start: firstBrace.end, + length: 0 + }, + newText: (newLine + indent) + content, + filePath: classTarget.getSourceFile().fileName + }; + refactorings.push(refactoring); + } + } + }); + + return refactorings; + } + } +} \ No newline at end of file diff --git a/src/services/quickfixes/semantic/implementInterface.ts b/src/services/quickfixes/semantic/implementInterface.ts new file mode 100644 index 0000000000000..43a301425025b --- /dev/null +++ b/src/services/quickfixes/semantic/implementInterface.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// +namespace ts.quickFix.semantic { + export class ImplementInterface extends MessageAndCodeBasedQuickFixBase { + protected getPattern(): RegExp { + return /Class \'(\w+)\' incorrectly implements interface \'(\w+)\'.*Property \'(\w+)\' is missing in type \'(\w+)\'./; + } + protected getSupportedErrorCode(): number { + return Diagnostics.Class_0_incorrectly_implements_interface_1.code; + } + protected getIdentifierAndClassNameFromMatch(info: QuickFixQueryInformation, error: Diagnostic, match: RegExpMatchArray): IdentifierAndClassName[] { + var [, className, interfaceName, identifierName] = match; + return [{ className, interfaceName, identifierName }]; + } + protected getDisplay(context: QuickFixContext): string { + var {interfaceName, className, identifierName} = context.identifierAndClassName; + return `Implement ${identifierName} from ${interfaceName} in ${className}`; + } + + protected getRefactorings(context: QuickFixContext) { + let info = context.info; + var {className, interfaceName, identifierName} = context.identifierAndClassName; + + let interfaceTarget = getNodeByKindAndName(info.program.getSourceFiles(), ts.SyntaxKind.InterfaceDeclaration, interfaceName); + let classTarget = getNodeByKindAndName([info.sourceFile], ts.SyntaxKind.ClassDeclaration, className); + let firstBrace = classTarget.getChildren().filter(x => x.kind === ts.SyntaxKind.OpenBraceToken)[0]; + + let formatCodeOptions = info.project.projectService.getFormatCodeOptions(); + + var newLine = formatCodeOptions.NewLineCharacter; + var indentLength = info.service.getIndentationAtPosition(classTarget.getSourceFile().fileName, firstBrace.end, formatCodeOptions); + + var indent = formatting.getIndentationString(indentLength + formatCodeOptions.IndentSize, formatCodeOptions); + var indentForContent = formatting.getIndentationString(indentLength + 2 * formatCodeOptions.IndentSize, formatCodeOptions); + + let refactorings: ts.server.protocol.Refactoring[] = []; + interfaceTarget.members.forEach(function (member) { + let name = member.name && (member.name).text; + if (name && identifierName === name) { + let content = ""; + if (member.kind === ts.SyntaxKind.MethodSignature) { + var memberAsText = member.getFullText(); + content = (newLine + indent) + memberAsText.replace('abstract', ''); + if (content.lastIndexOf(';') === content.length - 1) { + content = content.substring(0, content.length - 1); + } + content += ' {' + newLine + indentForContent + 'return null;' + newLine + indent + '}'; + } else { + content = (newLine + indent) + member.getFullText(); + } + var refactoring = { + span: { + start: firstBrace.end, + length: 0 + }, + newText: content, + filePath: classTarget.getSourceFile().fileName + }; + refactorings.push(refactoring); + } + }); + + return refactorings; + } + } +} \ No newline at end of file diff --git a/src/services/quickfixes/semantic/messageAndCodeBasedQuickFixBase.ts b/src/services/quickfixes/semantic/messageAndCodeBasedQuickFixBase.ts new file mode 100644 index 0000000000000..2f75d25ba896c --- /dev/null +++ b/src/services/quickfixes/semantic/messageAndCodeBasedQuickFixBase.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// +/// +/// +/// + +namespace ts.quickFix.semantic { + export interface QuickFixContext { + identifierAndClassName: IdentifierAndClassName; + error: Diagnostic; + positionNode: Node; + identifier: Identifier; + info: QuickFixQueryInformation; + } + + export interface IdentifierAndClassName { + identifierName?: string, + className?: string, + file?: string, + abstractClassName?: string, + interfaceName?: string + } + + export abstract class MessageAndCodeBasedQuickFixBase implements QuickFix { + getIdentifierAndClassNames(info: QuickFixQueryInformation, errorText: string, error: Diagnostic): IdentifierAndClassName[] { + + var match = errorText.match(this.getPattern()); + if (!match) { + return undefined; + } + return this.getIdentifierAndClassNameFromMatch(info, error, match); + } + + private getRelevantIdentifierAndClassName(info: QuickFixQueryInformation): QuickFixContext[] { + var errors = info.positionErrors.filter(x => x.code === this.getSupportedErrorCode()); + let quickFixContents: QuickFixContext[] = []; + let errorToErrorText = errors.forEach(error => { + if (error) { + let positionNode = getTokenAtPosition(info.sourceFile, error.start); + if (positionNode.kind === SyntaxKind.Identifier) { + const flattenedErrors = this.flatten(error.messageText); + flattenedErrors.map(errorText => { + let mainError = flattenedErrors[0]; + + errorText = mainError + errorText; + + var identifierAndClassNames = this.getIdentifierAndClassNames(info, errorText, error); + + if (identifierAndClassNames && identifierAndClassNames.length) { + let identifier: Identifier = positionNode; + identifierAndClassNames.forEach(identifierAndClassName => { + quickFixContents.push({ identifierAndClassName, error, positionNode, identifier, info }); + }); + } + }); + } + } + }) + + return quickFixContents; + } + + public provideFix(info: QuickFixQueryInformation): ts.server.protocol.QuickFix[] { + let quickFixContexts = this.getRelevantIdentifierAndClassName(info) + if (quickFixContexts.length === 0) return []; + return quickFixContexts.map(context => { + return { display: this.getDisplay(context), refactorings: this.getRefactorings(context) } + }); + } + + private flatten(messageText: string | DiagnosticMessageChain): string[] { + if (typeof messageText === "string") { + return [messageText]; + } + else { + let diagnosticChain = messageText; + let result: string[] = []; + + let indent = 0; + while (diagnosticChain) { + result.push(diagnosticChain.messageText); + diagnosticChain = diagnosticChain.next; + } + + return result; + } + } + + protected abstract getPattern(): RegExp; + protected abstract getSupportedErrorCode(): number; + protected abstract getDisplay(context: QuickFixContext): string; + protected abstract getRefactorings(context: QuickFixContext): server.protocol.Refactoring[]; + protected abstract getIdentifierAndClassNameFromMatch(info: QuickFixQueryInformation, error: Diagnostic, match: RegExpMatchArray): IdentifierAndClassName[]; + + + } +} \ No newline at end of file diff --git a/src/services/quickfixes/semantic/typeAssertPropertyAccessToAny.ts b/src/services/quickfixes/semantic/typeAssertPropertyAccessToAny.ts new file mode 100644 index 0000000000000..5284b02bce5db --- /dev/null +++ b/src/services/quickfixes/semantic/typeAssertPropertyAccessToAny.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// +namespace ts.quickFix.semantic { + export class TypeAssertPropertyAccessToAny extends MessageAndCodeBasedQuickFixBase { + protected getPattern(): RegExp { + return /Property \'(\w+)\' does not exist on type \.*/; + } + protected getSupportedErrorCode(): number { + return Diagnostics.Property_0_does_not_exist_on_type_1.code; + } + protected getIdentifierAndClassNameFromMatch(info: QuickFixQueryInformation, error: Diagnostic, match: RegExpMatchArray): IdentifierAndClassName[] { + var [, identifierName] = match; + return [{ identifierName }]; + } + protected getDisplay(context: QuickFixContext): string { + var {identifierName} = context.identifierAndClassName; + return `Assert "any" for property access "${identifierName}"`; + } + + protected getRefactorings(context: QuickFixContext) { + var {identifierName} = context.identifierAndClassName; + let {info, error} = context; + let positionNode = getTokenAtPosition(error.file, error.start); + let parent = positionNode.parent; + if (parent.kind === ts.SyntaxKind.PropertyAccessExpression) { + let propertyAccess = parent; + let start = propertyAccess.getStart(); + let end = propertyAccess.dotToken.getStart(); + + let oldText = propertyAccess.getText().substr(0, end - start); + + let FileSpan: ts.server.protocol.Refactoring = { + filePath: parent.getSourceFile().fileName, + span: { + start: start, + length: end - start, + }, + newText: `(${oldText} as any)` + }; + + return [FileSpan]; + } + return []; + } + } +} \ No newline at end of file diff --git a/src/services/quickfixes/utils.ts b/src/services/quickfixes/utils.ts new file mode 100644 index 0000000000000..48c1437e5dece --- /dev/null +++ b/src/services/quickfixes/utils.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// +/// + +namespace ts.quickFix { + let path = require('path'); + + export interface GetPathCompletions { + prefix: string; + allFiles: string[]; + program: ts.Program; + filePath: string; + sourceFile: ts.SourceFile; + } + export interface PathCompletion { + fileName: string; + relativePath: string; + fullPath: string; + } + function getFileName(fullFilePath: string) { + let parts = fullFilePath.split('/'); + return parts[parts.length - 1]; + } + + export function removeExt(filePath: string) { + return filePath.substr(0, filePath.lastIndexOf('.')); + } + + export function makeRelativePath(relativeFolder: string, filePath: string) { + var relativePath = path.relative(relativeFolder, filePath).split('\\').join('/'); + if (relativePath[0] !== '.') { + relativePath = './' + relativePath; + } + return relativePath; + } + + var forEachChild = ts.forEachChild; + + export function getClassesOrInterfacesBasedOnName(sourcefiles: SourceFile[], name: string): Array { + let results: Array = []; + function findNode(node: Node) { + if (node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.InterfaceDeclaration) { + let typedNode = node; + let nodeName = typedNode.name && typedNode.name.text; + if (nodeName === name) { + results.push(typedNode); + } + } + forEachChild(node, findNode); + } + + for (let file of sourcefiles) { + forEachChild(file, findNode); + } + + return results; + } + + export function getNodeByKindAndName(sourcefiles: SourceFile[], kind: SyntaxKind, name: string): Node { + let found: Node = undefined; + + function findNode(node: Node) { + if (node.kind === kind) { + // Now lookup name: + if (node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.InterfaceDeclaration) { + let nodeName = (node).name && ((node).name).text; + if (nodeName === name) { + found = node; + } + } + } + + if (!found) { forEachChild(node, findNode); } + } + + for (let file of sourcefiles) { + forEachChild(file, findNode); + } + + return found; + } +} \ No newline at end of file diff --git a/src/services/services.ts b/src/services/services.ts index 13df3a54a30be..f4c42eaca06c0 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -10,6 +10,7 @@ /// /// /// +/// namespace ts { /** The version of the language service API */