diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1cd7e00d0dd6c..ec9a5fe31499d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -5239,7 +5239,18 @@ namespace ts { if (!nodeIsSynthesized(node) && getParseTreeNode(node) === node) { return node; } - return setTextRange(factory.cloneNode(visitEachChild(node, deepCloneOrReuseNode, nullTransformationContext)), node); + return setTextRange(factory.cloneNode(visitEachChild(node, deepCloneOrReuseNode, nullTransformationContext, deepCloneOrReuseNodes)), node); + } + + function deepCloneOrReuseNodes(nodes: NodeArray, visitor: Visitor | undefined, test?: (node: Node) => boolean, start?: number, count?: number): NodeArray; + function deepCloneOrReuseNodes(nodes: NodeArray | undefined, visitor: Visitor | undefined, test?: (node: Node) => boolean, start?: number, count?: number): NodeArray | undefined; + function deepCloneOrReuseNodes(nodes: NodeArray | undefined, visitor: Visitor | undefined, test?: (node: Node) => boolean, start?: number, count?: number): NodeArray | undefined { + if (nodes && nodes.length === 0) { + // Ensure we explicitly make a copy of an empty array; visitNodes will not do this unless the array has elements, + // which can lead to us reusing the same empty NodeArray more than once within the same AST during type noding. + return setTextRange(factory.createNodeArray(/*nodes*/ undefined, nodes.hasTrailingComma), nodes); + } + return visitNodes(nodes, visitor, test, start, count); } } diff --git a/tests/cases/fourslash/completionsOverridingMethodCrash1.ts b/tests/cases/fourslash/completionsOverridingMethodCrash1.ts new file mode 100644 index 0000000000000..49233409b485a --- /dev/null +++ b/tests/cases/fourslash/completionsOverridingMethodCrash1.ts @@ -0,0 +1,28 @@ +/// + +// @newline: LF +// @Filename: a.ts +////declare class Component { +//// setState(stateHandler: ((oldState: T, newState: T) => void)): void; +////} +//// +////class SubComponent extends Component<{}> { +//// /*$*/ +////} + +verify.completions({ + marker: "$", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "setState", + sortText: completion.SortText.ClassMemberSnippets, + insertText: "setState(stateHandler: (oldState: {}, newState: {}) => void): void {\n}", + } + ] +});