475 lines
13 KiB
Go
475 lines
13 KiB
Go
package astnav_test
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/parser"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/repo"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/testutil/baseline"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/testutil/jstest"
|
|
"gotest.tools/v3/assert"
|
|
)
|
|
|
|
var testFiles = []string{
|
|
filepath.Join(repo.TypeScriptSubmodulePath, "src/services/mapCode.ts"),
|
|
}
|
|
|
|
func TestGetTokenAtPosition(t *testing.T) {
|
|
t.Parallel()
|
|
repo.SkipIfNoTypeScriptSubmodule(t)
|
|
jstest.SkipIfNoNodeJS(t)
|
|
|
|
t.Run("baseline", func(t *testing.T) {
|
|
t.Parallel()
|
|
baselineTokens(
|
|
t,
|
|
"GetTokenAtPosition",
|
|
false, /*includeEOF*/
|
|
func(fileText string, positions []int) []*tokenInfo {
|
|
return tsGetTokensAtPositions(t, fileText, positions)
|
|
},
|
|
func(file *ast.SourceFile, pos int) *tokenInfo {
|
|
return toTokenInfo(astnav.GetTokenAtPosition(file, pos))
|
|
},
|
|
)
|
|
})
|
|
|
|
t.Run("pointer equality", func(t *testing.T) {
|
|
t.Parallel()
|
|
fileText := `
|
|
function foo() {
|
|
return 0;
|
|
}
|
|
`
|
|
file := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
|
FileName: "/file.ts",
|
|
Path: "/file.ts",
|
|
}, fileText, core.ScriptKindTS)
|
|
assert.Equal(t, astnav.GetTokenAtPosition(file, 0), astnav.GetTokenAtPosition(file, 0))
|
|
})
|
|
}
|
|
|
|
func TestGetTouchingPropertyName(t *testing.T) {
|
|
t.Parallel()
|
|
jstest.SkipIfNoNodeJS(t)
|
|
repo.SkipIfNoTypeScriptSubmodule(t)
|
|
|
|
baselineTokens(
|
|
t,
|
|
"GetTouchingPropertyName",
|
|
false, /*includeEOF*/
|
|
func(fileText string, positions []int) []*tokenInfo {
|
|
return tsGetTouchingPropertyName(t, fileText, positions)
|
|
},
|
|
func(file *ast.SourceFile, pos int) *tokenInfo {
|
|
return toTokenInfo(astnav.GetTouchingPropertyName(file, pos))
|
|
},
|
|
)
|
|
}
|
|
|
|
func baselineTokens(t *testing.T, testName string, includeEOF bool, getTSTokens func(fileText string, positions []int) []*tokenInfo, getGoToken func(file *ast.SourceFile, pos int) *tokenInfo) {
|
|
for _, fileName := range testFiles {
|
|
t.Run(filepath.Base(fileName), func(t *testing.T) {
|
|
t.Parallel()
|
|
fileText, err := os.ReadFile(fileName)
|
|
assert.NilError(t, err)
|
|
|
|
positions := make([]int, len(fileText)+core.IfElse(includeEOF, 1, 0))
|
|
for i := range positions {
|
|
positions[i] = i
|
|
}
|
|
tsTokens := getTSTokens(string(fileText), positions)
|
|
file := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
|
FileName: "/file.ts",
|
|
Path: "/file.ts",
|
|
}, string(fileText), core.ScriptKindTS)
|
|
|
|
var output strings.Builder
|
|
currentRange := core.NewTextRange(0, 0)
|
|
currentDiff := tokenDiff{}
|
|
|
|
for pos, tsToken := range tsTokens {
|
|
goToken := getGoToken(file, pos)
|
|
diff := tokenDiff{goToken: goToken, tsToken: tsToken}
|
|
|
|
if !diffEqual(currentDiff, diff) {
|
|
if !tokensEqual(currentDiff.goToken, currentDiff.tsToken) {
|
|
writeRangeDiff(&output, file, currentDiff, currentRange, pos)
|
|
}
|
|
currentDiff = diff
|
|
currentRange = core.NewTextRange(pos, pos)
|
|
}
|
|
currentRange = currentRange.WithEnd(pos)
|
|
}
|
|
|
|
if !tokensEqual(currentDiff.goToken, currentDiff.tsToken) {
|
|
writeRangeDiff(&output, file, currentDiff, currentRange, len(tsTokens)-1)
|
|
}
|
|
|
|
baseline.Run(
|
|
t,
|
|
fmt.Sprintf("%s.%s.baseline.txt", testName, filepath.Base(fileName)),
|
|
core.IfElse(output.Len() > 0, output.String(), baseline.NoContent),
|
|
baseline.Options{
|
|
Subfolder: "astnav",
|
|
},
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
type tokenDiff struct {
|
|
goToken *tokenInfo
|
|
tsToken *tokenInfo
|
|
}
|
|
|
|
type tokenInfo struct {
|
|
Kind string `json:"kind"`
|
|
Pos int `json:"pos"`
|
|
End int `json:"end"`
|
|
}
|
|
|
|
func toTokenInfo(node *ast.Node) *tokenInfo {
|
|
if node == nil {
|
|
return nil
|
|
}
|
|
kind := strings.Replace(node.Kind.String(), "Kind", "", 1)
|
|
switch kind {
|
|
case "EndOfFile":
|
|
kind = "EndOfFileToken"
|
|
}
|
|
return &tokenInfo{
|
|
Kind: kind,
|
|
Pos: node.Pos(),
|
|
End: node.End(),
|
|
}
|
|
}
|
|
|
|
func diffEqual(a, b tokenDiff) bool {
|
|
return tokensEqual(a.goToken, b.goToken) && tokensEqual(a.tsToken, b.tsToken)
|
|
}
|
|
|
|
func tokensEqual(t1, t2 *tokenInfo) bool {
|
|
if t1 == nil || t2 == nil {
|
|
return t1 == t2
|
|
}
|
|
return *t1 == *t2
|
|
}
|
|
|
|
func tsGetTokensAtPositions(t testing.TB, fileText string, positions []int) []*tokenInfo {
|
|
dir := t.TempDir()
|
|
err := os.WriteFile(filepath.Join(dir, "file.ts"), []byte(fileText), 0o644)
|
|
assert.NilError(t, err)
|
|
|
|
err = os.WriteFile(filepath.Join(dir, "positions.json"), []byte(core.Must(core.StringifyJson(positions, "", ""))), 0o644)
|
|
assert.NilError(t, err)
|
|
|
|
script := `
|
|
import fs from "fs";
|
|
export default (ts) => {
|
|
const positions = JSON.parse(fs.readFileSync("positions.json", "utf8"));
|
|
const fileText = fs.readFileSync("file.ts", "utf8");
|
|
const file = ts.createSourceFile(
|
|
"file.ts",
|
|
fileText,
|
|
{ languageVersion: ts.ScriptTarget.Latest, jsDocParsingMode: ts.JSDocParsingMode.ParseAll },
|
|
/*setParentNodes*/ true
|
|
);
|
|
return positions.map(position => {
|
|
let token = ts.getTokenAtPosition(file, position);
|
|
if (token.kind === ts.SyntaxKind.SyntaxList) {
|
|
token = token.parent;
|
|
}
|
|
return {
|
|
kind: ts.Debug.formatSyntaxKind(token.kind),
|
|
pos: token.pos,
|
|
end: token.end,
|
|
};
|
|
});
|
|
};`
|
|
|
|
info, err := jstest.EvalNodeScriptWithTS[[]*tokenInfo](t, script, dir, "")
|
|
assert.NilError(t, err)
|
|
return info
|
|
}
|
|
|
|
func tsGetTouchingPropertyName(t testing.TB, fileText string, positions []int) []*tokenInfo {
|
|
dir := t.TempDir()
|
|
err := os.WriteFile(filepath.Join(dir, "file.ts"), []byte(fileText), 0o644)
|
|
assert.NilError(t, err)
|
|
|
|
err = os.WriteFile(filepath.Join(dir, "positions.json"), []byte(core.Must(core.StringifyJson(positions, "", ""))), 0o644)
|
|
assert.NilError(t, err)
|
|
|
|
script := `
|
|
import fs from "fs";
|
|
export default (ts) => {
|
|
const positions = JSON.parse(fs.readFileSync("positions.json", "utf8"));
|
|
const fileText = fs.readFileSync("file.ts", "utf8");
|
|
const file = ts.createSourceFile(
|
|
"file.ts",
|
|
fileText,
|
|
{ languageVersion: ts.ScriptTarget.Latest, jsDocParsingMode: ts.JSDocParsingMode.ParseAll },
|
|
/*setParentNodes*/ true
|
|
);
|
|
return positions.map(position => {
|
|
let token = ts.getTouchingPropertyName(file, position);
|
|
if (token.kind === ts.SyntaxKind.SyntaxList) {
|
|
token = token.parent;
|
|
}
|
|
return {
|
|
kind: ts.Debug.formatSyntaxKind(token.kind),
|
|
pos: token.pos,
|
|
end: token.end,
|
|
};
|
|
});
|
|
};`
|
|
|
|
info, err := jstest.EvalNodeScriptWithTS[[]*tokenInfo](t, script, dir, "")
|
|
assert.NilError(t, err)
|
|
return info
|
|
}
|
|
|
|
func writeRangeDiff(output *strings.Builder, file *ast.SourceFile, diff tokenDiff, rng core.TextRange, position int) {
|
|
lines := file.ECMALineMap()
|
|
|
|
tsTokenPos := position
|
|
goTokenPos := position
|
|
tsTokenEnd := position
|
|
goTokenEnd := position
|
|
if diff.tsToken != nil {
|
|
tsTokenPos = diff.tsToken.Pos
|
|
tsTokenEnd = diff.tsToken.End
|
|
}
|
|
if diff.goToken != nil {
|
|
goTokenPos = diff.goToken.Pos
|
|
goTokenEnd = diff.goToken.End
|
|
}
|
|
tsStartLine, _ := core.PositionToLineAndCharacter(tsTokenPos, lines)
|
|
tsEndLine, _ := core.PositionToLineAndCharacter(tsTokenEnd, lines)
|
|
goStartLine, _ := core.PositionToLineAndCharacter(goTokenPos, lines)
|
|
goEndLine, _ := core.PositionToLineAndCharacter(goTokenEnd, lines)
|
|
|
|
contextLines := 2
|
|
startLine := min(tsStartLine, goStartLine)
|
|
endLine := max(tsEndLine, goEndLine)
|
|
markerLines := []int{tsStartLine, tsEndLine, goStartLine, goEndLine}
|
|
slices.Sort(markerLines)
|
|
contextStart := max(0, startLine-contextLines)
|
|
contextEnd := min(len(lines)-1, endLine+contextLines)
|
|
digits := len(strconv.Itoa(contextEnd))
|
|
|
|
shouldTruncate := func(line int) (result bool, skipTo int) {
|
|
index, _ := slices.BinarySearch(markerLines, line)
|
|
if index == 0 || index == len(markerLines) {
|
|
return false, 0
|
|
}
|
|
low := markerLines[index-1]
|
|
high := markerLines[index]
|
|
if line-low > 5 && high-line > 5 {
|
|
return true, high - 5
|
|
}
|
|
return false, 0
|
|
}
|
|
|
|
if output.Len() > 0 {
|
|
output.WriteString("\n\n")
|
|
}
|
|
|
|
output.WriteString(fmt.Sprintf("〚Positions: [%d, %d]〛\n", rng.Pos(), rng.End()))
|
|
if diff.tsToken != nil {
|
|
output.WriteString(fmt.Sprintf("【TS: %s [%d, %d)】\n", diff.tsToken.Kind, tsTokenPos, tsTokenEnd))
|
|
} else {
|
|
output.WriteString("【TS: nil】\n")
|
|
}
|
|
if diff.goToken != nil {
|
|
output.WriteString(fmt.Sprintf("《Go: %s [%d, %d)》\n", diff.goToken.Kind, goTokenPos, goTokenEnd))
|
|
} else {
|
|
output.WriteString("《Go: nil》\n")
|
|
}
|
|
for line := contextStart; line <= contextEnd; line++ {
|
|
if truncate, skipTo := shouldTruncate(line); truncate {
|
|
output.WriteString(fmt.Sprintf("%s │........ %d lines omitted ........\n", strings.Repeat(" ", digits), skipTo-line+1))
|
|
line = skipTo
|
|
}
|
|
output.WriteString(fmt.Sprintf("%*d │", digits, line+1))
|
|
end := len(file.Text()) + 1
|
|
if line < len(lines)-1 {
|
|
end = int(lines[line+1])
|
|
}
|
|
for pos := int(lines[line]); pos < end; pos++ {
|
|
if pos == rng.End()+1 {
|
|
output.WriteString("〛")
|
|
}
|
|
if diff.tsToken != nil && pos == tsTokenEnd {
|
|
output.WriteString("】")
|
|
}
|
|
if diff.goToken != nil && pos == goTokenEnd {
|
|
output.WriteString("》")
|
|
}
|
|
|
|
if diff.goToken != nil && pos == goTokenPos {
|
|
output.WriteString("《")
|
|
}
|
|
if diff.tsToken != nil && pos == tsTokenPos {
|
|
output.WriteString("【")
|
|
}
|
|
if pos == rng.Pos() {
|
|
output.WriteString("〚")
|
|
}
|
|
|
|
if pos < len(file.Text()) {
|
|
output.WriteByte(file.Text()[pos])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFindPrecedingToken(t *testing.T) {
|
|
t.Parallel()
|
|
repo.SkipIfNoTypeScriptSubmodule(t)
|
|
jstest.SkipIfNoNodeJS(t)
|
|
|
|
t.Run("baseline", func(t *testing.T) {
|
|
t.Parallel()
|
|
baselineTokens(
|
|
t,
|
|
"FindPrecedingToken",
|
|
true, /*includeEOF*/
|
|
func(fileText string, positions []int) []*tokenInfo {
|
|
return tsFindPrecedingTokens(t, fileText, positions)
|
|
},
|
|
func(file *ast.SourceFile, pos int) *tokenInfo {
|
|
return toTokenInfo(astnav.FindPrecedingToken(file, pos))
|
|
},
|
|
)
|
|
})
|
|
}
|
|
|
|
func TestUnitFindPrecedingToken(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
name string
|
|
fileContent string
|
|
position int
|
|
expectedKind ast.Kind
|
|
}{
|
|
{
|
|
name: "after dot in jsdoc",
|
|
fileContent: `import {
|
|
CharacterCodes,
|
|
compareStringsCaseInsensitive,
|
|
compareStringsCaseSensitive,
|
|
compareValues,
|
|
Comparison,
|
|
Debug,
|
|
endsWith,
|
|
equateStringsCaseInsensitive,
|
|
equateStringsCaseSensitive,
|
|
GetCanonicalFileName,
|
|
getDeclarationFileExtension,
|
|
getStringComparer,
|
|
identity,
|
|
lastOrUndefined,
|
|
Path,
|
|
some,
|
|
startsWith,
|
|
} from "./_namespaces/ts.js";
|
|
|
|
/**
|
|
* Internally, we represent paths as strings with '/' as the directory separator.
|
|
* When we make system calls (eg: LanguageServiceHost.getDirectory()),
|
|
* we expect the host to correctly handle paths in our specified format.
|
|
*
|
|
* @internal
|
|
*/
|
|
export const directorySeparator = "/";
|
|
/** @internal */
|
|
export const altDirectorySeparator = "\\";
|
|
const urlSchemeSeparator = "://";
|
|
const backslashRegExp = /\\/g;
|
|
|
|
|
|
backslashRegExp.
|
|
|
|
//Path Tests
|
|
|
|
/**
|
|
* Determines whether a charCode corresponds to '/' or '\'.
|
|
*
|
|
* @internal
|
|
*/
|
|
export function isAnyDirectorySeparator(charCode: number): boolean {
|
|
return charCode === CharacterCodes.slash || charCode === CharacterCodes.backslash;
|
|
}`,
|
|
position: 839,
|
|
expectedKind: ast.KindDotToken,
|
|
},
|
|
{
|
|
name: "after comma in parameter list",
|
|
fileContent: `takesCb((n, s, ))`,
|
|
position: 15,
|
|
expectedKind: ast.KindCommaToken,
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
file := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
|
FileName: "/file.ts",
|
|
Path: "/file.ts",
|
|
}, testCase.fileContent, core.ScriptKindTS)
|
|
token := astnav.FindPrecedingToken(file, testCase.position)
|
|
assert.Equal(t, token.Kind, testCase.expectedKind)
|
|
})
|
|
}
|
|
}
|
|
|
|
func tsFindPrecedingTokens(t *testing.T, fileText string, positions []int) []*tokenInfo {
|
|
dir := t.TempDir()
|
|
err := os.WriteFile(filepath.Join(dir, "file.ts"), []byte(fileText), 0o644)
|
|
assert.NilError(t, err)
|
|
|
|
err = os.WriteFile(filepath.Join(dir, "positions.json"), []byte(core.Must(core.StringifyJson(positions, "", ""))), 0o644)
|
|
assert.NilError(t, err)
|
|
|
|
script := `
|
|
import fs from "fs";
|
|
export default (ts) => {
|
|
const positions = JSON.parse(fs.readFileSync("positions.json", "utf8"));
|
|
const fileText = fs.readFileSync("file.ts", "utf8");
|
|
const file = ts.createSourceFile(
|
|
"file.ts",
|
|
fileText,
|
|
{ languageVersion: ts.ScriptTarget.Latest, jsDocParsingMode: ts.JSDocParsingMode.ParseAll },
|
|
/*setParentNodes*/ true
|
|
);
|
|
return positions.map(position => {
|
|
let token = ts.findPrecedingToken(position, file);
|
|
if (token === undefined) {
|
|
return undefined;
|
|
}
|
|
if (token.kind === ts.SyntaxKind.SyntaxList) {
|
|
token = token.parent;
|
|
}
|
|
return {
|
|
kind: ts.Debug.formatSyntaxKind(token.kind),
|
|
pos: token.pos,
|
|
end: token.end,
|
|
};
|
|
});
|
|
};`
|
|
info, err := jstest.EvalNodeScriptWithTS[[]*tokenInfo](t, script, dir, "")
|
|
assert.NilError(t, err)
|
|
return info
|
|
}
|