diff --git a/internal/format/comment_test.go b/internal/format/comment_test.go new file mode 100644 index 0000000000..b7063bfe5d --- /dev/null +++ b/internal/format/comment_test.go @@ -0,0 +1,71 @@ +package format_test + +import ( + "strings" + "testing" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/parser" + "gotest.tools/v3/assert" +) + +func TestCommentFormatting(t *testing.T) { + t.Parallel() + + t.Run("format comment issue reproduction", 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") + + // Original code that causes the bug + originalText := `class C { + /** + * + */ + async x() {} +}` + + sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: "/test.ts", + Path: "/test.ts", + }, originalText, core.ScriptKindTS) + + // Apply formatting once + edits := format.FormatDocument(ctx, sourceFile) + firstFormatted := applyBulkEdits(originalText, edits) + + // Check that the asterisk is not corrupted + assert.Check(t, !contains(firstFormatted, "*/\n /"), "should not corrupt */ to /") + assert.Check(t, contains(firstFormatted, "*/"), "should preserve */ token") + assert.Check(t, contains(firstFormatted, "async"), "should preserve async keyword") + + // Apply formatting a second time to test stability + sourceFile2 := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: "/test.ts", + Path: "/test.ts", + }, firstFormatted, core.ScriptKindTS) + + edits2 := format.FormatDocument(ctx, sourceFile2) + secondFormatted := applyBulkEdits(firstFormatted, edits2) + + // Check that second formatting doesn't introduce corruption + assert.Check(t, !contains(secondFormatted, " sync x()"), "should not corrupt async to sync") + assert.Check(t, contains(secondFormatted, "async"), "should preserve async keyword on second pass") + }) +} + +func contains(s, substr string) bool { + return len(substr) > 0 && strings.Contains(s, substr) +} diff --git a/internal/format/span.go b/internal/format/span.go index 16695cb19c..a457e71596 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -922,7 +922,7 @@ func (w *formatSpanWorker) indentMultilineComment(commentRange core.TextRange, i for line := startLine; line < endLine; line++ { endOfLine := scanner.GetEndLinePosition(w.sourceFile, line) parts = append(parts, core.NewTextRange(startPos, endOfLine)) - startPos = int(scanner.GetLineStarts(w.sourceFile)[line]) + startPos = int(scanner.GetLineStarts(w.sourceFile)[line+1]) } if indentFinalLine { @@ -953,12 +953,24 @@ func (w *formatSpanWorker) indentMultilineComment(commentRange core.TextRange, i if i != 0 { nonWhitespaceCharacter, nonWhitespaceColumn = findFirstNonWhitespaceCharacterAndColumn(parts[i].Pos(), parts[i].End(), w.sourceFile, w.formattingContext.Options) } + + // Check if the first non-whitespace character is '*' (comment continuation) + // If so, we should only replace the whitespace before the '*', not the '*' itself + charactersToReplace := nonWhitespaceCharacter + if nonWhitespaceCharacter > 0 && startLinePos+nonWhitespaceCharacter < len(w.sourceFile.Text()) { + firstNonWhitespaceChar := w.sourceFile.Text()[startLinePos+nonWhitespaceCharacter] + if firstNonWhitespaceChar == '*' { + // Only replace whitespace before the '*', not the '*' itself + charactersToReplace = nonWhitespaceCharacter - 1 + } + } + newIndentation := nonWhitespaceColumn + delta if newIndentation > 0 { indentationString := getIndentationString(newIndentation, w.formattingContext.Options) - w.recordReplace(startLinePos, nonWhitespaceCharacter, indentationString) + w.recordReplace(startLinePos, charactersToReplace, indentationString) } else { - w.recordDelete(startLinePos, nonWhitespaceCharacter) + w.recordDelete(startLinePos, charactersToReplace) } startLine++