Skip to content

Commit

Permalink
Add method signature completions (microsoft#46370)
Browse files Browse the repository at this point in the history
* prototype creation for method override completion snippet

* WIP: start using codefix `addNewNodeForMemberSymbol` to create method decl node

* update type of addNewNodeForMemberSymbol

* add more tests and support more cases

* add more tests and fix some details

* wip: more fixes and tests

* expose check override modifier in checker

* fix test

* WIP: add snippet support

* WIP: snippet support on emitter, adding snippets in completions

* make add snippets work with overloads (not synced)

* fix snippet adding

* rebase

* WIP: try to add snippet escaping in emitter

* support escaping in snippets

* small fixes; fixed tests

* more tests and fixes

* fix new tests

* fix modifier inheritance for overloads

* merge conflict fixes; remove comments

* throw error if setOptions is called but not implemented

* fix newline handling

* fix weird stuff

* fix tests

* fix more tests

* Fix unbound host.getNewLine

* fix isParameterDeclaration changes

* rename diagnostic to status and remove snippets from public api

* rename emitter functions + fix indentation

* check completion kind before calling isclasslikemembercompletion

* fix missing type parameters

* Revert "fix missing type parameters"

This reverts commit 7bdeaa8.

* add isAmbient flag to addNewNodeForMemberSymbol

* add test for abstract overloads

* refactor snippet escaping support

* add user preference flag for enabling class member snippets

* update API baseline

* update tabstop order

Co-authored-by: Andrew Branch <[email protected]>
  • Loading branch information
2 people authored and mprobst committed Jan 10, 2022
1 parent c75f69c commit 5f7406e
Show file tree
Hide file tree
Showing 28 changed files with 1,457 additions and 81 deletions.
198 changes: 163 additions & 35 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@ namespace ts {
isDeclarationVisible,
isPropertyAccessible,
getTypeOnlyAliasDeclaration,
getMemberOverrideModifierStatus,
};

function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode): Signature | undefined {
Expand Down Expand Up @@ -38469,7 +38470,7 @@ namespace ts {
}
}

checkMembersForMissingOverrideModifier(node, type, typeWithThis, staticType);
checkMembersForOverrideModifier(node, type, typeWithThis, staticType);

const implementedTypeNodes = getEffectiveImplementsTypeNodes(node);
if (implementedTypeNodes) {
Expand Down Expand Up @@ -38506,8 +38507,7 @@ namespace ts {
}
}

function checkMembersForMissingOverrideModifier(node: ClassLikeDeclaration, type: InterfaceType, typeWithThis: Type, staticType: ObjectType) {
const nodeInAmbientContext = !!(node.flags & NodeFlags.Ambient);
function checkMembersForOverrideModifier(node: ClassLikeDeclaration, type: InterfaceType, typeWithThis: Type, staticType: ObjectType) {
const baseTypeNode = getEffectiveBaseTypeNode(node);
const baseTypes = baseTypeNode && getBaseTypes(type);
const baseWithThis = baseTypes?.length ? getTypeWithThisArgument(first(baseTypes), type.thisType) : undefined;
Expand All @@ -38521,77 +38521,163 @@ namespace ts {
if (isConstructorDeclaration(member)) {
forEach(member.parameters, param => {
if (isParameterPropertyDeclaration(param, member)) {
checkClassMember(param, /*memberIsParameterProperty*/ true);
checkExistingMemberForOverrideModifier(
node,
staticType,
baseStaticType,
baseWithThis,
type,
typeWithThis,
param,
/* memberIsParameterProperty */ true
);
}
});
}
checkClassMember(member);
checkExistingMemberForOverrideModifier(
node,
staticType,
baseStaticType,
baseWithThis,
type,
typeWithThis,
member,
/* memberIsParameterProperty */ false,
);
}
}

function checkClassMember(member: ClassElement | ParameterPropertyDeclaration, memberIsParameterProperty?: boolean) {
const hasOverride = hasOverrideModifier(member);
const hasStatic = isStatic(member);
const isJs = isInJSFile(member);
if (baseWithThis && (hasOverride || compilerOptions.noImplicitOverride)) {
const declaredProp = member.name && getSymbolAtLocation(member.name) || getSymbolAtLocation(member);
if (!declaredProp) {
return;
}

const thisType = hasStatic ? staticType : typeWithThis;
const baseType = hasStatic ? baseStaticType : baseWithThis;
const prop = getPropertyOfType(thisType, declaredProp.escapedName);
const baseProp = getPropertyOfType(baseType, declaredProp.escapedName);
/**
* @param member Existing member node to be checked.
* Note: `member` cannot be a synthetic node.
*/
function checkExistingMemberForOverrideModifier(
node: ClassLikeDeclaration,
staticType: ObjectType,
baseStaticType: Type,
baseWithThis: Type | undefined,
type: InterfaceType,
typeWithThis: Type,
member: ClassElement | ParameterPropertyDeclaration,
memberIsParameterProperty: boolean,
reportErrors = true,
): MemberOverrideStatus {
const declaredProp = member.name
&& getSymbolAtLocation(member.name)
|| getSymbolAtLocation(member);
if (!declaredProp) {
return MemberOverrideStatus.Ok;
}

return checkMemberForOverrideModifier(
node,
staticType,
baseStaticType,
baseWithThis,
type,
typeWithThis,
hasOverrideModifier(member),
hasAbstractModifier(member),
isStatic(member),
memberIsParameterProperty,
symbolName(declaredProp),
reportErrors ? member : undefined,
);
}

const baseClassName = typeToString(baseWithThis);
if (prop && !baseProp && hasOverride) {
const suggestion = getSuggestedSymbolForNonexistentClassMember(symbolName(declaredProp), baseType);
/**
* Checks a class member declaration for either a missing or an invalid `override` modifier.
* Note: this function can be used for speculative checking,
* i.e. checking a member that does not yet exist in the program.
* An example of that would be to call this function in a completions scenario,
* when offering a method declaration as completion.
* @param errorNode The node where we should report an error, or undefined if we should not report errors.
*/
function checkMemberForOverrideModifier(
node: ClassLikeDeclaration,
staticType: ObjectType,
baseStaticType: Type,
baseWithThis: Type | undefined,
type: InterfaceType,
typeWithThis: Type,
memberHasOverrideModifier: boolean,
memberHasAbstractModifier: boolean,
memberIsStatic: boolean,
memberIsParameterProperty: boolean,
memberName: string,
errorNode?: Node,
): MemberOverrideStatus {
const isJs = isInJSFile(node);
const nodeInAmbientContext = !!(node.flags & NodeFlags.Ambient);
if (baseWithThis && (memberHasOverrideModifier || compilerOptions.noImplicitOverride)) {
const memberEscapedName = escapeLeadingUnderscores(memberName);
const thisType = memberIsStatic ? staticType : typeWithThis;
const baseType = memberIsStatic ? baseStaticType : baseWithThis;
const prop = getPropertyOfType(thisType, memberEscapedName);
const baseProp = getPropertyOfType(baseType, memberEscapedName);

const baseClassName = typeToString(baseWithThis);
if (prop && !baseProp && memberHasOverrideModifier) {
if (errorNode) {
const suggestion = getSuggestedSymbolForNonexistentClassMember(memberName, baseType); // Again, using symbol name: note that's different from `symbol.escapedName`
suggestion ?
error(
member,
errorNode,
isJs ?
Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_it_is_not_declared_in_the_base_class_0_Did_you_mean_1 :
Diagnostics.This_member_cannot_have_an_override_modifier_because_it_is_not_declared_in_the_base_class_0_Did_you_mean_1,
baseClassName,
symbolToString(suggestion)) :
error(
member,
errorNode,
isJs ?
Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_it_is_not_declared_in_the_base_class_0 :
Diagnostics.This_member_cannot_have_an_override_modifier_because_it_is_not_declared_in_the_base_class_0,
baseClassName);
}
else if (prop && baseProp?.declarations && compilerOptions.noImplicitOverride && !nodeInAmbientContext) {
const baseHasAbstract = some(baseProp.declarations, hasAbstractModifier);
if (hasOverride) {
return;
}
return MemberOverrideStatus.HasInvalidOverride;
}
else if (prop && baseProp?.declarations && compilerOptions.noImplicitOverride && !nodeInAmbientContext) {
const baseHasAbstract = some(baseProp.declarations, hasAbstractModifier);
if (memberHasOverrideModifier) {
return MemberOverrideStatus.Ok;
}

if (!baseHasAbstract) {
if (!baseHasAbstract) {
if (errorNode) {
const diag = memberIsParameterProperty ?
isJs ?
Diagnostics.This_parameter_property_must_have_a_JSDoc_comment_with_an_override_tag_because_it_overrides_a_member_in_the_base_class_0 :
Diagnostics.This_parameter_property_must_have_an_override_modifier_because_it_overrides_a_member_in_base_class_0 :
isJs ?
Diagnostics.This_member_must_have_a_JSDoc_comment_with_an_override_tag_because_it_overrides_a_member_in_the_base_class_0 :
Diagnostics.This_member_must_have_an_override_modifier_because_it_overrides_a_member_in_the_base_class_0;
error(member, diag, baseClassName);
error(errorNode, diag, baseClassName);
}
else if (hasAbstractModifier(member) && baseHasAbstract) {
error(member, Diagnostics.This_member_must_have_an_override_modifier_because_it_overrides_an_abstract_method_that_is_declared_in_the_base_class_0, baseClassName);
return MemberOverrideStatus.NeedsOverride;
}
else if (memberHasAbstractModifier && baseHasAbstract) {
if (errorNode) {
error(errorNode, Diagnostics.This_member_must_have_an_override_modifier_because_it_overrides_an_abstract_method_that_is_declared_in_the_base_class_0, baseClassName);
}
return MemberOverrideStatus.NeedsOverride;
}
}
else if (hasOverride) {
}
else if (memberHasOverrideModifier) {
if (errorNode) {
const className = typeToString(type);
error(
member,
errorNode,
isJs ?
Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_its_containing_class_0_does_not_extend_another_class :
Diagnostics.This_member_cannot_have_an_override_modifier_because_its_containing_class_0_does_not_extend_another_class,
className);
}
return MemberOverrideStatus.HasInvalidOverride;
}

return MemberOverrideStatus.Ok;
}

function issueMemberSpecificError(node: ClassLikeDeclaration, typeWithThis: Type, baseWithThis: Type, broadDiag: DiagnosticMessage) {
Expand Down Expand Up @@ -38638,6 +38724,48 @@ namespace ts {
}
}

/**
* Checks a member declaration node to see if has a missing or invalid `override` modifier.
* @param node Class-like node where the member is declared.
* @param member Member declaration node.
* Note: `member` can be a synthetic node without a parent.
*/
function getMemberOverrideModifierStatus(node: ClassLikeDeclaration, member: ClassElement): MemberOverrideStatus {
if (!member.name) {
return MemberOverrideStatus.Ok;
}

const symbol = getSymbolOfNode(node);
const type = getDeclaredTypeOfSymbol(symbol) as InterfaceType;
const typeWithThis = getTypeWithThisArgument(type);
const staticType = getTypeOfSymbol(symbol) as ObjectType;

const baseTypeNode = getEffectiveBaseTypeNode(node);
const baseTypes = baseTypeNode && getBaseTypes(type);
const baseWithThis = baseTypes?.length ? getTypeWithThisArgument(first(baseTypes), type.thisType) : undefined;
const baseStaticType = getBaseConstructorTypeOfClass(type);

const memberHasOverrideModifier = member.parent
? hasOverrideModifier(member)
: hasSyntacticModifier(member, ModifierFlags.Override);

const memberName = unescapeLeadingUnderscores(getTextOfPropertyName(member.name));

return checkMemberForOverrideModifier(
node,
staticType,
baseStaticType,
baseWithThis,
type,
typeWithThis,
memberHasOverrideModifier,
hasAbstractModifier(member),
isStatic(member),
/* memberIsParameterProperty */ false,
memberName,
);
}

function getTargetSymbol(s: Symbol) {
// if symbol is instantiated its flags are not copied from the 'target'
// so we'll need to get back original 'target' symbol to work with correct set of flags
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,10 @@ namespace ts {
return formatEnum(kind, (ts as any).SyntaxKind, /*isFlags*/ false);
}

export function formatSnippetKind(kind: SnippetKind | undefined): string {
return formatEnum(kind, (ts as any).SnippetKind, /*isFlags*/ false);
}

export function formatNodeFlags(flags: NodeFlags | undefined): string {
return formatEnum(flags, (ts as any).NodeFlags, /*isFlags*/ true);
}
Expand Down
44 changes: 43 additions & 1 deletion src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1283,7 +1283,13 @@ namespace ts {
currentParenthesizerRule = undefined;
}

function pipelineEmitWithHintWorker(hint: EmitHint, node: Node): void {
function pipelineEmitWithHintWorker(hint: EmitHint, node: Node, allowSnippets = true): void {
if (allowSnippets) {
const snippet = getSnippetElement(node);
if (snippet) {
return emitSnippetNode(hint, node, snippet);
}
}
if (hint === EmitHint.SourceFile) return emitSourceFile(cast(node, isSourceFile));
if (hint === EmitHint.IdentifierName) return emitIdentifier(cast(node, isIdentifier));
if (hint === EmitHint.JsxAttributeValue) return emitLiteral(cast(node, isStringLiteral), /*jsxAttributeEscape*/ true);
Expand Down Expand Up @@ -1924,6 +1930,32 @@ namespace ts {
}
}

//
// Snippet Elements
//

function emitSnippetNode(hint: EmitHint, node: Node, snippet: SnippetElement) {
switch (snippet.kind) {
case SnippetKind.Placeholder:
emitPlaceholder(hint, node, snippet);
break;
case SnippetKind.TabStop:
emitTabStop(snippet);
break;
}
}

function emitPlaceholder(hint: EmitHint, node: Node, snippet: Placeholder) {
nonEscapingWrite(`\$\{${snippet.order}:`); // `${2:`
pipelineEmitWithHintWorker(hint, node, /*allowSnippets*/ false); // `...`
nonEscapingWrite(`\}`); // `}`
// `${2:...}`
}

function emitTabStop(snippet: TabStop) {
nonEscapingWrite(`\$${snippet.order}`);
}

//
// Identifiers
//
Expand Down Expand Up @@ -4457,6 +4489,16 @@ namespace ts {
writer.writeProperty(s);
}

function nonEscapingWrite(s: string) {
// This should be defined in a snippet-escaping text writer.
if (writer.nonEscapingWrite) {
writer.nonEscapingWrite(s);
}
else {
writer.write(s);
}
}

function writeLine(count = 1) {
for (let i = 0; i < count; i++) {
writer.writeLine(i > 0);
Expand Down
18 changes: 18 additions & 0 deletions src/compiler/factory/emitNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,24 @@ namespace ts {
}
}

/**
* Gets the SnippetElement of a node.
*/
/* @internal */
export function getSnippetElement(node: Node): SnippetElement | undefined {
return node.emitNode?.snippetElement;
}

/**
* Sets the SnippetElement of a node.
*/
/* @internal */
export function setSnippetElement<T extends Node>(node: T, snippet: SnippetElement): T {
const emitNode = getOrCreateEmitNode(node);
emitNode.snippetElement = snippet;
return node;
}

/* @internal */
export function ignoreSourceNewlines<T extends Node>(node: T): T {
getOrCreateEmitNode(node).flags |= EmitFlags.IgnoreSourceNewlines;
Expand Down
Loading

0 comments on commit 5f7406e

Please sign in to comment.