Support naming tuple members (#38234)

* Initial draft of named tuple members

* Show tuple labels and documentation in completions

* Swap allowed syntax to parameter-like

* Add quickfix for labeled tuple syntax mistakes

* Add refactoring to convert list of signatures to single overload

* Fix small bug in visitor verification

* Signature help for rest parameters which are unions of tuples are displayed as seperate entries now

* Expand sanity check test cases in conformance suite

* Add tests and code for preserving tuple names through spreads where possible

* More refactoring tests, some comment preservation and some fixed formatting of multiline tuples

* Handle missing parameter named in isValidDeclarationForTupleLabel

* Minor text fixes
This commit is contained in:
Wesley Wigham
2020-05-19 15:54:02 -07:00
committed by GitHub
parent 24f53b06d2
commit 5f597e69b2
62 changed files with 2175 additions and 488 deletions
@@ -0,0 +1,52 @@
/* @internal */
namespace ts.codefix {
const fixId = "fixIncorrectNamedTupleSyntax";
const errorCodes = [
Diagnostics.A_labeled_tuple_element_is_declared_as_optional_with_a_question_mark_after_the_name_and_before_the_colon_rather_than_after_the_type.code,
Diagnostics.A_labeled_tuple_element_is_declared_as_rest_with_a_before_the_name_rather_than_before_the_type.code
];
registerCodeFix({
errorCodes,
getCodeActions: context => {
const { sourceFile, span } = context;
const namedTupleMember = getNamedTupleMember(sourceFile, span.start);
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, namedTupleMember));
return [createCodeFixAction(fixId, changes, Diagnostics.Move_labeled_tuple_element_modifiers_to_labels, fixId, Diagnostics.Move_labeled_tuple_element_modifiers_to_labels)];
},
fixIds: [fixId]
});
function getNamedTupleMember(sourceFile: SourceFile, pos: number) {
const token = getTokenAtPosition(sourceFile, pos);
return findAncestor(token, t => t.kind === SyntaxKind.NamedTupleMember) as NamedTupleMember | undefined;
}
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, namedTupleMember?: NamedTupleMember) {
if (!namedTupleMember) {
return;
}
let unwrappedType = namedTupleMember.type;
let sawOptional = false;
let sawRest = false;
while (unwrappedType.kind === SyntaxKind.OptionalType || unwrappedType.kind === SyntaxKind.RestType || unwrappedType.kind === SyntaxKind.ParenthesizedType) {
if (unwrappedType.kind === SyntaxKind.OptionalType) {
sawOptional = true;
}
else if (unwrappedType.kind === SyntaxKind.RestType) {
sawRest = true;
}
unwrappedType = (unwrappedType as OptionalTypeNode | RestTypeNode | ParenthesizedTypeNode).type;
}
const updated = updateNamedTupleMember(
namedTupleMember,
namedTupleMember.dotDotDotToken || (sawRest ? createToken(SyntaxKind.DotDotDotToken) : undefined),
namedTupleMember.name,
namedTupleMember.questionToken || (sawOptional ? createToken(SyntaxKind.QuestionToken) : undefined),
unwrappedType
);
if (updated === namedTupleMember) {
return;
}
changes.replaceNode(sourceFile, namedTupleMember, updated);
}
}
+1 -1
View File
@@ -574,7 +574,7 @@ namespace ts.formatting {
return childKind !== SyntaxKind.JsxClosingFragment;
case SyntaxKind.IntersectionType:
case SyntaxKind.UnionType:
if (childKind === SyntaxKind.TypeLiteral) {
if (childKind === SyntaxKind.TypeLiteral || childKind === SyntaxKind.TupleType) {
return false;
}
// falls through
@@ -0,0 +1,219 @@
/* @internal */
namespace ts.refactor.addOrRemoveBracesToArrowFunction {
const refactorName = "Convert overload list to single signature";
const refactorDescription = Diagnostics.Convert_overload_list_to_single_signature.message;
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
const { file, startPosition, program } = context;
const info = getConvertableOverloadListAtPosition(file, startPosition, program);
if (!info) return emptyArray;
return [{
name: refactorName,
description: refactorDescription,
actions: [{
name: refactorName,
description: refactorDescription
}]
}];
}
function getEditsForAction(context: RefactorContext): RefactorEditInfo | undefined {
const { file, startPosition, program } = context;
const signatureDecls = getConvertableOverloadListAtPosition(file, startPosition, program);
if (!signatureDecls) return undefined;
const checker = program.getTypeChecker();
const lastDeclaration = signatureDecls[signatureDecls.length - 1];
let updated = lastDeclaration;
switch (lastDeclaration.kind) {
case SyntaxKind.MethodSignature: {
updated = updateMethodSignature(
lastDeclaration,
lastDeclaration.typeParameters,
getNewParametersForCombinedSignature(signatureDecls),
lastDeclaration.type,
lastDeclaration.name,
lastDeclaration.questionToken
);
break;
}
case SyntaxKind.MethodDeclaration: {
updated = updateMethod(
lastDeclaration,
lastDeclaration.decorators,
lastDeclaration.modifiers,
lastDeclaration.asteriskToken,
lastDeclaration.name,
lastDeclaration.questionToken,
lastDeclaration.typeParameters,
getNewParametersForCombinedSignature(signatureDecls),
lastDeclaration.type,
lastDeclaration.body
);
break;
}
case SyntaxKind.CallSignature: {
updated = updateCallSignature(
lastDeclaration,
lastDeclaration.typeParameters,
getNewParametersForCombinedSignature(signatureDecls),
lastDeclaration.type,
);
break;
}
case SyntaxKind.Constructor: {
updated = updateConstructor(
lastDeclaration,
lastDeclaration.decorators,
lastDeclaration.modifiers,
getNewParametersForCombinedSignature(signatureDecls),
lastDeclaration.body
);
break;
}
case SyntaxKind.ConstructSignature: {
updated = updateConstructSignature(
lastDeclaration,
lastDeclaration.typeParameters,
getNewParametersForCombinedSignature(signatureDecls),
lastDeclaration.type,
);
break;
}
case SyntaxKind.FunctionDeclaration: {
updated = updateFunctionDeclaration(
lastDeclaration,
lastDeclaration.decorators,
lastDeclaration.modifiers,
lastDeclaration.asteriskToken,
lastDeclaration.name,
lastDeclaration.typeParameters,
getNewParametersForCombinedSignature(signatureDecls),
lastDeclaration.type,
lastDeclaration.body
);
break;
}
default: return Debug.failBadSyntaxKind(lastDeclaration, "Unhandled signature kind in overload list conversion refactoring");
}
if (updated === lastDeclaration) {
return; // No edits to apply, do nothing
}
const edits = textChanges.ChangeTracker.with(context, t => {
t.replaceNodeRange(file, signatureDecls[0], signatureDecls[signatureDecls.length - 1], updated);
});
return { renameFilename: undefined, renameLocation: undefined, edits };
function getNewParametersForCombinedSignature(signatureDeclarations: (MethodSignature | MethodDeclaration | CallSignatureDeclaration | ConstructorDeclaration | ConstructSignatureDeclaration | FunctionDeclaration)[]): NodeArray<ParameterDeclaration> {
const lastSig = signatureDeclarations[signatureDeclarations.length - 1];
if (isFunctionLikeDeclaration(lastSig) && lastSig.body) {
// Trim away implementation signature arguments (they should already be compatible with overloads, but are likely less precise to guarantee compatability with the overloads)
signatureDeclarations = signatureDeclarations.slice(0, signatureDeclarations.length - 1);
}
return createNodeArray([
createParameter(
/*decorators*/ undefined,
/*modifiers*/ undefined,
createToken(SyntaxKind.DotDotDotToken),
"args",
/*questionToken*/ undefined,
createUnionTypeNode(map(signatureDeclarations, convertSignatureParametersToTuple))
)
]);
}
function convertSignatureParametersToTuple(decl: MethodSignature | MethodDeclaration | CallSignatureDeclaration | ConstructorDeclaration | ConstructSignatureDeclaration | FunctionDeclaration): TupleTypeNode {
const members = map(decl.parameters, convertParameterToNamedTupleMember);
return setEmitFlags(createTupleTypeNode(members), some(members, m => !!length(getSyntheticLeadingComments(m))) ? EmitFlags.None : EmitFlags.SingleLine);
}
function convertParameterToNamedTupleMember(p: ParameterDeclaration): NamedTupleMember {
Debug.assert(isIdentifier(p.name)); // This is checked during refactoring applicability checking
const result = setTextRange(createNamedTupleMember(
p.dotDotDotToken,
p.name,
p.questionToken,
p.type || createKeywordTypeNode(SyntaxKind.AnyKeyword)
), p);
const parameterDocComment = p.symbol && p.symbol.getDocumentationComment(checker);
if (parameterDocComment) {
const newComment = displayPartsToString(parameterDocComment);
if (newComment.length) {
setSyntheticLeadingComments(result, [{
text: `*
${newComment.split("\n").map(c => ` * ${c}`).join("\n")}
`,
kind: SyntaxKind.MultiLineCommentTrivia,
pos: -1,
end: -1,
hasTrailingNewLine: true,
hasLeadingNewline: true,
}]);
}
}
return result;
}
}
function isConvertableSignatureDeclaration(d: Node): d is MethodSignature | MethodDeclaration | CallSignatureDeclaration | ConstructorDeclaration | ConstructSignatureDeclaration | FunctionDeclaration {
switch (d.kind) {
case SyntaxKind.MethodSignature:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.CallSignature:
case SyntaxKind.Constructor:
case SyntaxKind.ConstructSignature:
case SyntaxKind.FunctionDeclaration:
return true;
}
return false;
}
function getConvertableOverloadListAtPosition(file: SourceFile, startPosition: number, program: Program) {
const node = getTokenAtPosition(file, startPosition);
const containingDecl = findAncestor(node, isConvertableSignatureDeclaration);
if (!containingDecl) {
return;
}
const checker = program.getTypeChecker();
const signatureSymbol = containingDecl.symbol;
if (!signatureSymbol) {
return;
}
const decls = signatureSymbol.declarations;
if (length(decls) <= 1) {
return;
}
if (!every(decls, d => getSourceFileOfNode(d) === file)) {
return;
}
if (!isConvertableSignatureDeclaration(decls[0])) {
return;
}
const kindOne = decls[0].kind;
if (!every(decls, d => d.kind === kindOne)) {
return;
}
const signatureDecls = decls as (MethodSignature | MethodDeclaration | CallSignatureDeclaration | ConstructorDeclaration | ConstructSignatureDeclaration | FunctionDeclaration)[];
if (some(signatureDecls, d => !!d.typeParameters || some(d.parameters, p => !!p.decorators || !!p.modifiers || !isIdentifier(p.name)))) {
return;
}
const signatures = mapDefined(signatureDecls, d => checker.getSignatureFromDeclaration(d));
if (length(signatures) !== length(decls)) {
return;
}
const returnOne = checker.getReturnTypeOfSignature(signatures[0]);
if (!every(signatures, s => checker.getReturnTypeOfSignature(s) === returnOne)) {
return;
}
return signatureDecls;
}
}
+5
View File
@@ -145,6 +145,11 @@ namespace ts.refactor {
}
}
}
if (file && isTupleTypeNode(node) && (getLineAndCharacterOfPosition(file, node.pos).line === getLineAndCharacterOfPosition(file, node.end).line)) {
setEmitFlags(node, EmitFlags.SingleLine);
}
return forEachChild(node, visitor);
}
}
+8 -1
View File
@@ -328,7 +328,14 @@ namespace ts {
getDocumentationComment(checker: TypeChecker | undefined): SymbolDisplayPart[] {
if (!this.documentationComment) {
this.documentationComment = emptyArray; // Set temporarily to avoid an infinite loop finding inherited docs
this.documentationComment = getDocumentationComment(this.declarations, checker);
if (!this.declarations && (this as Symbol as TransientSymbol).target && ((this as Symbol as TransientSymbol).target as TransientSymbol).tupleLabelDeclaration) {
const labelDecl = ((this as Symbol as TransientSymbol).target as TransientSymbol).tupleLabelDeclaration!;
this.documentationComment = getDocumentationComment([labelDecl], checker);
}
else {
this.documentationComment = getDocumentationComment(this.declarations, checker);
}
}
return this.documentationComment;
}
+52 -19
View File
@@ -500,16 +500,37 @@ namespace ts.SignatureHelp {
const enclosingDeclaration = getEnclosingDeclarationFromInvocation(invocation);
const callTargetSymbol = invocation.kind === InvocationKind.Contextual ? invocation.symbol : typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation));
const callTargetDisplayParts = callTargetSymbol ? symbolToDisplayParts(typeChecker, callTargetSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined) : emptyArray;
const items = candidates.map(candidateSignature => getSignatureHelpItem(candidateSignature, callTargetDisplayParts, isTypeParameterList, typeChecker, enclosingDeclaration, sourceFile));
const items = map(candidates, candidateSignature => getSignatureHelpItem(candidateSignature, callTargetDisplayParts, isTypeParameterList, typeChecker, enclosingDeclaration, sourceFile));
if (argumentIndex !== 0) {
Debug.assertLessThan(argumentIndex, argumentCount);
}
const selectedItemIndex = candidates.indexOf(resolvedSignature);
let selectedItemIndex = 0;
let itemsSeen = 0;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (candidates[i] === resolvedSignature) {
selectedItemIndex = itemsSeen;
if (item.length > 1) {
// check to see if any items in the list better match than the first one, as the checker isn't filtering the nested lists
// (those come from tuple parameter expansion)
let count = 0;
for (const i of item) {
if (i.isVariadic || i.parameters.length >= argumentCount) {
selectedItemIndex = itemsSeen + count;
break;
}
count++;
}
}
}
itemsSeen += item.length;
}
Debug.assert(selectedItemIndex !== -1); // If candidates is non-empty it should always include bestSignature. We check for an empty candidates before calling this function.
return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount };
return { items: flatMapToMutable(items, identity), applicableSpan, selectedItemIndex, argumentIndex, argumentCount };
}
function createTypeHelpItems(
@@ -538,13 +559,15 @@ namespace ts.SignatureHelp {
const separatorDisplayParts: SymbolDisplayPart[] = [punctuationPart(SyntaxKind.CommaToken), spacePart()];
function getSignatureHelpItem(candidateSignature: Signature, callTargetDisplayParts: readonly SymbolDisplayPart[], isTypeParameterList: boolean, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem {
const { isVariadic, parameters, prefix, suffix } = (isTypeParameterList ? itemInfoForTypeParameters : itemInfoForParameters)(candidateSignature, checker, enclosingDeclaration, sourceFile);
const prefixDisplayParts = [...callTargetDisplayParts, ...prefix];
const suffixDisplayParts = [...suffix, ...returnTypeToDisplayParts(candidateSignature, enclosingDeclaration, checker)];
const documentation = candidateSignature.getDocumentationComment(checker);
const tags = candidateSignature.getJsDocTags();
return { isVariadic, prefixDisplayParts, suffixDisplayParts, separatorDisplayParts, parameters, documentation, tags };
function getSignatureHelpItem(candidateSignature: Signature, callTargetDisplayParts: readonly SymbolDisplayPart[], isTypeParameterList: boolean, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem[] {
const infos = (isTypeParameterList ? itemInfoForTypeParameters : itemInfoForParameters)(candidateSignature, checker, enclosingDeclaration, sourceFile);
return map(infos, ({ isVariadic, parameters, prefix, suffix }) => {
const prefixDisplayParts = [...callTargetDisplayParts, ...prefix];
const suffixDisplayParts = [...suffix, ...returnTypeToDisplayParts(candidateSignature, enclosingDeclaration, checker)];
const documentation = candidateSignature.getDocumentationComment(checker);
const tags = candidateSignature.getJsDocTags();
return { isVariadic, prefixDisplayParts, suffixDisplayParts, separatorDisplayParts, parameters, documentation, tags };
});
}
function returnTypeToDisplayParts(candidateSignature: Signature, enclosingDeclaration: Node, checker: TypeChecker): readonly SymbolDisplayPart[] {
@@ -563,19 +586,22 @@ namespace ts.SignatureHelp {
interface SignatureHelpItemInfo { readonly isVariadic: boolean; readonly parameters: SignatureHelpParameter[]; readonly prefix: readonly SymbolDisplayPart[]; readonly suffix: readonly SymbolDisplayPart[]; }
function itemInfoForTypeParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo {
function itemInfoForTypeParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo[] {
const typeParameters = (candidateSignature.target || candidateSignature).typeParameters;
const printer = createPrinter({ removeComments: true });
const parameters = (typeParameters || emptyArray).map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer));
const parameterParts = mapToDisplayParts(writer => {
const thisParameter = candidateSignature.thisParameter ? [checker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!] : [];
const params = createNodeArray([...thisParameter, ...checker.getExpandedParameters(candidateSignature).map(param => checker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)]);
printer.writeList(ListFormat.CallExpressionArguments, params, sourceFile, writer);
const thisParameter = candidateSignature.thisParameter ? [checker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!] : [];
return checker.getExpandedParameters(candidateSignature).map(paramList => {
const params = createNodeArray([...thisParameter, ...map(paramList, param => checker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)]);
const parameterParts = mapToDisplayParts(writer => {
printer.writeList(ListFormat.CallExpressionArguments, params, sourceFile, writer);
});
return { isVariadic: false, parameters, prefix: [punctuationPart(SyntaxKind.LessThanToken)], suffix: [punctuationPart(SyntaxKind.GreaterThanToken), ...parameterParts] };
});
return { isVariadic: false, parameters, prefix: [punctuationPart(SyntaxKind.LessThanToken)], suffix: [punctuationPart(SyntaxKind.GreaterThanToken), ...parameterParts] };
}
function itemInfoForParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo {
function itemInfoForParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo[] {
const isVariadic = checker.hasEffectiveRestParameter(candidateSignature);
const printer = createPrinter({ removeComments: true });
const typeParameterParts = mapToDisplayParts(writer => {
@@ -584,8 +610,15 @@ namespace ts.SignatureHelp {
printer.writeList(ListFormat.TypeParameters, args, sourceFile, writer);
}
});
const parameters = checker.getExpandedParameters(candidateSignature).map(p => createSignatureHelpParameterForParameter(p, checker, enclosingDeclaration, sourceFile, printer));
return { isVariadic, parameters, prefix: [...typeParameterParts, punctuationPart(SyntaxKind.OpenParenToken)], suffix: [punctuationPart(SyntaxKind.CloseParenToken)] };
const lists = checker.getExpandedParameters(candidateSignature);
return lists.map(parameterList => {
return {
isVariadic: isVariadic && (lists.length === 1 || !!((parameterList[parameterList.length - 1] as TransientSymbol).checkFlags & CheckFlags.RestParameter)),
parameters: parameterList.map(p => createSignatureHelpParameterForParameter(p, checker, enclosingDeclaration, sourceFile, printer)),
prefix: [...typeParameterParts, punctuationPart(SyntaxKind.OpenParenToken)],
suffix: [punctuationPart(SyntaxKind.CloseParenToken)]
};
});
}
function createSignatureHelpParameterForParameter(parameter: Symbol, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter {
+8
View File
@@ -481,6 +481,14 @@ namespace ts.SymbolDisplay {
else {
addRange(displayParts, typeToDisplayParts(typeChecker, type, enclosingDeclaration));
}
if ((symbol as TransientSymbol).target && ((symbol as TransientSymbol).target as TransientSymbol).tupleLabelDeclaration) {
const labelDecl = ((symbol as TransientSymbol).target as TransientSymbol).tupleLabelDeclaration!;
Debug.assertNode(labelDecl.name, isIdentifier);
displayParts.push(spacePart());
displayParts.push(punctuationPart(SyntaxKind.OpenParenToken));
displayParts.push(textPart(idText(labelDecl.name)));
displayParts.push(punctuationPart(SyntaxKind.CloseParenToken));
}
}
else if (symbolFlags & SymbolFlags.Function ||
symbolFlags & SymbolFlags.Method ||
+2
View File
@@ -64,6 +64,7 @@
"codefixes/fixClassIncorrectlyImplementsInterface.ts",
"codefixes/importFixes.ts",
"codefixes/fixImplicitThis.ts",
"codefixes/fixIncorrectNamedTupleSyntax.ts",
"codefixes/fixSpelling.ts",
"codefixes/returnValueCorrect.ts",
"codefixes/fixAddMissingMember.ts",
@@ -101,6 +102,7 @@
"codefixes/fixExpectedComma.ts",
"refactors/convertExport.ts",
"refactors/convertImport.ts",
"refactors/convertOverloadListToSingleSignature.ts",
"refactors/extractSymbol.ts",
"refactors/extractType.ts",
"refactors/generateGetAccessorAndSetAccessor.ts",