diff --git a/internal/format/comment_test.go b/internal/format/comment_test.go index 50762ab56d..ddd4dce876 100644 --- a/internal/format/comment_test.go +++ b/internal/format/comment_test.go @@ -106,3 +106,42 @@ func TestCommentFormatting(t *testing.T) { func contains(s, substr string) bool { return len(substr) > 0 && strings.Contains(s, substr) } + +func TestSliceBoundsPanic(t *testing.T) { + t.Parallel() + + t.Run("format code with trailing semicolon should not panic", func(t *testing.T) { + t.Parallel() + ctx := format.WithFormatCodeSettings(t.Context(), &format.FormatCodeSettings{ + EditorSettings: format.EditorSettings{ + TabSize: 4, + IndentSize: 4, + BaseIndentSize: 4, + NewLineCharacter: "\n", + ConvertTabsToSpaces: true, + IndentStyle: format.IndentStyleSmart, + TrimTrailingWhitespace: true, + }, + InsertSpaceBeforeTypeAnnotation: core.TSTrue, + }, "\n") + + // Code from the issue that causes slice bounds panic + originalText := `const _enableDisposeWithListenerWarning = false + // || Boolean("TRUE") // causes a linter warning so that it cannot be pushed + ; +` + + sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: "/test.ts", + Path: "/test.ts", + }, originalText, core.ScriptKindTS) + + // This should not panic + edits := format.FormatDocument(ctx, sourceFile) + formatted := applyBulkEdits(originalText, edits) + + // Basic sanity checks + assert.Check(t, len(formatted) > 0, "formatted text should not be empty") + assert.Check(t, contains(formatted, "_enableDisposeWithListenerWarning"), "should preserve variable name") + }) +} diff --git a/internal/format/span.go b/internal/format/span.go index c8d0096bed..a44b5764b6 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -883,7 +883,12 @@ func (w *formatSpanWorker) characterToColumn(startLinePosition int, characterInL } func (w *formatSpanWorker) indentationIsDifferent(indentationString string, startLinePosition int) bool { - return indentationString != w.sourceFile.Text()[startLinePosition:startLinePosition+len(indentationString)] + // Check bounds to prevent slice panic + endPosition := startLinePosition + len(indentationString) + if endPosition > len(w.sourceFile.Text()) { + return true + } + return indentationString != w.sourceFile.Text()[startLinePosition:endPosition] } func (w *formatSpanWorker) indentTriviaItems(trivia []TextRangeWithKind, commentIndentation int, indentNextTokenOrTrivia bool, indentSingleLine func(item TextRangeWithKind)) bool { @@ -969,6 +974,12 @@ func (w *formatSpanWorker) indentMultilineComment(commentRange core.TextRange, i } func getIndentationString(indentation int, options *FormatCodeSettings) string { + // Handle negative indentation (e.g., Constants.Unknown = -1) + // Return empty string like TypeScript's repeatString does when count is negative + if indentation < 0 { + return "" + } + // go's `strings.Repeat` already has static, global caching for repeated tabs and spaces, so there's no need to cache here like in strada if !options.ConvertTabsToSpaces { tabs := int(math.Floor(float64(indentation) / float64(options.TabSize)))