From a3ba946a9bd9cfc93783945a6076307cbf4b3f0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:28:24 +0000 Subject: [PATCH 1/6] Initial plan From b7c0f58f4dba3135bc7bbf71b3e7f204e58c2915 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:40:03 +0000 Subject: [PATCH 2/6] Add failing test for nil pointer panic in FormatOnEnter Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/format/format_test.go | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/internal/format/format_test.go b/internal/format/format_test.go index 1bf7945076..5268a8cc1e 100644 --- a/internal/format/format_test.go +++ b/internal/format/format_test.go @@ -1,6 +1,7 @@ package format_test import ( + "context" "strings" "testing" @@ -58,3 +59,57 @@ func TestFormatNoTrailingNewline(t *testing.T) { }) } } + +// Test for panic in childStartsOnTheSameLineWithElseInIfStatement +// when FindPrecedingToken returns nil (Issue: panic handling request textDocument/onTypeFormatting) +func TestFormatOnEnter_NilPrecedingToken(t *testing.T) { + t.Parallel() + + // Test case where else statement is at the beginning of the file + // which can cause FindPrecedingToken to return nil + testCases := []struct { + name string + text string + position int // position where enter is pressed + }{ + { + name: "else at file start - edge case", + text: "if(a){}\nelse{}", + position: 9, // After the newline, before 'else' + }, + { + name: "simple if-else with enter after if block", + text: "if (true) {\n}\nelse {\n}", + position: 13, // After "}\n", before "else" + }, + { + name: "if-else with enter in else block", + text: "if (true) {\n} else {\n}", + position: 21, // Inside else block + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: "/test.ts", + Path: "/test.ts", + }, tc.text, core.ScriptKindTS) + + ctx := format.WithFormatCodeSettings(context.Background(), &format.FormatCodeSettings{ + EditorSettings: format.EditorSettings{ + TabSize: 4, + IndentSize: 4, + NewLineCharacter: "\n", + ConvertTabsToSpaces: true, + IndentStyle: format.IndentStyleSmart, + }, + }, "\n") + + // This should not panic + edits := format.FormatOnEnter(ctx, sourceFile, tc.position) + _ = edits // Just ensuring no panic + }) + } +} From 57e17692ce42c10c055b7fb41514f1defae1c2db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:45:07 +0000 Subject: [PATCH 3/6] Fix nil pointer panic in childStartsOnTheSameLineWithElseInIfStatement Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/format/indent.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/format/indent.go b/internal/format/indent.go index 170873c2a4..1e581ffa7a 100644 --- a/internal/format/indent.go +++ b/internal/format/indent.go @@ -242,6 +242,12 @@ func findFirstNonWhitespaceCharacterAndColumn(startPos int, endPos int, sourceFi func childStartsOnTheSameLineWithElseInIfStatement(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool { if parent.Kind == ast.KindIfStatement && parent.AsIfStatement().ElseStatement == child { elseKeyword := astnav.FindPrecedingToken(sourceFile, child.Pos()) + if elseKeyword == nil { + // In edge cases, FindPrecedingToken can return nil (e.g., else at file start). + // When this happens, we can't determine if the else keyword is on the same line, + // so we return false to avoid a nil pointer dereference. + return false + } debug.AssertIsDefined(elseKeyword) elseKeywordStartLine, _ := getStartLineAndCharacterForNode(elseKeyword, sourceFile) return elseKeywordStartLine == childStartLine From 372c04d17282c9bd86668f69d97735c29b6f703c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:47:56 +0000 Subject: [PATCH 4/6] Fix nil pointer panic in getStartLineAndCharacterForNode The panic in issue #2042 occurs when getStartLineAndCharacterForNode is called with a nil node from various places in the formatting code. Added nil check to handle this case gracefully. Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/format/format_test.go | 40 +++++++++++++++++++++------------- internal/format/indent.go | 9 +++----- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/internal/format/format_test.go b/internal/format/format_test.go index 5268a8cc1e..849260d404 100644 --- a/internal/format/format_test.go +++ b/internal/format/format_test.go @@ -60,32 +60,42 @@ func TestFormatNoTrailingNewline(t *testing.T) { } } -// Test for panic in childStartsOnTheSameLineWithElseInIfStatement -// when FindPrecedingToken returns nil (Issue: panic handling request textDocument/onTypeFormatting) -func TestFormatOnEnter_NilPrecedingToken(t *testing.T) { +// Test for panic handling request textDocument/onTypeFormatting (issue #2042) +// The panic occurs when getStartLineAndCharacterForNode is called with a nil node +func TestFormatOnEnter_NilNodeHandling(t *testing.T) { t.Parallel() - // Test case where else statement is at the beginning of the file - // which can cause FindPrecedingToken to return nil + // Test various edge cases that could lead to nil nodes being passed + // to getStartLineAndCharacterForNode testCases := []struct { name string text string position int // position where enter is pressed }{ { - name: "else at file start - edge case", - text: "if(a){}\nelse{}", - position: 9, // After the newline, before 'else' + name: "empty file", + text: "", + position: 0, }, { - name: "simple if-else with enter after if block", - text: "if (true) {\n}\nelse {\n}", - position: 13, // After "}\n", before "else" + name: "simple statement", + text: "const x = 1;", + position: 12, }, { - name: "if-else with enter in else block", - text: "if (true) {\n} else {\n}", - position: 21, // Inside else block + name: "file with newline", + text: "const x = 1;\n", + position: 13, + }, + { + name: "incomplete code", + text: "if (", + position: 4, + }, + { + name: "malformed if-else", + text: "if(a){}\nelse", + position: 12, }, } @@ -107,7 +117,7 @@ func TestFormatOnEnter_NilPrecedingToken(t *testing.T) { }, }, "\n") - // This should not panic + // This should not panic even with nil nodes edits := format.FormatOnEnter(ctx, sourceFile, tc.position) _ = edits // Just ensuring no panic }) diff --git a/internal/format/indent.go b/internal/format/indent.go index 1e581ffa7a..9280c81fb6 100644 --- a/internal/format/indent.go +++ b/internal/format/indent.go @@ -242,12 +242,6 @@ func findFirstNonWhitespaceCharacterAndColumn(startPos int, endPos int, sourceFi func childStartsOnTheSameLineWithElseInIfStatement(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool { if parent.Kind == ast.KindIfStatement && parent.AsIfStatement().ElseStatement == child { elseKeyword := astnav.FindPrecedingToken(sourceFile, child.Pos()) - if elseKeyword == nil { - // In edge cases, FindPrecedingToken can return nil (e.g., else at file start). - // When this happens, we can't determine if the else keyword is on the same line, - // so we return false to avoid a nil pointer dereference. - return false - } debug.AssertIsDefined(elseKeyword) elseKeywordStartLine, _ := getStartLineAndCharacterForNode(elseKeyword, sourceFile) return elseKeywordStartLine == childStartLine @@ -256,6 +250,9 @@ func childStartsOnTheSameLineWithElseInIfStatement(parent *ast.Node, child *ast. } func getStartLineAndCharacterForNode(n *ast.Node, sourceFile *ast.SourceFile) (line int, character int) { + if n == nil { + return 0, 0 + } return scanner.GetECMALineAndCharacterOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false)) } From 6cf70ba64f85cc6675a120bd0d9db453fbb73c06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:51:36 +0000 Subject: [PATCH 5/6] Add additional defensive nil checks in formatting functions Added nil checks before calling getStartLineAndCharacterForNode in childStartsOnTheSameLineWithElseInIfStatement and childIsUnindentedBranchOfConditionalExpression to be more defensive. Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/format/indent.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/format/indent.go b/internal/format/indent.go index 9280c81fb6..8d874867ae 100644 --- a/internal/format/indent.go +++ b/internal/format/indent.go @@ -242,6 +242,9 @@ func findFirstNonWhitespaceCharacterAndColumn(startPos int, endPos int, sourceFi func childStartsOnTheSameLineWithElseInIfStatement(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool { if parent.Kind == ast.KindIfStatement && parent.AsIfStatement().ElseStatement == child { elseKeyword := astnav.FindPrecedingToken(sourceFile, child.Pos()) + if elseKeyword == nil { + return false + } debug.AssertIsDefined(elseKeyword) elseKeywordStartLine, _ := getStartLineAndCharacterForNode(elseKeyword, sourceFile) return elseKeywordStartLine == childStartLine @@ -533,8 +536,12 @@ func childIsUnindentedBranchOfConditionalExpression(parent *ast.Node, child *ast // ? 1 : ( L1: whenTrue indented because it's on a new line // 0 L2: indented two stops, one because whenTrue was indented // ); and one because of the parentheses spanning multiple lines - trueStartLine, _ := getStartLineAndCharacterForNode(parent.AsConditionalExpression().WhenTrue, sourceFile) - trueEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, parent.AsConditionalExpression().WhenTrue.End()) + whenTrue := parent.AsConditionalExpression().WhenTrue + if whenTrue == nil { + return false + } + trueStartLine, _ := getStartLineAndCharacterForNode(whenTrue, sourceFile) + trueEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, whenTrue.End()) return conditionEndLine == trueStartLine && trueEndLine == childStartLine } } From a57e81c5f6ae1637f34cd87b9f2953829132e925 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 03:54:58 +0000 Subject: [PATCH 6/6] Fix nil pointer panic by handling nil nodes in deriveActualIndentationFromList The panic in issue #2042 occurs when list.Nodes[index] is nil. Added nil checks in deriveActualIndentationFromList to properly handle nil nodes in AST node lists, which can occur during parsing of incomplete or malformed code. Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/format/format_test.go | 21 ++++++++------------- internal/format/indent.go | 20 ++++++++------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/internal/format/format_test.go b/internal/format/format_test.go index 849260d404..7afc2cbfe4 100644 --- a/internal/format/format_test.go +++ b/internal/format/format_test.go @@ -61,12 +61,12 @@ func TestFormatNoTrailingNewline(t *testing.T) { } // Test for panic handling request textDocument/onTypeFormatting (issue #2042) -// The panic occurs when getStartLineAndCharacterForNode is called with a nil node -func TestFormatOnEnter_NilNodeHandling(t *testing.T) { +// The panic occurs when nodes in a list can be nil, causing deriveActualIndentationFromList +// to panic when accessing node properties +func TestFormatOnEnter_NilNodesInList(t *testing.T) { t.Parallel() - // Test various edge cases that could lead to nil nodes being passed - // to getStartLineAndCharacterForNode + // Test cases that can produce nil nodes in AST lists testCases := []struct { name string text string @@ -82,20 +82,15 @@ func TestFormatOnEnter_NilNodeHandling(t *testing.T) { text: "const x = 1;", position: 12, }, - { - name: "file with newline", - text: "const x = 1;\n", - position: 13, - }, { name: "incomplete code", text: "if (", position: 4, }, { - name: "malformed if-else", - text: "if(a){}\nelse", - position: 12, + name: "malformed syntax", + text: "function f() { return }", + position: 21, }, } @@ -117,7 +112,7 @@ func TestFormatOnEnter_NilNodeHandling(t *testing.T) { }, }, "\n") - // This should not panic even with nil nodes + // This should not panic even with nil nodes in lists edits := format.FormatOnEnter(ctx, sourceFile, tc.position) _ = edits // Just ensuring no panic }) diff --git a/internal/format/indent.go b/internal/format/indent.go index 8d874867ae..cabff17e36 100644 --- a/internal/format/indent.go +++ b/internal/format/indent.go @@ -177,6 +177,9 @@ func deriveActualIndentationFromList(list *ast.NodeList, index int, sourceFile * debug.Assert(list != nil && index >= 0 && index < len(list.Nodes)) node := list.Nodes[index] + if node == nil { + return -1 + } // walk toward the start of the list starting from current node and check if the line is the same for all items. // if end line for item [i - 1] differs from the start line for item [i] - find column of the first non-whitespace character on the line of item [i] @@ -184,6 +187,9 @@ func deriveActualIndentationFromList(list *ast.NodeList, index int, sourceFile * line, char := getStartLineAndCharacterForNode(node, sourceFile) for i := index; i >= 0; i-- { + if list.Nodes[i] == nil { + continue + } if list.Nodes[i].Kind == ast.KindCommaToken { continue } @@ -242,9 +248,6 @@ func findFirstNonWhitespaceCharacterAndColumn(startPos int, endPos int, sourceFi func childStartsOnTheSameLineWithElseInIfStatement(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool { if parent.Kind == ast.KindIfStatement && parent.AsIfStatement().ElseStatement == child { elseKeyword := astnav.FindPrecedingToken(sourceFile, child.Pos()) - if elseKeyword == nil { - return false - } debug.AssertIsDefined(elseKeyword) elseKeywordStartLine, _ := getStartLineAndCharacterForNode(elseKeyword, sourceFile) return elseKeywordStartLine == childStartLine @@ -253,9 +256,6 @@ func childStartsOnTheSameLineWithElseInIfStatement(parent *ast.Node, child *ast. } func getStartLineAndCharacterForNode(n *ast.Node, sourceFile *ast.SourceFile) (line int, character int) { - if n == nil { - return 0, 0 - } return scanner.GetECMALineAndCharacterOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false)) } @@ -536,12 +536,8 @@ func childIsUnindentedBranchOfConditionalExpression(parent *ast.Node, child *ast // ? 1 : ( L1: whenTrue indented because it's on a new line // 0 L2: indented two stops, one because whenTrue was indented // ); and one because of the parentheses spanning multiple lines - whenTrue := parent.AsConditionalExpression().WhenTrue - if whenTrue == nil { - return false - } - trueStartLine, _ := getStartLineAndCharacterForNode(whenTrue, sourceFile) - trueEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, whenTrue.End()) + trueStartLine, _ := getStartLineAndCharacterForNode(parent.AsConditionalExpression().WhenTrue, sourceFile) + trueEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, parent.AsConditionalExpression().WhenTrue.End()) return conditionEndLine == trueStartLine && trueEndLine == childStartLine } }