Skip to content

Commit 30802cd

Browse files
committed
Handle loose type parameters in Extract Method
Known limitations: 1. If a type parameter on an inner symbol shadows a type parameter on an outer symbol, the generated code will be incorrect. We should either rename one or more type parameters or forbid the extraction. 2. Type arguments are always passed explicitly, even if they would be inferred correctly.
1 parent 2350d46 commit 30802cd

File tree

3 files changed

+175
-7
lines changed

3 files changed

+175
-7
lines changed

src/compiler/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,20 @@ namespace ts {
651651
}
652652

653653
export interface SignatureDeclaration extends NamedDeclaration {
654+
kind: SyntaxKind.CallSignature
655+
| SyntaxKind.ConstructSignature
656+
| SyntaxKind.MethodSignature
657+
| SyntaxKind.IndexSignature
658+
| SyntaxKind.FunctionType
659+
| SyntaxKind.ConstructorType
660+
| SyntaxKind.JSDocFunctionType
661+
| SyntaxKind.FunctionDeclaration
662+
| SyntaxKind.MethodDeclaration
663+
| SyntaxKind.Constructor
664+
| SyntaxKind.GetAccessor
665+
| SyntaxKind.SetAccessor
666+
| SyntaxKind.FunctionExpression
667+
| SyntaxKind.ArrowFunction;
654668
name?: PropertyName;
655669
typeParameters?: NodeArray<TypeParameterDeclaration>;
656670
parameters: NodeArray<ParameterDeclaration>;
@@ -1816,6 +1830,7 @@ namespace ts {
18161830
export type DeclarationWithTypeParameters = SignatureDeclaration | ClassLikeDeclaration | InterfaceDeclaration | TypeAliasDeclaration | JSDocTemplateTag;
18171831

18181832
export interface ClassLikeDeclaration extends NamedDeclaration {
1833+
kind: SyntaxKind.ClassDeclaration | SyntaxKind.ClassExpression;
18191834
name?: Identifier;
18201835
typeParameters?: NodeArray<TypeParameterDeclaration>;
18211836
heritageClauses?: NodeArray<HeritageClause>;

src/compiler/utilities.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,38 @@ namespace ts {
477477
return false;
478478
}
479479

480+
/* @internal */
481+
export function isDeclarationWithTypeParameters(node: Node): node is DeclarationWithTypeParameters;
482+
export function isDeclarationWithTypeParameters(node: DeclarationWithTypeParameters): node is DeclarationWithTypeParameters {
483+
switch (node.kind) {
484+
case SyntaxKind.CallSignature:
485+
case SyntaxKind.ConstructSignature:
486+
case SyntaxKind.MethodSignature:
487+
case SyntaxKind.IndexSignature:
488+
case SyntaxKind.FunctionType:
489+
case SyntaxKind.ConstructorType:
490+
case SyntaxKind.JSDocFunctionType:
491+
case SyntaxKind.ClassDeclaration:
492+
case SyntaxKind.ClassExpression:
493+
case SyntaxKind.InterfaceDeclaration:
494+
case SyntaxKind.TypeAliasDeclaration:
495+
case SyntaxKind.JSDocTemplateTag:
496+
case SyntaxKind.FunctionDeclaration:
497+
case SyntaxKind.MethodDeclaration:
498+
case SyntaxKind.Constructor:
499+
case SyntaxKind.GetAccessor:
500+
case SyntaxKind.SetAccessor:
501+
case SyntaxKind.FunctionExpression:
502+
case SyntaxKind.ArrowFunction:
503+
return true;
504+
default:
505+
staticAssertNever(node);
506+
return false;
507+
}
508+
}
509+
510+
function staticAssertNever(_: never): void {}
511+
480512
// Gets the nearest enclosing block scope container that has the provided node
481513
// as a descendant, that is not the provided node.
482514
export function getEnclosingBlockScopeContainer(node: Node): Node {

src/services/refactors/extractMethod.ts

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ namespace ts.refactor.extractMethod {
610610
export function extractFunctionInScope(
611611
node: Statement | Expression | Block,
612612
scope: Scope,
613-
{ usages: usagesInScope, substitutions }: ScopeUsages,
613+
{ usages: usagesInScope, typeParameterUsages, substitutions }: ScopeUsages,
614614
range: TargetRange,
615615
context: RefactorContext): ExtractResultForScope {
616616

@@ -652,7 +652,18 @@ namespace ts.refactor.extractMethod {
652652
callArguments.push(createIdentifier(name));
653653
});
654654

655-
// Provide explicit return types for contexutally-typed functions
655+
const typeParametersAndDeclarations = arrayFrom(typeParameterUsages.values()).map(type => ({ type, declaration: getFirstDeclaration(type) }));
656+
const sortedTypeParametersAndDeclarations = typeParametersAndDeclarations.sort(compareTypesByDeclarationOrder);
657+
658+
const typeParameters: ReadonlyArray<TypeParameterDeclaration> = sortedTypeParametersAndDeclarations.map(t => t.declaration as TypeParameterDeclaration);
659+
660+
// Strictly speaking, we should check whether each name actually binds to the appropriate type
661+
// parameter. In cases of shadowing, they may not.
662+
const callTypeArguments: ReadonlyArray<TypeNode> | undefined = typeParameters.length > 0
663+
? typeParameters.map(decl => createTypeReferenceNode(decl.name, /*typeArguments*/ undefined))
664+
: undefined;
665+
666+
// Provide explicit return types for contextually-typed functions
656667
// to avoid problems when there are literal types present
657668
if (isExpression(node) && !isJS) {
658669
const contextualType = checker.getContextualType(node);
@@ -677,7 +688,7 @@ namespace ts.refactor.extractMethod {
677688
range.facts & RangeFacts.IsGenerator ? createToken(SyntaxKind.AsteriskToken) : undefined,
678689
functionName,
679690
/*questionToken*/ undefined,
680-
/*typeParameters*/[],
691+
typeParameters,
681692
parameters,
682693
returnType,
683694
body
@@ -689,7 +700,7 @@ namespace ts.refactor.extractMethod {
689700
range.facts & RangeFacts.IsAsyncFunction ? [createToken(SyntaxKind.AsyncKeyword)] : undefined,
690701
range.facts & RangeFacts.IsGenerator ? createToken(SyntaxKind.AsteriskToken) : undefined,
691702
functionName,
692-
/*typeParameters*/[],
703+
typeParameters,
693704
parameters,
694705
returnType,
695706
body
@@ -704,7 +715,7 @@ namespace ts.refactor.extractMethod {
704715
// replace range with function call
705716
let call: Expression = createCall(
706717
isClassLike(scope) ? createPropertyAccess(range.facts & RangeFacts.InStaticRegion ? createIdentifier(scope.name.getText()) : createThis(), functionReference) : functionReference,
707-
/*typeArguments*/ undefined,
718+
callTypeArguments, // Note that no attempt is made to take advantage of type argument inference
708719
callArguments);
709720
if (range.facts & RangeFacts.IsGenerator) {
710721
call = createYield(createToken(SyntaxKind.AsteriskToken), call);
@@ -774,6 +785,51 @@ namespace ts.refactor.extractMethod {
774785
changes: changeTracker.getChanges()
775786
};
776787

788+
function getFirstDeclaration(type: Type): Declaration | undefined {
789+
let firstDeclaration = undefined;
790+
791+
const symbol = type.symbol;
792+
if (symbol && symbol.declarations) {
793+
for (const declaration of symbol.declarations) {
794+
if (firstDeclaration === undefined || declaration.pos < firstDeclaration.pos) {
795+
firstDeclaration = declaration;
796+
}
797+
}
798+
}
799+
800+
return firstDeclaration;
801+
}
802+
803+
function compareTypesByDeclarationOrder(
804+
{type: type1, declaration: declaration1}: {type: Type, declaration?: Declaration},
805+
{type: type2, declaration: declaration2}: {type: Type, declaration?: Declaration}) {
806+
807+
if (declaration1) {
808+
if (declaration2) {
809+
const positionDiff = declaration1.pos - declaration2.pos;
810+
if (positionDiff !== 0) {
811+
return positionDiff;
812+
}
813+
}
814+
else {
815+
return 1; // Sort undeclared type parameters to the front.
816+
}
817+
}
818+
else if (declaration2) {
819+
return -1; // Sort undeclared type parameters to the front.
820+
}
821+
822+
const name1 = type1.symbol ? type1.symbol.getName() : "";
823+
const name2 = type2.symbol ? type2.symbol.getName() : "";
824+
const nameDiff = compareStrings(name1, name2);
825+
if (nameDiff !== 0) {
826+
return nameDiff;
827+
}
828+
829+
// IDs are guaranteed to be unique, so this ensures a total ordering.
830+
return type1.id - type2.id;
831+
}
832+
777833
function getPropertyAssignmentsForWrites(writes: UsageEntry[]) {
778834
return writes.map(w => createShorthandPropertyAssignment(w.symbol.name));
779835
}
@@ -867,6 +923,7 @@ namespace ts.refactor.extractMethod {
867923

868924
export interface ScopeUsages {
869925
usages: Map<UsageEntry>;
926+
typeParameterUsages: Map<TypeParameter>; // Key is type ID
870927
substitutions: Map<Node>;
871928
}
872929

@@ -877,23 +934,58 @@ namespace ts.refactor.extractMethod {
877934
sourceFile: SourceFile,
878935
checker: TypeChecker) {
879936

937+
const allTypeParameterUsages = createMap<TypeParameter>(); // Key is type ID
880938
const usagesPerScope: ScopeUsages[] = [];
881939
const substitutionsPerScope: Map<Node>[] = [];
882940
const errorsPerScope: Diagnostic[][] = [];
883941
const visibleDeclarationsInExtractedRange: Symbol[] = [];
884942

885943
// initialize results
886944
for (const _ of scopes) {
887-
usagesPerScope.push({ usages: createMap<UsageEntry>(), substitutions: createMap<Expression>() });
945+
usagesPerScope.push({ usages: createMap<UsageEntry>(), typeParameterUsages: createMap<TypeParameter>(), substitutions: createMap<Expression>() });
888946
substitutionsPerScope.push(createMap<Expression>());
889947
errorsPerScope.push([]);
890948
}
891949
const seenUsages = createMap<Usage>();
892950
const target = isReadonlyArray(targetRange.range) ? createBlock(<Statement[]>targetRange.range) : targetRange.range;
893951
const containingLexicalScopeOfExtraction = isBlockScope(scopes[0], scopes[0].parent) ? scopes[0] : getEnclosingBlockScopeContainer(scopes[0]);
894952

953+
const unmodifiedNode = isReadonlyArray(targetRange.range) ? targetRange.range[0] : targetRange.range;
954+
const inGenericContext = isInGenericContext(unmodifiedNode);
955+
895956
collectUsages(target);
896957

958+
if (allTypeParameterUsages.size > 0) {
959+
const seenTypeParameterUsages = createMap<TypeParameter>(); // Key is type ID
960+
961+
let i = 0;
962+
for (let curr: Node = unmodifiedNode; curr !== undefined && i < scopes.length; curr = curr.parent) {
963+
if (curr === scopes[i]) {
964+
// Copy current contents of seenTypeParameterUsages into scope.
965+
seenTypeParameterUsages.forEach((typeParameter, id) => {
966+
usagesPerScope[i].typeParameterUsages.set(id, typeParameter);
967+
});
968+
969+
i++;
970+
}
971+
972+
// Note that we add the current node's type parameters *after* updating the corresponding scope.
973+
if (isDeclarationWithTypeParameters(curr) && curr.typeParameters) {
974+
for (const typeParameterDecl of curr.typeParameters) {
975+
const typeParameter = checker.getTypeAtLocation(typeParameterDecl) as TypeParameter;
976+
if (allTypeParameterUsages.has(typeParameter.id.toString())) {
977+
seenTypeParameterUsages.set(typeParameter.id.toString(), typeParameter);
978+
}
979+
}
980+
}
981+
}
982+
983+
// If we didn't get through all the scopes, then there were some that weren't in our
984+
// parent chain (impossible at time of writing). A conservative solution would be to
985+
// copy allTypeParameterUsages into all remaining scopes.
986+
Debug.assert(i === scopes.length);
987+
}
988+
897989
for (let i = 0; i < scopes.length; i++) {
898990
let hasWrite = false;
899991
let readonlyClassPropertyWrite: Declaration | undefined = undefined;
@@ -912,7 +1004,7 @@ namespace ts.refactor.extractMethod {
9121004
errorsPerScope[i].push(createDiagnosticForNode(targetRange.range, Messages.CannotCombineWritesAndReturns));
9131005
}
9141006
else if (readonlyClassPropertyWrite && i > 0) {
915-
errorsPerScope[i].push(createDiagnosticForNode(readonlyClassPropertyWrite, Messages.CannotCombineWritesAndReturns));
1007+
errorsPerScope[i].push(createDiagnosticForNode(readonlyClassPropertyWrite, Messages.CannotExtractReadonlyPropertyInitializerOutsideConstructor));
9161008
}
9171009
}
9181010

@@ -924,7 +1016,36 @@ namespace ts.refactor.extractMethod {
9241016

9251017
return { target, usagesPerScope, errorsPerScope };
9261018

1019+
function hasTypeParameters(node: Node) {
1020+
return isDeclarationWithTypeParameters(node) &&
1021+
node.typeParameters !== undefined &&
1022+
node.typeParameters.length > 0;
1023+
}
1024+
1025+
function isInGenericContext(node: Node) {
1026+
for (; node; node = node.parent) {
1027+
if (hasTypeParameters(node)) {
1028+
return true;
1029+
}
1030+
}
1031+
1032+
return false;
1033+
}
1034+
9271035
function collectUsages(node: Node, valueUsage = Usage.Read) {
1036+
if (inGenericContext) {
1037+
const type = checker.getTypeAtLocation(node);
1038+
1039+
const symbolWalker = checker.getSymbolWalker();
1040+
const {visitedTypes} = symbolWalker.walkType(type);
1041+
1042+
for (const visitedType of visitedTypes) {
1043+
if (visitedType.flags & TypeFlags.TypeParameter) {
1044+
allTypeParameterUsages.set(visitedType.id.toString(), visitedType as TypeParameter);
1045+
}
1046+
}
1047+
}
1048+
9281049
if (isDeclaration(node) && node.symbol) {
9291050
visibleDeclarationsInExtractedRange.push(node.symbol);
9301051
}

0 commit comments

Comments
 (0)