Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/tsoptions"
"github.com/microsoft/TypeScript/tsc/internal/tspath"
"github.com/zeebo/xxh3"
"unicode/utf8"
)

// CheckMode
Expand Down Expand Up @@ -8785,7 +8786,9 @@ func (c *Checker) resolveCallExpression(node *ast.Node, candidatesOutArray *[]*S
if len(node.Arguments()) == 1 {
text := ast.GetSourceFileOfNode(node).Text()
options := scanner.SkipTriviaOptions{StopAfterLineBreak: true}
if stringutil.IsLineBreak(rune(text[scanner.SkipTriviaEx(text, node.Expression().End(), &options)-1])) {
pos := scanner.SkipTriviaEx(text, node.Expression().End(), &options)
lineBreak, _ := utf8.DecodeLastRuneInString(text[:pos])
if stringutil.IsLineBreak(lineBreak) {
relatedInformation = createDiagnosticForNode(node.Expression(), diagnostics.Are_you_missing_a_semicolon)
}
}
Expand Down
9 changes: 6 additions & 3 deletions tsc/internal/ls/change/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package change

import (
"slices"
"unicode/utf8"

"github.com/microsoft/TypeScript/tsc/internal/ast"
"github.com/microsoft/TypeScript/tsc/internal/astnav"
Expand Down Expand Up @@ -234,12 +235,14 @@ func (t *Tracker) endPositionToDeleteNodeInList(sourceFile *ast.SourceFile, node
if isSeparator(prevNode, prevToken) {
pos := scanner.SkipTriviaEx(sourceFile.Text(), token.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true})
if positionsAreOnSameLine(astnav.GetStartOfNode(prevToken, sourceFile, false), astnav.GetStartOfNode(token, sourceFile, false), sourceFile) {
if pos > 0 && stringutil.IsLineBreak(rune(sourceFile.Text()[pos-1])) {
return pos - 1
lastRune, size := utf8.DecodeLastRuneInString(sourceFile.Text()[:pos])
if pos > 0 && stringutil.IsLineBreak(lastRune) {
return pos - size
}
return pos
}
if stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) {
lineBreak, _ := utf8.DecodeRuneInString(sourceFile.Text()[pos:])
if stringutil.IsLineBreak(lineBreak) {
return pos
}
}
Expand Down
11 changes: 8 additions & 3 deletions tsc/internal/ls/change/tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package change
import (
"context"
"slices"
"unicode/utf8"

"github.com/microsoft/TypeScript/tsc/internal/ast"
"github.com/microsoft/TypeScript/tsc/internal/astnav"
Expand Down Expand Up @@ -498,9 +499,13 @@ func (t *Tracker) InsertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.N
indentation := format.FindFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, t.formatSettings)
// insert element before the line break on the line that contains 'after' element
insertPos := scanner.SkipTriviaEx(sourceFile.Text(), end, &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: false})
// find position before "\n" or "\r\n"
for insertPos != end && stringutil.IsLineBreak(rune(sourceFile.Text()[insertPos-1])) {
insertPos--
// find position before the line break
for insertPos != end {
lastRune, size := utf8.DecodeLastRuneInString(sourceFile.Text()[end:insertPos])
if !stringutil.IsLineBreak(lastRune) {
break
}
insertPos -= size
}
insertLSPos := core.TextPos(insertPos)
t.ReplaceRange(
Expand Down
4 changes: 3 additions & 1 deletion tsc/internal/ls/change/trackerimpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"slices"
"strings"
"unicode"
"unicode/utf8"

"github.com/microsoft/TypeScript/tsc/internal/ast"
"github.com/microsoft/TypeScript/tsc/internal/astnav"
Expand Down Expand Up @@ -394,7 +395,8 @@ func (t *Tracker) getAdjustedEndPosition(sourceFile *ast.SourceFile, node *ast.N

newEnd := scanner.SkipTriviaEx(sourceFile.Text(), node.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true})

if newEnd != node.End() && (TrailingTriviaOption == TrailingTriviaOptionInclude || stringutil.IsLineBreak(rune(sourceFile.Text()[newEnd-1]))) {
lastRune, _ := utf8.DecodeLastRuneInString(sourceFile.Text()[:newEnd])
if newEnd != node.End() && (TrailingTriviaOption == TrailingTriviaOptionInclude || stringutil.IsLineBreak(lastRune)) {
return newEnd
}
return node.End()
Expand Down
82 changes: 80 additions & 2 deletions tsc/internal/ls/change/trackerimpl_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package change

import (
"testing"

"context"
"github.com/microsoft/TypeScript/tsc/internal/ast"
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/ls/lsutil"
"github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto"
"github.com/microsoft/TypeScript/tsc/internal/parser"
"strings"
"testing"
)

func TestTextEditsConflictAtSameInsertionPointAcrossProjections(t *testing.T) {
Expand All @@ -20,3 +25,76 @@ func TestTextEditsConflictAtSameInsertionPointAcrossProjections(t *testing.T) {
t.Fatal("insertions at the same position within one projection should retain their existing ordering")
}
}
func TestInsertNodeInListAfterUnicodeLineSeparators(t *testing.T) {
t.Parallel()

tests := []struct {
name string
separator string
}{
{name: "U+2028", separator: "\u2028"},
{name: "U+2029", separator: "\u2029"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

text := "const values = [\n 1,\n 2 " + test.separator + "];"

sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/index.ts",
PathKey: "/index.ts",
}, text, core.ScriptKindTS)

declaration := sourceFile.Statements.Nodes[0].
AsVariableStatement().
DeclarationList.
AsVariableDeclarationList().
Declarations.Nodes[0]

array := declaration.AsVariableDeclaration().Initializer
elements := array.AsArrayLiteralExpression().Elements.Nodes
after := elements[1]

tracker := NewTracker(
context.Background(),
core.EmptyCompilerOptions,
lsutil.GetDefaultFormatCodeSettings(),
nil,
)

newNode := tracker.NewNumericLiteral("3", ast.TokenFlagsNone)

tracker.InsertNodeInListAfter(
sourceFile,
after,
newNode,
array.AsArrayLiteralExpression().Elements,
)

expectedPosition := strings.Index(text, test.separator)
found := false

for _, edit := range tracker.changes.Get(sourceFile) {
if edit.Node == newNode {
found = true
actualPosition := edit.TextRange.Pos()

if actualPosition != expectedPosition {
t.Fatalf(
"insertion position = %d, want %d",
actualPosition,
expectedPosition,
)
}
break
}
}

if !found {
t.Fatal("could not find the edit inserting the new node")
}
})
}
}
7 changes: 7 additions & 0 deletions tsc/internal/scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2388,6 +2388,13 @@ func SkipTriviaEx(text string, pos int, options *SkipTriviaOptions) int {
}
canConsumeStar = options.InJSDoc
continue
case '\u2028', '\u2029':
pos += size
if options.StopAfterLineBreak {
return pos
}
canConsumeStar = options.InJSDoc
continue
case '\t', '\v', '\f', ' ':
pos++
continue
Expand Down
70 changes: 70 additions & 0 deletions tsc/internal/scanner/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,73 @@ func TestRegExpLiteralReplacementCharacterInClassRange(t *testing.T) {
})
}
}

func TestSkipTriviaExHandlesUnicodeLineSeparators(t *testing.T) {
t.Parallel()

tests := []struct {
name string
text string
stopAfterLineBreak bool
expected int
}{
{
name: "U+2028 stops after line separator",
text: " \u2028 next",
stopAfterLineBreak: true,
expected: len(" \u2028"),
},
{
name: "U+2029 stops after paragraph separator",
text: " \u2029 next",
stopAfterLineBreak: true,
expected: len(" \u2029"),
},
{
name: "U+2028 continues when not stopping",
text: " \u2028 next",
expected: len(" \u2028 "),
},
{
name: "U+2029 continues when not stopping",
text: " \u2029 next",
expected: len(" \u2029 "),
},
{
name: "LF preserves existing behavior",
text: " \n next",
stopAfterLineBreak: true,
expected: len(" \n"),
},
{
name: "CRLF preserves existing behavior",
text: " \r\n next",
stopAfterLineBreak: true,
expected: len(" \r\n"),
},
{
name: "line comment stops at U+2028",
text: "// comment\u2028 next",
stopAfterLineBreak: true,
expected: len("// comment\u2028"),
},
{
name: "line comment continues after U+2029",
text: "// comment\u2029 next",
expected: len("// comment\u2029 "),
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

options := &SkipTriviaOptions{
StopAfterLineBreak: test.stopAfterLineBreak,
}

actual := SkipTriviaEx(test.text, 0, options)
assert.Equal(t, actual, test.expected)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ betterErrorForAccidentalCall.ts(10,1): error TS2349: This expression is not call
Type 'String' has no call signatures.
betterErrorForAccidentalCall.ts(13,1): error TS2349: This expression is not callable.
Type 'String' has no call signatures.
betterErrorForAccidentalCall.ts(16,1): error TS2349: This expression is not callable.
Type 'String' has no call signatures.
betterErrorForAccidentalCall.ts(19,1): error TS2349: This expression is not callable.
Type 'String' has no call signatures.


==== betterErrorForAccidentalCall.ts (5 errors) ====
==== betterErrorForAccidentalCall.ts (7 errors) ====
declare function foo(): string;

foo()(1 as number).toString();
Expand Down Expand Up @@ -43,4 +47,16 @@ betterErrorForAccidentalCall.ts(13,1): error TS2349: This expression is not call
!!! error TS2349: Type 'String' has no call signatures.
!!! related TS2734 betterErrorForAccidentalCall.ts:13:1: Are you missing a semicolon?
(<number>1).toString();


foo()
(1 as number).toString();
~~~~~
!!! error TS2349: This expression is not callable.
!!! error TS2349: Type 'String' has no call signatures.
!!! related TS2734 betterErrorForAccidentalCall.ts:16:1: Are you missing a semicolon?

foo()
(1 as number).toString();


!!! error TS2349: This expression is not callable.
!!! error TS2349: Type 'String' has no call signatures.
!!! related TS2734 betterErrorForAccidentalCall.ts:19:1: Are you missing a semicolon?
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ foo()

foo()
(<number>1).toString();

foo()
(1 as number).toString();

foo()
(1 as number).toString();


//// [betterErrorForAccidentalCall.js]
Expand All @@ -24,3 +28,5 @@ foo()(1).toString();
foo()(1).toString();
foo()(1 + 2).toString();
foo()(1).toString();
foo()(1).toString();
foo()(1).toString();
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,13 @@ foo()

(<number>1).toString();

foo()
>foo : Symbol(foo, Decl(betterErrorForAccidentalCall.ts, 0, 0))

(1 as number).toString();

foo()
>foo : Symbol(foo, Decl(betterErrorForAccidentalCall.ts, 0, 0))

(1 as number).toString();

Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,27 @@ foo()
>1 : 1
>toString : any

foo()
>foo()
(1 as number).toString() : any
>foo()
(1 as number).toString : any
>foo()
(1 as number) : any
>foo() : string
>foo : () => string

(1 as number).toString();
>1 as number : number
>1 : 1
>toString : any

foo()
>foo()
(1 as number).toString() : any
>foo()
(1 as number).toString : any
>foo()
(1 as number) : any
>foo() : string
>foo : () => string

(1 as number).toString();
>1 as number : number
>1 : 1
>toString : any

Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ foo()

foo()
(<number>1).toString();

foo()
(1 as number).toString();

foo()
(1 as number).toString();