Skip to content

Conversation

@hk2166
Copy link

@hk2166 hk2166 commented Nov 25, 2025

#62780

Prevents the "Move to file" refactor from appearing when the selection
is inside an unbraced if/else branch (e.g., if (true) const x = 1;).
Previously, invoking the refactor would generate invalid code by
extracting only part of a statement.

Added guards at three validation points:

  • getAvailableActions: blocks refactor from appearing in UI
  • doChange: safety check prevents invalid transformations
  • getStatementsToMove: early rejection during statement collection

Statements inside braced blocks remain unaffected and continue to work
correctly.

Fixes invalid code generation when refactoring unbraced control flow.

Copilot AI review requested due to automatic review settings November 25, 2025 18:09
@github-project-automation github-project-automation bot moved this to Not started in PR Backlog Nov 25, 2025
@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Nov 25, 2025
@typescript-bot
Copy link
Collaborator

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

@hk2166 hk2166 added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Nov 25, 2025
@hk2166
Copy link
Author

hk2166 commented Nov 25, 2025

@sheetalkamat @ahejlsberg can you this PR if there are any more tweaks required then please let me know

Copilot finished reviewing on behalf of hk2166 November 25, 2025 18:12
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to fix the "Move to file" refactor to prevent invalid code generation when dealing with statements in unbraced if/else branches. However, the PR contains several unrelated changes that should be separated into different PRs.

Key concerns:

  • Multiple unrelated changes mixed into one PR (type alias error messages, debug scripts)
  • Dead code that should be removed
  • Logic bugs in the validation
  • Large import reorganization that obscures the actual fix

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/services/refactors/moveToFile.ts Main refactor fix with validation guards, but contains unnecessary import reorganization, dead code, logic bugs, and code duplication
test-refactor.js Debug script that should not be committed - appears to be accidental inclusion
tests/cases/compiler/typeAliasUsedAsValue.ts Unrelated test about type aliases used as values - should be in separate PR
src/compiler/diagnosticMessages.json Unrelated diagnostic message for type aliases - should be in separate PR
src/compiler/checker.ts Unrelated type checking enhancement - should be in separate PR

Comment on lines +27660 to +27664
// Check if there's a type symbol with the same name
const typeSymbol = resolveName(node, node.escapedText, SymbolFlags.Type, /*nameNotFoundMessage*/ undefined, /*isUse*/ false);
if (typeSymbol && !(typeSymbol.flags & SymbolFlags.Value)) {
return Diagnostics.Cannot_find_name_0_1_only_refers_to_a_type_but_is_being_used_as_a_value_here;
}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checker enhancement is unrelated to the PR's stated purpose of fixing the "Move to file" refactor for unbraced if/else statements. This appears to improve error messages when type aliases are used as values, which matches the typeAliasUsedAsValue.ts test but has no connection to the refactoring bug fix. These changes should be in a separate PR.

Copilot uses AI. Check for mistakes.
Comment on lines +2843 to 2847
"Cannot find name '{0}'. '{1}' only refers to a type, but is being used as a value here.": {
"category": "Error",
"code": 2888
},
"Computed values are not permitted in an enum with string valued members.": {
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new diagnostic message is unrelated to the PR's stated purpose of fixing the "Move to file" refactor for unbraced if/else statements. This message is about type aliases being used as values, not about refactoring. This change should be in a separate PR along with the related checker and test changes.

Suggested change
"Cannot find name '{0}'. '{1}' only refers to a type, but is being used as a value here.": {
"category": "Error",
"code": 2888
},
"Computed values are not permitted in an enum with string valued members.": {
"Computed values are not permitted in an enum with string valued members.": {
"category": "Error",
"code": 2553
},

Copilot uses AI. Check for mistakes.
Comment on lines +1608 to +1619
const first = selectedStatements[0].kind;
switch (first) {
case SyntaxKind.IfStatement:
case SyntaxKind.ForStatement:
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement:
case SyntaxKind.WhileStatement:
case SyntaxKind.DoStatement:
case SyntaxKind.SwitchStatement:
case SyntaxKind.TryStatement:
case SyntaxKind.WithStatement:
return undefined;
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic only checks the kind of the first selected statement, but if multiple statements are selected and any of them are control-flow statements, they should all be rejected. The current code would allow moving an if-statement if it's not the first in a multi-statement selection. The check should iterate through all selectedStatements instead of only checking selectedStatements[0].

Suggested change
const first = selectedStatements[0].kind;
switch (first) {
case SyntaxKind.IfStatement:
case SyntaxKind.ForStatement:
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement:
case SyntaxKind.WhileStatement:
case SyntaxKind.DoStatement:
case SyntaxKind.SwitchStatement:
case SyntaxKind.TryStatement:
case SyntaxKind.WithStatement:
return undefined;
const controlFlowKinds = [
SyntaxKind.IfStatement,
SyntaxKind.ForStatement,
SyntaxKind.ForInStatement,
SyntaxKind.ForOfStatement,
SyntaxKind.WhileStatement,
SyntaxKind.DoStatement,
SyntaxKind.SwitchStatement,
SyntaxKind.TryStatement,
SyntaxKind.WithStatement,
];
if (selectedStatements.some(stmt => controlFlowKinds.includes(stmt.kind))) {
return undefined;

Copilot uses AI. Check for mistakes.
import {
addTargetFileImports,
registerRefactor,
} from "../_namespaces/ts.refactor.js";

// NOTE: we do NOT import doChange from ts.refactor; we provide our own safe implementation below.
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is misleading. The code doesn't provide its "own safe implementation" of doChange - it's just the modified existing doChange function with additional safety checks. The comment should be removed or clarified to say "doChange has been enhanced with additional validation" rather than implying it's a completely separate implementation.

Suggested change
// NOTE: we do NOT import doChange from ts.refactor; we provide our own safe implementation below.
// NOTE: we do NOT import doChange from ts.refactor; instead, doChange is re-implemented below with additional validation for safety.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +53
// Quick test to understand the AST structure of bug.ts
const ts = require("./built/local/typescript.js");
const fs = require("fs");

const code = fs.readFileSync("bug.ts", "utf8");
const sourceFile = ts.createSourceFile(
"bug.ts",
code,
ts.ScriptTarget.Latest,
true
);

console.log("=== AST Structure ===");

function printNode(node, indent = 0) {
const prefix = " ".repeat(indent);
console.log(`${prefix}${ts.SyntaxKind[node.kind]} (${node.pos}-${node.end})`);
if (ts.isIfStatement(node)) {
console.log(
`${prefix} thenStatement parent check: ${
node.thenStatement.parent === node
}`
);
console.log(
`${prefix} thenStatement.kind: ${ts.SyntaxKind[node.thenStatement.kind]}`
);
}
node.forEachChild((child) => printNode(child, indent + 1));
}

printNode(sourceFile);

// Now test with the position of "const x = 1"
console.log("\n=== Testing position 11 (start of 'const') ===");
const token = ts.getTokenAtPosition(sourceFile, 11);
console.log(
`Token at pos 11: ${ts.SyntaxKind[token.kind]} (${token.pos}-${token.end})`
);

let current = token;
while (current) {
console.log(
` Ancestor: ${ts.SyntaxKind[current.kind]} (parent: ${
current.parent ? ts.SyntaxKind[current.parent.kind] : "none"
})`
);
if (ts.isStatement(current)) {
console.log(` ^^^ This is a statement!`);
console.log(` Parent is SourceFile? ${ts.isSourceFile(current.parent)}`);
break;
}
current = current.parent;
}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This debug script file should not be committed to the repository. Debug/temporary test files belong in .gitignore or should be removed before committing. The file references a non-existent "bug.ts" file and appears to be a temporary debugging aid that was accidentally included in the PR.

Suggested change
// Quick test to understand the AST structure of bug.ts
const ts = require("./built/local/typescript.js");
const fs = require("fs");
const code = fs.readFileSync("bug.ts", "utf8");
const sourceFile = ts.createSourceFile(
"bug.ts",
code,
ts.ScriptTarget.Latest,
true
);
console.log("=== AST Structure ===");
function printNode(node, indent = 0) {
const prefix = " ".repeat(indent);
console.log(`${prefix}${ts.SyntaxKind[node.kind]} (${node.pos}-${node.end})`);
if (ts.isIfStatement(node)) {
console.log(
`${prefix} thenStatement parent check: ${
node.thenStatement.parent === node
}`
);
console.log(
`${prefix} thenStatement.kind: ${ts.SyntaxKind[node.thenStatement.kind]}`
);
}
node.forEachChild((child) => printNode(child, indent + 1));
}
printNode(sourceFile);
// Now test with the position of "const x = 1"
console.log("\n=== Testing position 11 (start of 'const') ===");
const token = ts.getTokenAtPosition(sourceFile, 11);
console.log(
`Token at pos 11: ${ts.SyntaxKind[token.kind]} (${token.pos}-${token.end})`
);
let current = token;
while (current) {
console.log(
` Ancestor: ${ts.SyntaxKind[current.kind]} (parent: ${
current.parent ? ts.SyntaxKind[current.parent.kind] : "none"
})`
);
if (ts.isStatement(current)) {
console.log(` ^^^ This is a statement!`);
console.log(` Parent is SourceFile? ${ts.isSourceFile(current.parent)}`);
break;
}
current = current.parent;
}

Copilot uses AI. Check for mistakes.
Comment on lines +1595 to +1602
for (const stmt of statements) {
const intersects = !(
stmt.end <= selectedRange.pos || stmt.pos >= selectedRange.end
);
const fullyInside =
stmt.pos >= selectedRange.pos && stmt.end <= selectedRange.end;

if (intersects && !fullyInside) {
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop iterates through all statements in the file to check for partial overlaps, which is O(n) where n is the total number of statements. However, since selectedStatements was already filtered from statements, you could optimize by only checking statements that weren't already in selectedStatements, or by using a more efficient range-based approach. For large files, this could be a performance concern.

Suggested change
for (const stmt of statements) {
const intersects = !(
stmt.end <= selectedRange.pos || stmt.pos >= selectedRange.end
);
const fullyInside =
stmt.pos >= selectedRange.pos && stmt.end <= selectedRange.end;
if (intersects && !fullyInside) {
const selectedSet = new Set(selectedStatements);
for (const stmt of statements) {
if (selectedSet.has(stmt)) continue;
const intersects = !(
stmt.end <= selectedRange.pos || stmt.pos >= selectedRange.end
);
// Only care about statements not fully inside the selection
if (intersects) {

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 160
ModifierLike,
ModuleDeclaration,
ModuleKind,
moduleSpecifiers,
moduleSpecifierToValidIdentifier,
DeclarationStatement,
mapDefined,
StringLiteralLike,
NamedImportBindings,
Node,
NodeFlags,
nodeSeenTracker,
normalizePath,
ObjectBindingElementWithoutPropertyName,
Program,
PropertyAccessExpression,
PropertyAssignment,
QuotePreference,
rangeContainsRange,
RefactorContext,
RefactorEditInfo,
isVariableDeclaration,
VariableDeclaration,
VariableDeclarationList,
CallExpression,
cast,
ImportDeclaration,
ImportEqualsDeclaration,
VariableStatement,
ExternalModuleReference,
RequireOrImportCall,
resolvePath,
ScriptTarget,
skipAlias,
some,
SourceFile,
Statement,
StringLiteralLike,
Symbol,
SymbolFlags,
symbolNameNoDefault,
SyntaxKind,
getNameForExportedSymbol,
getEmitScriptTarget,
ExportKind,
takeWhile,
textChanges,
TextRange,
rangeContainsRange,
isImportSpecifier,
isImportClause,
isNamespaceImport,
some,
every,
TransformFlags,
tryCast,
isDeclarationName,
isValidTypeOnlyAliasUseSite,
findIndex,
arrayFrom,
ClassDeclaration,
FunctionDeclaration,
EnumDeclaration,
TypeAliasDeclaration,
TypeChecker,
TypeNode,
UserPreferences,
VariableDeclaration,
VariableDeclarationList,
VariableStatement,
InterfaceDeclaration,
ModuleDeclaration,
BinaryExpression,
PropertyAccessExpression,
Declaration,
getRefactorContextSpan,
createTextRangeFromSpan,
forEachKey,
firstDefined,
findLast,
isExportDeclaration,
isExportSpecifier,
isNamedExports,
isNamedImports,
isObjectBindingPattern,
isVariableDeclarationInitializedToRequire,
getSourceFileOfNode,
isFunctionLikeDeclaration,
isBinaryExpression,
getAssignmentDeclarationKind,
AssignmentDeclarationKind,
isOmittedExpression,
length,
BindingElement,
ExpressionStatement,
ExportDeclaration,
extensionFromPath,
importFromModuleSpecifier,
filter,
} from "../_namespaces/ts.js";
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The massive import reorganization (lines 1-160) makes this PR much harder to review and creates unnecessary merge conflicts. The imports have been completely reordered alphabetically within the block, but this doesn't relate to the stated fix for unbraced if/else statements. Import reorganization should be done in a separate PR to keep changes focused. Only the newly added imports (like isBlock, isIfStatement, etc. on lines 7-11) are actually needed for this fix.

Copilot uses AI. Check for mistakes.
Comment on lines +363 to +368
const el = ifStmt.elseStatement;
if (el && !isBlock(el) && !isIfStatement(el)) {
const elseStart = el.getStart(file);
const elseEnd = el.end;
if (startPosition >= elseStart && endPosition <= elseEnd) return true;
}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for else-if statements (!isIfStatement(el)) prevents blocking the refactor for statements inside unbraced else-if chains like if (a) x = 1; else if (b) y = 2;. However, the y = 2 statement here is still an unbraced statement that would suffer from the same issue this PR aims to fix. The condition should only check !isBlock(el) to consistently block all unbraced else clauses, including else-if.

Copilot uses AI. Check for mistakes.
// Final validation: ensure statements are not inside unbraced if/else
// This is a safety check in case getStatementsToMove was bypassed
if (isInsideIfStatementWithoutBraces(context)) {
return; // Silently abort - should have been caught earlier
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The safety check that silently returns when statements are inside unbraced if/else is defensive programming, but silently failing could mask bugs or confuse users. The comment says "should have been caught earlier" - if this case should never occur, consider using Debug.assert or Debug.fail instead of silently returning. If it can legitimately occur, the function should return an error that provides feedback to the user.

Suggested change
return; // Silently abort - should have been caught earlier
Debug.fail("Attempted to move statements inside unbraced if/else. This should have been caught earlier.");

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +26
// @filename: mytypes.ts
export class MyClass {
public a: number;
}

export type MyClassAlias = MyClass;

// @filename: main.ts
import {MyClassAlias} from './mytypes';

let a: MyClassAlias = new MyClassAlias(); // Error: should show better message
let b = MyClassAlias; // Error: should show better message

// Test with local type alias
type LocalAlias = string;
let c = LocalAlias; // Error: should show better message

// Test with interface
interface MyInterface {
prop: string;
}
let d = MyInterface; // Error: should show better message

// Test with generic type alias
type GenericAlias<T> = T[];
let e = GenericAlias; // Error: should show better message
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file appears unrelated to the PR's stated purpose of fixing "Move to file" refactor for unbraced if/else statements. The test is about type aliases being used as values, not about the move-to-file refactor. This should either be removed from this PR or the PR description should be updated to explain why this test is included.

Suggested change
// @filename: mytypes.ts
export class MyClass {
public a: number;
}
export type MyClassAlias = MyClass;
// @filename: main.ts
import {MyClassAlias} from './mytypes';
let a: MyClassAlias = new MyClassAlias(); // Error: should show better message
let b = MyClassAlias; // Error: should show better message
// Test with local type alias
type LocalAlias = string;
let c = LocalAlias; // Error: should show better message
// Test with interface
interface MyInterface {
prop: string;
}
let d = MyInterface; // Error: should show better message
// Test with generic type alias
type GenericAlias<T> = T[];
let e = GenericAlias; // Error: should show better message

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

2 participants