Skip to content

Commit b834096

Browse files
authored
Fix emit/formatting issues in refactors (microsoft#39506)
* Fix microsoft#37948 * Fix formatter skipping tab/space fixup on comments, handle trailing commas in list closing line count. Fixes microsoft#37944 * Add newline between imports and main body of new file in moveToNewFile Fixes microsoft#37941 * Update baseline (probably broken before?)
1 parent e76d172 commit b834096

23 files changed

+140
-15
lines changed

src/compiler/emitter.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,7 @@ namespace ts {
845845
let reservedNamesStack: Set<string>[]; // Stack of TempFlags reserved in enclosing name generation scopes.
846846
let reservedNames: Set<string>; // TempFlags to reserve in nested name generation scopes.
847847
let preserveSourceNewlines = printerOptions.preserveSourceNewlines; // Can be overridden inside nodes with the `IgnoreSourceNewlines` emit flag.
848+
let nextListElementPos: number | undefined; // See comment in `getLeadingLineTerminatorCount`.
848849

849850
let writer: EmitTextWriter;
850851
let ownWriter: EmitTextWriter; // Reusable `EmitTextWriter` for basic printing.
@@ -4104,6 +4105,7 @@ namespace ts {
41044105
shouldEmitInterveningComments = mayEmitInterveningComments;
41054106
}
41064107

4108+
nextListElementPos = child.pos;
41074109
emit(child);
41084110

41094111
if (shouldDecreaseIndentAfterEmit) {
@@ -4312,11 +4314,32 @@ namespace ts {
43124314
if (firstChild === undefined) {
43134315
return rangeIsOnSingleLine(parentNode, currentSourceFile!) ? 0 : 1;
43144316
}
4317+
if (firstChild.pos === nextListElementPos) {
4318+
// If this child starts at the beginning of a list item in a parent list, its leading
4319+
// line terminators have already been written as the separating line terminators of the
4320+
// parent list. Example:
4321+
//
4322+
// class Foo {
4323+
// constructor() {}
4324+
// public foo() {}
4325+
// }
4326+
//
4327+
// The outer list is the list of class members, with one line terminator between the
4328+
// constructor and the method. The constructor is written, the separating line terminator
4329+
// is written, and then we start emitting the method. Its modifiers ([public]) constitute an inner
4330+
// list, so we look for its leading line terminators. If we didn't know that we had already
4331+
// written a newline as part of the parent list, it would appear that we need to write a
4332+
// leading newline to start the modifiers.
4333+
return 0;
4334+
}
43154335
if (firstChild.kind === SyntaxKind.JsxText) {
43164336
// JsxText will be written with its leading whitespace, so don't add more manually.
43174337
return 0;
43184338
}
4319-
if (!positionIsSynthesized(parentNode.pos) && !nodeIsSynthesized(firstChild) && (!firstChild.parent || firstChild.parent === parentNode)) {
4339+
if (!positionIsSynthesized(parentNode.pos) &&
4340+
!nodeIsSynthesized(firstChild) &&
4341+
(!firstChild.parent || getOriginalNode(firstChild.parent) === getOriginalNode(parentNode as Node))
4342+
) {
43204343
if (preserveSourceNewlines) {
43214344
return getEffectiveLines(
43224345
includeComments => getLinesBetweenPositionAndPrecedingNonWhitespaceCharacter(
@@ -4376,9 +4399,10 @@ namespace ts {
43764399
}
43774400
if (!positionIsSynthesized(parentNode.pos) && !nodeIsSynthesized(lastChild) && (!lastChild.parent || lastChild.parent === parentNode)) {
43784401
if (preserveSourceNewlines) {
4402+
const end = isNodeArray(children) && !positionIsSynthesized(children.end) ? children.end : lastChild.end;
43794403
return getEffectiveLines(
43804404
includeComments => getLinesBetweenPositionAndNextNonWhitespaceCharacter(
4381-
lastChild.end,
4405+
end,
43824406
parentNode.end,
43834407
currentSourceFile!,
43844408
includeComments));

src/services/formatting/formatting.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,10 +1092,6 @@ namespace ts.formatting {
10921092
const nonWhitespaceColumnInFirstPart =
10931093
SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(startLinePos, parts[0].pos, sourceFile, options);
10941094

1095-
if (indentation === nonWhitespaceColumnInFirstPart.column && !jsxTextStyleIndent) {
1096-
return;
1097-
}
1098-
10991095
let startIndex = 0;
11001096
if (firstLineIsIndented) {
11011097
startIndex = 1;

src/services/refactors/moveToNewFile.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ namespace ts.refactor {
109109

110110
function getNewStatementsAndRemoveFromOldFile(
111111
oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, newModuleName: string, preferences: UserPreferences,
112-
): readonly Statement[] {
112+
) {
113113
const checker = program.getTypeChecker();
114114

115115
if (!oldFile.externalModuleIndicator && !oldFile.commonJsModuleIndicator) {
@@ -129,9 +129,19 @@ namespace ts.refactor {
129129

130130
updateImportsInOtherFiles(changes, program, oldFile, usage.movedSymbols, newModuleName);
131131

132+
const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEs6ModuleSyntax, quotePreference);
133+
const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEs6ModuleSyntax);
134+
if (imports.length && body.length) {
135+
return [
136+
...imports,
137+
SyntaxKind.NewLineTrivia as const,
138+
...body
139+
];
140+
}
141+
132142
return [
133-
...getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEs6ModuleSyntax, quotePreference),
134-
...addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEs6ModuleSyntax),
143+
...imports,
144+
...body,
135145
];
136146
}
137147

src/services/textChanges.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ namespace ts.textChanges {
249249

250250
export class ChangeTracker {
251251
private readonly changes: Change[] = [];
252-
private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly Statement[] }[] = [];
252+
private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = [];
253253
private readonly classesWithNodesInsertedAtStart = new Map<string, { readonly node: ClassDeclaration | InterfaceDeclaration | ObjectLiteralExpression, readonly sourceFile: SourceFile }>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
254254
private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray<TypeParameterDeclaration> }[] = [];
255255

@@ -868,7 +868,7 @@ namespace ts.textChanges {
868868
return changes;
869869
}
870870

871-
public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly Statement[]): void {
871+
public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void {
872872
this.newFiles.push({ oldFile, fileName, statements });
873873
}
874874
}
@@ -922,14 +922,14 @@ namespace ts.textChanges {
922922
});
923923
}
924924

925-
export function newFileChanges(oldFile: SourceFile | undefined, fileName: string, statements: readonly Statement[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges {
925+
export function newFileChanges(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges {
926926
const text = newFileChangesWorker(oldFile, getScriptKindFromFileName(fileName), statements, newLineCharacter, formatContext);
927927
return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true };
928928
}
929929

930-
export function newFileChangesWorker(oldFile: SourceFile | undefined, scriptKind: ScriptKind, statements: readonly Statement[], newLineCharacter: string, formatContext: formatting.FormatContext): string {
930+
export function newFileChangesWorker(oldFile: SourceFile | undefined, scriptKind: ScriptKind, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): string {
931931
// TODO: this emits the file, parses it back, then formats it that -- may be a less roundabout way to do this
932-
const nonFormattedText = statements.map(s => getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter);
932+
const nonFormattedText = statements.map(s => s === SyntaxKind.NewLineTrivia ? "" : getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter);
933933
const sourceFile = createSourceFile("any file name", nonFormattedText, ScriptTarget.ESNext, /*setParentNodes*/ true, scriptKind);
934934
const changes = formatting.formatDocument(sourceFile, formatContext);
935935
return applyChanges(nonFormattedText, changes) + newLineCharacter;

tests/baselines/reference/textChanges/replaceRangeWithForcedIndentation.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ var x = 1; // comment 2
1414
// comment 3
1515
public class class1 implements interface1
1616
{
17-
property1: boolean;
17+
property1: boolean;
1818
}
1919
var a = 4; // comment 7

tests/cases/fourslash/moveToNewFile.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ a; y;`,
2323
"/y.ts":
2424
`import { b } from './other';
2525
import { p } from './a';
26+
2627
export const y: Date = p + b;
2728
`,
2829
},

tests/cases/fourslash/moveToNewFile_cleanUpLastNamedImport.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ console.log(defaultExport)
2121

2222
"/bar.ts":
2323
`import { Exported } from "./has-exports";
24+
2425
export const bar = (logger: Exported) => 0;
2526
`,
2627
}

tests/cases/fourslash/moveToNewFile_defaultImport.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ verify.moveToNewFile({
1212

1313
"/x.ts":
1414
`import f from "./a";
15+
1516
const x = f();
1617
`,
1718
},

tests/cases/fourslash/moveToNewFile_exportImport.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ M;`,
1515

1616
"/O.ts":
1717
`import { N } from "./a";
18+
1819
export import O = N;
1920
`,
2021
},

tests/cases/fourslash/moveToNewFile_getter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ verify.moveToNewFile({
2323

2424
"/g.ts":
2525
`import { C } from "./a";
26+
2627
export const { g, h: i } = new C();
2728
`,
2829

0 commit comments

Comments
 (0)