Skip to content

Commit 9097a07

Browse files
committed
Improve insertion positions of extracted constants
...mostly by putting them closer to the extraction site. They now also follow all leading comments in a file (unfortunately, including the doc comment on the first declaration). Bonus: Special case declaration lists - declare the new variable as part of the list, rather than before it, in case it depends on an earlier entry.
1 parent 8e4c559 commit 9097a07

File tree

42 files changed

+981
-37
lines changed

Some content is hidden

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

42 files changed

+981
-37
lines changed

src/harness/unittests/extractConstants.ts

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ namespace ts {
4040
}
4141
}`);
4242

43-
testExtractConstant("extractConstant_ClassInsertionPosition",
43+
testExtractConstant("extractConstant_ClassInsertionPosition1",
4444
`class C {
4545
a = 1;
4646
b = 2;
@@ -51,6 +51,28 @@ namespace ts {
5151
}
5252
}`);
5353

54+
testExtractConstant("extractConstant_ClassInsertionPosition2",
55+
`class C {
56+
a = 1;
57+
M1() { }
58+
b = 2;
59+
M2() { }
60+
M3() {
61+
let x = [#|1|];
62+
}
63+
}`);
64+
65+
testExtractConstant("extractConstant_ClassInsertionPosition3",
66+
`class C {
67+
M1() { }
68+
a = 1;
69+
b = 2;
70+
M2() { }
71+
M3() {
72+
let x = [#|1|];
73+
}
74+
}`);
75+
5476
testExtractConstant("extractConstant_Parameters",
5577
`function F() {
5678
let w = 1;
@@ -62,7 +84,7 @@ namespace ts {
6284
let x = [#|t + 1|];
6385
}`);
6486

65-
// TODO (acasey): handle repeated substitution
87+
// TODO (18857): handle repeated substitution
6688
// testExtractConstant("extractConstant_RepeatedSubstitution",
6789
// `namespace X {
6890
// export const j = 10;
@@ -75,6 +97,121 @@ namespace ts {
7597
let x = [#|i + 1|];
7698
}
7799
}`);
100+
101+
testExtractConstant("extractConstant_VariableList_const",
102+
`const a = 1, b = [#|a + 1|];`);
103+
104+
// NOTE: this test isn't normative - it just documents our sub-optimal behavior.
105+
testExtractConstant("extractConstant_VariableList_let",
106+
`let a = 1, b = [#|a + 1|];`);
107+
108+
// NOTE: this test isn't normative - it just documents our sub-optimal behavior.
109+
testExtractConstant("extractConstant_VariableList_MultipleLines",
110+
`const /*About A*/a = 1,
111+
/*About B*/b = [#|a + 1|];`);
112+
113+
// NOTE: this test isn't normative - it just documents our sub-optimal behavior.
114+
// `i` doesn't bind in the target scope (file-level), so the extraction is disallowed.
115+
// TODO (17098): should probably allow extraction into the same scope
116+
testExtractConstantFailed("extractConstant_BlockScopeMismatch", `
117+
for (let i = 0; i < 10; i++) {
118+
for (let j = 0; j < 10; j++) {
119+
const x = [#|i + 1|];
120+
}
121+
}
122+
`);
123+
124+
testExtractConstant("extractConstant_StatementInsertionPosition1", `
125+
const i = 0;
126+
for (let j = 0; j < 10; j++) {
127+
const x = [#|i + 1|];
128+
}
129+
`);
130+
131+
testExtractConstant("extractConstant_StatementInsertionPosition2", `
132+
const i = 0;
133+
function F() {
134+
for (let j = 0; j < 10; j++) {
135+
const x = [#|i + 1|];
136+
}
137+
}
138+
`);
139+
140+
testExtractConstant("extractConstant_StatementInsertionPosition3", `
141+
for (let j = 0; j < 10; j++) {
142+
const x = [#|2 + 1|];
143+
}
144+
`);
145+
146+
testExtractConstant("extractConstant_StatementInsertionPosition4", `
147+
function F() {
148+
for (let j = 0; j < 10; j++) {
149+
const x = [#|2 + 1|];
150+
}
151+
}
152+
`);
153+
154+
testExtractConstant("extractConstant_StatementInsertionPosition5", `
155+
function F0() {
156+
function F1() {
157+
function F2(x = [#|2 + 1|]) {
158+
}
159+
}
160+
}
161+
`);
162+
163+
testExtractConstant("extractConstant_StatementInsertionPosition6", `
164+
class C {
165+
x = [#|2 + 1|];
166+
}
167+
`);
168+
169+
testExtractConstant("extractConstant_StatementInsertionPosition7", `
170+
const i = 0;
171+
class C {
172+
M() {
173+
for (let j = 0; j < 10; j++) {
174+
x = [#|i + 1|];
175+
}
176+
}
177+
}
178+
`);
179+
180+
testExtractConstant("extractConstant_TripleSlash", `
181+
/// <reference path="path.js"/>
182+
183+
const x = [#|2 + 1|];
184+
`);
185+
186+
testExtractConstant("extractConstant_PinnedComment", `
187+
/*! Copyright */
188+
189+
const x = [#|2 + 1|];
190+
`);
191+
192+
testExtractConstant("extractConstant_Directive", `
193+
"strict";
194+
195+
const x = [#|2 + 1|];
196+
`);
197+
198+
testExtractConstant("extractConstant_MultipleHeaders", `
199+
/*! Copyright */
200+
201+
/// <reference path="path.js"/>
202+
203+
"strict";
204+
205+
const x = [#|2 + 1|];
206+
`);
207+
208+
// NOTE: this test isn't normative - it just documents our sub-optimal behavior.
209+
testExtractConstant("extractConstant_PinnedCommentAndDocComment", `
210+
/*! Copyright */
211+
212+
/* About x */
213+
const x = [#|2 + 1|];
214+
`);
78215
});
79216

80217
function testExtractConstant(caption: string, text: string) {

src/harness/unittests/extractFunctions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ function parsePrimaryExpression(): any {
365365
[#|function G() { }|]
366366
}`);
367367

368-
// TODO (acasey): handle repeated substitution
368+
// TODO (18857): handle repeated substitution
369369
// testExtractFunction("extractFunction_RepeatedSubstitution",
370370
// `namespace X {
371371
// export const j = 10;

src/harness/unittests/extractTestHelpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ namespace ts {
113113

114114
if (hasSyntacticDiagnostics(program)) {
115115
// Don't bother generating JS baselines for inputs that aren't valid JS.
116-
assert.equal(Extension.Js, extension);
116+
assert.equal(Extension.Js, extension, "Syntactic diagnostics found in non-JS file");
117117
return;
118118
}
119119

src/services/refactors/extractSymbol.ts

Lines changed: 103 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -890,33 +890,56 @@ namespace ts.refactor.extractSymbol {
890890
createIdentifier(localNameText));
891891

892892
// Declare
893-
const minInsertionPos = node.end;
894-
const nodeToInsertBefore = getNodeToInsertConstantBefore(minInsertionPos, scope);
893+
const maxInsertionPos = node.pos;
894+
const nodeToInsertBefore = getNodeToInsertPropertyBefore(maxInsertionPos, scope);
895895
changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariable, { suffix: context.newLineCharacter + context.newLineCharacter });
896896

897897
// Consume
898898
changeTracker.replaceNodeWithNodes(context.file, node, [localReference], { nodeSeparator: context.newLineCharacter });
899899
}
900900
else {
901-
const newVariable = createVariableStatement(
902-
/*modifiers*/ undefined,
903-
createVariableDeclarationList(
904-
[createVariableDeclaration(localNameText, variableType, initializer)],
905-
NodeFlags.Const));
901+
const newVariableDeclaration = createVariableDeclaration(localNameText, variableType, initializer);
902+
903+
// If the node is part of an initializer in a list of variable declarations, insert a new
904+
// variable declaration into the list (in case it depends on earlier ones).
905+
// CONSIDER: If the declaration list isn't const, we might want to split it into multiple
906+
// lists so that the newly extracted one can be const.
907+
const oldVariableDeclaration = getContainingVariableDeclarationIfInList(node, scope);
908+
if (oldVariableDeclaration) {
909+
// Declare
910+
// CONSIDER: could detect that each is on a separate line
911+
changeTracker.insertNodeAt(context.file, oldVariableDeclaration.getStart(), newVariableDeclaration, { suffix: ", " });
906912

907-
// If the parent is an expression statement, replace the statement with the declaration
908-
if (node.parent.kind === SyntaxKind.ExpressionStatement) {
909-
changeTracker.replaceNodeWithNodes(context.file, node.parent, [newVariable], { nodeSeparator: context.newLineCharacter });
913+
// Consume
914+
const localReference = createIdentifier(localNameText);
915+
changeTracker.replaceRange(context.file, { pos: node.getStart(), end: node.end }, localReference);
916+
}
917+
else if (node.parent.kind === SyntaxKind.ExpressionStatement) {
918+
// If the parent is an expression statement, replace the statement with the declaration.
919+
const newVariableStatement = createVariableStatement(
920+
/*modifiers*/ undefined,
921+
createVariableDeclarationList([newVariableDeclaration], NodeFlags.Const));
922+
changeTracker.replaceNodeWithNodes(context.file, node.parent, [newVariableStatement], { nodeSeparator: context.newLineCharacter });
910923
}
911924
else {
925+
const newVariableStatement = createVariableStatement(
926+
/*modifiers*/ undefined,
927+
createVariableDeclarationList([newVariableDeclaration], NodeFlags.Const));
928+
912929
// Declare
913-
const minInsertionPos = node.end;
914-
const nodeToInsertBefore = getNodeToInsertConstantBefore(minInsertionPos, scope);
915-
changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariable, { suffix: context.newLineCharacter + context.newLineCharacter });
930+
const nodeToInsertBefore = getNodeToInsertConstantBefore(node, scope);
931+
if (nodeToInsertBefore.pos === 0) {
932+
// If we're at the beginning of the file, we need to take care not to insert before header comments
933+
// (e.g. copyright, triple-slash references).
934+
changeTracker.insertNodeAt(context.file, nodeToInsertBefore.getStart(), newVariableStatement, { suffix: context.newLineCharacter + context.newLineCharacter });
935+
}
936+
else {
937+
changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariableStatement, { suffix: context.newLineCharacter + context.newLineCharacter });
938+
}
916939

917940
// Consume
918941
const localReference = createIdentifier(localNameText);
919-
changeTracker.replaceNodeWithNodes(context.file, node, [localReference], { nodeSeparator: context.newLineCharacter });
942+
changeTracker.replaceRange(context.file, { pos: node.getStart(), end: node.end }, localReference);
920943
}
921944
}
922945

@@ -927,6 +950,22 @@ namespace ts.refactor.extractSymbol {
927950
return { renameFilename, renameLocation, edits };
928951
}
929952

953+
function getContainingVariableDeclarationIfInList(node: Node, scope: Scope) {
954+
let prevNode = undefined;
955+
while (node !== undefined && node !== scope) {
956+
if (isVariableDeclaration(node) &&
957+
node.initializer === prevNode &&
958+
isVariableDeclarationList(node.parent) &&
959+
node.parent.declarations.length > 1) {
960+
961+
return node;
962+
}
963+
964+
prevNode = node;
965+
node = node.parent;
966+
}
967+
}
968+
930969
/**
931970
* @return The index of the (only) reference to the extracted symbol. We want the cursor
932971
* to be on the reference, rather than the declaration, because it's closer to where the
@@ -1109,26 +1148,61 @@ namespace ts.refactor.extractSymbol {
11091148
child.pos >= minPos && isFunctionLikeDeclaration(child) && !isConstructorDeclaration(child));
11101149
}
11111150

1112-
// TODO (acasey): need to dig into nested statements
1113-
// TODO (acasey): don't insert before pinned comments, directives, or triple-slash references
1114-
function getNodeToInsertConstantBefore(maxPos: number, scope: Scope): Node {
1115-
const children = getStatementsOrClassElements(scope);
1116-
Debug.assert(children.length > 0); // There must be at least one child, since we extracted from one.
1151+
function getNodeToInsertPropertyBefore(maxPos: number, scope: ClassLikeDeclaration): Node {
1152+
const members = scope.members;
1153+
Debug.assert(members.length > 0); // There must be at least one child, since we extracted from one.
1154+
1155+
let prevMember: ClassElement | undefined = undefined;
1156+
let allProperties = true;
1157+
for (const member of members) {
1158+
if (member.pos > maxPos) {
1159+
return prevMember || members[0];
1160+
}
1161+
if (allProperties && !isPropertyDeclaration(member)) {
1162+
// If it is non-vacuously true that all preceding members are properties,
1163+
// insert before the current member (i.e. at the end of the list of properties).
1164+
if (prevMember !== undefined) {
1165+
return member;
1166+
}
11171167

1118-
const isClassLikeScope = isClassLike(scope);
1119-
let prevChild: Statement | ClassElement | undefined = undefined;
1120-
for (const child of children) {
1121-
if (child.pos >= maxPos) {
1122-
break;
1168+
allProperties = false;
11231169
}
1124-
prevChild = child;
1125-
if (isClassLikeScope && !isPropertyDeclaration(child)) {
1126-
break;
1170+
prevMember = member;
1171+
}
1172+
1173+
Debug.assert(prevMember !== undefined); // If the loop didn't return, then it did set prevMember.
1174+
return prevMember;
1175+
}
1176+
1177+
function getNodeToInsertConstantBefore(node: Node, scope: Scope): Node {
1178+
Debug.assert(!isClassLike(scope));
1179+
1180+
let prevScope: Scope | undefined = undefined;
1181+
for (let curr = node; curr !== scope; curr = curr.parent) {
1182+
if (isScope(curr)) {
1183+
prevScope = curr;
11271184
}
11281185
}
11291186

1130-
Debug.assert(prevChild !== undefined);
1131-
return prevChild;
1187+
for (let curr = (prevScope || node).parent; ; curr = curr.parent) {
1188+
if (isBlockLike(curr)) {
1189+
let prevStatement = undefined;
1190+
for (const statement of curr.statements) {
1191+
if (statement.pos > node.pos) {
1192+
break;
1193+
}
1194+
prevStatement = statement;
1195+
}
1196+
// There must be at least one statement since we started in one.
1197+
Debug.assert(prevStatement !== undefined);
1198+
return prevStatement;
1199+
}
1200+
1201+
if (curr === scope) {
1202+
Debug.fail("Didn't encounter a block-like before encountering scope");
1203+
break;
1204+
}
1205+
}
11321206
}
11331207

11341208
function getPropertyAssignmentsForWrites(writes: ReadonlyArray<UsageEntry>): ShorthandPropertyAssignment[] {

tests/baselines/reference/extractConstant/extractConstant_BlockScopes_NoDependencies.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ for (let i = 0; i < 10; i++) {
55
}
66
}
77
// ==SCOPE::Extract to constant in global scope==
8-
const newLocal = 1;
9-
108
for (let i = 0; i < 10; i++) {
119
for (let j = 0; j < 10; j++) {
10+
const newLocal = 1;
11+
1212
let x = /*RENAME*/newLocal;
1313
}
1414
}

tests/baselines/reference/extractConstant/extractConstant_BlockScopes_NoDependencies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ for (let i = 0; i < 10; i++) {
55
}
66
}
77
// ==SCOPE::Extract to constant in global scope==
8-
const newLocal = 1;
9-
108
for (let i = 0; i < 10; i++) {
119
for (let j = 0; j < 10; j++) {
10+
const newLocal = 1;
11+
1212
let x = /*RENAME*/newLocal;
1313
}
1414
}

0 commit comments

Comments
 (0)