UNPKG

alm

Version:

The best IDE for TypeScript

123 lines (96 loc) 5.09 kB
import {QuickFix, QuickFixQueryInformation, Refactoring, CanProvideFixResponse} from "../quickFix"; import * as ast from "../../modules/astUtils"; const EOL = '\n'; function getIdentifierAndClassNames(error: ts.Diagnostic) { var errorText: string = <any>error.messageText; if (typeof errorText !== 'string') { console.error('I have no idea what this is:', errorText); return undefined; }; // see https://github.com/Microsoft/TypeScript/blob/6637f49209ceb5ed719573998381eab010fa48c9/src/compiler/diagnosticMessages.json#L842 var match = errorText.match(/Property \'(\w+)\' does not exist on type \'(\w+)\'./); // Happens when the type name is an alias. We can't refactor in this case anyways if (!match) return; var [, identifierName, className] = match; return { identifierName, className }; } /** foo.a => a */ function getLastNameAfterDot(text: string) { return text.substr(text.lastIndexOf('.') + 1); } function getTypeStringForNode(node: ts.Node, typeChecker: ts.TypeChecker) { var type = typeChecker.getTypeAtLocation(node); /** Discoverd from review of `services.getQuickInfoAtPosition` */ return ts.displayPartsToString(ts.typeToDisplayParts(typeChecker, type)).replace(/\s+/g, ' '); } export class AddClassMember implements QuickFix { key = AddClassMember.name; canProvideFix(info: QuickFixQueryInformation): CanProvideFixResponse { var relevantError = info.positionErrors.filter(x=> x.code == ts.Diagnostics.Property_0_does_not_exist_on_type_1.code)[0]; if (!relevantError) return; if (info.positionNode.kind !== ts.SyntaxKind.Identifier) return; // TODO: use type checker to see if item of `.` before hand is a class // But for now just run with it. var match = getIdentifierAndClassNames(relevantError); if (!match) return; var {identifierName, className} = match; return { display: `Add ${identifierName} to ${className}` }; } provideFix(info: QuickFixQueryInformation): Refactoring[] { var relevantError = info.positionErrors.filter(x=> x.code == ts.Diagnostics.Property_0_does_not_exist_on_type_1.code)[0]; var identifier = <ts.Identifier>info.positionNode; var identifierName = identifier.text; var {className} = getIdentifierAndClassNames(relevantError); // Get the type of the stuff on the right if its an assignment var typeString = 'any'; var parentOfParent = identifier.parent.parent; if (parentOfParent.kind == ts.SyntaxKind.BinaryExpression && (<ts.BinaryExpression>parentOfParent).operatorToken.getText().trim() == '=') { let binaryExpression = <ts.BinaryExpression>parentOfParent; typeString = getTypeStringForNode(binaryExpression.right, info.typeChecker); } else if (parentOfParent.kind == ts.SyntaxKind.CallExpression) { let callExp = <ts.CallExpression>parentOfParent; let typeStringParts = ['(']; // Find the number of arguments let args = []; callExp.arguments.forEach(arg => { var argName = (getLastNameAfterDot(arg.getText())); var argType = 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 = ast.getNodeByKindAndName(info.program, ts.SyntaxKind.ClassDeclaration, className); if (!memberTarget) { // Find the containing interface declaration memberTarget = ast.getNodeByKindAndName(info.program, ts.SyntaxKind.InterfaceDeclaration, className); } if (!memberTarget) { return []; } // The following code will be same (and typesafe) for either class or interface let targetDeclaration = <ts.ClassDeclaration|ts.InterfaceDeclaration>memberTarget; // Then the first brace let firstBrace = targetDeclaration.getChildren().filter(x=> x.kind == ts.SyntaxKind.OpenBraceToken)[0]; // And the correct indent var indentLength = info.service.getIndentationAtPosition( memberTarget.getSourceFile().fileName, firstBrace.end, info.project.configFile.project.formatCodeOptions); var indent = Array(indentLength + info.formatOptions.indentSize + 1).join(' '); // And add stuff after the first brace let refactoring: Refactoring = { span: { start: firstBrace.end, length: 0 }, newText: `${EOL}${indent}${identifierName}: ${typeString};`, filePath: targetDeclaration.getSourceFile().fileName }; return [refactoring]; } }