Skip to content

Avoid generating implementation signatures in ambient contexts. #19708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2457,7 +2457,7 @@ namespace ts {
const result = context.encounteredError ? undefined : resultingNode;
return result;
},
signatureToSignatureDeclaration: (signature: Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags) => {
signatureToSignatureDeclaration: (signature: Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): SignatureDeclaration | undefined => {
Debug.assert(enclosingDeclaration === undefined || (enclosingDeclaration.flags & NodeFlags.Synthesized) === 0);
const context = createNodeBuilderContext(enclosingDeclaration, flags);
const resultingNode = signatureToSignatureDeclarationHelper(signature, kind, context);
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2657,7 +2657,7 @@ namespace ts {
/** Note that the resulting nodes cannot be checked. */
typeToTypeNode(type: Type, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): TypeNode;
/** Note that the resulting nodes cannot be checked. */
signatureToSignatureDeclaration(signature: Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): SignatureDeclaration;
signatureToSignatureDeclaration(signature: Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): SignatureDeclaration | undefined;
/** Note that the resulting nodes cannot be checked. */
indexInfoToIndexSignatureDeclaration(indexInfo: IndexInfo, kind: IndexKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): IndexSignatureDeclaration;

Expand Down
29 changes: 8 additions & 21 deletions src/services/codefixes/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,16 @@ namespace ts.codefix {
// (eg: an abstract method or interface declaration), there is a 1-1
// correspondence of declarations and signatures.
const signatures = checker.getSignaturesOfType(type, SignatureKind.Call);
const needsImplementation = !(enclosingDeclaration.flags & NodeFlags.Ambient);
if (!some(signatures)) {
return undefined;
}

if (declarations.length === 1) {
Debug.assert(signatures.length === 1);
const signature = signatures[0];
return signatureToMethodDeclaration(signature, enclosingDeclaration, createStubbedMethodBody());
const body = needsImplementation ? createStubbedMethodBody() : undefined;
return signatureToMethodDeclaration(signature, enclosingDeclaration, body);
}

const signatureDeclarations: MethodDeclaration[] = [];
Expand All @@ -119,7 +121,7 @@ namespace ts.codefix {
signatureDeclarations.push(methodDeclaration);
}
}
else {
else if (needsImplementation) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that declarations.length > signatures.length but we don't want to add an implementation?
While I'm here, TypeChecker.signatureToSignatureDeclaration has a non-nullable result. Then in signatureToMethodDeclaration it's cast to another non-nullable type. Then it's tested for existence!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that declarations.length > signatures.length but we don't want to add an implementation?

Good catch, but I don't even understand what that check is for and what it's doing. @aozgaa?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See this comment (a few lines above): https://github.com/Microsoft/TypeScript/pull/19708/files#diff-6d6034083b71c2ed75493db0fe725ec5R88.

An example (haven't checked by actual debugging) might be

class C {
     foo(x: number): void;
     foo(x: string): void;
     foo(x: number | string) { console.log(x); }
}

declare class D implements C {
}

Then I think adding foo to D would be an example.

Copy link
Contributor

@aozgaa aozgaa Nov 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check should be inside createMethodImplementingSignatures, which will just omit the body if we are in an ambient context.

EDIT: no, we do not need an implementation signature in this case. Doh.

Debug.assert(declarations.length === signatures.length);
const methodImplementingSignatures = createMethodImplementingSignatures(signatures, name, optional, modifiers);
signatureDeclarations.push(methodImplementingSignatures);
Expand Down Expand Up @@ -222,32 +224,17 @@ namespace ts.codefix {
parameters.push(restParameter);
}

return createStubbedMethod(
modifiers,
name,
optional,
/*typeParameters*/ undefined,
parameters,
/*returnType*/ undefined);
}

export function createStubbedMethod(
modifiers: ReadonlyArray<Modifier>,
name: PropertyName,
optional: boolean,
typeParameters: ReadonlyArray<TypeParameterDeclaration> | undefined,
parameters: ReadonlyArray<ParameterDeclaration>,
returnType: TypeNode | undefined) {
return createMethod(
/*decorators*/ undefined,
modifiers,
/*asteriskToken*/ undefined,
name,
optional ? createToken(SyntaxKind.QuestionToken) : undefined,
Copy link

@ghost ghost Nov 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's wierd to create an optional method with a body even if that's legal.
If the method is optional, though, we don't want to make it look like it must be implemented; maybe just add an // optional, delete if you don't want to implement this comment and let the user decide?

typeParameters,
/*typeParameters*/ undefined,
parameters,
returnType,
createStubbedMethodBody());
/*returnType*/ undefined,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might as well give them the return type from the signature, right?

Copy link
Member Author

@DanielRosenwasser DanielRosenwasser Nov 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically it's unsound to union the return types in most cases, but we could do that in a different PR.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intersection instead? It's better that they get an error when trying to return a value from the method, than that they get an error that the class failed to implement the interface, which are always hard to read..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intersection will be impossible to fulfill most of the time.

function foo(x: number): number;
function foo(x: string): string;
function foo(x: string | number): ??? {
  // An intersection is impossible to return
}

All I'm saying is that it's unsound to do the union, but we do allow it because it's just convenient. You're right that we should probably still do it. Just felt weird.

createStubbedMethodBody(),
);
}

function createStubbedMethodBody() {
Expand Down
11 changes: 11 additions & 0 deletions tests/cases/fourslash/codeFixAmbientClassImplementInterface01.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// <reference path="fourslash.ts" />

////interface Hook {
//// tap(): void
////}
////
////declare class SyncHook implements Hook {[| |]}

verify.rangeAfterCodeFix(`
tap(): void;
`);
13 changes: 13 additions & 0 deletions tests/cases/fourslash/codeFixAmbientClassImplementInterface02.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path="fourslash.ts" />

////interface Hook {
//// tap(): void;
//// tap(x: number): string;
////}
////
////declare class SyncHook implements Hook {[| |]

verify.rangeAfterCodeFix(`
tap(): void;
tap(x: number): string;
`);
3 changes: 1 addition & 2 deletions tests/cases/fourslash/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ declare namespace FourSlashInterface {
InsertSpaceAfterTypeAssertion: boolean;
PlaceOpenBraceOnNewLineForFunctions: boolean;
PlaceOpenBraceOnNewLineForControlBlocks: boolean;
[s: string]: boolean | number | string | undefined;
}
interface Range {
fileName: string;
Expand Down Expand Up @@ -375,7 +374,7 @@ declare namespace FourSlashInterface {
setFormatOptions(options: FormatCodeOptions): any;
selection(startMarker: string, endMarker: string): void;
onType(posMarker: string, key: string): void;
setOption(name: keyof FormatCodeOptions, value: number | string | boolean): void;
setOption<K extends keyof FormatCodeOptions>(name: K, value: FormatCodeOptions[K]): void;
}
class cancellation {
resetCancelled(): void;
Expand Down