Skip to content

Commit 5f597e6

Browse files
authored
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
1 parent 24f53b0 commit 5f597e6

File tree

62 files changed

+2176
-489
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2176
-489
lines changed

src/compiler/checker.ts

+190-64
Large diffs are not rendered by default.

src/compiler/diagnosticMessages.json

+24
Original file line numberDiff line numberDiff line change
@@ -3517,6 +3517,22 @@
35173517
"category": "Error",
35183518
"code": 5083
35193519
},
3520+
"Tuple members must all have names or all not have names.": {
3521+
"category": "Error",
3522+
"code": 5084
3523+
},
3524+
"A tuple member cannot be both optional and rest.": {
3525+
"category": "Error",
3526+
"code": 5085
3527+
},
3528+
"A labeled tuple element is declared as optional with a question mark after the name and before the colon, rather than after the type.": {
3529+
"category": "Error",
3530+
"code": 5086
3531+
},
3532+
"A labeled tuple element is declared as rest with a `...` before the name, rather than before the type.": {
3533+
"category": "Error",
3534+
"code": 5087
3535+
},
35203536

35213537
"Generates a sourcemap for each corresponding '.d.ts' file.": {
35223538
"category": "Message",
@@ -5677,6 +5693,14 @@
56775693
"category": "Message",
56785694
"code": 95116
56795695
},
5696+
"Move labeled tuple element modifiers to labels": {
5697+
"category": "Message",
5698+
"code": 95117
5699+
},
5700+
"Convert overload list to single signature": {
5701+
"category": "Message",
5702+
"code": 95118
5703+
},
56805704

56815705
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
56825706
"category": "Error",

src/compiler/emitter.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,8 @@ namespace ts {
13701370
case SyntaxKind.RestType:
13711371
case SyntaxKind.JSDocVariadicType:
13721372
return emitRestOrJSDocVariadicType(node as RestTypeNode | JSDocVariadicType);
1373+
case SyntaxKind.NamedTupleMember:
1374+
return emitNamedTupleMember(node as NamedTupleMember);
13731375

13741376
// Binding patterns
13751377
case SyntaxKind.ObjectBindingPattern:
@@ -2099,9 +2101,19 @@ namespace ts {
20992101
}
21002102

21012103
function emitTupleType(node: TupleTypeNode) {
2102-
writePunctuation("[");
2103-
emitList(node, node.elementTypes, ListFormat.TupleTypeElements);
2104-
writePunctuation("]");
2104+
emitTokenWithComment(SyntaxKind.OpenBracketToken, node.pos, writePunctuation, node);
2105+
const flags = getEmitFlags(node) & EmitFlags.SingleLine ? ListFormat.SingleLineTupleTypeElements : ListFormat.MultiLineTupleTypeElements;
2106+
emitList(node, node.elements, flags | ListFormat.NoSpaceIfEmpty);
2107+
emitTokenWithComment(SyntaxKind.CloseBracketToken, node.elements.end, writePunctuation, node);
2108+
}
2109+
2110+
function emitNamedTupleMember(node: NamedTupleMember) {
2111+
emit(node.dotDotDotToken);
2112+
emit(node.name);
2113+
emit(node.questionToken);
2114+
emitTokenWithComment(SyntaxKind.ColonToken, node.name.end, writePunctuation, node);
2115+
writeSpace();
2116+
emit(node.type);
21052117
}
21062118

21072119
function emitOptionalType(node: OptionalTypeNode) {
@@ -4968,7 +4980,7 @@ namespace ts {
49684980
}
49694981

49704982
function emitLeadingSynthesizedComment(comment: SynthesizedComment) {
4971-
if (comment.kind === SyntaxKind.SingleLineCommentTrivia) {
4983+
if (comment.hasLeadingNewline || comment.kind === SyntaxKind.SingleLineCommentTrivia) {
49724984
writer.writeLine();
49734985
}
49744986
writeSynthesizedComment(comment);

src/compiler/factoryPublic.ts

+38-5
Original file line numberDiff line numberDiff line change
@@ -810,15 +810,15 @@ namespace ts {
810810
: node;
811811
}
812812

813-
export function createTupleTypeNode(elementTypes: readonly TypeNode[]) {
813+
export function createTupleTypeNode(elements: readonly (TypeNode | NamedTupleMember)[]) {
814814
const node = createSynthesizedNode(SyntaxKind.TupleType) as TupleTypeNode;
815-
node.elementTypes = createNodeArray(elementTypes);
815+
node.elements = createNodeArray(elements);
816816
return node;
817817
}
818818

819-
export function updateTupleTypeNode(node: TupleTypeNode, elementTypes: readonly TypeNode[]) {
820-
return node.elementTypes !== elementTypes
821-
? updateNode(createTupleTypeNode(elementTypes), node)
819+
export function updateTupleTypeNode(node: TupleTypeNode, elements: readonly (TypeNode | NamedTupleMember)[]) {
820+
return node.elements !== elements
821+
? updateNode(createTupleTypeNode(elements), node)
822822
: node;
823823
}
824824

@@ -934,6 +934,24 @@ namespace ts {
934934
: node;
935935
}
936936

937+
export function createNamedTupleMember(dotDotDotToken: Token<SyntaxKind.DotDotDotToken> | undefined, name: Identifier, questionToken: Token<SyntaxKind.QuestionToken> | undefined, type: TypeNode) {
938+
const node = <NamedTupleMember>createSynthesizedNode(SyntaxKind.NamedTupleMember);
939+
node.dotDotDotToken = dotDotDotToken;
940+
node.name = name;
941+
node.questionToken = questionToken;
942+
node.type = type;
943+
return node;
944+
}
945+
946+
export function updateNamedTupleMember(node: NamedTupleMember, dotDotDotToken: Token<SyntaxKind.DotDotDotToken> | undefined, name: Identifier, questionToken: Token<SyntaxKind.QuestionToken> | undefined, type: TypeNode) {
947+
return node.dotDotDotToken !== dotDotDotToken
948+
|| node.name !== name
949+
|| node.questionToken !== questionToken
950+
|| node.type !== type
951+
? updateNode(createNamedTupleMember(dotDotDotToken, name, questionToken, type), node)
952+
: node;
953+
}
954+
937955
export function createThisTypeNode() {
938956
return <ThisTypeNode>createSynthesizedNode(SyntaxKind.ThisType);
939957
}
@@ -2616,6 +2634,21 @@ namespace ts {
26162634
return node;
26172635
}
26182636

2637+
2638+
/* @internal */
2639+
export function createJSDocVariadicType(type: TypeNode): JSDocVariadicType {
2640+
const node = createSynthesizedNode(SyntaxKind.JSDocVariadicType) as JSDocVariadicType;
2641+
node.type = type;
2642+
return node;
2643+
}
2644+
2645+
/* @internal */
2646+
export function updateJSDocVariadicType(node: JSDocVariadicType, type: TypeNode): JSDocVariadicType {
2647+
return node.type !== type
2648+
? updateNode(createJSDocVariadicType(type), node)
2649+
: node;
2650+
}
2651+
26192652
// JSX
26202653

26212654
export function createJsxElement(openingElement: JsxOpeningElement, children: readonly JsxChild[], closingElement: JsxClosingElement) {

src/compiler/parser.ts

+31-2
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ namespace ts {
179179
case SyntaxKind.ArrayType:
180180
return visitNode(cbNode, (<ArrayTypeNode>node).elementType);
181181
case SyntaxKind.TupleType:
182-
return visitNodes(cbNode, cbNodes, (<TupleTypeNode>node).elementTypes);
182+
return visitNodes(cbNode, cbNodes, (<TupleTypeNode>node).elements);
183183
case SyntaxKind.UnionType:
184184
case SyntaxKind.IntersectionType:
185185
return visitNodes(cbNode, cbNodes, (<UnionOrIntersectionTypeNode>node).types);
@@ -207,6 +207,11 @@ namespace ts {
207207
visitNode(cbNode, (<MappedTypeNode>node).type);
208208
case SyntaxKind.LiteralType:
209209
return visitNode(cbNode, (<LiteralTypeNode>node).literal);
210+
case SyntaxKind.NamedTupleMember:
211+
return visitNode(cbNode, (<NamedTupleMember>node).dotDotDotToken) ||
212+
visitNode(cbNode, (<NamedTupleMember>node).name) ||
213+
visitNode(cbNode, (<NamedTupleMember>node).questionToken) ||
214+
visitNode(cbNode, (<NamedTupleMember>node).type);
210215
case SyntaxKind.ObjectBindingPattern:
211216
case SyntaxKind.ArrayBindingPattern:
212217
return visitNodes(cbNode, cbNodes, (<BindingPattern>node).elements);
@@ -3056,9 +3061,33 @@ namespace ts {
30563061
return type;
30573062
}
30583063

3064+
function isNextTokenColonOrQuestionColon() {
3065+
return nextToken() === SyntaxKind.ColonToken || (token() === SyntaxKind.QuestionToken && nextToken() === SyntaxKind.ColonToken);
3066+
}
3067+
3068+
function isTupleElementName() {
3069+
if (token() === SyntaxKind.DotDotDotToken) {
3070+
return tokenIsIdentifierOrKeyword(nextToken()) && isNextTokenColonOrQuestionColon();
3071+
}
3072+
return tokenIsIdentifierOrKeyword(token()) && isNextTokenColonOrQuestionColon();
3073+
}
3074+
3075+
function parseTupleElementNameOrTupleElementType() {
3076+
if (lookAhead(isTupleElementName)) {
3077+
const node = <NamedTupleMember>createNode(SyntaxKind.NamedTupleMember);
3078+
node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken);
3079+
node.name = parseIdentifierName();
3080+
node.questionToken = parseOptionalToken(SyntaxKind.QuestionToken);
3081+
parseExpected(SyntaxKind.ColonToken);
3082+
node.type = parseTupleElementType();
3083+
return addJSDocComment(finishNode(node));
3084+
}
3085+
return parseTupleElementType();
3086+
}
3087+
30593088
function parseTupleType(): TupleTypeNode {
30603089
const node = <TupleTypeNode>createNode(SyntaxKind.TupleType);
3061-
node.elementTypes = parseBracketedList(ParsingContext.TupleElementTypes, parseTupleElementType, SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken);
3090+
node.elements = parseBracketedList(ParsingContext.TupleElementTypes, parseTupleElementNameOrTupleElementType, SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken);
30623091
return finishNode(node);
30633092
}
30643093

src/compiler/transformers/declarations.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,10 @@ namespace ts {
10181018
}
10191019
}
10201020

1021+
if (isTupleTypeNode(input) && (getLineAndCharacterOfPosition(currentSourceFile, input.pos).line === getLineAndCharacterOfPosition(currentSourceFile, input.end).line)) {
1022+
setEmitFlags(input, EmitFlags.SingleLine);
1023+
}
1024+
10211025
return cleanup(visitEachChild(input, visitDeclarationSubtree, context));
10221026

10231027
function cleanup<T extends Node>(returnValue: T | undefined): T | undefined {

src/compiler/types.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ namespace ts {
328328
IndexedAccessType,
329329
MappedType,
330330
LiteralType,
331+
NamedTupleMember,
331332
ImportType,
332333
// Binding patterns
333334
ObjectBindingPattern,
@@ -700,6 +701,7 @@ namespace ts {
700701
| ConstructorTypeNode
701702
| JSDocFunctionType
702703
| ExportDeclaration
704+
| NamedTupleMember
703705
| EndOfFileToken;
704706

705707
export type HasType =
@@ -1274,7 +1276,15 @@ namespace ts {
12741276

12751277
export interface TupleTypeNode extends TypeNode {
12761278
kind: SyntaxKind.TupleType;
1277-
elementTypes: NodeArray<TypeNode>;
1279+
elements: NodeArray<TypeNode | NamedTupleMember>;
1280+
}
1281+
1282+
export interface NamedTupleMember extends TypeNode, JSDocContainer, Declaration {
1283+
kind: SyntaxKind.NamedTupleMember;
1284+
dotDotDotToken?: Token<SyntaxKind.DotDotDotToken>;
1285+
name: Identifier;
1286+
questionToken?: Token<SyntaxKind.QuestionToken>;
1287+
type: TypeNode;
12781288
}
12791289

12801290
export interface OptionalTypeNode extends TypeNode {
@@ -1478,6 +1488,7 @@ namespace ts {
14781488
kind: SyntaxKind.SyntheticExpression;
14791489
isSpread: boolean;
14801490
type: Type;
1491+
tupleNameSource?: ParameterDeclaration | NamedTupleMember;
14811492
}
14821493

14831494
// see: https://tc39.github.io/ecma262/#prod-ExponentiationExpression
@@ -2590,6 +2601,7 @@ namespace ts {
25902601
text: string;
25912602
pos: -1;
25922603
end: -1;
2604+
hasLeadingNewline?: boolean;
25932605
}
25942606

25952607
// represents a top level: { type } expression in a JSDoc comment.
@@ -3495,7 +3507,7 @@ namespace ts {
34953507
*/
34963508
getResolvedSignature(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
34973509
/* @internal */ getResolvedSignatureForSignatureHelp(node: CallLikeExpression, candidatesOutArray?: Signature[], argumentCount?: number): Signature | undefined;
3498-
/* @internal */ getExpandedParameters(sig: Signature): readonly Symbol[];
3510+
/* @internal */ getExpandedParameters(sig: Signature): readonly (readonly Symbol[])[];
34993511
/* @internal */ hasEffectiveRestParameter(sig: Signature): boolean;
35003512
getSignatureFromDeclaration(declaration: SignatureDeclaration): Signature | undefined;
35013513
isImplementationOfOverload(node: SignatureDeclaration): boolean | undefined;
@@ -4148,6 +4160,7 @@ namespace ts {
41484160
cjsExportMerged?: Symbol; // Version of the symbol with all non export= exports merged with the export= target
41494161
typeOnlyDeclaration?: TypeOnlyCompatibleAliasDeclaration | false; // First resolved alias declaration that makes the symbol only usable in type constructs
41504162
isConstructorDeclaredProperty?: boolean; // Property declared through 'this.x = ...' assignment in constructor
4163+
tupleLabelDeclaration?: NamedTupleMember | ParameterDeclaration; // Declaration associated with the tuple's label
41514164
}
41524165

41534166
/* @internal */
@@ -4618,7 +4631,7 @@ namespace ts {
46184631
minLength: number;
46194632
hasRestElement: boolean;
46204633
readonly: boolean;
4621-
associatedNames?: __String[];
4634+
labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration)[];
46224635
}
46234636

46244637
export interface TupleTypeReference extends TypeReference {
@@ -6559,7 +6572,8 @@ namespace ts {
65596572
SingleLineTypeLiteralMembers = SingleLine | SpaceBetweenBraces | SpaceBetweenSiblings,
65606573
MultiLineTypeLiteralMembers = MultiLine | Indented | OptionalIfEmpty,
65616574

6562-
TupleTypeElements = CommaDelimited | SpaceBetweenSiblings | SingleLine,
6575+
SingleLineTupleTypeElements = CommaDelimited | SpaceBetweenSiblings | SingleLine,
6576+
MultiLineTupleTypeElements = CommaDelimited | Indented | SpaceBetweenSiblings | MultiLine,
65636577
UnionTypeConstituents = BarDelimited | SpaceBetweenSiblings | SingleLine,
65646578
IntersectionTypeConstituents = AmpersandDelimited | SpaceBetweenSiblings | SingleLine,
65656579
ObjectBindingPatternElements = SingleLine | AllowTrailingComma | SpaceBetweenBraces | CommaDelimited | SpaceBetweenSiblings | NoSpaceIfEmpty,

src/compiler/visitorPublic.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ namespace ts {
480480

481481
case SyntaxKind.TupleType:
482482
return updateTupleTypeNode((<TupleTypeNode>node),
483-
nodesVisitor((<TupleTypeNode>node).elementTypes, visitor, isTypeNode));
483+
nodesVisitor((<TupleTypeNode>node).elements, visitor, isTypeNode));
484484

485485
case SyntaxKind.OptionalType:
486486
return updateOptionalTypeNode((<OptionalTypeNode>node),
@@ -517,6 +517,14 @@ namespace ts {
517517
(<ImportTypeNode>node).isTypeOf
518518
);
519519

520+
case SyntaxKind.NamedTupleMember:
521+
return updateNamedTupleMember(<NamedTupleMember>node,
522+
visitNode((<NamedTupleMember>node).dotDotDotToken, visitor, isToken),
523+
visitNode((<NamedTupleMember>node).name, visitor, isIdentifier),
524+
visitNode((<NamedTupleMember>node).questionToken, visitor, isToken),
525+
visitNode((<NamedTupleMember>node).type, visitor, isTypeNode),
526+
);
527+
520528
case SyntaxKind.ParenthesizedType:
521529
return updateParenthesizedType(<ParenthesizedTypeNode>node,
522530
visitNode((<ParenthesizedTypeNode>node).type, visitor, isTypeNode));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
const fixId = "fixIncorrectNamedTupleSyntax";
4+
const errorCodes = [
5+
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,
6+
Diagnostics.A_labeled_tuple_element_is_declared_as_rest_with_a_before_the_name_rather_than_before_the_type.code
7+
];
8+
9+
registerCodeFix({
10+
errorCodes,
11+
getCodeActions: context => {
12+
const { sourceFile, span } = context;
13+
const namedTupleMember = getNamedTupleMember(sourceFile, span.start);
14+
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, namedTupleMember));
15+
return [createCodeFixAction(fixId, changes, Diagnostics.Move_labeled_tuple_element_modifiers_to_labels, fixId, Diagnostics.Move_labeled_tuple_element_modifiers_to_labels)];
16+
},
17+
fixIds: [fixId]
18+
});
19+
20+
function getNamedTupleMember(sourceFile: SourceFile, pos: number) {
21+
const token = getTokenAtPosition(sourceFile, pos);
22+
return findAncestor(token, t => t.kind === SyntaxKind.NamedTupleMember) as NamedTupleMember | undefined;
23+
}
24+
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, namedTupleMember?: NamedTupleMember) {
25+
if (!namedTupleMember) {
26+
return;
27+
}
28+
let unwrappedType = namedTupleMember.type;
29+
let sawOptional = false;
30+
let sawRest = false;
31+
while (unwrappedType.kind === SyntaxKind.OptionalType || unwrappedType.kind === SyntaxKind.RestType || unwrappedType.kind === SyntaxKind.ParenthesizedType) {
32+
if (unwrappedType.kind === SyntaxKind.OptionalType) {
33+
sawOptional = true;
34+
}
35+
else if (unwrappedType.kind === SyntaxKind.RestType) {
36+
sawRest = true;
37+
}
38+
unwrappedType = (unwrappedType as OptionalTypeNode | RestTypeNode | ParenthesizedTypeNode).type;
39+
}
40+
const updated = updateNamedTupleMember(
41+
namedTupleMember,
42+
namedTupleMember.dotDotDotToken || (sawRest ? createToken(SyntaxKind.DotDotDotToken) : undefined),
43+
namedTupleMember.name,
44+
namedTupleMember.questionToken || (sawOptional ? createToken(SyntaxKind.QuestionToken) : undefined),
45+
unwrappedType
46+
);
47+
if (updated === namedTupleMember) {
48+
return;
49+
}
50+
changes.replaceNode(sourceFile, namedTupleMember, updated);
51+
}
52+
}

src/services/formatting/smartIndenter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ namespace ts.formatting {
574574
return childKind !== SyntaxKind.JsxClosingFragment;
575575
case SyntaxKind.IntersectionType:
576576
case SyntaxKind.UnionType:
577-
if (childKind === SyntaxKind.TypeLiteral) {
577+
if (childKind === SyntaxKind.TypeLiteral || childKind === SyntaxKind.TupleType) {
578578
return false;
579579
}
580580
// falls through

0 commit comments

Comments
 (0)