Skip to content

Commit 39d2ccb

Browse files
committed
Add array like destruction
1 parent bfb782a commit 39d2ccb

5 files changed

+191
-30
lines changed

src/services/refactors/convertObjectDestruction.ts

Lines changed: 135 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,33 +37,49 @@ namespace ts.refactor {
3737
return { renameFilename: undefined, renameLocation: undefined, edits };
3838
}
3939

40-
function doChange(changeTracker: textChanges.ChangeTracker, file: SourceFile, info: Info) {
41-
const nameMap = new Map<string, string>();
42-
const bindingElements: BindingElement[] = [];
43-
info.referencedAccessExpression.forEach(([expr, name]) => {
44-
if (!nameMap.has(name)) {
45-
const needAlias = info.namesNeedUniqueName.has(name);
46-
const uniqueName = needAlias ? getUniqueName(name, file) : name;
47-
nameMap.set(name, uniqueName);
48-
bindingElements.push(getUniqueDestructionName(expr, needAlias, uniqueName));
40+
function getUniqueNumericAccessVariable(name: string | number, file: SourceFile) {
41+
const tempName = `index_${name}`;
42+
return isFileLevelUniqueName(file, tempName) ? tempName : getUniqueName(tempName, file);
43+
}
44+
45+
/**
46+
* `Dense` means we use array literal pattern to destruction the expression.
47+
* We allowed index up to 15 to avoid many omit expression.
48+
*/
49+
function getDenseNumericAccessInfo(infos: ReferencedAccessInfo[]): [max: number, indexSet: Set<number>] | undefined {
50+
let min = Infinity;
51+
let max = -Infinity;
52+
const indexSet = new Set<number>();
53+
for (const info of infos) {
54+
if (!info.isNumericAccess) {
55+
return undefined;
4956
}
5057

51-
const newName = nameMap.get(name);
52-
Debug.assertIsDefined(newName);
58+
const value = parseInt(info.name);
59+
min = Math.min(min, value);
60+
max = Math.max(max, value);
61+
indexSet.add(value);
5362

54-
changeTracker.replaceNode(
55-
file,
56-
expr,
57-
factory.createIdentifier(newName)
58-
);
59-
});
63+
if (isNaN(min) || isNaN(max) || min < 0 || max < 0) {
64+
return undefined;
65+
}
66+
}
67+
68+
if (max > 15) {
69+
return undefined;
70+
}
6071

72+
return [max, indexSet];
73+
}
74+
75+
function doChange(changeTracker: textChanges.ChangeTracker, file: SourceFile, info: Info) {
76+
const bindingPattern = getBindingPattern(info, file, changeTracker);
6177
const newBinding = factory.createVariableStatement(
6278
/* modifiers*/ undefined,
6379
factory.createVariableDeclarationList(
6480
[
6581
factory.createVariableDeclaration(
66-
factory.createObjectBindingPattern(bindingElements),
82+
bindingPattern,
6783
/*exclamationToken*/ undefined,
6884
/*type*/ undefined,
6985
info.replacementExpression
@@ -80,6 +96,80 @@ namespace ts.refactor {
8096
);
8197
}
8298

99+
function getBindingPattern(info: Info, file: SourceFile, changeTracker: textChanges.ChangeTracker): BindingPattern {
100+
const denseNumericInfo = getDenseNumericAccessInfo(info.referencedAccessExpression);
101+
if (denseNumericInfo) {
102+
const [max, indexSet] = denseNumericInfo;
103+
return getdenseNumericBindingPattern(info, file, max, indexSet, changeTracker);
104+
}
105+
return getObjectBindingPattern(info, file, changeTracker);
106+
}
107+
108+
function getObjectBindingPattern(info: Info, file: SourceFile, changeTracker: textChanges.ChangeTracker) {
109+
const nameMap = new Map<string, string>();
110+
const bindingElements: BindingElement[] = [];
111+
info.referencedAccessExpression.forEach(({ expression, name, isNumericAccess }) => {
112+
if (!nameMap.has(name)) {
113+
const needAlias = isNumericAccess || info.namesNeedUniqueName.has(name);
114+
const uniqueName = isNumericAccess ? getUniqueNumericAccessVariable(name, file) :
115+
needAlias ? getUniqueName(name, file) : name;
116+
nameMap.set(name, uniqueName);
117+
bindingElements.push(getUniqueDestructionName(expression, needAlias, uniqueName));
118+
}
119+
120+
const newName = nameMap.get(name);
121+
Debug.assertIsDefined(newName);
122+
123+
changeTracker.replaceNode(
124+
file,
125+
expression,
126+
factory.createIdentifier(newName)
127+
);
128+
});
129+
return factory.createObjectBindingPattern(bindingElements);
130+
}
131+
132+
function getdenseNumericBindingPattern(info: Info, file: SourceFile, max: number, indexSet: Set<number>, changeTracker: textChanges.ChangeTracker): ArrayBindingPattern {
133+
const nameMap = new Map<number, string>();
134+
const bindingElements: ArrayBindingElement[] = [];
135+
136+
for (let i = 0; i <= max; ++i) {
137+
if (indexSet.has(i)) {
138+
if (!nameMap.has(i)) {
139+
const name = getUniqueNumericAccessVariable(i, file);
140+
nameMap.set(i, name);
141+
}
142+
143+
const name = nameMap.get(i);
144+
Debug.assertIsDefined(name);
145+
146+
bindingElements.push(factory.createBindingElement(
147+
/* dotDotDotToken*/ undefined,
148+
/*propertyName*/ undefined,
149+
factory.createIdentifier(name)
150+
));
151+
}
152+
else {
153+
bindingElements.push(factory.createOmittedExpression());
154+
}
155+
}
156+
157+
info.referencedAccessExpression.forEach(({ expression, name }) => {
158+
const index = parseInt(name);
159+
160+
const newName = nameMap.get(index);
161+
Debug.assertIsDefined(newName);
162+
163+
changeTracker.replaceNode(
164+
file,
165+
expression,
166+
factory.createIdentifier(newName)
167+
);
168+
});
169+
170+
return factory.createArrayBindingPattern(bindingElements);
171+
}
172+
83173
function getUniqueDestructionName(expr: AccessExpression, needAlias: boolean, newName: string) {
84174
if (isPropertyAccessExpression(expr)) {
85175
const bindingName = cast(expr.name, isIdentifier);
@@ -99,9 +189,15 @@ namespace ts.refactor {
99189
}
100190
}
101191

192+
interface ReferencedAccessInfo {
193+
expression: AccessExpression
194+
name: string,
195+
isNumericAccess: boolean
196+
}
197+
102198
interface Info {
103199
replacementExpression: Expression,
104-
referencedAccessExpression: [AccessExpression, string][]
200+
referencedAccessExpression: ReferencedAccessInfo[]
105201
firstReferenced: Expression
106202
firstReferencedStatement: Statement
107203
namesNeedUniqueName: Set<string>
@@ -122,7 +218,7 @@ namespace ts.refactor {
122218
const references = FindAllReferences.getReferenceEntriesForNode(-1, node, program, [file], cancellationToken);
123219
let firstReferenced: Expression | undefined;
124220
let firstReferencedStatement: Statement | undefined;
125-
const referencedAccessExpression: [AccessExpression, string][] = [];
221+
const referencedAccessExpression: ReferencedAccessInfo[] = [];
126222
const allReferencedAcccessExpression: AccessExpression[] = [];
127223
const container = isParameter(symbol.valueDeclaration) ? symbol.valueDeclaration : findAncestor(symbol.valueDeclaration, or(isStatement, isSourceFile));
128224
Debug.assertIsDefined(container);
@@ -150,23 +246,32 @@ namespace ts.refactor {
150246
}
151247

152248
if (isElementAccessExpression(accessExpression)) {
153-
if (!isStringLiteralLike(accessExpression.argumentExpression)) {
249+
let isNumericAccess = false;
250+
if (!isStringLiteralLike(accessExpression.argumentExpression) && !isNumericLiteral(accessExpression.argumentExpression)) {
154251
return;
155252
}
156-
if (!isIdentifierText(accessExpression.argumentExpression.text, compilerOptions.target, compilerOptions.jsx ? LanguageVariant.JSX : LanguageVariant.Standard)) {
253+
if (isNumericLiteral(accessExpression.argumentExpression)) {
254+
isNumericAccess = true;
255+
}
256+
else if (!isIdentifierText(accessExpression.argumentExpression.text, compilerOptions.target, compilerOptions.jsx ? LanguageVariant.JSX : LanguageVariant.Standard)) {
157257
return;
158258
}
159259

160-
referencedAccessExpression.push([accessExpression, accessExpression.argumentExpression.text]);
161-
return;
162-
}
163-
if (!isIdentifierText(accessExpression.name.text, compilerOptions.target, compilerOptions.jsx ? LanguageVariant.JSX : LanguageVariant.Standard)) {
260+
referencedAccessExpression.push({
261+
expression: accessExpression,
262+
name: accessExpression.argumentExpression.text,
263+
isNumericAccess
264+
});
164265
return;
165266
}
166267

167-
referencedAccessExpression.push([accessExpression, accessExpression.name.text]);
268+
referencedAccessExpression.push({
269+
expression: accessExpression,
270+
name: accessExpression.name.text,
271+
isNumericAccess: false
272+
});
168273
});
169-
if (!firstReferenced || !firstReferencedStatement || !referencedAccessExpression.length || !some(referencedAccessExpression, ([r]) => rangeContainsRange(r, current))) return undefined;
274+
if (!firstReferenced || !firstReferencedStatement || !referencedAccessExpression.length || !some(referencedAccessExpression, ({ expression }) => rangeContainsRange(expression, current))) return undefined;
170275

171276
let hasUnconvertableReference = false;
172277
const namesNeedUniqueName = new Set<string>();
@@ -175,7 +280,7 @@ namespace ts.refactor {
175280
const referenceType = checker.getTypeAtLocation(expr);
176281
if (referenceType !== type) {
177282
const propName = isElementAccessExpression(expr) ?
178-
cast(expr.argumentExpression, isStringLiteralLike).text :
283+
cast(expr.argumentExpression, isStringOrNumericLiteralLike).text :
179284
checker.getSymbolAtLocation(expr)?.name;
180285

181286
const accessType = checker.getTypeAtLocation(expr);
@@ -190,7 +295,7 @@ namespace ts.refactor {
190295
if (hasUnconvertableReference) return undefined;
191296

192297
if (resolveUniqueName) {
193-
forEach(referencedAccessExpression, ([, name]) => {
298+
forEach(referencedAccessExpression, ({ name }) => {
194299
const symbol = checker.resolveName(name, /*location*/ undefined, SymbolFlags.Value, /*excludeGlobals*/ false);
195300
if (symbol) {
196301
namesNeedUniqueName.add(name);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// const item = [ 1, 2, 3 ] as const
4+
//// call(/*a*/item/*b*/[0], item[1], item[2])
5+
6+
goTo.select("a", "b");
7+
edit.applyRefactor({
8+
refactorName: "Introduce Destruction",
9+
actionName: "Introduce Destruction",
10+
actionDescription: "Convert property access to Object destruction",
11+
newContent: `const item = [ 1, 2, 3 ] as const
12+
const [index_0, index_1, index_2] = item
13+
call(index_0, index_1, index_2)`,
14+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// const item = [ 1, 2, 3 ] as const
4+
//// call(/*a*/item/*b*/[0], item[2], item[2])
5+
6+
goTo.select("a", "b");
7+
edit.applyRefactor({
8+
refactorName: "Introduce Destruction",
9+
actionName: "Introduce Destruction",
10+
actionDescription: "Convert property access to Object destruction",
11+
newContent: `const item = [ 1, 2, 3 ] as const
12+
const [index_0, , index_2] = item
13+
call(index_0, index_2, index_2)`,
14+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// const item = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ] as const
4+
//// call(/*a*/item/*b*/[14], item[8], item[3])
5+
6+
goTo.select("a", "b");
7+
edit.applyRefactor({
8+
refactorName: "Introduce Destruction",
9+
actionName: "Introduce Destruction",
10+
actionDescription: "Convert property access to Object destruction",
11+
newContent: `const item = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ] as const
12+
const [, , , index_3, , , , , index_8, , , , , , index_14] = item
13+
call(index_14, index_8, index_3)`,
14+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// const item = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 ] as const
4+
//// call(/*a*/item/*b*/[14], item[8], item[16])
5+
6+
goTo.select("a", "b");
7+
edit.applyRefactor({
8+
refactorName: "Introduce Destruction",
9+
actionName: "Introduce Destruction",
10+
actionDescription: "Convert property access to Object destruction",
11+
newContent: `const item = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 ] as const
12+
const { 14: index_14, 8: index_8, 16: index_16 } = item
13+
call(index_14, index_8, index_16)`,
14+
});

0 commit comments

Comments
 (0)