vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,44 @@
package ls
import (
"context"
"errors"
"fmt"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/checker"
)
var (
ErrNoSourceFile = errors.New("source file not found")
ErrNoTokenAtPosition = errors.New("no token found at position")
)
func (l *LanguageService) GetSymbolAtPosition(ctx context.Context, fileName string, position int) (*ast.Symbol, error) {
program, file := l.tryGetProgramAndFile(fileName)
if file == nil {
return nil, fmt.Errorf("%w: %s", ErrNoSourceFile, fileName)
}
node := astnav.GetTokenAtPosition(file, position)
if node == nil {
return nil, fmt.Errorf("%w: %s:%d", ErrNoTokenAtPosition, fileName, position)
}
checker, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
return checker.GetSymbolAtLocation(node), nil
}
func (l *LanguageService) GetSymbolAtLocation(ctx context.Context, node *ast.Node) *ast.Symbol {
program := l.GetProgram()
checker, done := program.GetTypeCheckerForFile(ctx, ast.GetSourceFileOfNode(node))
defer done()
return checker.GetSymbolAtLocation(node)
}
func (l *LanguageService) GetTypeOfSymbol(ctx context.Context, symbol *ast.Symbol) *checker.Type {
program := l.GetProgram()
checker, done := program.GetTypeChecker(ctx)
defer done()
return checker.GetTypeOfSymbolAtLocation(symbol, nil)
}

View File

@@ -0,0 +1,236 @@
package autoimport
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/binder"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/packagejson"
"github.com/microsoft/typescript-go/internal/symlinks"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
type pathAndFileName struct {
path tspath.Path
fileName string
}
type aliasResolver struct {
toPath func(fileName string) tspath.Path
host RegistryCloneHost
moduleResolver *module.Resolver
rootFiles []*ast.SourceFile
// symlinks maps from realpath to symlinked path and file name
symlinks map[tspath.Path]pathAndFileName
onFailedAmbientModuleLookup func(source ast.HasFileName, moduleName string)
resolvedModules collections.SyncMap[tspath.Path, *collections.SyncMap[module.ModeAwareCacheKey, *module.ResolvedModule]]
}
func newAliasResolver(
rootFiles []*ast.SourceFile,
symlinks map[tspath.Path]pathAndFileName,
host RegistryCloneHost,
moduleResolver *module.Resolver,
toPath func(fileName string) tspath.Path,
onFailedAmbientModuleLookup func(source ast.HasFileName, moduleName string),
) *aliasResolver {
r := &aliasResolver{
toPath: toPath,
host: host,
moduleResolver: moduleResolver,
rootFiles: rootFiles,
symlinks: symlinks,
onFailedAmbientModuleLookup: onFailedAmbientModuleLookup,
}
return r
}
// BindSourceFiles implements checker.Program.
func (r *aliasResolver) BindSourceFiles() {
// We will bind as we parse
}
// SourceFiles implements checker.Program.
func (r *aliasResolver) SourceFiles() []*ast.SourceFile {
return r.rootFiles
}
// Options implements checker.Program.
func (r *aliasResolver) Options() *core.CompilerOptions {
return &core.CompilerOptions{
NoCheck: core.TSTrue,
}
}
// GetCurrentDirectory implements checker.Program.
func (r *aliasResolver) GetCurrentDirectory() string {
return r.host.GetCurrentDirectory()
}
// UseCaseSensitiveFileNames implements checker.Program.
func (r *aliasResolver) UseCaseSensitiveFileNames() bool {
return r.host.FS().UseCaseSensitiveFileNames()
}
// GetSourceFile implements checker.Program.
func (r *aliasResolver) GetSourceFile(fileName string) *ast.SourceFile {
file := r.host.GetSourceFile(fileName, r.toPath(fileName))
// file may be nil due to symlink/realpath mismatch; see TestAutoImportBuilderFS
if file == nil {
return nil
}
binder.BindSourceFile(file)
return file
}
// GetDefaultResolutionModeForFile implements checker.Program.
func (r *aliasResolver) GetDefaultResolutionModeForFile(file ast.HasFileName) core.ResolutionMode {
return core.ModuleKindESNext
}
// GetEmitModuleFormatOfFile implements checker.Program.
func (r *aliasResolver) GetEmitModuleFormatOfFile(sourceFile ast.HasFileName) core.ModuleKind {
return core.ModuleKindESNext
}
// GetEmitSyntaxForUsageLocation implements checker.Program.
func (r *aliasResolver) GetEmitSyntaxForUsageLocation(sourceFile ast.HasFileName, usageLocation *ast.StringLiteralLike) core.ResolutionMode {
return core.ModuleKindESNext
}
// GetImpliedNodeFormatForEmit implements checker.Program.
func (r *aliasResolver) GetImpliedNodeFormatForEmit(sourceFile ast.HasFileName) core.ModuleKind {
return core.ModuleKindESNext
}
// GetModeForUsageLocation implements checker.Program.
func (r *aliasResolver) GetModeForUsageLocation(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) core.ResolutionMode {
return core.ModuleKindESNext
}
// GetResolvedModule implements checker.Program.
func (r *aliasResolver) GetResolvedModule(currentSourceFile ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule {
cache, _ := r.resolvedModules.LoadOrStore(currentSourceFile.Path(), &collections.SyncMap[module.ModeAwareCacheKey, *module.ResolvedModule]{})
if resolved, ok := cache.Load(module.ModeAwareCacheKey{Name: moduleReference, Mode: mode}); ok {
return resolved
}
resolved, _ := r.moduleResolver.ResolveModuleName(moduleReference, currentSourceFile.FileName(), mode, nil)
resolved, _ = cache.LoadOrStore(module.ModeAwareCacheKey{Name: moduleReference, Mode: mode}, resolved)
if !resolved.IsResolved() && !tspath.PathIsRelative(moduleReference) {
r.onFailedAmbientModuleLookup(currentSourceFile, moduleReference)
}
return resolved
}
// GetSourceFileForResolvedModule implements checker.Program.
func (r *aliasResolver) GetSourceFileForResolvedModule(fileName string) *ast.SourceFile {
return r.GetSourceFile(fileName)
}
// GetResolvedModules implements checker.Program.
func (r *aliasResolver) GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule] {
// only used when producing diagnostics, which hopefully the checker won't do
return nil
}
// ---
// GetSymlinkCache implements checker.Program.
func (r *aliasResolver) GetSymlinkCache() *symlinks.KnownSymlinks {
panic("unimplemented")
}
// GetSourceFileMetaData implements checker.Program.
func (r *aliasResolver) GetSourceFileMetaData(path tspath.Path) ast.SourceFileMetaData {
panic("unimplemented")
}
// CommonSourceDirectory implements checker.Program.
func (r *aliasResolver) CommonSourceDirectory() string {
panic("unimplemented")
}
// FileExists implements checker.Program.
func (r *aliasResolver) FileExists(fileName string) bool {
panic("unimplemented")
}
// GetGlobalTypingsCacheLocation implements checker.Program.
func (r *aliasResolver) GetGlobalTypingsCacheLocation() string {
panic("unimplemented")
}
// GetImportHelpersImportSpecifier implements checker.Program.
func (r *aliasResolver) GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node {
panic("unimplemented")
}
// GetJSXRuntimeImportSpecifier implements checker.Program.
func (r *aliasResolver) GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node) {
panic("unimplemented")
}
// GetNearestAncestorDirectoryWithPackageJson implements checker.Program.
func (r *aliasResolver) GetNearestAncestorDirectoryWithPackageJson(dirname string) string {
panic("unimplemented")
}
// GetPackageJsonInfo implements checker.Program.
func (r *aliasResolver) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry {
panic("unimplemented")
}
// GetProjectReferenceFromOutputDts implements checker.Program.
func (r *aliasResolver) GetProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference {
panic("unimplemented")
}
// GetProjectReferenceFromSource implements checker.Program.
func (r *aliasResolver) GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference {
panic("unimplemented")
}
// GetRedirectForResolution implements checker.Program.
func (r *aliasResolver) GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine {
panic("unimplemented")
}
// GetRedirectTargets implements checker.Program.
func (r *aliasResolver) GetRedirectTargets(path tspath.Path) []string {
panic("unimplemented")
}
// GetResolvedModuleFromModuleSpecifier implements checker.Program.
func (r *aliasResolver) GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) *module.ResolvedModule {
panic("unimplemented")
}
// GetSourceOfProjectReferenceIfOutputIncluded implements checker.Program.
func (r *aliasResolver) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string {
panic("unimplemented")
}
// IsSourceFileDefaultLibrary implements checker.Program.
func (r *aliasResolver) IsSourceFileDefaultLibrary(path tspath.Path) bool {
return false
}
// IsSourceFromProjectReference implements checker.Program.
func (r *aliasResolver) IsSourceFromProjectReference(path tspath.Path) bool {
panic("unimplemented")
}
// SourceFileMayBeEmitted implements checker.Program.
func (r *aliasResolver) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool {
panic("unimplemented")
}
func (r *aliasResolver) GetPackagesMap() map[string]bool {
return nil
}
var _ checker.Program = (*aliasResolver)(nil)

View File

@@ -0,0 +1,73 @@
package autoimport
import (
"context"
"testing"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/binder"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/packagejson"
"github.com/microsoft/typescript-go/internal/parser"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
)
type fakeCloneHost struct {
fs vfs.FS
}
func (h *fakeCloneHost) FS() vfs.FS { return h.fs }
func (h *fakeCloneHost) GetCurrentDirectory() string { return "/" }
func (h *fakeCloneHost) GetDefaultProject(path tspath.Path) (tspath.Path, *compiler.Program) {
return "", nil
}
func (h *fakeCloneHost) GetProgramForProject(projectPath tspath.Path) *compiler.Program { return nil }
func (h *fakeCloneHost) GetPackageJson(fileName string) *packagejson.InfoCacheEntry { return nil }
func (h *fakeCloneHost) GetSourceFile(fileName string, path tspath.Path) *ast.SourceFile { return nil }
func (h *fakeCloneHost) Dispose() {}
var _ RegistryCloneHost = (*fakeCloneHost)(nil)
// Regression test for microsoft/typescript-go#4322.
//
// During auto-import export extraction, the checker is built on top of an
// aliasResolver standing in for a real program. This file has a type error, and
// extracting exports should still complete without crashing.
func TestAliasResolverGetDiagnosticsDoesNotPanic(t *testing.T) {
t.Parallel()
const fileName = "/pkg/index.ts"
text := "declare function f(arg: { a: string }): () => void;\nexport const x = f({ a: 1 });\n"
fs := vfstest.FromMap(map[string]string{fileName: text}, true /*useCaseSensitiveFileNames*/)
host := &fakeCloneHost{fs: fs}
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: fileName,
Path: tspath.Path(fileName),
}, text, core.ScriptKindTS)
binder.BindSourceFile(sourceFile)
resolver := module.NewResolver(host, core.EmptyCompilerOptions, "", "")
r := newAliasResolver(
[]*ast.SourceFile{sourceFile},
nil,
host,
resolver,
func(f string) tspath.Path { return tspath.Path(f) },
func(ast.HasFileName, string) {},
)
ch, _ := checker.NewChecker(r, nil)
// Type-checking this file's diagnostics must not panic.
ch.GetDiagnostics(context.Background(), sourceFile)
}

View File

@@ -0,0 +1,142 @@
package autoimport
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/tspath"
)
//go:generate go tool golang.org/x/tools/cmd/stringer -type=ExportSyntax -output=export_stringer_generated.go
//go:generate npx dprint fmt export_stringer_generated.go
// ModuleID uniquely identifies a module across multiple declarations.
// If the export is from an ambient module declaration, this is the module name.
// If the export is from a module augmentation, this is the Path() of the resolved module file.
// Otherwise this is the Path() of the exporting source file.
type ModuleID string
type ExportID struct {
ModuleID ModuleID
ExportName string
}
type ExportSyntax int
const (
ExportSyntaxNone ExportSyntax = iota
// export const x = {}
ExportSyntaxModifier
// export { x }
ExportSyntaxNamed
// export default function f() {}
ExportSyntaxDefaultModifier
// export default f
ExportSyntaxDefaultDeclaration
// export = x
ExportSyntaxEquals
// export as namespace x
ExportSyntaxUMD
// export * from "module"
ExportSyntaxStar
// module.exports = {}
ExportSyntaxCommonJSModuleExports
// exports.x = {}
ExportSyntaxCommonJSExportsProperty
)
type Export struct {
ExportID
ModuleFileName string
Syntax ExportSyntax
Flags ast.SymbolFlags
localName string
// through is the name of the module symbol's export that this export was found on,
// either 'export=', InternalSymbolNameExportStar, or empty string.
through string
// Checker-set fields
Target ExportID
IsTypeOnly bool
ScriptElementKind lsutil.ScriptElementKind
ScriptElementKindModifiers lsutil.ScriptElementKindModifier
// The file where the export was found.
Path tspath.Path
PackageName string
}
func (e *Export) Name() string {
if e.localName != "" {
return e.localName
}
if e.ExportName == ast.InternalSymbolNameExportEquals {
return e.Target.ExportName
}
return e.ExportName
}
func (e *Export) IsRenameable() bool {
return e.ExportName == ast.InternalSymbolNameExportEquals || e.ExportName == ast.InternalSymbolNameDefault
}
func (e *Export) AmbientModuleName() string {
if !tspath.IsExternalModuleNameRelative(string(e.ModuleID)) {
return string(e.ModuleID)
}
return ""
}
func (e *Export) IsUnresolvedAlias() bool {
return e.Flags == ast.SymbolFlagsAlias
}
func SymbolToExport(symbol *ast.Symbol, ch *checker.Checker) *Export {
if symbol.Parent != nil && checker.IsExternalModuleSymbol(symbol.Parent) {
if moduleID, moduleFileName, ok := tryGetModuleIDAndFileNameOfModuleSymbol(symbol.Parent); ok {
return extractFirstExport(symbol, ch, moduleID, moduleFileName, ast.GetSourceFileOfModule(symbol.Parent))
}
return nil
}
declaration := core.FirstOrNil(symbol.Declarations)
if declaration == nil {
return nil
}
file := ast.GetSourceFileOfNode(declaration)
if file.Symbol == nil {
return nil
}
moduleSymbol := ch.GetMergedSymbol(file.Symbol)
moduleID := ModuleID(file.Path())
moduleFileName := file.FileName()
target := ch.GetMergedSymbol(ch.SkipAlias(symbol))
if export := tryGetModuleExport(ast.InternalSymbolNameDefault, target, moduleSymbol, ch, moduleID, moduleFileName, file); export != nil {
return export
}
if export := tryGetModuleExport(ast.InternalSymbolNameExportEquals, target, moduleSymbol, ch, moduleID, moduleFileName, file); export != nil {
return export
}
return tryGetModuleExport(symbol.Name, target, moduleSymbol, ch, moduleID, moduleFileName, file)
}
func tryGetModuleExport(exportName string, target *ast.Symbol, moduleSymbol *ast.Symbol, ch *checker.Checker, moduleID ModuleID, moduleFileName string, file *ast.SourceFile) *Export {
exported := ch.TryGetMemberInModuleExportsAndProperties(exportName, moduleSymbol)
if exported != nil && ch.GetMergedSymbol(ch.SkipAlias(exported)) == target {
return extractFirstExport(exported, ch, moduleID, moduleFileName, file)
}
return nil
}
func extractFirstExport(symbol *ast.Symbol, ch *checker.Checker, moduleID ModuleID, moduleFileName string, file *ast.SourceFile) *Export {
var exports []*Export
extractor := newSymbolExtractor("", ch, nil, nil)
extractor.extractFromSymbol(symbol.Name, symbol, moduleID, moduleFileName, file, &exports)
return core.FirstOrNil(exports)
}

View File

@@ -0,0 +1,33 @@
// Code generated by "stringer -type=ExportSyntax -output=export_stringer_generated.go"; DO NOT EDIT.
package autoimport
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ExportSyntaxNone-0]
_ = x[ExportSyntaxModifier-1]
_ = x[ExportSyntaxNamed-2]
_ = x[ExportSyntaxDefaultModifier-3]
_ = x[ExportSyntaxDefaultDeclaration-4]
_ = x[ExportSyntaxEquals-5]
_ = x[ExportSyntaxUMD-6]
_ = x[ExportSyntaxStar-7]
_ = x[ExportSyntaxCommonJSModuleExports-8]
_ = x[ExportSyntaxCommonJSExportsProperty-9]
}
const _ExportSyntax_name = "ExportSyntaxNoneExportSyntaxModifierExportSyntaxNamedExportSyntaxDefaultModifierExportSyntaxDefaultDeclarationExportSyntaxEqualsExportSyntaxUMDExportSyntaxStarExportSyntaxCommonJSModuleExportsExportSyntaxCommonJSExportsProperty"
var _ExportSyntax_index = [...]uint8{0, 16, 36, 53, 80, 110, 128, 143, 159, 192, 227}
func (i ExportSyntax) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_ExportSyntax_index)-1 {
return "ExportSyntax(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ExportSyntax_name[_ExportSyntax_index[idx]:_ExportSyntax_index[idx+1]]
}

View File

@@ -0,0 +1,461 @@
package autoimport
import (
"slices"
"sync/atomic"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/binder"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/tspath"
)
type symbolExtractor struct {
packageName string
stats *extractorStats
localNameResolver *binder.NameResolver
checker *checker.Checker
toPath func(fileName string) tspath.Path
// realpath, if set, is used to resolve symlinks for ModuleID generation.
// This ensures that symlinked packages use their realpath as ModuleID,
// deduplicating exports from files that appear via multiple symlink paths.
realpath func(fileName string) string
}
type exportExtractor struct {
*symbolExtractor
moduleResolver *module.Resolver
}
type extractorStats struct {
exports atomic.Int32
usedChecker atomic.Int32
}
func (e *exportExtractor) Stats() *extractorStats {
return e.stats
}
type checkerLease struct {
used bool
checker *checker.Checker
}
func (l *checkerLease) GetChecker() *checker.Checker {
l.used = true
return l.checker
}
func (l *checkerLease) TryChecker() *checker.Checker {
if l.used {
return l.checker
}
return nil
}
func newSymbolExtractor(packageName string, checker *checker.Checker, toPath func(string) tspath.Path, realpath func(string) string) *symbolExtractor {
return &symbolExtractor{
packageName: packageName,
checker: checker,
localNameResolver: &binder.NameResolver{
CompilerOptions: core.EmptyCompilerOptions,
},
stats: &extractorStats{},
toPath: toPath,
realpath: realpath,
}
}
func (b *registryBuilder) newExportExtractor(packageName string, checker *checker.Checker, moduleResolver *module.Resolver, realpath func(string) string) *exportExtractor {
return &exportExtractor{
symbolExtractor: newSymbolExtractor(packageName, checker, b.base.toPath, realpath),
moduleResolver: moduleResolver,
}
}
// getModuleID returns the ModuleID for a file, using realpath if available.
func (e *symbolExtractor) getModuleID(file *ast.SourceFile) ModuleID {
if e.realpath != nil && e.toPath != nil {
realpath := e.realpath(file.FileName())
return ModuleID(e.toPath(realpath))
}
return ModuleID(file.Path())
}
// getModuleIDForSymbol returns the ModuleID for a module symbol, using realpath
// normalization when available for source files.
func (e *symbolExtractor) getModuleIDForSymbol(symbol *ast.Symbol) (ModuleID, bool) {
moduleID, fileName, ok := tryGetModuleIDAndFileNameOfModuleSymbol(symbol)
if !ok {
return "", false
}
// If fileName is set, this is a source file that may need realpath normalization
if fileName != "" && e.realpath != nil {
decl := ast.GetNonAugmentationDeclaration(symbol)
if decl != nil && decl.Kind == ast.KindSourceFile {
return e.getModuleID(decl.AsSourceFile()), true
}
}
return moduleID, true
}
func (e *exportExtractor) extractFromFile(file *ast.SourceFile) []*Export {
if file.Symbol != nil {
return e.extractFromModule(file)
}
if len(file.AmbientModuleNames) > 0 {
moduleDeclarations := core.Filter(file.Statements.Nodes, ast.IsModuleWithStringLiteralName)
var exportCount int
for _, decl := range moduleDeclarations {
exportCount += len(decl.AsModuleDeclaration().Symbol.Exports)
}
exports := make([]*Export, 0, exportCount)
for _, decl := range moduleDeclarations {
e.extractFromModuleDeclaration(decl.AsModuleDeclaration(), file, ModuleID(decl.Name().Text()), "", &exports)
}
return exports
}
return nil
}
func (e *exportExtractor) extractFromModule(file *ast.SourceFile) []*Export {
moduleAugmentations := core.MapNonNil(file.ModuleAugmentations, func(name *ast.ModuleName) *ast.ModuleDeclaration {
decl := name.Parent
if ast.IsGlobalScopeAugmentation(decl) {
return nil
}
return decl.AsModuleDeclaration()
})
var augmentationExportCount int
for _, decl := range moduleAugmentations {
augmentationExportCount += len(decl.Symbol.Exports)
}
moduleID := e.getModuleID(file)
exports := make([]*Export, 0, len(file.Symbol.Exports)+augmentationExportCount)
for name, symbol := range file.Symbol.Exports {
e.extractFromSymbol(name, symbol, moduleID, file.FileName(), file, &exports)
}
for _, decl := range moduleAugmentations {
name := decl.Name().AsStringLiteral().Text
moduleID := ModuleID(name)
var moduleFileName string
if tspath.IsExternalModuleNameRelative(name) {
if resolved, _ := e.moduleResolver.ResolveModuleName(name, file.FileName(), core.ModuleKindCommonJS, nil); resolved.IsResolved() {
moduleFileName = resolved.ResolvedFileName
moduleID = ModuleID(e.toPath(moduleFileName))
} else {
// :shrug:
moduleFileName = tspath.ResolvePath(tspath.GetDirectoryPath(file.FileName()), name)
moduleID = ModuleID(e.toPath(moduleFileName))
}
}
e.extractFromModuleDeclaration(decl, file, moduleID, moduleFileName, &exports)
}
return exports
}
func (e *exportExtractor) extractFromModuleDeclaration(decl *ast.ModuleDeclaration, file *ast.SourceFile, moduleID ModuleID, moduleFileName string, exports *[]*Export) {
for name, symbol := range decl.Symbol.Exports {
e.extractFromSymbol(name, symbol, moduleID, moduleFileName, file, exports)
}
}
func (e *symbolExtractor) extractFromSymbol(name string, symbol *ast.Symbol, moduleID ModuleID, moduleFileName string, file *ast.SourceFile, exports *[]*Export) {
if shouldIgnoreSymbol(symbol) {
return
}
if name == ast.InternalSymbolNameExportStar {
checkerLease := &checkerLease{checker: e.checker}
allExports := e.checker.GetExportsOfModule(symbol.Parent)
// allExports includes named exports from the file that will be processed separately;
// we want to add only the ones that come from the star
for name, namedExport := range symbol.Parent.Exports {
if name != ast.InternalSymbolNameExportStar {
idx := slices.Index(allExports, namedExport)
if idx >= 0 || shouldIgnoreSymbol(namedExport) {
allExports = slices.Delete(allExports, idx, idx+1)
}
}
}
*exports = slices.Grow(*exports, len(allExports))
for _, reexportedSymbol := range allExports {
export, _ := e.createExport(reexportedSymbol, moduleID, moduleFileName, ExportSyntaxStar, file, checkerLease)
if export != nil {
parent := checkerLease.GetChecker().GetMergedSymbol(reexportedSymbol.Parent)
if parent != nil && parent.IsExternalModule() {
if targetModuleID, ok := e.getModuleIDForSymbol(parent); ok {
export.Target = ExportID{
ExportName: reexportedSymbol.Name,
ModuleID: targetModuleID,
}
}
}
export.through = ast.InternalSymbolNameExportStar
*exports = append(*exports, export)
}
}
return
}
syntax := getSyntax(symbol)
checkerLease := &checkerLease{checker: e.checker}
export, target := e.createExport(symbol, moduleID, moduleFileName, syntax, file, checkerLease)
if export == nil {
return
}
*exports = append(*exports, export)
if target != nil {
if syntax == ExportSyntaxEquals && target.Flags&ast.SymbolFlagsNamespace != 0 {
*exports = slices.Grow(*exports, len(target.Exports))
for innerName, namedExport := range target.Exports {
if innerName != ast.InternalSymbolNameExportStar {
export, _ := e.createExport(namedExport, moduleID, moduleFileName, syntax, file, checkerLease)
if export != nil {
export.through = name
*exports = append(*exports, export)
}
}
}
}
} else if syntax == ExportSyntaxCommonJSModuleExports {
expression := symbol.Declarations[0].AsBinaryExpression().Right
if expression.Kind == ast.KindObjectLiteralExpression {
// what is actually desirable here? I think it would be reasonable to only treat these as exports
// if *every* property is a shorthand property or identifier: identifier
// At least, it would be sketchy if there were any methods, computed properties...
*exports = slices.Grow(*exports, len(expression.AsObjectLiteralExpression().Properties.Nodes))
for _, prop := range expression.AsObjectLiteralExpression().Properties.Nodes {
if ast.IsShorthandPropertyAssignment(prop) || ast.IsPropertyAssignment(prop) && prop.AsPropertyAssignment().Name().Kind == ast.KindIdentifier {
export, _ := e.createExport(expression.Symbol().Members[prop.Name().Text()], moduleID, moduleFileName, syntax, file, checkerLease)
if export != nil {
export.through = name
*exports = append(*exports, export)
}
}
}
}
}
}
// createExport creates an Export for the given symbol, returning the Export and the target symbol if the export is an alias.
func (e *symbolExtractor) createExport(symbol *ast.Symbol, moduleID ModuleID, moduleFileName string, syntax ExportSyntax, file *ast.SourceFile, checkerLease *checkerLease) (*Export, *ast.Symbol) {
if shouldIgnoreSymbol(symbol) {
return nil, nil
}
export := &Export{
ExportID: ExportID{
ExportName: symbol.Name,
ModuleID: moduleID,
},
ModuleFileName: moduleFileName,
Syntax: syntax,
Flags: symbol.CombinedLocalAndExportSymbolFlags(),
Path: file.Path(),
PackageName: e.packageName,
}
if syntax == ExportSyntaxUMD {
export.ExportName = ast.InternalSymbolNameExportEquals
export.localName = symbol.Name
}
var targetSymbol *ast.Symbol
if symbol.Flags&ast.SymbolFlagsAlias != 0 {
targetSymbol = e.tryResolveSymbol(symbol, syntax, checkerLease)
if targetSymbol != nil {
var decl *ast.Node
if len(targetSymbol.Declarations) > 0 {
decl = targetSymbol.Declarations[0]
} else if targetSymbol.CheckFlags&ast.CheckFlagsMapped != 0 {
if mappedDecl := checkerLease.GetChecker().GetMappedTypeSymbolOfProperty(targetSymbol); mappedDecl != nil && len(mappedDecl.Declarations) > 0 {
decl = mappedDecl.Declarations[0]
}
}
if decl == nil {
// !!! consider GetImmediateAliasedSymbol to go as far as we can
decl = symbol.Declarations[0]
}
if decl == nil {
panic("no declaration for aliased symbol")
}
parent := targetSymbol.Parent
if checker := checkerLease.TryChecker(); checker != nil {
export.Flags = checker.GetSymbolFlags(targetSymbol)
export.IsTypeOnly = checker.GetTypeOnlyAliasDeclaration(symbol) != nil
parent = checker.GetMergedSymbol(parent)
} else {
export.Flags = targetSymbol.Flags
export.IsTypeOnly = core.Some(symbol.Declarations, ast.IsPartOfTypeOnlyImportOrExportDeclaration)
}
export.ScriptElementKind = lsutil.GetSymbolKind(checkerLease.TryChecker(), targetSymbol, decl)
export.ScriptElementKindModifiers = lsutil.GetSymbolModifiers(checkerLease.TryChecker(), targetSymbol)
targetModuleID := ModuleID(ast.GetSourceFileOfNode(decl).Path())
if parent != nil && parent.IsExternalModule() {
if id, ok := e.getModuleIDForSymbol(parent); ok {
targetModuleID = id
}
}
export.Target = ExportID{
ExportName: targetSymbol.Name,
ModuleID: targetModuleID,
}
}
} else {
export.ScriptElementKind = lsutil.GetSymbolKind(checkerLease.TryChecker(), symbol, symbol.Declarations[0])
export.ScriptElementKindModifiers = lsutil.GetSymbolModifiers(checkerLease.TryChecker(), symbol)
}
if symbol.Name == ast.InternalSymbolNameDefault || symbol.Name == ast.InternalSymbolNameExportEquals {
namedSymbol := symbol
if s := binder.GetLocalSymbolForExportDefault(symbol); s != nil {
namedSymbol = s
}
export.localName = getDefaultLikeExportNameFromDeclaration(namedSymbol)
if isUnusableName(export.localName) {
export.localName = export.Target.ExportName
}
if isUnusableName(export.localName) {
if targetSymbol != nil {
namedSymbol = targetSymbol
if s := binder.GetLocalSymbolForExportDefault(targetSymbol); s != nil {
namedSymbol = s
}
export.localName = getDefaultLikeExportNameFromDeclaration(namedSymbol)
}
}
if isUnusableName(export.localName) {
// Last resort: derive identifier from the file name. Use FileName() (original
// casing) rather than ModuleID/Path() which is lowercased on case-insensitive
// file systems, losing PascalCase.
export.localName = lsutil.ModuleSpecifierToValidIdentifier(fileNameForDefaultExportName(targetSymbol, moduleFileName, moduleID), false)
}
}
if isUnusableName(export.Name()) {
return nil, nil
}
e.stats.exports.Add(1)
if checkerLease.TryChecker() != nil {
e.stats.usedChecker.Add(1)
}
return export, targetSymbol
}
func (e *symbolExtractor) tryResolveSymbol(symbol *ast.Symbol, syntax ExportSyntax, checkerLease *checkerLease) *ast.Symbol {
if !ast.IsNonLocalAlias(symbol, ast.SymbolFlagsNone) {
return symbol
}
var loc *ast.Node
var name string
switch syntax {
case ExportSyntaxNamed:
decl := ast.GetDeclarationOfKind(symbol, ast.KindExportSpecifier)
if decl.Parent.Parent.AsExportDeclaration().ModuleSpecifier == nil {
if n := core.FirstNonZero(decl.Name(), decl.PropertyName()); n.Kind == ast.KindIdentifier {
loc = n
name = n.Text()
}
}
// !!! check if module.exports = foo is marked as an alias
case ExportSyntaxEquals:
if symbol.Name != ast.InternalSymbolNameExportEquals {
break
}
fallthrough
case ExportSyntaxDefaultDeclaration:
decl := ast.GetDeclarationOfKind(symbol, ast.KindExportAssignment)
if decl.Expression().Kind == ast.KindIdentifier {
loc = decl.Expression()
name = loc.Text()
}
}
if loc != nil {
local := e.localNameResolver.Resolve(loc, name, ast.SymbolFlagsAll, nil, false, false)
if local != nil && !ast.IsNonLocalAlias(local, ast.SymbolFlagsNone) {
return local
}
}
checker := checkerLease.GetChecker()
if resolved := checker.GetAliasedSymbol(symbol); !checker.IsUnknownSymbol(resolved) {
return resolved
}
return nil
}
func shouldIgnoreSymbol(symbol *ast.Symbol) bool {
if symbol.Flags&ast.SymbolFlagsPrototype != 0 {
return true
}
return false
}
func getSyntax(symbol *ast.Symbol) ExportSyntax {
for _, decl := range symbol.Declarations {
switch decl.Kind {
case ast.KindExportSpecifier:
return ExportSyntaxNamed
case ast.KindExportAssignment:
return core.IfElse(
decl.AsExportAssignment().IsExportEquals,
ExportSyntaxEquals,
ExportSyntaxDefaultDeclaration,
)
case ast.KindNamespaceExportDeclaration:
return ExportSyntaxUMD
case ast.KindBinaryExpression:
switch ast.GetAssignmentDeclarationKind(decl) {
case ast.JSDeclarationKindModuleExports:
return ExportSyntaxCommonJSModuleExports
case ast.JSDeclarationKindExportsProperty:
return ExportSyntaxCommonJSExportsProperty
}
default:
if ast.GetCombinedModifierFlags(decl)&ast.ModifierFlagsDefault != 0 {
return ExportSyntaxDefaultModifier
} else {
return ExportSyntaxModifier
}
}
}
return ExportSyntaxNone
}
func isUnusableName(name string) bool {
return name == "" ||
name == "_default" ||
name == ast.InternalSymbolNameExportStar ||
name == ast.InternalSymbolNameDefault ||
name == ast.InternalSymbolNameExportEquals
}
// fileNameForDefaultExportName returns the best file name to use when deriving
// a fallback identifier for a default-like export. It prefers the target symbol's
// source file (closest to the export origin), falls back to the module's original
// file name, and uses the lowercased moduleID only for ambient modules where no
// original file name is available.
func fileNameForDefaultExportName(targetSymbol *ast.Symbol, moduleFileName string, moduleID ModuleID) string {
if targetSymbol != nil && len(targetSymbol.Declarations) > 0 {
if fn := ast.GetSourceFileOfNode(targetSymbol.Declarations[0]).FileName(); fn != "" {
return fn
}
}
if moduleFileName != "" {
return moduleFileName
}
return string(moduleID)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,501 @@
package autoimport
import (
"context"
"fmt"
"maps"
"slices"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/debug"
"github.com/microsoft/typescript-go/internal/locale"
// "github.com/microsoft/typescript-go/internal/ls"
"github.com/microsoft/typescript-go/internal/ls/change"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/nodebuilder"
)
type ImportAdder interface {
HasFixes() bool
AddImportFromExportedSymbol(symbol *ast.Symbol, isValidTypeOnlyUseSite bool)
AddImportFix(fix *Fix)
Edits() []*lsproto.TextEdit
}
// addToExistingState tracks modifications to an existing import clause or binding pattern
type addToExistingState struct {
importClauseOrBindingPattern *ast.ImportClauseOrBindingPattern
defaultImport *newImportBinding
namedImports map[string]*newImportBinding
}
// importsCollection tracks new imports to be created for a given module specifier
type importsCollection struct {
defaultImport *newImportBinding
namedImports map[string]*newImportBinding
namespaceLikeImport *newImportBinding
useRequire bool
}
func newImportsKey(moduleSpecifier string, topLevelTypeOnly bool) string {
if topLevelTypeOnly {
return "1|" + moduleSpecifier
}
return "0|" + moduleSpecifier
}
type importAdder struct {
// Context
ctx context.Context
checker *checker.Checker
view *View
formatOptions lsutil.FormatCodeSettings
converters *lsconv.Converters
preferences lsutil.UserPreferences
// State
addToNamespace []*Fix // Namespace fixes don't conflict, so just build a list
importType []*Fix // JSDoc type import fixes
addToExisting map[*ast.ImportClauseOrBindingPattern]*addToExistingState // importClauseOrBindingPattern -> default or named bindings
newImports map[string]*importsCollection // module specifier + type only -> imports
// !!! removeExisting, verbatimImports?
}
func NewImportAdder(
ctx context.Context,
program *compiler.Program,
checker *checker.Checker,
file *ast.SourceFile,
view *View,
formatOptions lsutil.FormatCodeSettings,
converters *lsconv.Converters,
preferences lsutil.UserPreferences,
) ImportAdder {
return &importAdder{
ctx: ctx,
checker: checker,
view: view,
formatOptions: formatOptions,
converters: converters,
preferences: preferences,
addToNamespace: nil,
importType: nil,
addToExisting: make(map[*ast.Node]*addToExistingState),
newImports: make(map[string]*importsCollection),
}
}
func (adder *importAdder) HasFixes() bool {
return len(adder.addToNamespace) > 0 ||
len(adder.importType) > 0 ||
len(adder.addToExisting) > 0 ||
len(adder.newImports) > 0
}
// !!! referenceImport
func (adder *importAdder) AddImportFromExportedSymbol(exportedSymbol *ast.Symbol, isValidTypeOnlyUseSite bool) {
symbol := adder.checker.GetMergedSymbol(adder.checker.SkipAlias(exportedSymbol))
exportInfos := adder.getAllExportsForSymbol(symbol)
if len(exportInfos) == 0 {
// If no exportInfo is found, this means export could not be resolved when we have filtered for autoImportFileExcludePatterns,
// so we should not generate an import.
// debug.Assert(len(adder.ls.UserPreferences().AutoImportFileExcludePatterns) > 0)
return
}
fix := adder.getImportFixForSymbol(adder.view, adder.view.importingFile, exportInfos, isValidTypeOnlyUseSite)
if fix != nil {
// !!! referenceImport -> propertyName
adder.AddImportFix(fix)
}
}
func (adder *importAdder) Edits() []*lsproto.TextEdit {
// !!! organize imports?
tracker := change.NewTracker(adder.ctx, adder.view.program.Options(), adder.formatOptions, adder.converters)
quotePreference := lsutil.GetQuotePreference(adder.view.importingFile, adder.preferences)
for _, fix := range adder.addToNamespace {
addNamespaceQualifier(fix, tracker, adder.view.importingFile, locale.Default)
}
for _, fix := range adder.importType {
addImportType(fix, adder.view.importingFile, adder.preferences, tracker, locale.Default)
}
for clauseOrPattern, entry := range adder.addToExisting {
addToExistingImport(
tracker,
adder.view.importingFile,
clauseOrPattern,
entry.defaultImport,
sortedNamedImports(entry.namedImports),
adder.preferences,
)
}
var newDeclarations []*ast.AnyImportOrRequireStatement
for key, newImport := range adder.newImports {
moduleSpecifier := key[2:] // From `${0 | 1}|${moduleSpecifier}` format
var declarations []*ast.AnyImportOrRequireStatement
if newImport.useRequire {
declarations = getNewRequires(
tracker,
moduleSpecifier,
quotePreference,
newImport.defaultImport,
sortedNamedImports(newImport.namedImports),
newImport.namespaceLikeImport,
adder.view.program.Options(),
)
} else {
declarations = getNewImports(
tracker,
moduleSpecifier,
quotePreference,
newImport.defaultImport,
sortedNamedImports(newImport.namedImports),
newImport.namespaceLikeImport,
adder.view.program.Options(),
adder.preferences,
)
}
newDeclarations = append(newDeclarations, declarations...)
}
if len(newDeclarations) > 0 {
insertImports(tracker, adder.view.importingFile, newDeclarations, true /*blankLineBetween*/, adder.preferences)
}
return tracker.GetChanges()[adder.view.importingFile.FileName()]
}
func sortedNamedImports(m map[string]*newImportBinding) []*newImportBinding {
keys := slices.Sorted(maps.Keys(m))
result := make([]*newImportBinding, 0, len(keys))
for _, k := range keys {
result = append(result, m[k])
}
return result
}
// AddImportFix adds a fix to the import adder, accumulating it with other fixes
// so that multiple imports from the same module are coalesced into a single import statement.
func (adder *importAdder) AddImportFix(fix *Fix) {
symbolName := fix.Name
compilerOptions := adder.view.program.Options()
switch fix.Kind {
case lsproto.AutoImportFixKindUseNamespace:
adder.addToNamespace = append(adder.addToNamespace, fix)
case lsproto.AutoImportFixKindJsdocTypeImport:
adder.importType = append(adder.importType, fix)
case lsproto.AutoImportFixKindAddToExisting:
existingFix := getAddToExistingImportFix(adder.view.importingFile, fix)
entry := adder.addToExisting[existingFix.importClauseOrBindingPattern]
if entry == nil {
entry = &addToExistingState{
importClauseOrBindingPattern: existingFix.importClauseOrBindingPattern,
namedImports: make(map[string]*newImportBinding),
}
adder.addToExisting[existingFix.importClauseOrBindingPattern] = entry
}
if fix.ImportKind == lsproto.ImportKindNamed {
prevImport := entry.namedImports[symbolName]
var prevTypeOnly lsproto.AddAsTypeOnly
if prevImport != nil {
prevTypeOnly = prevImport.addAsTypeOnly
}
entry.namedImports[symbolName] = &newImportBinding{
kind: lsproto.ImportKindNamed,
name: symbolName,
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
propertyName: existingFix.namedImport.propertyName,
}
} else {
// Default import
debug.Assert(
entry.defaultImport == nil || entry.defaultImport.name == symbolName,
"(Add to Existing) Default import should be missing or match symbolName",
)
var prevTypeOnly lsproto.AddAsTypeOnly
if entry.defaultImport != nil {
prevTypeOnly = entry.defaultImport.addAsTypeOnly
}
entry.defaultImport = &newImportBinding{
kind: lsproto.ImportKindDefault,
name: symbolName,
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
}
}
case lsproto.AutoImportFixKindAddNew:
entry := adder.getNewImportEntry(fix.ModuleSpecifier, fix.ImportKind, fix.UseRequire, fix.AddAsTypeOnly)
debug.Assert(
entry.useRequire == fix.UseRequire,
"(Add new) Tried to add an `import` and a `require` for the same module",
)
switch fix.ImportKind {
case lsproto.ImportKindDefault:
debug.Assert(
entry.defaultImport == nil || entry.defaultImport.name == symbolName,
"(Add new) Default import should be missing or match symbolName",
)
var prevTypeOnly lsproto.AddAsTypeOnly
if entry.defaultImport != nil {
prevTypeOnly = entry.defaultImport.addAsTypeOnly
}
entry.defaultImport = &newImportBinding{
kind: lsproto.ImportKindDefault,
name: symbolName,
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
}
case lsproto.ImportKindNamed:
if entry.namedImports == nil {
entry.namedImports = make(map[string]*newImportBinding)
}
prevImport := entry.namedImports[symbolName]
var prevTypeOnly lsproto.AddAsTypeOnly
if prevImport != nil {
prevTypeOnly = prevImport.addAsTypeOnly
}
entry.namedImports[symbolName] = &newImportBinding{
kind: lsproto.ImportKindNamed,
name: symbolName,
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
// !!! propertyName
}
case lsproto.ImportKindCommonJS:
if compilerOptions.VerbatimModuleSyntax == core.TSTrue {
if entry.namedImports == nil {
entry.namedImports = make(map[string]*newImportBinding)
}
prevImport := entry.namedImports[symbolName]
var prevTypeOnly lsproto.AddAsTypeOnly
if prevImport != nil {
prevTypeOnly = prevImport.addAsTypeOnly
}
entry.namedImports[symbolName] = &newImportBinding{
kind: lsproto.ImportKindCommonJS,
name: symbolName,
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
// !!! propertyName
}
} else {
debug.Assert(
entry.namespaceLikeImport == nil || entry.namespaceLikeImport.name == symbolName,
"Namespacelike import should be missing or match symbolName",
)
entry.namespaceLikeImport = &newImportBinding{
kind: lsproto.ImportKindCommonJS,
name: symbolName,
addAsTypeOnly: fix.AddAsTypeOnly,
}
}
case lsproto.ImportKindNamespace:
debug.Assert(
entry.namespaceLikeImport == nil || entry.namespaceLikeImport.name == symbolName,
"Namespacelike import should be missing or match symbolName",
)
entry.namespaceLikeImport = &newImportBinding{
kind: lsproto.ImportKindNamespace,
name: symbolName,
addAsTypeOnly: fix.AddAsTypeOnly,
}
}
case lsproto.AutoImportFixKindPromoteTypeOnly:
// Excluding from fix-all
default:
debug.Fail(fmt.Sprintf("Unexpected fix kind: %v", fix.Kind))
}
}
// `NotAllowed` overrides `Required` because one addition of a new import might be required to be type-only
// because of `--importsNotUsedAsValues=error`, but if a second addition of the same import is `NotAllowed`
// to be type-only, the reason the first one was `Required` - the unused runtime dependency - is now moot.
// Alternatively, if one addition is `Required` because it has no value meaning under `--preserveValueImports`
// and `--isolatedModules`, it should be impossible for another addition to be `NotAllowed` since that would
// mean a type is being referenced in a value location.
func reduceAddAsTypeOnlyValues(prevValue, newValue lsproto.AddAsTypeOnly) lsproto.AddAsTypeOnly {
if newValue > prevValue {
return newValue
}
return prevValue
}
func (adder *importAdder) getNewImportEntry(moduleSpecifier string, importKind lsproto.ImportKind, useRequire bool, addAsTypeOnly lsproto.AddAsTypeOnly) *importsCollection {
// A default import that requires type-only makes the whole import type-only.
// (We could add `default` as a named import, but that style seems undesirable.)
// Under `--preserveValueImports` and `--importsNotUsedAsValues=error`, if a
// module default-exports a type but named-exports some values (weird), you would
// have to use a type-only default import and non-type-only named imports. These
// require two separate import declarations, so we build this into the map key.
typeOnlyKey := newImportsKey(moduleSpecifier, true /*topLevelTypeOnly*/)
nonTypeOnlyKey := newImportsKey(moduleSpecifier, false /*topLevelTypeOnly*/)
typeOnlyEntry := adder.newImports[typeOnlyKey]
nonTypeOnlyEntry := adder.newImports[nonTypeOnlyKey]
newEntry := &importsCollection{
useRequire: useRequire,
}
if importKind == lsproto.ImportKindDefault && addAsTypeOnly == lsproto.AddAsTypeOnlyRequired {
if typeOnlyEntry != nil {
return typeOnlyEntry
}
adder.newImports[typeOnlyKey] = newEntry
return newEntry
}
if addAsTypeOnly == lsproto.AddAsTypeOnlyAllowed && (typeOnlyEntry != nil || nonTypeOnlyEntry != nil) {
if typeOnlyEntry != nil {
return typeOnlyEntry
}
return nonTypeOnlyEntry
}
if nonTypeOnlyEntry != nil {
return nonTypeOnlyEntry
}
adder.newImports[nonTypeOnlyKey] = newEntry
return newEntry
}
func (adder *importAdder) getAllExportsForSymbol(
symbol *ast.Symbol,
) []*Export {
if export := SymbolToExport(symbol, adder.checker); export != nil {
return adder.view.SearchByExportID(export.ExportID)
}
return nil
}
func TypeToAutoImportableTypeNode(
c *checker.Checker,
importAdder ImportAdder,
t *checker.Type,
contextNode *ast.Node, // !!! flags
) *ast.TypeNode {
idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol)
typeNode := c.TypeToTypeNode(t, contextNode, nodebuilder.FlagsNone, idToSymbol)
if typeNode == nil {
return nil
}
return TypeNodeToAutoImportableTypeNode(typeNode, importAdder, idToSymbol)
}
// TypeNodeToAutoImportableTypeNode converts import type references in a type node to
// simple type references and registers needed imports with the import adder.
func TypeNodeToAutoImportableTypeNode(
typeNode *ast.TypeNode,
importAdder ImportAdder,
idToSymbol map[*ast.IdentifierNode]*ast.Symbol,
) *ast.TypeNode {
referenceTypeNode, importableSymbols := TryGetAutoImportableReferenceFromTypeNode(typeNode, idToSymbol)
if referenceTypeNode != nil {
if importAdder != nil {
importSymbols(importAdder, importableSymbols)
}
typeNode = referenceTypeNode
}
// !!! handle type node reuse: nodes needs to be fresh here but also preserve symbols
return typeNode
}
func importSymbols(importAdder ImportAdder, symbols []*ast.Symbol) {
for _, symbol := range symbols {
importAdder.AddImportFromExportedSymbol(symbol, true /*isValidTypeOnlyUseSite*/)
}
}
// Given a type node containing 'import("./a").SomeType<import("./b").OtherType<...>>',
// returns an equivalent type reference node with any nested ImportTypeNodes also replaced
// with type references, and a list of symbols that must be imported to use the type reference.
// TryGetAutoImportableReferenceFromTypeNode converts import type references in a type node
// to simple type references and returns the transformed type node and the symbols that need
// to be imported.
func TryGetAutoImportableReferenceFromTypeNode(importTypeNode *ast.TypeNode, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) (*ast.TypeNode, []*ast.Symbol) {
var symbols []*ast.Symbol
var visitor *ast.NodeVisitor
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
visit := func(node *ast.Node) *ast.Node {
if ast.IsLiteralImportTypeNode(node) && node.AsImportTypeNode().Qualifier != nil {
importTypeNode := node.AsImportTypeNode()
// Symbol for the left-most thing after the dot
firstIdentifier := ast.GetFirstIdentifier(importTypeNode.Qualifier)
symbol := idToSymbol[firstIdentifier]
if symbol == nil {
// if symbol is missing then this doesn't come from a synthesized import type node
// it has to be an import type node authored by the user and thus it has to be valid
// it can't refer to reserved internal symbol names and such
return node.VisitEachChild(visitor)
}
name := getNameForExportedSymbol(symbol, false /*preferCapitalized*/)
var qualifier *ast.EntityName
if name != firstIdentifier.Text() {
qualifier = replaceFirstIdentifierOfEntityName(factory, importTypeNode.Qualifier, factory.NewIdentifier(name))
} else {
qualifier = importTypeNode.Qualifier
}
symbols = append(symbols, symbol)
typeArguments := visitor.VisitNodes(importTypeNode.TypeArguments)
return factory.NewTypeReferenceNode(qualifier, typeArguments)
}
return visitor.VisitEachChild(node)
}
visitor = ast.NewNodeVisitor(visit, factory, ast.NodeVisitorHooks{})
typeNode := visitor.VisitNode(importTypeNode)
debug.Assert(typeNode == nil || ast.IsTypeNode(typeNode), "expected a type node")
return typeNode, symbols
}
// If a type checker and multiple files are available, consider using `forEachNameOfDefaultExport`
// instead, which searches for names of re-exported defaults/namespaces in target files.
func getNameForExportedSymbol(symbol *ast.Symbol, preferCapitalized bool) string {
if symbol.Name == ast.InternalSymbolNameExportEquals || symbol.Name == ast.InternalSymbolNameDefault {
// Names for default exports:
// - export default foo => foo
// - export { foo as default } => foo
// - export default 0 => filename converted to camelCase
name := getDefaultLikeExportNameFromDeclaration(symbol)
if name != "" {
return name
}
debug.Assert(symbol.Parent != nil, "Expected exported symbol to have module symbol as parent")
return lsutil.ModuleSymbolToValidIdentifier(symbol.Parent, preferCapitalized)
}
return symbol.Name
}
func replaceFirstIdentifierOfEntityName(factory *ast.NodeFactory, name *ast.EntityName, newIdentifier *ast.IdentifierNode) *ast.EntityName {
if name.Kind == ast.KindIdentifier {
return newIdentifier
}
return factory.NewQualifiedName(
replaceFirstIdentifierOfEntityName(factory, name.AsQualifiedName().Left, newIdentifier),
name.AsQualifiedName().Right,
)
}
func (adder *importAdder) getImportFixForSymbol(view *View, file *ast.SourceFile, exports []*Export, isValidTypeOnlyUseSite bool) *Fix {
fixes := core.FlatMap(exports, func(export *Export) []*Fix {
return view.GetFixes(adder.ctx, export, false /*forJSX*/, isValidTypeOnlyUseSite, nil /*usagePosition*/)
})
slices.SortFunc(fixes, func(a, b *Fix) int {
return view.CompareFixesForRanking(a, b)
})
if len(fixes) > 0 {
return fixes[0]
}
return nil
}

View File

@@ -0,0 +1,186 @@
package autoimport
import (
"strings"
"unicode"
"unicode/utf8"
"github.com/microsoft/typescript-go/internal/core"
)
// Named is a constraint for types that can provide their name.
type Named interface {
Name() string
}
// Index stores entries with an index mapping uppercase letters to entries whose name
// starts with that letter, and lowercase letters to entries whose name contains a
// word starting with that letter.
type Index[T Named] struct {
entries []T
index map[rune][]int
}
func (idx *Index[T]) Find(name string, caseSensitive bool) []T {
if len(idx.entries) == 0 || len(name) == 0 {
return nil
}
firstRune := core.FirstResult(utf8.DecodeRuneInString(name))
if firstRune == utf8.RuneError {
return nil
}
firstRuneUpper := unicode.ToUpper(firstRune)
candidates, ok := idx.index[firstRuneUpper]
if !ok {
return nil
}
var results []T
for _, entryIndex := range candidates {
entry := idx.entries[entryIndex]
entryName := entry.Name()
if (caseSensitive && entryName == name) || (!caseSensitive && strings.EqualFold(entryName, name)) {
results = append(results, entry)
}
}
return results
}
// SearchWordPrefix returns each entry whose name contains a word beginning with
// the first character of 'prefix', and whose name contains all characters
// of 'prefix' in order (case-insensitive). If 'filter' is provided, only entries
// for which filter(entry) returns true are included.
func (idx *Index[T]) SearchWordPrefix(prefix string) []T {
if len(idx.entries) == 0 {
return nil
}
if len(prefix) == 0 {
return idx.entries
}
prefix = strings.ToLower(prefix)
firstRune, _ := utf8.DecodeRuneInString(prefix)
if firstRune == utf8.RuneError {
return nil
}
firstRuneUpper := unicode.ToUpper(firstRune)
firstRuneLower := unicode.ToLower(firstRune)
// Look up entries that have words starting with this letter
var wordStarts []int
nameStarts, _ := idx.index[firstRuneUpper]
if firstRuneUpper != firstRuneLower {
wordStarts, _ = idx.index[firstRuneLower]
}
count := len(nameStarts) + len(wordStarts)
if count == 0 {
return nil
}
// Filter entries by checking if they contain all characters in order
results := make([]T, 0, count)
for _, starts := range [][]int{nameStarts, wordStarts} {
for _, i := range starts {
entry := idx.entries[i]
if containsCharsInOrder(entry.Name(), prefix) {
results = append(results, entry)
}
}
}
return results
}
// containsCharsInOrder checks if str contains all characters from pattern in order (case-insensitive).
func containsCharsInOrder(str, pattern string) bool {
str = strings.ToLower(str)
pattern = strings.ToLower(pattern)
patternIdx := 0
for _, ch := range str {
if patternIdx < len(pattern) {
patternRune, size := utf8.DecodeRuneInString(pattern[patternIdx:])
if ch == patternRune {
patternIdx += size
}
}
}
return patternIdx == len(pattern)
}
// insertAsWords adds a value to the index keyed by the first letter of each word in its name.
func (idx *Index[T]) insertAsWords(value T) {
if idx.index == nil {
idx.index = make(map[rune][]int)
}
name := value.Name()
if len(name) == 0 {
panic("Cannot index entry with empty name")
}
entryIndex := len(idx.entries)
idx.entries = append(idx.entries, value)
indices := wordIndices(name)
seenRunes := make(map[rune]bool)
for i, start := range indices {
substr := name[start:]
firstRune, _ := utf8.DecodeRuneInString(substr)
if firstRune == utf8.RuneError {
continue
}
if i == 0 {
// Name start keyed by uppercase
firstRune = unicode.ToUpper(firstRune)
idx.index[firstRune] = append(idx.index[firstRune], entryIndex)
seenRunes[firstRune] = true // (Still set seenRunes in case first character is non-alphabetic)
} else {
// Subsequent word starts keyed by lowercase
firstRune = unicode.ToLower(firstRune)
if !seenRunes[firstRune] {
idx.index[firstRune] = append(idx.index[firstRune], entryIndex)
seenRunes[firstRune] = true
}
}
}
}
// Clone creates a new Index containing only entries for which filter returns true.
func (idx *Index[T]) Clone(filter func(T) bool) *Index[T] {
if idx == nil {
return nil
}
newIdx := &Index[T]{
entries: make([]T, 0, len(idx.entries)),
index: make(map[rune][]int, len(idx.index)),
}
// Build mapping from old index to new index for filtered entries
oldToNew := make(map[int]int, len(idx.entries))
for oldIndex, entry := range idx.entries {
if filter(entry) {
newIndex := len(newIdx.entries)
newIdx.entries = append(newIdx.entries, entry)
oldToNew[oldIndex] = newIndex
}
}
// Rebuild the index with remapped indices
for r, oldIndices := range idx.index {
newIndices := make([]int, 0, len(oldIndices))
for _, oldIndex := range oldIndices {
if newIndex, ok := oldToNew[oldIndex]; ok {
newIndices = append(newIndices, newIndex)
}
}
if len(newIndices) > 0 {
newIdx.index[r] = newIndices
}
}
return newIdx
}

View File

@@ -0,0 +1,79 @@
package autoimport
import (
"testing"
"gotest.tools/v3/assert"
)
type testEntry struct {
name string
package_ string
}
func (e *testEntry) Name() string { return e.name }
func TestIndexClone(t *testing.T) {
t.Parallel()
t.Run("filters entries by package", func(t *testing.T) {
t.Parallel()
idx := &Index[*testEntry]{}
idx.insertAsWords(&testEntry{name: "fooBar", package_: "pkg-a"})
idx.insertAsWords(&testEntry{name: "bazQux", package_: "pkg-b"})
idx.insertAsWords(&testEntry{name: "fooQux", package_: "pkg-a"})
// Clone excluding pkg-b
cloned := idx.Clone(func(e *testEntry) bool {
return e.package_ != "pkg-b"
})
// Original should have all 3 entries
assert.Equal(t, len(idx.entries), 3)
// Cloned should have 2 entries (only pkg-a)
assert.Equal(t, len(cloned.entries), 2)
// Search should work on cloned index
results := cloned.Find("fooBar", true)
assert.Equal(t, len(results), 1)
assert.Equal(t, results[0].name, "fooBar")
// bazQux should not be in cloned index
results = cloned.Find("bazQux", true)
assert.Equal(t, len(results), 0)
// Word prefix search should work
results = cloned.SearchWordPrefix("foo")
assert.Equal(t, len(results), 2)
})
t.Run("handles nil index", func(t *testing.T) {
t.Parallel()
var idx *Index[*testEntry]
cloned := idx.Clone(func(e *testEntry) bool { return true })
assert.Assert(t, cloned == nil)
})
t.Run("handles empty index", func(t *testing.T) {
t.Parallel()
idx := &Index[*testEntry]{}
cloned := idx.Clone(func(e *testEntry) bool { return true })
assert.Equal(t, len(cloned.entries), 0)
})
t.Run("filters all entries", func(t *testing.T) {
t.Parallel()
idx := &Index[*testEntry]{}
idx.insertAsWords(&testEntry{name: "fooBar", package_: "pkg-a"})
idx.insertAsWords(&testEntry{name: "bazQux", package_: "pkg-b"})
cloned := idx.Clone(func(e *testEntry) bool { return false })
assert.Equal(t, len(cloned.entries), 0)
assert.Equal(t, len(cloned.index), 0)
})
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
package autoimport
import (
"strings"
"github.com/microsoft/typescript-go/internal/modulespecifiers"
)
func (v *View) GetModuleSpecifier(
export *Export,
userPreferences modulespecifiers.UserPreferences,
) (string, modulespecifiers.ResultKind) {
// Ambient module
if modulespecifiers.PathIsBareSpecifier(string(export.ModuleID)) {
specifier := string(export.ModuleID)
if modulespecifiers.IsExcludedByRegex(specifier, userPreferences.AutoImportSpecifierExcludeRegexes) {
return "", modulespecifiers.ResultKindNone
}
return string(export.ModuleID), modulespecifiers.ResultKindAmbient
}
if export.PackageName != "" {
if entrypoints, ok := v.registry.entrypoints[export.Path]; ok {
for _, entrypoint := range entrypoints {
if entrypoint.IncludeConditions.IsSubsetOf(v.conditions) && !v.conditions.Intersects(entrypoint.ExcludeConditions) {
specifier := modulespecifiers.ProcessEntrypointEnding(
entrypoint,
userPreferences,
v.program,
v.program.Options(),
v.importingFile,
v.getAllowedEndings(),
)
if !modulespecifiers.IsExcludedByRegex(specifier, userPreferences.AutoImportSpecifierExcludeRegexes) {
return specifier, modulespecifiers.ResultKindNodeModules
}
}
}
return "", modulespecifiers.ResultKindNone
}
}
cache := v.registry.specifierCache[v.importingFile.Path()]
if export.PackageName == "" {
if specifier, ok := cache.Load(export.Path); ok {
if specifier == "" {
return "", modulespecifiers.ResultKindNone
}
return specifier, modulespecifiers.ResultKindRelative
}
}
specifiers, kind := modulespecifiers.GetModuleSpecifiersForFileWithInfo(
v.importingFile,
export.ModuleFileName,
v.program.Options(),
v.program,
userPreferences,
modulespecifiers.ModuleSpecifierOptions{},
true,
)
// !!! unsure when this could return multiple specifiers combined with the
// new node_modules code. Possibly with local symlinks, which should be
// very rare.
for _, specifier := range specifiers {
if strings.Contains(specifier, "/node_modules/") {
continue
}
cache.Store(export.Path, specifier)
return specifier, kind
}
cache.Store(export.Path, "")
return "", modulespecifiers.ResultKindNone
}

View File

@@ -0,0 +1,14 @@
package autoimport_test
import (
"testing"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/testutil/baseline"
)
func TestMain(m *testing.M) {
core.ApplyDebugStackLimit()
defer baseline.Track()()
m.Run()
}

View File

@@ -0,0 +1,323 @@
package autoimport
import (
"context"
"runtime"
"strings"
"sync/atomic"
"unicode"
"unicode/utf8"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/modulespecifiers"
"github.com/microsoft/typescript-go/internal/packagejson"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/wrapvfs"
)
func tryGetModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, string, bool) {
if !symbol.IsExternalModule() {
return "", "", false
}
decl := ast.GetNonAugmentationDeclaration(symbol)
if decl == nil {
return "", "", false
}
if decl.Kind == ast.KindSourceFile {
return ModuleID(decl.AsSourceFile().Path()), decl.AsSourceFile().FileName(), true
}
if ast.IsModuleWithStringLiteralName(decl) {
return ModuleID(decl.Name().Text()), "", true
}
return "", "", false
}
func getModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, string) {
if !symbol.IsExternalModule() {
panic("symbol is not an external module")
}
decl := ast.GetNonAugmentationDeclaration(symbol)
if decl == nil {
panic("module symbol has no non-augmentation declaration")
}
if decl.Kind == ast.KindSourceFile {
return ModuleID(decl.AsSourceFile().Path()), decl.AsSourceFile().FileName()
}
if ast.IsModuleWithStringLiteralName(decl) {
return ModuleID(decl.Name().Text()), ""
}
panic("could not determine module ID of module symbol")
}
// wordIndices splits an identifier into its constituent words based on camelCase and snake_case conventions
// by returning the starting byte indices of each word. The first index is always 0.
// - CamelCase
// ^ ^
// - snake_case
// ^ ^
// - ParseURL
// ^ ^
// - __proto__
// ^
func wordIndices(s string) []int {
var indices []int
for byteIndex, runeValue := range s {
if byteIndex == 0 {
indices = append(indices, byteIndex)
continue
}
if runeValue == '_' {
if byteIndex+1 < len(s) && s[byteIndex+1] != '_' {
indices = append(indices, byteIndex+1)
}
continue
}
if unicode.IsUpper(runeValue) && (unicode.IsLower(core.FirstResult(utf8.DecodeLastRuneInString(s[:byteIndex]))) || (byteIndex+1 < len(s) && unicode.IsLower(core.FirstResult(utf8.DecodeRuneInString(s[byteIndex+1:]))))) {
indices = append(indices, byteIndex)
}
}
return indices
}
func getPackageNamesInNodeModules(nodeModulesDir string, fs vfs.FS) *collections.Set[string] {
packageNames := &collections.Set[string]{}
if tspath.GetBaseFileName(nodeModulesDir) != "node_modules" {
panic("nodeModulesDir is not a node_modules directory")
}
// A missing node_modules directory yields no entries (GetAccessibleEntries returns
// empty), so there's no need to check existence first: a deleted node_modules is
// handled upstream in updateBucketAndDirectoryExistence, which drops the bucket.
entries := fs.GetAccessibleEntries(nodeModulesDir)
for _, baseName := range entries.Directories {
if baseName[0] == '.' {
continue
}
if baseName[0] == '@' {
scopedDirPath := tspath.CombinePaths(nodeModulesDir, baseName)
for _, scopedPackageDirName := range fs.GetAccessibleEntries(scopedDirPath).Directories {
scopedBaseName := tspath.GetBaseFileName(scopedPackageDirName)
if baseName == "@types" {
packageNames.Add(module.GetPackageNameFromTypesPackageName(tspath.CombinePaths("@types", scopedBaseName)))
} else {
packageNames.Add(tspath.CombinePaths(baseName, scopedBaseName))
}
}
continue
}
packageNames.Add(baseName)
}
return packageNames
}
func getDefaultLikeExportNameFromDeclaration(symbol *ast.Symbol) string {
for _, d := range symbol.Declarations {
// "export default" in this case. See `ExportAssignment`for more details.
if ast.IsExportAssignment(d) {
if innerExpression := ast.SkipOuterExpressions(d.Expression(), ast.OEKAll); ast.IsIdentifier(innerExpression) {
return innerExpression.Text()
}
continue
}
// "export { ~ as default }"
if ast.IsExportSpecifier(d) && d.Symbol().Flags == ast.SymbolFlagsAlias && d.PropertyName() != nil {
if d.PropertyName().Kind == ast.KindIdentifier {
return d.PropertyName().Text()
}
continue
}
// GH#52694
if name := ast.GetNameOfDeclaration(d); name != nil && name.Kind == ast.KindIdentifier {
return name.Text()
}
if symbol.Parent != nil && !checker.IsExternalModuleSymbol(symbol.Parent) {
return symbol.Parent.Name
}
}
return ""
}
func getResolvedPackageNames(ctx context.Context, program *compiler.Program) *collections.Set[string] {
rawNames := program.ResolvedPackageNames()
unresolvedPackageNames := program.UnresolvedPackageNames()
// Normalize @types/ package names to their actual package names
// (e.g., "@types/react" → "react"). ResolvedPackageNames can contain
// @types names when the program resolves an import like "react" to
// "@types/react/index.d.ts" via the PackageId.Name field.
resolvedPackageNames := collections.NewSetWithSizeHint[string](rawNames.Len())
for name := range rawNames.Keys() {
resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(name))
}
for _, name := range program.Options().Types {
if name != "*" {
resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(name))
}
}
if unresolvedPackageNames.Len() > 0 {
checker, done := program.GetTypeChecker(ctx)
defer done()
for name := range unresolvedPackageNames.Keys() {
if symbol := checker.TryFindAmbientModule(name); symbol != nil {
declaringFile := ast.GetSourceFileOfModule(symbol)
if packageName := modulespecifiers.GetPackageNameFromDirectory(declaringFile.FileName()); packageName != "" {
resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(packageName))
}
}
}
}
return resolvedPackageNames
}
// addProjectReferenceOutputMappings adds output .d.ts to source file mappings
// from a program's project references to the provided map.
// This is used during node_modules bucket building to redirect extraction
// from output files to source files when the output is from a project reference.
func addProjectReferenceOutputMappings(program *compiler.Program, result map[tspath.Path]string) {
refs := program.GetResolvedProjectReferences()
for _, ref := range refs {
if ref == nil {
continue
}
ref.ParseInputOutputNames()
for outputDtsPath, mapping := range ref.OutputDtsToProjectReference() {
// Only add if not already present (first program wins)
if _, exists := result[outputDtsPath]; !exists {
result[outputDtsPath] = mapping.Source
}
}
}
}
func createCheckerPool(program checker.Program) (getChecker func() (*checker.Checker, func()), closePool func(), getCreatedCount func() int32) {
maxSize := int32(runtime.GOMAXPROCS(0))
pool := make(chan *checker.Checker, maxSize)
var created atomic.Int32
return func() (*checker.Checker, func()) {
// Try to get an existing checker
select {
case ch := <-pool:
return ch, func() { pool <- ch }
default:
break
}
// Try to create a new one if under limit
for {
current := created.Load()
if current >= maxSize {
// At limit, wait for one to become available
ch := <-pool
return ch, func() { pool <- ch }
}
if created.CompareAndSwap(current, current+1) {
ch := core.FirstResult(checker.NewChecker(program, nil))
return ch, func() { pool <- ch }
}
}
}, func() {
close(pool)
}, func() int32 {
return created.Load()
}
}
// addPackageJsonDependencies adds all dependencies and peerDependencies from a package.json
// to the given set, canonicalizing @types package names to their base names.
func addPackageJsonDependencies(contents *packagejson.PackageJson, deps *collections.Set[string]) {
contents.RangeDependencies(func(name, _, field string) bool {
if name == "" || name == "@types/" || name[0] == '.' {
// Edge cases that could make us blow up probably
return true
}
if field == "dependencies" || field == "peerDependencies" {
deps.Add(module.GetPackageNameFromTypesPackageName(name))
}
return true
})
}
// getPackageRealpathFuncs returns functions to transform between symlink and realpath for files within a package.
// It calls FS.Realpath once per package directory and uses prefix substitution for files within that directory,
// avoiding expensive realpath syscalls for each file. For files outside the package (e.g. re-exported
// dependencies reached through node_modules symlinks), it resolves the file's directory realpath once,
// finds the symlink boundary (the package root where the symlink lives), and caches that prefix mapping.
// All subsequent files under the same symlinked package directory use prefix substitution with no syscalls.
func getPackageRealpathFuncs(fs vfs.FS, packageDir string) (toRealpath, toSymlink func(string) string) {
realPackageDir := fs.Realpath(packageDir)
isSymlinked := realPackageDir != packageDir
// Cache of package-directory-level symlink→realpath prefix mappings for
// external packages encountered via re-exports. Keyed by the node_modules
// package directory (e.g. "/app/node_modules/dep"), so all files under
// that package reuse a single realpath lookup.
dirCache := make(map[string]string)
toRealpath = func(fileName string) string {
// Fast path: files within the package use prefix substitution.
if isSymlinked {
if after, ok := strings.CutPrefix(fileName, packageDir); ok {
return realPackageDir + after
}
}
// Files outside the package (e.g. re-exports into symlinked deps):
// find the node_modules package directory, resolve it once, and cache.
pkgDir := module.ParseNodeModuleFromPath(fileName, false /*isFolder*/)
if pkgDir == "" {
return fileName
}
if realDir, ok := dirCache[pkgDir]; ok {
if realDir == pkgDir {
return fileName
}
return realDir + fileName[len(pkgDir):]
}
realDir := fs.Realpath(pkgDir)
dirCache[pkgDir] = realDir
if realDir == pkgDir {
return fileName
}
return realDir + fileName[len(pkgDir):]
}
if !isSymlinked {
return toRealpath, core.Identity
}
// toSymlink only handles files within the package directory (reversing the
// packageDir→realPackageDir substitution). It does not handle arbitrary external
// paths; callers should only use it for files known to be within the package.
toSymlink = func(fileName string) string {
if after, ok := strings.CutPrefix(fileName, realPackageDir); ok {
return packageDir + after
}
return fileName
}
return toRealpath, toSymlink
}
type resolutionHost struct {
fs vfs.FS
currentDirectory string
}
var _ module.ResolutionHost = (*resolutionHost)(nil)
func (rh *resolutionHost) GetCurrentDirectory() string {
return rh.currentDirectory
}
func (rh *resolutionHost) FS() vfs.FS {
return rh.fs
}
func getModuleResolver(host RegistryCloneHost, realpath func(string) string, opts module.ResolverOptions) *module.Resolver {
rh := &resolutionHost{
fs: wrapvfs.Wrap(host.FS(), wrapvfs.Replacements{Realpath: realpath}),
currentDirectory: host.GetCurrentDirectory(),
}
return module.NewResolverWithOptions(rh, core.EmptyCompilerOptions, "", "", opts)
}

View File

@@ -0,0 +1,220 @@
package autoimport
import (
"reflect"
"testing"
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
"gotest.tools/v3/assert"
)
func TestWordIndices(t *testing.T) {
t.Parallel()
tests := []struct {
input string
expectedWords []string
}{
// Basic camelCase
{
input: "camelCase",
expectedWords: []string{"camelCase", "Case"},
},
// snake_case
{
input: "snake_case",
expectedWords: []string{"snake_case", "case"},
},
// ParseURL - uppercase sequence followed by lowercase
{
input: "ParseURL",
expectedWords: []string{"ParseURL", "URL"},
},
// XMLHttpRequest - multiple uppercase sequences
{
input: "XMLHttpRequest",
expectedWords: []string{"XMLHttpRequest", "HttpRequest", "Request"},
},
// Single word lowercase
{
input: "hello",
expectedWords: []string{"hello"},
},
// Single word uppercase
{
input: "HELLO",
expectedWords: []string{"HELLO"},
},
// Mixed with numbers
{
input: "parseHTML5Parser",
expectedWords: []string{"parseHTML5Parser", "HTML5Parser", "Parser"},
},
// Underscore variations
{
input: "__proto__",
expectedWords: []string{"__proto__", "proto__"},
},
{
input: "_private_member",
expectedWords: []string{"_private_member", "member"},
},
// Single character
{
input: "a",
expectedWords: []string{"a"},
},
{
input: "A",
expectedWords: []string{"A"},
},
// Consecutive underscores
{
input: "test__double__underscore",
expectedWords: []string{"test__double__underscore", "double__underscore", "underscore"},
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
t.Parallel()
indices := wordIndices(tt.input)
// Convert indices to actual word slices for comparison
var actualWords []string
for _, idx := range indices {
actualWords = append(actualWords, tt.input[idx:])
}
if !reflect.DeepEqual(actualWords, tt.expectedWords) {
t.Errorf("wordIndices(%q) produced words %v, want %v", tt.input, actualWords, tt.expectedWords)
}
})
}
}
// TestGetPackageRealpathFuncs_FollowsNodeModulesSymlinks tests that toRealpath correctly
// follows symlinks for files outside the package directory (e.g. node_modules entries).
// Without this, the module resolver uses unresolved symlink paths as cache keys, causing
// the same file to be loaded multiple times and triggering massive memory usage when
// barrel files re-export from many symlinked packages (issue #2780).
func TestGetPackageRealpathFuncs_FollowsNodeModulesSymlinks(t *testing.T) {
t.Parallel()
// Simulate a layout where the package directory is itself a symlink (e.g. Bazel's
// convenience symlinks or pnpm's virtual store):
// /symlink-bin/pkg/ -> symlink to /real/bin/pkg/
// /real/bin/pkg/node_modules/dep -> symlink to /real/dep/
//
// When toRealpath is used as the module resolver's Realpath, it must follow
// the node_modules symlink so that /real/bin/pkg/node_modules/dep/index.d.ts
// resolves to /real/dep/index.d.ts — otherwise the same dep file gets different
// cache keys depending on which path it was reached through.
fs := vfstest.FromMap(map[string]any{
"/symlink-bin/pkg": vfstest.Symlink("/real/bin/pkg"),
"/real/bin/pkg/index.d.ts": "export declare const a: number;",
"/real/bin/pkg/node_modules/dep": vfstest.Symlink("/real/dep"),
"/real/dep/index.d.ts": "export declare const b: number;",
"/real/dep/src/utils/helper.d.ts": "export declare const c: number;",
}, true)
toRealpath, _ := getPackageRealpathFuncs(fs, "/symlink-bin/pkg")
// Files inside the package should be converted via string replacement (fast path).
assert.Equal(
t,
toRealpath("/symlink-bin/pkg/index.d.ts"),
"/real/bin/pkg/index.d.ts",
"package files should be converted via prefix replacement",
)
// Files outside the package (e.g. node_modules symlinks) should be resolved via
// fs.Realpath so the cache key is the canonical realpath, not the symlink path.
assert.Equal(
t,
toRealpath("/real/bin/pkg/node_modules/dep/index.d.ts"),
"/real/dep/index.d.ts",
"node_modules symlinks must be followed so the same file gets a consistent cache key",
)
// Files in subdirectories of an already-resolved external package should
// use the cached prefix mapping without additional realpath calls.
assert.Equal(
t,
toRealpath("/real/bin/pkg/node_modules/dep/src/utils/helper.d.ts"),
"/real/dep/src/utils/helper.d.ts",
"subdirectories of a resolved external package should use cached prefix mapping",
)
}
// TestGetPackageRealpathFuncs_DuplicateCacheKeys demonstrates how the broken toRealpath
// causes the same physical file to get different cache keys when reached through different
// symlink paths. In pnpm/Bazel monorepos, multiple packages may have node_modules symlinks
// that point to the same physical dependency. Because toRealpath doesn't follow symlinks
// for files outside the package directory, each path is treated as distinct, leading to
// duplicate file loads and memory bloat (issue #2780).
func TestGetPackageRealpathFuncs_DuplicateCacheKeys(t *testing.T) {
t.Parallel()
// Simulate two packages (app-a, app-b) that each have a node_modules symlink to
// the same shared dependency. This is a typical pnpm/Bazel layout:
// /workspace/packages/app-a/ -> symlink to /store/app-a/
// /workspace/packages/app-b/ -> symlink to /store/app-b/
// /store/app-a/node_modules/shared-lib -> symlink to /store/shared-lib/
// /store/app-b/node_modules/shared-lib -> symlink to /store/shared-lib/
fs := vfstest.FromMap(map[string]any{
"/workspace/packages/app-a": vfstest.Symlink("/store/app-a"),
"/workspace/packages/app-b": vfstest.Symlink("/store/app-b"),
"/store/app-a/index.d.ts": "export declare const a: number;",
"/store/app-b/index.d.ts": "export declare const b: number;",
"/store/app-a/node_modules/shared-lib": vfstest.Symlink("/store/shared-lib"),
"/store/app-b/node_modules/shared-lib": vfstest.Symlink("/store/shared-lib"),
"/store/shared-lib/index.d.ts": "export declare const shared: string;",
}, true)
toRealpathA, _ := getPackageRealpathFuncs(fs, "/workspace/packages/app-a")
toRealpathB, _ := getPackageRealpathFuncs(fs, "/workspace/packages/app-b")
sharedFileViaA := "/store/app-a/node_modules/shared-lib/index.d.ts"
sharedFileViaB := "/store/app-b/node_modules/shared-lib/index.d.ts"
resolvedA := toRealpathA(sharedFileViaA)
resolvedB := toRealpathB(sharedFileViaB)
// Both should resolve to the same canonical realpath so the module resolver
// uses a single cache key for the shared dependency, avoiding duplicate loads.
expectedRealpath := "/store/shared-lib/index.d.ts"
assert.Equal(t, resolvedA, expectedRealpath,
"app-a's toRealpath should follow the node_modules symlink to the realpath")
assert.Equal(t, resolvedB, expectedRealpath,
"app-b's toRealpath should follow the node_modules symlink to the realpath")
}
// TestGetPackageRealpathFuncs_NonSymlinkedPackageWithSymlinkedDeps tests that even when the
// package directory itself is NOT a symlink, toRealpath still follows symlinks for files
// outside the package (e.g. re-exports reaching into symlinked node_modules dependencies).
func TestGetPackageRealpathFuncs_NonSymlinkedPackageWithSymlinkedDeps(t *testing.T) {
t.Parallel()
fs := vfstest.FromMap(map[string]any{
"/real/my-pkg/index.d.ts": "export declare const a: number;",
"/real/my-pkg/node_modules/dep": vfstest.Symlink("/real/dep"),
"/real/dep/index.d.ts": "export declare const b: number;",
}, true)
toRealpath, _ := getPackageRealpathFuncs(fs, "/real/my-pkg")
// Files inside the (non-symlinked) package should be returned unchanged.
assert.Equal(
t,
toRealpath("/real/my-pkg/index.d.ts"),
"/real/my-pkg/index.d.ts",
)
// Files outside the package reached via symlinked node_modules should still be resolved.
assert.Equal(
t,
toRealpath("/real/my-pkg/node_modules/dep/index.d.ts"),
"/real/dep/index.d.ts",
"symlinked deps must be resolved even when the package dir itself is not a symlink",
)
}

View File

@@ -0,0 +1,256 @@
package autoimport
import (
"context"
"slices"
"strings"
"unicode"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/modulespecifiers"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/tspath"
)
type View struct {
registry *Registry
importingFile *ast.SourceFile
program *compiler.Program
preferences modulespecifiers.UserPreferences
projectKey tspath.Path
allowedEndings []modulespecifiers.ModuleSpecifierEnding
conditions *collections.Set[string]
shouldUseUriStyleNodeCoreModules core.Tristate
existingImports *collections.MultiMap[ModuleID, existingImport]
shouldUseRequireForFixes *bool
}
func NewView(registry *Registry, importingFile *ast.SourceFile, projectKey tspath.Path, program *compiler.Program, preferences modulespecifiers.UserPreferences) *View {
return &View{
registry: registry,
importingFile: importingFile,
program: program,
projectKey: projectKey,
preferences: preferences,
conditions: collections.NewSetFromItems(
module.GetConditions(program.Options(),
program.GetDefaultResolutionModeForFile(importingFile))...,
),
shouldUseUriStyleNodeCoreModules: lsutil.ShouldUseUriStyleNodeCoreModules(importingFile, program),
}
}
func (v *View) getAllowedEndings() []modulespecifiers.ModuleSpecifierEnding {
if v.allowedEndings == nil {
resolutionMode := v.program.GetDefaultResolutionModeForFile(v.importingFile)
v.allowedEndings = modulespecifiers.GetAllowedEndingsInPreferredOrder(
v.preferences,
v.program,
v.program.Options(),
v.importingFile,
"",
resolutionMode,
)
}
return v.allowedEndings
}
type QueryKind int
const (
QueryKindWordPrefix QueryKind = iota
QueryKindExactMatch
QueryKindCaseInsensitiveMatch
)
func (v *View) Search(query string, kind QueryKind) []*Export {
searchFn := func(bucket *RegistryBucket) []*Export {
switch kind {
case QueryKindWordPrefix:
return bucket.Index.SearchWordPrefix(query)
case QueryKindExactMatch:
return bucket.Index.Find(query, true)
case QueryKindCaseInsensitiveMatch:
return bucket.Index.Find(query, false)
default:
panic("unreachable")
}
}
return v.search(searchFn)
}
func (v *View) SearchByExportID(id ExportID) []*Export {
search := func(bucket *RegistryBucket) []*Export {
return core.Filter(bucket.Index.entries, func(e *Export) bool {
return e.ExportID == id
})
}
return v.search(search)
}
func (v *View) search(searchFn func(*RegistryBucket) []*Export) []*Export {
var results []*Export
if bucket, ok := v.registry.projects[v.projectKey]; ok {
exports := searchFn(bucket)
results = slices.Grow(results, len(exports))
for _, e := range exports {
if string(e.ModuleID) == string(v.importingFile.Path()) {
// Don't auto-import from the importing file itself
continue
}
results = append(results, e)
}
}
// Compute the set of packages accessible to the importing file.
// This includes packages from package.json dependencies (aggregated from ancestor directories)
// plus packages that are directly imported by the project's program files.
// If no package.json is found, allowedPackages remains nil and all packages are allowed.
var allowedPackages *collections.Set[string]
tspath.ForEachAncestorDirectoryPath(v.importingFile.Path().GetDirectoryPath(), func(dirPath tspath.Path) (result any, stop bool) {
if dir, ok := v.registry.directories[dirPath]; ok {
if pj := dir.packageJson; pj.Exists() && pj.Contents.Parseable {
// Initialize to empty set if this is the first package.json we've seen
if allowedPackages == nil {
allowedPackages = &collections.Set[string]{}
}
addPackageJsonDependencies(pj.Contents, allowedPackages)
}
}
return nil, false
})
// If we found at least one package.json, also include packages directly imported by the project
if allowedPackages != nil {
if bucket, ok := v.registry.projects[v.projectKey]; ok {
allowedPackages = allowedPackages.UnionedWith(bucket.ResolvedPackageNames)
}
}
excludePackages := &collections.Set[string]{}
tspath.ForEachAncestorDirectoryPath(v.importingFile.Path().GetDirectoryPath(), func(dirPath tspath.Path) (result any, stop bool) {
if nodeModulesBucket, ok := v.registry.nodeModules[dirPath]; ok {
exports := searchFn(nodeModulesBucket)
results = slices.Grow(results, len(exports))
for _, e := range exports {
// Exclude packages found in lower node_modules (shadowing)
if excludePackages.Has(e.PackageName) {
continue
}
// If allowedPackages is nil, no package.json was found, so include all packages.
// Otherwise, only include packages that are dependencies or directly imported.
if allowedPackages != nil && !allowedPackages.Has(e.PackageName) {
continue
}
results = append(results, e)
}
// As we go up the directory tree, exclude packages found in lower node_modules
for pkgName := range nodeModulesBucket.PackageFiles {
excludePackages.Add(pkgName)
}
}
return nil, false
})
return results
}
type FixAndExport struct {
Fix *Fix
Export *Export
}
func (v *View) GetCompletions(ctx context.Context, prefix string, position lsproto.Position, forJSX bool, isTypeOnlyLocation bool) []*FixAndExport {
results := v.Search(prefix, QueryKindWordPrefix)
type exportGroupKey struct {
target ExportID
name string
ambientModuleOrPackageName string
}
grouped := make(map[exportGroupKey][]*Export, len(results))
outer:
for _, e := range results {
name := e.Name()
if !scanner.IsIdentifierText(name, core.LanguageVariantStandard) {
continue
}
if forJSX && !(unicode.IsUpper(rune(name[0])) || e.IsRenameable()) {
continue
}
target := e.ExportID
if e.Target != (ExportID{}) {
target = e.Target
}
key := exportGroupKey{
target: target,
name: name,
ambientModuleOrPackageName: core.FirstNonZero(e.AmbientModuleName(), e.PackageName),
}
if e.PackageName == "@types/node" || strings.Contains(string(e.Path), "/node_modules/@types/node/") {
if _, ok := core.UnprefixedNodeCoreModules[key.ambientModuleOrPackageName]; ok {
// Group URI-style and non-URI style node core modules together so the ranking logic
// is allowed to drop one if an explicit preference is detected.
key.ambientModuleOrPackageName = "node:" + key.ambientModuleOrPackageName
}
}
if existing, ok := grouped[key]; ok {
for i, ex := range existing {
if e.ExportID == ex.ExportID {
grouped[key] = slices.Replace(existing, i, i+1, &Export{
ExportID: e.ExportID,
ModuleFileName: e.ModuleFileName,
PackageName: e.PackageName,
IsTypeOnly: e.IsTypeOnly || ex.IsTypeOnly,
Syntax: min(e.Syntax, ex.Syntax),
Flags: e.Flags | ex.Flags,
ScriptElementKind: min(e.ScriptElementKind, ex.ScriptElementKind),
ScriptElementKindModifiers: e.ScriptElementKindModifiers | ex.ScriptElementKindModifiers,
localName: e.localName,
Target: e.Target,
Path: e.Path,
})
continue outer
}
}
}
grouped[key] = append(grouped[key], e)
}
fixes := make([]*FixAndExport, 0, len(results))
compareFixes := func(a, b *FixAndExport) int {
return v.CompareFixesForRanking(a.Fix, b.Fix)
}
for _, exps := range grouped {
fixesForGroup := make([]*FixAndExport, 0, len(exps))
for _, e := range exps {
for _, fix := range v.GetFixes(ctx, e, forJSX, isTypeOnlyLocation, &position) {
fixesForGroup = append(fixesForGroup, &FixAndExport{
Fix: fix,
Export: e,
})
}
}
fixes = append(fixes, core.MinAllFunc(fixesForGroup, compareFixes)...)
}
// The client will do additional sorting by SortText and Label, so we don't
// need to consider the name in our sorting here; we only need to produce a
// stable relative ordering between completions that the client will consider
// equivalent.
slices.SortFunc(fixes, func(a, b *FixAndExport) int {
return v.CompareFixesForSorting(a.Fix, b.Fix)
})
return fixes
}

View File

@@ -0,0 +1,99 @@
package ls
import (
"context"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
)
func (l *LanguageService) ProvideOnAutoInsert(ctx context.Context, params *lsproto.VSOnAutoInsertParams) (lsproto.VSOnAutoInsertResponse, error) {
if l.UserPreferences().EnableAutoClosingTags.IsFalse() {
return lsproto.VSOnAutoInsertResponse{}, nil
}
if params.VSCh != ">" {
return lsproto.VSOnAutoInsertResponse{}, nil
}
_, sourceFile := l.getProgramAndFile(params.VSTextDocument.Uri)
position := l.converters.LineAndCharacterToPosition(sourceFile, params.VSPosition)
token := astnav.FindPrecedingToken(sourceFile, int(position))
if token == nil {
return lsproto.VSOnAutoInsertResponse{}, nil
}
var closingText string
var element *ast.Node
if token.Kind == ast.KindGreaterThanToken && ast.IsJsxOpeningElement(token.Parent) {
element = token.Parent.Parent
} else if ast.IsJsxText(token) && ast.IsJsxElement(token.Parent) {
element = token.Parent
}
if element != nil && isUnclosedTag(element.AsJsxElement()) {
tagNameNode := element.AsJsxElement().OpeningElement.TagName()
// Slight divergence from Strada - we don't use the verbatim text from the opening tag.
closingText = "</" + ast.EntityNameToString(tagNameNode, scanner.GetTextOfNode) + ">"
} else {
var fragment *ast.Node
if token.Kind == ast.KindGreaterThanToken && ast.IsJsxOpeningFragment(token.Parent) {
fragment = token.Parent.Parent
} else if ast.IsJsxText(token) && ast.IsJsxFragment(token.Parent) {
fragment = token.Parent
}
if fragment != nil && isUnclosedFragment(fragment.AsJsxFragment()) {
closingText = "</>"
}
}
if closingText == "" {
return lsproto.VSOnAutoInsertResponse{}, nil
}
return lsproto.VSOnAutoInsertResponse{
VSOnAutoInsertResponseItem: &lsproto.VSOnAutoInsertResponseItem{
VSTextEditFormat: lsproto.InsertTextFormatSnippet,
VSTextEdit: &lsproto.TextEdit{
Range: lsproto.Range{Start: params.VSPosition, End: params.VSPosition},
// Tag names can contain `$` (valid JSX identifier characters), so
// escape the closing text to avoid being interpreted as a snippet
// placeholder/variable.
NewText: "$0" + escapeSnippetText(closingText),
},
},
}, nil
}
func isUnclosedTag(node *ast.JsxElement) bool {
openingElement := node.OpeningElement
closingElement := node.ClosingElement
if !ast.TagNamesAreEquivalent(openingElement.TagName(), closingElement.TagName()) {
return true
}
parent := node.Parent
if ast.IsJsxElement(parent) {
parent := parent.AsJsxElement()
return ast.TagNamesAreEquivalent(openingElement.TagName(), parent.OpeningElement.TagName()) && isUnclosedTag(parent)
}
return false
}
func isUnclosedFragment(node *ast.JsxFragment) bool {
closingFragment := node.ClosingFragment
if closingFragment.Flags&ast.NodeFlagsThisNodeHasError != 0 {
return true
}
parent := node.Parent
if ast.IsJsxFragment(parent) && isUnclosedFragment(parent.AsJsxFragment()) {
return true
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,270 @@
package change
import (
"slices"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/debug"
"github.com/microsoft/typescript-go/internal/format"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/stringutil"
)
// deleteDeclaration deletes a node with smart handling for different node types.
// This handles special cases like import specifiers in lists, parameters, etc.
func deleteDeclaration(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) {
switch node.Kind {
case ast.KindParameter:
oldFunction := node.Parent
if oldFunction.Kind == ast.KindArrowFunction &&
len(oldFunction.AsArrowFunction().Parameters.Nodes) == 1 &&
astnav.FindChildOfKind(oldFunction, ast.KindOpenParenToken, sourceFile) == nil {
// Lambdas with exactly one parameter are special because, after removal, there
// must be an empty parameter list (i.e. `()`) and this won't necessarily be the
// case if the parameter is simply removed (e.g. in `x => 1`).
t.ReplaceRangeWithText(sourceFile, t.GetAdjustedRange(sourceFile, node, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude), "()")
} else {
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
}
case ast.KindImportDeclaration, ast.KindImportEqualsDeclaration:
imports := sourceFile.Imports()
isFirstImport := len(imports) > 0 && node == imports[0].Parent ||
node == core.Find(sourceFile.Statements.Nodes, func(s *ast.Node) bool { return ast.IsAnyImportSyntax(s) })
// For first import, leave header comment in place, otherwise only delete JSDoc comments
leadingTrivia := LeadingTriviaOptionStartLine
if isFirstImport {
leadingTrivia = LeadingTriviaOptionExclude
} else if hasJSDocNodes(node) {
leadingTrivia = LeadingTriviaOptionJSDoc
}
deleteNode(t, sourceFile, node, leadingTrivia, TrailingTriviaOptionInclude)
case ast.KindBindingElement:
pattern := node.Parent
preserveComma := pattern.Kind == ast.KindArrayBindingPattern &&
node != pattern.AsBindingPattern().Elements.Nodes[len(pattern.AsBindingPattern().Elements.Nodes)-1]
if preserveComma {
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionExclude)
} else {
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
}
case ast.KindVariableDeclaration:
deleteVariableDeclaration(t, deletedNodesInLists, sourceFile, node)
case ast.KindTypeParameter:
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
case ast.KindImportSpecifier:
namedImports := node.Parent
if len(namedImports.AsNamedImports().Elements.Nodes) == 1 {
deleteImportBinding(t, sourceFile, namedImports)
} else {
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
}
case ast.KindNamespaceImport:
deleteImportBinding(t, sourceFile, node)
case ast.KindSemicolonToken:
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionExclude)
case ast.KindTypeKeyword:
// For type keyword in import clauses, we need to delete the keyword and any trailing space
// The trailing space is part of the next token's leading trivia, so we include it
deleteNode(t, sourceFile, node, LeadingTriviaOptionExclude, TrailingTriviaOptionInclude)
case ast.KindFunctionKeyword:
deleteNode(t, sourceFile, node, LeadingTriviaOptionExclude, TrailingTriviaOptionInclude)
case ast.KindClassDeclaration, ast.KindFunctionDeclaration:
leadingTrivia := LeadingTriviaOptionStartLine
if hasJSDocNodes(node) {
leadingTrivia = LeadingTriviaOptionJSDoc
}
deleteNode(t, sourceFile, node, leadingTrivia, TrailingTriviaOptionInclude)
default:
if node.Parent == nil {
// a misbehaving client can reach here with the SourceFile node
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
} else if node.Parent.Kind == ast.KindImportClause && node.Parent.AsImportClause().Name() == node {
deleteDefaultImport(t, sourceFile, node.Parent)
} else if node.Parent.Kind == ast.KindCallExpression && slices.Contains(node.Parent.AsCallExpression().Arguments.Nodes, node) {
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
} else {
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
}
}
}
func deleteDefaultImport(t *Tracker, sourceFile *ast.SourceFile, importClause *ast.Node) {
clause := importClause.AsImportClause()
if clause.NamedBindings == nil {
// Delete the whole import
deleteNode(t, sourceFile, importClause.Parent, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
} else {
// import |d,| * as ns from './file'
name := clause.Name()
start := astnav.GetStartOfNode(name, sourceFile, false)
nextToken := astnav.GetTokenAtPosition(sourceFile, name.End())
if nextToken != nil && nextToken.Kind == ast.KindCommaToken {
// shift first non-whitespace position after comma to the start position of the node
end := scanner.SkipTriviaEx(sourceFile.Text(), nextToken.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: false, StopAtComments: true})
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(start))
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(end))
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
} else {
deleteNode(t, sourceFile, name, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
}
}
}
func deleteImportBinding(t *Tracker, sourceFile *ast.SourceFile, node *ast.Node) {
importClause := node.Parent.AsImportClause()
if importClause.Name() != nil {
// Delete named imports while preserving the default import
// import d|, * as ns| from './file'
// import d|, { a }| from './file'
previousToken := astnav.GetTokenAtPosition(sourceFile, node.Pos()-1)
debug.Assert(previousToken != nil, "previousToken should not be nil")
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(astnav.GetStartOfNode(previousToken, sourceFile, false)))
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(node.End()))
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
} else {
// Delete the entire import declaration
// |import * as ns from './file'|
// |import { a } from './file'|
importDecl := ast.FindAncestorKind(node, ast.KindImportDeclaration)
debug.Assert(importDecl != nil, "importDecl should not be nil")
deleteNode(t, sourceFile, importDecl, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
}
}
func deleteVariableDeclaration(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) {
parent := node.Parent
if parent.Kind == ast.KindCatchClause {
// TODO: There's currently no unused diagnostic for this, could be a suggestion
openParen := astnav.FindChildOfKind(parent, ast.KindOpenParenToken, sourceFile)
closeParen := astnav.FindChildOfKind(parent, ast.KindCloseParenToken, sourceFile)
debug.Assert(openParen != nil && closeParen != nil, "catch clause should have parens")
t.DeleteNodeRange(sourceFile, openParen, closeParen, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
return
}
if len(parent.AsVariableDeclarationList().Declarations.Nodes) != 1 {
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
return
}
gp := parent.Parent
switch gp.Kind {
case ast.KindForOfStatement, ast.KindForInStatement:
t.ReplaceNode(sourceFile, node, t.NodeFactory.NewObjectLiteralExpression(t.NodeFactory.NewNodeList([]*ast.Node{}), false), nil)
case ast.KindForStatement:
deleteNode(t, sourceFile, parent, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
case ast.KindVariableStatement:
leadingTrivia := LeadingTriviaOptionStartLine
if hasJSDocNodes(gp) {
leadingTrivia = LeadingTriviaOptionJSDoc
}
deleteNode(t, sourceFile, gp, leadingTrivia, TrailingTriviaOptionInclude)
default:
debug.Fail("Unexpected grandparent kind: " + gp.Kind.String())
}
}
// deleteNode deletes a node with the specified trivia options.
// Warning: This deletes comments too.
func deleteNode(t *Tracker, sourceFile *ast.SourceFile, node *ast.Node, leadingTrivia LeadingTriviaOption, trailingTrivia TrailingTriviaOption) {
startPosition := t.getAdjustedStartPosition(sourceFile, node, leadingTrivia, false)
endPosition := t.getAdjustedEndPosition(sourceFile, node, trailingTrivia)
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPosition))
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(endPosition))
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
}
func deleteNodeInList(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) {
containingList := format.GetContainingList(node, sourceFile)
debug.Assert(containingList != nil, "containingList should not be nil")
index := slices.Index(containingList.Nodes, node)
debug.Assert(index != -1, "node should be in containing list")
if len(containingList.Nodes) == 1 {
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
return
}
// Note: We will only delete a comma *after* a node. This will leave a trailing comma if we delete the last node.
// That's handled in the end by finishTrailingCommaAfterDeletingNodesInList.
debug.Assert(!deletedNodesInLists[node], "Deleting a node twice")
deletedNodesInLists[node] = true
startPos := t.startPositionToDeleteNodeInList(sourceFile, node)
var endPos int
if index == len(containingList.Nodes)-1 {
endPos = t.getAdjustedEndPosition(sourceFile, node, TrailingTriviaOptionNone)
} else {
prevNode := (*ast.Node)(nil)
if index > 0 {
prevNode = containingList.Nodes[index-1]
}
endPos = t.endPositionToDeleteNodeInList(sourceFile, node, prevNode, containingList.Nodes[index+1])
}
startLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPos))
endLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(endPos))
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startLSPos, End: endLSPos}, "")
}
// startPositionToDeleteNodeInList finds the first non-whitespace position in the leading trivia of the node
func (t *Tracker) startPositionToDeleteNodeInList(sourceFile *ast.SourceFile, node *ast.Node) int {
start := t.getAdjustedStartPosition(sourceFile, node, LeadingTriviaOptionIncludeAll, false)
return scanner.SkipTriviaEx(sourceFile.Text(), start, &scanner.SkipTriviaOptions{StopAfterLineBreak: false, StopAtComments: true})
}
func (t *Tracker) endPositionToDeleteNodeInList(sourceFile *ast.SourceFile, node *ast.Node, prevNode *ast.Node, nextNode *ast.Node) int {
end := t.startPositionToDeleteNodeInList(sourceFile, nextNode)
if prevNode == nil || positionsAreOnSameLine(t.getAdjustedEndPosition(sourceFile, node, TrailingTriviaOptionInclude), end, sourceFile) {
return end
}
token := astnav.FindPrecedingToken(sourceFile, astnav.GetStartOfNode(nextNode, sourceFile, false))
if isSeparator(node, token) {
prevToken := astnav.FindPrecedingToken(sourceFile, astnav.GetStartOfNode(node, sourceFile, false))
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
}
return pos
}
if stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) {
return pos
}
}
}
return end
}
func positionsAreOnSameLine(pos1, pos2 int, sourceFile *ast.SourceFile) bool {
return format.GetLineStartPositionForPosition(pos1, sourceFile) == format.GetLineStartPositionForPosition(pos2, sourceFile)
}
// hasJSDocNodes checks if a node has JSDoc comments
func hasJSDocNodes(node *ast.Node) bool {
if node == nil {
return false
}
// nil is ok for JSDoc - it will return empty slice if not available
jsdocs := node.JSDoc(nil)
return len(jsdocs) > 0
}

View File

@@ -0,0 +1,751 @@
package change
import (
"context"
"slices"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/format"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/printer"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/stringutil"
)
type NodeOptions struct {
// Text to be inserted before the new node
Prefix string
// Text to be inserted after the new node
Suffix string
// Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node
indentation *int
// Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind
delta *int
LeadingTriviaOption
TrailingTriviaOption
joiner string
}
type LeadingTriviaOption int
const (
LeadingTriviaOptionNone LeadingTriviaOption = 0
LeadingTriviaOptionExclude LeadingTriviaOption = 1
LeadingTriviaOptionIncludeAll LeadingTriviaOption = 2
LeadingTriviaOptionJSDoc LeadingTriviaOption = 3
LeadingTriviaOptionStartLine LeadingTriviaOption = 4
)
type TrailingTriviaOption int
const (
TrailingTriviaOptionNone TrailingTriviaOption = 0
TrailingTriviaOptionExclude TrailingTriviaOption = 1
TrailingTriviaOptionExcludeWhitespace TrailingTriviaOption = 2
TrailingTriviaOptionInclude TrailingTriviaOption = 3
)
type trackerEditKind int
const (
trackerEditKindText trackerEditKind = 1
trackerEditKindRemove trackerEditKind = 2
trackerEditKindReplaceWithSingleNode trackerEditKind = 3
trackerEditKindReplaceWithMultipleNodes trackerEditKind = 4
)
type trackerEdit struct {
kind trackerEditKind
lsproto.Range
NewText string // kind == text
*ast.Node // single
nodes []*ast.Node // multiple
options NodeOptions
}
type nodesInsertedAtStartState struct {
node *ast.Node
sourceFile *ast.SourceFile
}
type Tracker struct {
// initialized with
formatSettings lsutil.FormatCodeSettings
newLine string
converters *lsconv.Converters
ctx context.Context
*printer.EmitContext
*ast.NodeFactory
changes *collections.MultiMap[*ast.SourceFile, *trackerEdit]
deletedNodes []deletedNode
nodesWithInsertionsAtStart map[*ast.Node]*nodesInsertedAtStartState
// created during call to getChanges
writer *printer.ChangeTrackerWriter
// printer
}
type deletedNode struct {
sourceFile *ast.SourceFile
node *ast.Node
}
func NewTracker(ctx context.Context, compilerOptions *core.CompilerOptions, formatOptions lsutil.FormatCodeSettings, converters *lsconv.Converters) *Tracker {
emitContext := printer.NewEmitContext()
newLine := compilerOptions.NewLine.GetNewLineCharacter()
ctx = format.WithFormatCodeSettings(ctx, formatOptions, newLine) // !!! formatSettings in context?
return &Tracker{
EmitContext: emitContext,
NodeFactory: &emitContext.Factory.NodeFactory,
changes: &collections.MultiMap[*ast.SourceFile, *trackerEdit]{},
ctx: ctx,
converters: converters,
formatSettings: formatOptions,
newLine: newLine,
nodesWithInsertionsAtStart: make(map[*ast.Node]*nodesInsertedAtStartState),
}
}
// GetChanges returns the accumulated text edits.
// Note: after calling this, the Tracker object must be discarded!
func (t *Tracker) GetChanges() map[string][]*lsproto.TextEdit {
t.finishDeleteDeclarations()
t.finishNodesWithInsertionsAtStart()
changes := t.getTextChangesFromChanges()
// !!! changes for new files
return changes
}
func (t *Tracker) ReplaceNode(sourceFile *ast.SourceFile, oldNode *ast.Node, newNode *ast.Node, options *NodeOptions) {
if options == nil {
// defaults to `useNonAdjustedPositions`
options = &NodeOptions{
LeadingTriviaOption: LeadingTriviaOptionExclude,
TrailingTriviaOption: TrailingTriviaOptionExclude,
}
}
t.ReplaceRange(sourceFile, t.GetAdjustedRange(sourceFile, oldNode, oldNode, options.LeadingTriviaOption, options.TrailingTriviaOption), newNode, *options)
}
func (t *Tracker) ReplaceNodeWithNodes(sourceFile *ast.SourceFile, oldNode *ast.Node, newNodes []*ast.Node, options *NodeOptions) {
if options == nil {
options = &NodeOptions{
LeadingTriviaOption: LeadingTriviaOptionExclude,
TrailingTriviaOption: TrailingTriviaOptionExclude,
}
}
t.ReplaceRangeWithNodes(sourceFile, t.GetAdjustedRange(sourceFile, oldNode, oldNode, options.LeadingTriviaOption, options.TrailingTriviaOption), newNodes, *options)
}
func (t *Tracker) ReplaceRange(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNode *ast.Node, options NodeOptions) {
t.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithSingleNode, Range: lsprotoRange, options: options, Node: newNode})
}
func (t *Tracker) ReplaceRangeWithText(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, text string) {
t.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindText, Range: lsprotoRange, NewText: text})
}
func (t *Tracker) ReplaceRangeWithNodes(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNodes []*ast.Node, options NodeOptions) {
if len(newNodes) == 1 {
t.ReplaceRange(sourceFile, lsprotoRange, newNodes[0], options)
return
}
t.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithMultipleNodes, Range: lsprotoRange, nodes: newNodes, options: options})
}
func (t *Tracker) InsertText(sourceFile *ast.SourceFile, pos lsproto.Position, text string) {
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: pos, End: pos}, text)
}
func (t *Tracker) InsertNodeAt(sourceFile *ast.SourceFile, pos core.TextPos, newNode *ast.Node, options NodeOptions) {
lsPos := t.converters.PositionToLineAndCharacter(sourceFile, pos)
t.ReplaceRange(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNode, options)
}
func (t *Tracker) InsertNodesAt(sourceFile *ast.SourceFile, pos core.TextPos, newNodes []*ast.Node, options NodeOptions) {
lsPos := t.converters.PositionToLineAndCharacter(sourceFile, pos)
t.ReplaceRangeWithNodes(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNodes, options)
}
func (t *Tracker) InsertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) {
endPosition := t.endPosForInsertNodeAfter(sourceFile, after, newNode)
t.InsertNodeAt(sourceFile, endPosition, newNode, t.getInsertNodeAfterOptions(sourceFile, after))
}
func (t *Tracker) InsertNodesAfter(sourceFile *ast.SourceFile, after *ast.Node, newNodes []*ast.Node) {
endPosition := t.endPosForInsertNodeAfter(sourceFile, after, newNodes[0])
t.InsertNodesAt(sourceFile, endPosition, newNodes, t.getInsertNodeAfterOptions(sourceFile, after))
}
func (t *Tracker) InsertNodeBefore(sourceFile *ast.SourceFile, before *ast.Node, newNode *ast.Node, blankLineBetween bool, leadingTriviaOption LeadingTriviaOption) {
t.InsertNodeAt(sourceFile, core.TextPos(t.getAdjustedStartPosition(sourceFile, before, leadingTriviaOption, false)), newNode, t.getOptionsForInsertNodeBefore(before, newNode, blankLineBetween))
}
// TryInsertTypeAnnotation inserts a type annotation after the appropriate position on a node
// (after the close paren for function-like, after the name/exclamation/question for variable-like).
// Returns true if successful.
func (t *Tracker) TryInsertTypeAnnotation(sourceFile *ast.SourceFile, node *ast.Node, typeNode *ast.Node) bool {
var endNode *ast.Node
if ast.IsFunctionLike(node) {
endNode = astnav.FindChildOfKind(node, ast.KindCloseParenToken, sourceFile)
if endNode == nil {
if !ast.IsArrowFunction(node) {
return false
}
// If no `)`, is an arrow function `x => x`, so use the end of the first parameter
params := node.Parameters()
if len(params) == 0 {
return false
}
endNode = params[0]
}
} else {
switch node.Kind {
case ast.KindVariableDeclaration:
endNode = node.AsVariableDeclaration().ExclamationToken
case ast.KindPropertySignature:
endNode = node.AsPropertySignatureDeclaration().PostfixToken
case ast.KindPropertyDeclaration:
endNode = node.AsPropertyDeclaration().PostfixToken
case ast.KindParameter:
endNode = node.AsParameterDeclaration().QuestionToken
}
if endNode == nil {
endNode = node.Name()
}
}
if endNode == nil {
return false
}
t.InsertNodeAt(sourceFile, core.TextPos(endNode.End()), typeNode, NodeOptions{Prefix: ": "})
return true
}
// ParenthesizeArrowParameters wraps the parameters of a paren-less arrow function in `(` and `)`.
// This is a no-op if the arrow function already has parens.
func (t *Tracker) ParenthesizeArrowParameters(sourceFile *ast.SourceFile, arrowFunc *ast.Node) {
if astnav.FindChildOfKind(arrowFunc, ast.KindCloseParenToken, sourceFile) != nil {
return
}
params := arrowFunc.Parameters()
if len(params) == 0 {
return
}
firstParam := params[0]
lastParam := params[len(params)-1]
startPos := astnav.GetStartOfNode(firstParam, sourceFile, false)
t.InsertText(sourceFile, t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPos)), "(")
t.InsertText(sourceFile, t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(lastParam.End())), ")")
}
// InsertModifierBefore inserts a modifier token (like 'type') before a node with a trailing space.
func (t *Tracker) InsertModifierBefore(sourceFile *ast.SourceFile, modifier ast.Kind, before *ast.Node) {
pos := astnav.GetStartOfNode(before, sourceFile, false)
token := t.NewToken(modifier)
token.Loc = core.NewTextRange(pos, pos)
token.Parent = before.Parent
t.InsertNodeAt(sourceFile, core.TextPos(pos), token, NodeOptions{Suffix: " "})
}
// Delete queues a node for deletion with smart handling of list items, imports, etc.
// The actual deletion happens in finishDeleteDeclarations during GetChanges.
func (t *Tracker) Delete(sourceFile *ast.SourceFile, node *ast.Node) {
t.deletedNodes = append(t.deletedNodes, deletedNode{sourceFile: sourceFile, node: node})
}
// DeleteRange deletes a text range from the source file.
func (t *Tracker) DeleteRange(sourceFile *ast.SourceFile, textRange core.TextRange) {
lspRange := t.converters.ToLSPRange(sourceFile, textRange)
t.ReplaceRangeWithText(sourceFile, lspRange, "")
}
// DeleteNode deletes a node immediately with specified trivia options.
// Stop! Consider using Delete instead, which has logic for deleting nodes from delimited lists.
func (t *Tracker) DeleteNode(sourceFile *ast.SourceFile, node *ast.Node, leadingTrivia LeadingTriviaOption, trailingTrivia TrailingTriviaOption) {
rng := t.GetAdjustedRange(sourceFile, node, node, leadingTrivia, trailingTrivia)
t.ReplaceRangeWithText(sourceFile, rng, "")
}
// DeleteNodeRange deletes a range of nodes with specified trivia options.
func (t *Tracker) DeleteNodeRange(sourceFile *ast.SourceFile, startNode *ast.Node, endNode *ast.Node, leadingTrivia LeadingTriviaOption, trailingTrivia TrailingTriviaOption) {
startPosition := t.getAdjustedStartPosition(sourceFile, startNode, leadingTrivia, false)
endPosition := t.getAdjustedEndPosition(sourceFile, endNode, trailingTrivia)
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPosition))
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(endPosition))
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
}
// finishDeleteDeclarations processes all queued deletions with smart handling for lists and trailing commas.
func (t *Tracker) finishDeleteDeclarations() {
deletedNodesInLists := make(map[*ast.Node]bool)
for _, deleted := range t.deletedNodes {
// Skip if this node is contained within another deleted node
isContained := false
for _, other := range t.deletedNodes {
if other.sourceFile == deleted.sourceFile && other.node != deleted.node &&
rangeContainsRangeExclusive(other.node, deleted.node) {
isContained = true
break
}
}
if isContained {
continue
}
deleteDeclaration(t, deletedNodesInLists, deleted.sourceFile, deleted.node)
}
// Handle trailing commas for last elements in lists
for node := range deletedNodesInLists {
sourceFile := ast.GetSourceFileOfNode(node)
list := format.GetContainingList(node, sourceFile)
if list == nil || node != list.Nodes[len(list.Nodes)-1] {
continue
}
lastNonDeletedIndex := -1
for i := len(list.Nodes) - 2; i >= 0; i-- {
if !deletedNodesInLists[list.Nodes[i]] {
lastNonDeletedIndex = i
break
}
}
if lastNonDeletedIndex != -1 {
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(list.Nodes[lastNonDeletedIndex].End()))
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(t.startPositionToDeleteNodeInList(sourceFile, list.Nodes[lastNonDeletedIndex+1])))
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
}
}
}
func (t *Tracker) endPosForInsertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) core.TextPos {
if needSemicolonBetween(after, newNode) && (rune(sourceFile.Text()[after.End()-1]) != ';') {
// check if previous statement ends with semicolon
// if not - insert semicolon to preserve the code from changing the meaning due to ASI
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End()))
semicolon := t.NewToken(ast.KindSemicolonToken)
semicolon.Loc = core.NewTextRange(after.End(), after.End())
semicolon.Parent = after.Parent
t.ReplaceRange(
sourceFile,
lsproto.Range{Start: endPos, End: endPos},
semicolon,
NodeOptions{},
)
}
return core.TextPos(t.getAdjustedEndPosition(sourceFile, after, TrailingTriviaOptionNone))
}
/**
* This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range,
* i.e. arguments in arguments lists, parameters in parameter lists etc.
* Note that separators are part of the node in statements and class elements.
*/
func (t *Tracker) InsertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node, containingList *ast.NodeList) {
if containingList == nil {
containingList = format.GetContainingList(after, sourceFile)
}
if containingList == nil {
// Debug.fail("node is not a list element")
return
}
index := slices.Index(containingList.Nodes, after)
if index < 0 {
return
}
end := after.End()
if index != len(containingList.Nodes)-1 {
// any element except the last one
// use next sibling as an anchor
if nextToken := astnav.GetTokenAtPosition(sourceFile, after.End()); nextToken != nil && isSeparator(after, nextToken) {
// for list
// a, b, c
// create change for adding 'e' after 'a' as
// - find start of next element after a (it is b)
// - use next element start as start and end position in final change
// - build text of change by formatting the text of node + whitespace trivia of b
// in multiline case it will work as
// a,
// b,
// c,
// result - '*' denotes leading trivia that will be inserted after new text (displayed as '#')
// a,
// insertedtext<separator>#
// ###b,
// c,
nextNode := containingList.Nodes[index+1]
startPos := scanner.SkipTriviaEx(sourceFile.Text(), nextNode.Pos(), &scanner.SkipTriviaOptions{StopAfterLineBreak: false, StopAtComments: true})
// write separator and leading trivia of the next element as suffix
suffix := scanner.TokenToString(nextToken.Kind) + sourceFile.Text()[nextToken.End():startPos]
t.InsertNodesAt(sourceFile, core.TextPos(startPos), []*ast.Node{newNode}, NodeOptions{Suffix: suffix})
}
return
}
afterStart := astnav.GetStartOfNode(after, sourceFile, false)
afterStartLinePosition := format.GetLineStartPositionForPosition(afterStart, sourceFile)
// insert element after the last element in the list that has more than one item
// pick the element preceding the after element to:
// - pick the separator
// - determine if list is a multiline
multilineList := false
// if list has only one element then we'll format is as multiline if node has comment in trailing trivia, or as singleline otherwise
// i.e. var x = 1 // this is x
// | new element will be inserted at this position
separator := ast.KindCommaToken // SyntaxKind.CommaToken | SyntaxKind.SemicolonToken
if len(containingList.Nodes) != 1 {
// otherwise, if list has more than one element, pick separator from the list
tokenBeforeInsertPosition := astnav.FindPrecedingToken(sourceFile, after.Pos())
separator = core.IfElse(isSeparator(after, tokenBeforeInsertPosition), tokenBeforeInsertPosition.Kind, ast.KindCommaToken)
// determine if list is multiline by checking lines of after element and element that precedes it.
afterMinusOneStartLinePosition := format.GetLineStartPositionForPosition(astnav.GetStartOfNode(containingList.Nodes[index-1], sourceFile, false), sourceFile)
multilineList = afterMinusOneStartLinePosition != afterStartLinePosition
}
if hasCommentsBeforeLineBreak(sourceFile.Text(), after.End()) || !positionsAreOnSameLine(containingList.Pos(), containingList.End(), sourceFile) {
// in this case we'll always treat containing list as multiline
multilineList = true
}
if multilineList {
// insert separator immediately following the 'after' node to preserve comments in trailing trivia
separatorToken := t.NewToken(separator)
separatorString := scanner.TokenToString(separator)
separatorToken.Loc = core.NewTextRange(end, end+len(separatorString))
separatorToken.Parent = after.Parent
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(end))
t.ReplaceRange(sourceFile, lsproto.Range{Start: endPos, End: endPos}, separatorToken, NodeOptions{})
// use the same indentation as 'after' item
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--
}
insertLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(insertPos))
t.ReplaceRange(
sourceFile,
lsproto.Range{Start: insertLSPos, End: insertLSPos},
newNode,
NodeOptions{
indentation: &indentation,
Prefix: t.newLine,
},
)
} else {
separatorString := scanner.TokenToString(separator)
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(end))
t.ReplaceRange(sourceFile, lsproto.Range{Start: endPos, End: endPos}, newNode, NodeOptions{Prefix: separatorString + " "})
}
}
// InsertImportSpecifierAtIndex inserts a new import specifier at the specified index in a NamedImports list
func (t *Tracker) InsertImportSpecifierAtIndex(sourceFile *ast.SourceFile, newSpecifier *ast.Node, namedImports *ast.Node, index int) {
namedImportsNode := namedImports.AsNamedImports()
elements := namedImportsNode.Elements.Nodes
var prevSpecifier *ast.Node
if index > 0 && index-1 < len(elements) {
prevSpecifier = elements[index-1]
}
if prevSpecifier != nil {
t.InsertNodeInListAfter(sourceFile, prevSpecifier, newSpecifier, nil)
} else {
t.InsertNodeBefore(
sourceFile,
elements[0],
newSpecifier,
!positionsAreOnSameLine(astnav.GetStartOfNode(elements[0], sourceFile, false), astnav.GetStartOfNode(namedImports.Parent.Parent, sourceFile, false), sourceFile),
LeadingTriviaOptionNone,
)
}
}
func (t *Tracker) InsertAtTopOfFile(sourceFile *ast.SourceFile, insert []*ast.Statement, blankLineBetween bool) {
if len(insert) == 0 {
return
}
pos := t.getInsertionPositionAtSourceFileTop(sourceFile)
options := NodeOptions{}
if pos != 0 {
options.Prefix = t.newLine
}
if len(sourceFile.Text()) == 0 || !stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) {
options.Suffix = t.newLine
}
if blankLineBetween {
options.Suffix += t.newLine
}
if len(insert) == 1 {
t.InsertNodeAt(sourceFile, core.TextPos(pos), insert[0], options)
} else {
t.InsertNodesAt(sourceFile, core.TextPos(pos), insert, options)
}
}
func (t *Tracker) InsertMemberAtStart(sourceFile *ast.SourceFile, node *ast.Node, newElement *ast.Node) {
t.insertNodeAtStartWorker(sourceFile, node, newElement)
}
func (t *Tracker) insertNodeAtStartWorker(sourceFile *ast.SourceFile, node *ast.Node, newElement *ast.Node) {
indentation := t.tryComputeIndentationFromExistingMembers(sourceFile, node)
if indentation < 0 {
indentation = t.tryComputeIndentationForNewMember(sourceFile, node)
}
members := getMembersOrProperties(node)
if members == nil {
return
}
t.InsertNodeAt(sourceFile, core.TextPos(members.Pos()), newElement, t.getInsertNodeAtStartInsertOptions(sourceFile, node, indentation))
}
func (t *Tracker) tryComputeIndentationForNewMember(sourceFile *ast.SourceFile, node *ast.Node) int {
nodeStart := astnav.GetStartOfNode(node, sourceFile, false)
lineStart := format.GetLineStartPositionForPosition(nodeStart, sourceFile)
tabSize := t.formatSettings.TabSize
if tabSize <= 0 {
tabSize = 4
}
indentSize := t.formatSettings.IndentSize
if indentSize <= 0 {
indentSize = 4
}
return max(findIndentationColumn(sourceFile.Text(), lineStart, nodeStart, tabSize), 0) + indentSize
}
func (t *Tracker) tryComputeIndentationFromExistingMembers(sourceFile *ast.SourceFile, node *ast.Node) int {
members := getMembersOrProperties(node)
if members == nil {
return -1
}
indentation := -1
text := sourceFile.Text()
tabSize := t.formatSettings.TabSize
last := node
if tabSize <= 0 {
tabSize = 4
}
for _, member := range members.Nodes {
if member == nil {
continue
}
if printer.RangeStartPositionsAreOnSameLine(last.Loc, member.Loc, sourceFile) {
return -1
}
memberStart := astnav.GetStartOfNode(member, sourceFile, false)
lineStart := format.GetLineStartPositionForPosition(memberStart, sourceFile)
column := findIndentationColumn(text, lineStart, memberStart, tabSize)
if column < 0 {
return -1
}
if indentation >= 0 {
if indentation != column {
return -1
}
last = member
continue
}
indentation = column
last = member
}
return indentation
}
func (t *Tracker) getInsertNodeAfterOptions(sourceFile *ast.SourceFile, node *ast.Node) NodeOptions {
newLineChar := t.newLine
var options NodeOptions
switch node.Kind {
case ast.KindParameter:
// default opts
options = NodeOptions{}
case ast.KindClassDeclaration, ast.KindModuleDeclaration:
options = NodeOptions{Prefix: newLineChar, Suffix: newLineChar}
case ast.KindVariableDeclaration, ast.KindStringLiteral, ast.KindIdentifier:
options = NodeOptions{Prefix: ", "}
case ast.KindPropertyAssignment:
options = NodeOptions{Suffix: "," + newLineChar}
case ast.KindExportKeyword:
options = NodeOptions{Prefix: " "}
default:
if !(ast.IsStatement(node) || ast.IsClassOrTypeElement(node)) {
// Else we haven't handled this kind of node yet -- add it
panic("unimplemented node type " + node.Kind.String() + " in changeTracker.getInsertNodeAfterOptions")
}
options = NodeOptions{Suffix: newLineChar}
}
if node.End() == sourceFile.End() && ast.IsStatement(node) {
options.Prefix = t.newLine + options.Prefix
}
return options
}
func (t *Tracker) getOptionsForInsertNodeBefore(before *ast.Node, inserted *ast.Node, blankLineBetween bool) NodeOptions {
if ast.IsStatement(before) || ast.IsClassOrTypeElement(before) {
if blankLineBetween {
return NodeOptions{Suffix: t.newLine + t.newLine}
}
return NodeOptions{Suffix: t.newLine}
} else if before.Kind == ast.KindVariableDeclaration {
// insert `x = 1, ` into `const x = 1, y = 2;
return NodeOptions{Suffix: ", "}
} else if before.Kind == ast.KindParameter {
if inserted.Kind == ast.KindParameter {
return NodeOptions{Suffix: ", "}
}
return NodeOptions{}
} else if (before.Kind == ast.KindStringLiteral && before.Parent != nil && before.Parent.Kind == ast.KindImportDeclaration) || before.Kind == ast.KindNamedImports {
return NodeOptions{Suffix: ", "}
} else if before.Kind == ast.KindImportSpecifier {
suffix := ","
if blankLineBetween {
suffix += t.newLine
} else {
suffix += " "
}
return NodeOptions{Suffix: suffix}
}
// We haven't handled this kind of node yet -- add it
panic("unimplemented node type " + before.Kind.String() + " in changeTracker.getOptionsForInsertNodeBefore")
}
func (t *Tracker) getInsertNodeAtStartInsertOptions(sourceFile *ast.SourceFile, node *ast.Node, indentation int) NodeOptions {
state := t.nodesWithInsertionsAtStart[node]
hasPreviousInsertion := state != nil
if state == nil {
state = &nodesInsertedAtStartState{
node: node,
sourceFile: sourceFile,
}
t.nodesWithInsertionsAtStart[node] = state
}
members := getMembersOrProperties(node)
isObjectLiteral := ast.IsObjectLiteralExpression(node)
isJSON := ast.IsJsonSourceFile(sourceFile)
hasMembers := members != nil && len(members.Nodes) > 0
insertTrailingComma := isObjectLiteral && (hasMembers || !isJSON)
insertLeadingComma := isObjectLiteral && isJSON && !hasMembers && hasPreviousInsertion
suffix := ""
if insertTrailingComma {
suffix = ","
} else if ast.IsInterfaceDeclaration(node) && !hasMembers {
suffix = ";"
}
prefix := t.newLine
if insertLeadingComma {
prefix = "," + prefix
}
return NodeOptions{indentation: &indentation, Prefix: prefix, Suffix: suffix}
}
func (t *Tracker) finishNodesWithInsertionsAtStart() {
for _, state := range t.nodesWithInsertionsAtStart {
if state == nil {
continue
}
openBrace := astnav.FindChildOfKind(state.node, ast.KindOpenBraceToken, state.sourceFile)
if openBrace == nil {
continue
}
closeBrace := astnav.FindChildOfKind(state.node, ast.KindCloseBraceToken, state.sourceFile)
if closeBrace == nil {
continue
}
members := getMembersOrProperties(state.node)
isEmpty := members == nil || len(members.Nodes) == 0
isSingleLine := positionsAreOnSameLine(openBrace.End(), closeBrace.End(), state.sourceFile)
if isEmpty && isSingleLine && openBrace.End() != closeBrace.End()-1 {
t.DeleteRange(state.sourceFile, core.NewTextRange(openBrace.End(), closeBrace.End()-1))
}
if isSingleLine {
t.InsertText(state.sourceFile, t.converters.PositionToLineAndCharacter(state.sourceFile, core.TextPos(closeBrace.End()-1)), t.newLine)
}
}
}
func getMembersOrProperties(node *ast.Node) *ast.NodeList {
if ast.IsObjectLiteralExpression(node) {
return node.PropertyList()
}
return node.MemberList()
}
func rangeContainsRangeExclusive(outer *ast.Node, inner *ast.Node) bool {
return outer.Pos() < inner.Pos() && inner.End() < outer.End()
}
func isSeparator(node *ast.Node, candidate *ast.Node) bool {
return candidate != nil && node.Parent != nil && (candidate.Kind == ast.KindCommaToken || (candidate.Kind == ast.KindSemicolonToken && node.Parent.Kind == ast.KindObjectLiteralExpression))
}
func findIndentationColumn(text string, lineStart, memberStart, tabSize int) int {
column := 0
for i := lineStart; i < memberStart && i < len(text); i++ {
ch := rune(text[i])
if stringutil.IsLineBreak(ch) {
return -1
}
if stringutil.IsWhiteSpaceSingleLine(ch) {
column = advanceIndentationColumn(column, ch, tabSize)
continue
}
return column
}
return column
}
func advanceIndentationColumn(column int, ch rune, tabSize int) int {
if ch == '\t' {
return column + tabSize - (column % tabSize)
}
return column + 1
}

View File

@@ -0,0 +1,402 @@
package change
import (
"fmt"
"slices"
"strings"
"unicode"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/format"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/parser"
"github.com/microsoft/typescript-go/internal/printer"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/stringutil"
)
func (t *Tracker) getTextChangesFromChanges() map[string][]*lsproto.TextEdit {
changes := map[string][]*lsproto.TextEdit{}
for sourceFile, changesInFile := range t.changes.M {
// order changes by start position
// If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa.
slices.SortStableFunc(changesInFile, func(a, b *trackerEdit) int { return lsproto.CompareRanges(a.Range, b.Range) })
// verify that change intervals do not overlap, except possibly at end points.
for i := range len(changesInFile) - 1 {
if lsproto.ComparePositions(changesInFile[i].Range.End, changesInFile[i+1].Range.Start) > 0 {
// assert change[i].End <= change[i + 1].Start
panic(fmt.Sprintf("changes overlap: %v and %v", changesInFile[i].Range, changesInFile[i+1].Range))
}
}
textChanges := core.MapNonNil(changesInFile, func(change *trackerEdit) *lsproto.TextEdit {
// !!! targetSourceFile
newText := t.computeNewText(change, sourceFile, sourceFile)
// span := createTextSpanFromRange(c.Range)
// !!!
// Filter out redundant changes.
// if (span.length == newText.length && stringContainsAt(targetSourceFile.text, newText, span.start)) { return nil }
return &lsproto.TextEdit{
NewText: newText,
Range: change.Range,
}
})
if len(textChanges) > 0 {
changes[sourceFile.FileName()] = textChanges
}
}
return changes
}
func (t *Tracker) computeNewText(change *trackerEdit, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile) string {
switch change.kind {
case trackerEditKindRemove:
return ""
case trackerEditKindText:
return change.NewText
}
pos := int(t.converters.LineAndCharacterToPosition(sourceFile, change.Range.Start))
formatNode := func(n *ast.Node) string {
return t.getFormattedTextOfNode(n, targetSourceFile, sourceFile, pos, change.options)
}
var text string
switch change.kind {
case trackerEditKindReplaceWithMultipleNodes:
if change.options.joiner == "" {
change.options.joiner = t.newLine
}
text = strings.Join(core.Map(change.nodes, func(n *ast.Node) string { return strings.TrimSuffix(formatNode(n), t.newLine) }), change.options.joiner)
case trackerEditKindReplaceWithSingleNode:
text = formatNode(change.Node)
default:
panic(fmt.Sprintf("change kind %d should have been handled earlier", change.kind))
}
// strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line
noIndent := text
if !(change.options.indentation != nil || format.GetLineStartPositionForPosition(pos, targetSourceFile) == pos) {
noIndent = strings.TrimLeftFunc(text, unicode.IsSpace)
}
return change.options.Prefix + noIndent + core.IfElse(strings.HasSuffix(noIndent, change.options.Suffix), "", change.options.Suffix)
}
/** Note: this may mutate `nodeIn`. */
func (t *Tracker) getFormattedTextOfNode(nodeIn *ast.Node, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile, pos int, options NodeOptions) string {
text, sourceFileLike := t.getNonformattedText(nodeIn, targetSourceFile)
// !!! if (validate) validate(node, text);
formatOptions := getFormatCodeSettingsForWriting(t.formatSettings, targetSourceFile)
var initialIndentation, delta int
if options.indentation == nil {
initialIndentation = format.GetIndentation(pos, sourceFile, formatOptions, options.Prefix == t.newLine || format.GetLineStartPositionForPosition(pos, targetSourceFile) == pos)
} else {
initialIndentation = *options.indentation
}
if options.delta != nil {
delta = *options.delta
} else if formatOptions.IndentSize != 0 && format.ShouldIndentChildNode(formatOptions, nodeIn, nil, nil) {
delta = formatOptions.IndentSize
}
changes := format.FormatNodeGivenIndentation(t.ctx, sourceFileLike, sourceFileLike.AsSourceFile(), targetSourceFile.LanguageVariant, initialIndentation, delta)
return core.ApplyBulkEdits(text, changes)
}
func getFormatCodeSettingsForWriting(options lsutil.FormatCodeSettings, sourceFile *ast.SourceFile) lsutil.FormatCodeSettings {
shouldAutoDetectSemicolonPreference := options.Semicolons == lsutil.SemicolonPreferenceIgnore
shouldRemoveSemicolons := options.Semicolons == lsutil.SemicolonPreferenceRemove || shouldAutoDetectSemicolonPreference && !lsutil.ProbablyUsesSemicolons(sourceFile)
if shouldRemoveSemicolons {
options.Semicolons = lsutil.SemicolonPreferenceRemove
}
return options
}
func (t *Tracker) getNonformattedText(node *ast.Node, sourceFile *ast.SourceFile) (string, *ast.Node) {
writer := printer.NewChangeTrackerWriter(t.newLine, t.formatSettings.IndentSize)
printer.NewPrinter(
printer.PrinterOptions{
NewLine: core.GetNewLineKind(t.newLine),
NeverAsciiEscape: true,
PreserveSourceNewlines: true,
TerminateUnterminatedLiterals: true,
},
writer.GetPrintHandlers(),
t.EmitContext,
).Write(node, sourceFile, writer, nil)
text := writer.String()
text = strings.TrimSuffix(text, t.newLine)
nodeOut := writer.AssignPositionsToNode(node, t.NodeFactory)
eofToken := t.Factory.NewToken(ast.KindEndOfFile)
nodeList := t.Factory.NewNodeList([]*ast.Node{nodeOut})
nodeList.Loc = nodeOut.Loc
eofToken.Loc = core.NewTextRange(nodeOut.End(), nodeOut.End())
sourceFileLike := t.Factory.NewSourceFile(
ast.SourceFileParseOptions{FileName: sourceFile.FileName(), Path: sourceFile.Path()},
text,
nodeList,
eofToken,
)
sourceFileLike.ForEachChild(func(child *ast.Node) bool {
child.Parent = sourceFileLike
return true
})
sourceFileLike.Loc = nodeOut.Loc
return text, sourceFileLike
}
// method on the changeTracker because use of converters
// GetAdjustedRange computes the adjusted range for a node in a source file, accounting for trivia.
func (t *Tracker) GetAdjustedRange(sourceFile *ast.SourceFile, startNode *ast.Node, endNode *ast.Node, leadingOption LeadingTriviaOption, trailingOption TrailingTriviaOption) lsproto.Range {
return t.converters.ToLSPRange(
sourceFile,
core.NewTextRange(
t.getAdjustedStartPosition(sourceFile, startNode, leadingOption, false),
t.getAdjustedEndPosition(sourceFile, endNode, trailingOption),
),
)
}
// method on the changeTracker because use of converters
func (t *Tracker) getAdjustedStartPosition(sourceFile *ast.SourceFile, node *ast.Node, leadingOption LeadingTriviaOption, hasTrailingComment bool) int {
if leadingOption == LeadingTriviaOptionJSDoc {
if JSDocComments := parser.GetJSDocCommentRanges(t.NodeFactory, nil, node, sourceFile.Text()); len(JSDocComments) > 0 {
return format.GetLineStartPositionForPosition(JSDocComments[0].Pos(), sourceFile)
}
}
start := astnav.GetStartOfNode(node, sourceFile, false)
startOfLinePos := format.GetLineStartPositionForPosition(start, sourceFile)
switch leadingOption {
case LeadingTriviaOptionExclude:
return start
case LeadingTriviaOptionStartLine:
if node.Loc.ContainsInclusive(startOfLinePos) {
return startOfLinePos
}
return start
}
fullStart := node.Pos()
if fullStart == start {
return start
}
lineStarts := sourceFile.ECMALineMap()
fullStartLineIndex := scanner.ComputeLineOfPosition(lineStarts, fullStart)
fullStartLinePos := int(lineStarts[fullStartLineIndex])
if startOfLinePos == fullStartLinePos {
// full start and start of the node are on the same line
// a, b;
// ^ ^
// | start
// fullstart
// when b is replaced - we usually want to keep the leading trvia
// when b is deleted - we delete it
if leadingOption == LeadingTriviaOptionIncludeAll {
return fullStart
}
return start
}
// if node has a trailing comments, use comment end position as the text has already been included.
if hasTrailingComment {
// Check first for leading comments as if the node is the first import, we want to exclude the trivia;
// otherwise we get the trailing comments.
comments := slices.Collect(scanner.GetLeadingCommentRanges(t.NodeFactory, sourceFile.Text(), fullStart))
if len(comments) == 0 {
comments = slices.Collect(scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), fullStart))
}
if len(comments) > 0 {
return scanner.SkipTriviaEx(sourceFile.Text(), comments[0].End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true})
}
}
// get start position of the line following the line that contains fullstart position
// (but only if the fullstart isn't the very beginning of the file)
nextLineStart := core.IfElse(fullStart > 0, 1, 0)
adjustedStartPosition := int(lineStarts[fullStartLineIndex+nextLineStart])
// skip whitespaces/newlines
adjustedStartPosition = scanner.SkipTriviaEx(sourceFile.Text(), adjustedStartPosition, &scanner.SkipTriviaOptions{StopAtComments: true})
return int(lineStarts[scanner.ComputeLineOfPosition(lineStarts, adjustedStartPosition)])
}
// method on the changeTracker because of converters
// Return the end position of a multiline comment of it is on another line; otherwise returns `undefined`;
func (t *Tracker) getEndPositionOfMultilineTrailingComment(sourceFile *ast.SourceFile, node *ast.Node, trailingOpt TrailingTriviaOption) int {
if trailingOpt == TrailingTriviaOptionInclude {
// If the trailing comment is a multiline comment that extends to the next lines,
// return the end of the comment and track it for the next nodes to adjust.
lineStarts := sourceFile.ECMALineMap()
nodeEndLine := scanner.ComputeLineOfPosition(lineStarts, node.End())
for comment := range scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End()) {
// Single line can break the loop as trivia will only be this line.
// Comments on subsequent lines are also ignored.
if comment.Kind == ast.KindSingleLineCommentTrivia || scanner.ComputeLineOfPosition(lineStarts, comment.Pos()) > nodeEndLine {
break
}
// Get the end line of the comment and compare against the end line of the node.
// If the comment end line position and the multiline comment extends to multiple lines,
// then is safe to return the end position.
if commentEndLine := scanner.ComputeLineOfPosition(lineStarts, comment.End()); commentEndLine > nodeEndLine {
return scanner.SkipTriviaEx(sourceFile.Text(), comment.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true})
}
}
}
return 0
}
// method on the changeTracker because of converters
func (t *Tracker) getAdjustedEndPosition(sourceFile *ast.SourceFile, node *ast.Node, TrailingTriviaOption TrailingTriviaOption) int {
if TrailingTriviaOption == TrailingTriviaOptionExclude {
return node.End()
}
if TrailingTriviaOption == TrailingTriviaOptionExcludeWhitespace {
if comments := slices.AppendSeq(
slices.Collect(scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End())),
scanner.GetLeadingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End()),
); len(comments) > 0 {
if realEnd := comments[len(comments)-1].End(); realEnd != 0 {
return realEnd
}
}
return node.End()
}
if multilineEndPosition := t.getEndPositionOfMultilineTrailingComment(sourceFile, node, TrailingTriviaOption); multilineEndPosition != 0 {
return multilineEndPosition
}
newEnd := scanner.SkipTriviaEx(sourceFile.Text(), node.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true})
if newEnd != node.End() && (TrailingTriviaOption == TrailingTriviaOptionInclude || stringutil.IsLineBreak(rune(sourceFile.Text()[newEnd-1]))) {
return newEnd
}
return node.End()
}
// ============= utilities =============
func hasCommentsBeforeLineBreak(text string, start int) bool {
for _, ch := range []rune(text[start:]) {
if !stringutil.IsWhiteSpaceSingleLine(ch) {
return ch == '/'
}
}
return false
}
func needSemicolonBetween(a, b *ast.Node) bool {
return (ast.IsPropertySignatureDeclaration(a) || ast.IsPropertyDeclaration(a)) &&
ast.IsClassOrTypeElement(b) &&
b.Name().Kind == ast.KindComputedPropertyName ||
ast.IsStatementButNotDeclaration(a) &&
ast.IsStatementButNotDeclaration(b) // TODO: only if b would start with a `(` or `[`
}
func (t *Tracker) getInsertionPositionAtSourceFileTop(sourceFile *ast.SourceFile) int {
var lastPrologue *ast.Node
for _, node := range sourceFile.Statements.Nodes {
if ast.IsPrologueDirective(node) {
lastPrologue = node
} else {
break
}
}
position := 0
text := sourceFile.Text()
advancePastLineBreak := func() {
if position >= len(text) {
return
}
if char := rune(text[position]); stringutil.IsLineBreak(char) {
position++
if position < len(text) && char == '\r' && rune(text[position]) == '\n' {
position++
}
}
}
if lastPrologue != nil {
position = lastPrologue.End()
advancePastLineBreak()
return position
}
shebang := scanner.GetShebang(text)
if shebang != "" {
position = len(shebang)
advancePastLineBreak()
}
ranges := slices.Collect(scanner.GetLeadingCommentRanges(t.NodeFactory, text, position))
if len(ranges) == 0 {
return position
}
// Find the first attached comment to the first node and add before it
var lastComment *ast.CommentRange
pinnedOrTripleSlash := false
firstNodeLine := -1
lenStatements := len(sourceFile.Statements.Nodes)
lineMap := sourceFile.ECMALineMap()
for _, r := range ranges {
if r.Kind == ast.KindMultiLineCommentTrivia {
if printer.IsPinnedComment(text, r) {
lastComment = &r
pinnedOrTripleSlash = true
continue
}
} else if printer.IsRecognizedTripleSlashComment(text, r) {
lastComment = &r
pinnedOrTripleSlash = true
continue
}
if lastComment != nil {
// Always insert after pinned or triple slash comments
if pinnedOrTripleSlash {
break
}
// There was a blank line between the last comment and this comment.
// This comment is not part of the copyright comments
commentLine := scanner.ComputeLineOfPosition(lineMap, r.Pos())
lastCommentEndLine := scanner.ComputeLineOfPosition(lineMap, lastComment.End())
if commentLine >= lastCommentEndLine+2 {
break
}
}
if lenStatements > 0 {
if firstNodeLine == -1 {
firstNodeLine = scanner.ComputeLineOfPosition(lineMap, astnav.GetStartOfNode(sourceFile.Statements.Nodes[0], sourceFile, false))
}
commentEndLine := scanner.ComputeLineOfPosition(lineMap, r.End())
if firstNodeLine < commentEndLine+2 {
break
}
}
lastComment = &r
pinnedOrTripleSlash = false
}
if lastComment != nil {
position = lastComment.End()
advancePastLineBreak()
}
return position
}

View File

@@ -0,0 +1,399 @@
package ls
import (
"cmp"
"context"
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
)
// CodeFixProvider represents a provider for a specific type of code fix
type CodeFixProvider struct {
ErrorCodes []int32
GetCodeActions func(ctx context.Context, fixContext *CodeFixContext) ([]*CodeAction, error)
FixIds []string
GetAllCodeActions func(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error)
}
// CodeFixContext contains the context needed to generate code fixes
type CodeFixContext struct {
SourceFile *ast.SourceFile
Span core.TextRange
ErrorCode int32
Program *compiler.Program
LS *LanguageService
Diagnostic *lsproto.Diagnostic
Params *lsproto.CodeActionParams
}
// CodeAction represents a single code action fix
type CodeAction struct {
Description string
Changes []*lsproto.TextEdit
FixID string
FixAllDescription string
}
// Compare defines a total ordering for CodeAction values, comparing description
// then text edits lexicographically. Used with slices.BinarySearchFunc.
func (a *CodeAction) Compare(b *CodeAction) int {
if c := strings.Compare(a.Description, b.Description); c != 0 {
return c
}
if c := cmp.Compare(len(a.Changes), len(b.Changes)); c != 0 {
return c
}
for i, edit := range a.Changes {
if c := edit.Compare(b.Changes[i]); c != 0 {
return c
}
}
return 0
}
// CombinedCodeActions represents combined code actions for fix-all scenarios
type CombinedCodeActions struct {
Description string
Changes []*lsproto.TextEdit
}
// codeFixProviders is the list of all registered code fix providers
var codeFixProviders = []*CodeFixProvider{
ImportFixProvider,
IsolatedDeclarationsFixProvider,
FixClassIncorrectlyImplementsInterfaceProvider,
// Add more code fix providers here as they are implemented
}
// ProvideCodeActions returns code actions for the given range and context
func (l *LanguageService) ProvideCodeActions(ctx context.Context, params *lsproto.CodeActionParams) (lsproto.CodeActionResponse, error) {
program, file := l.getProgramAndFile(params.TextDocument.Uri)
var actions []lsproto.CommandOrCodeAction
if params.Context != nil && params.Context.Only != nil {
for _, kind := range *params.Context.Only {
matchingKinds := getOrganizeImportsActionsForKind(kind)
for _, matchingKind := range matchingKinds {
organizeAction := l.createOrganizeImportsAction(ctx, program, file, matchingKind)
actions = append(actions, *organizeAction)
}
if isFixAllKind(kind) {
fixAllAction, err := l.createFixAllAction(ctx, program, file, params.TextDocument.Uri)
if err != nil {
return lsproto.CodeActionResponse{}, err
}
if fixAllAction != nil {
actions = append(actions, *fixAllAction)
}
}
}
}
if params.Context != nil && params.Context.Diagnostics != nil && wantsQuickFixes(params.Context.Only) {
fixIdSeen := make(map[string]*CodeFixProvider)
var seen []*CodeAction // sorted for binary search dedup, dedup across all diagnostics and providers so if multiple diags produce the same codefix, only one is returned
for _, diag := range params.Context.Diagnostics {
if diag.Code == nil || diag.Code.Integer == nil {
continue
}
errorCode := *diag.Code.Integer
for _, provider := range codeFixProviders {
if !containsErrorCode(provider.ErrorCodes, errorCode) {
continue
}
position := l.converters.LineAndCharacterToPosition(file, diag.Range.Start)
endPosition := l.converters.LineAndCharacterToPosition(file, diag.Range.End)
fixContext := &CodeFixContext{
SourceFile: file,
Span: core.NewTextRange(int(position), int(endPosition)),
ErrorCode: errorCode,
Program: program,
LS: l,
Diagnostic: diag,
Params: params,
}
providerActions, err := provider.GetCodeActions(ctx, fixContext)
if err != nil {
return lsproto.CodeActionResponse{}, err
}
for _, action := range providerActions {
i, found := slices.BinarySearchFunc(seen, action, (*CodeAction).Compare)
if found {
continue
}
seen = slices.Insert(seen, i, action)
actions = append(actions, convertToLSPCodeAction(action, diag, params.TextDocument.Uri))
if action.FixID != "" {
fixIdSeen[action.FixID] = provider
}
}
}
}
fixAllActions, err := l.getFixAllQuickFixes(ctx, program, file, params.TextDocument.Uri, fixIdSeen)
if err != nil {
return lsproto.CodeActionResponse{}, err
}
actions = append(actions, fixAllActions...)
}
return lsproto.CommandOrCodeActionArrayOrNull{CommandOrCodeActionArray: &actions}, nil
}
// getFixAllQuickFixes returns per-provider "Fix all in file" quickfix entries for providers
// that matched at least 2 diagnostics in the full file.
func (l *LanguageService) getFixAllQuickFixes(
ctx context.Context,
program *compiler.Program,
file *ast.SourceFile,
uri lsproto.DocumentUri,
fixIdSeen map[string]*CodeFixProvider,
) ([]lsproto.CommandOrCodeAction, error) {
var actions []lsproto.CommandOrCodeAction
// Deduplicate providers; multiple fixIds may map to the same provider.
var seen collections.Set[*CodeFixProvider]
for _, provider := range fixIdSeen {
if seen.Has(provider) {
continue
}
seen.Add(provider)
if provider.GetAllCodeActions == nil {
continue
}
if !hasMultipleFixableDiagnostics(ctx, program, file, provider.ErrorCodes) {
continue
}
fixContext := &CodeFixContext{
SourceFile: file,
Program: program,
LS: l,
}
combined, err := provider.GetAllCodeActions(ctx, fixContext)
if err != nil {
return nil, err
}
if combined != nil && len(combined.Changes) > 0 {
kind := lsproto.CodeActionKindQuickFix
changes := map[lsproto.DocumentUri][]*lsproto.TextEdit{
uri: combined.Changes,
}
actions = append(actions, lsproto.CommandOrCodeAction{
CodeAction: &lsproto.CodeAction{
Title: combined.Description,
Kind: &kind,
Edit: &lsproto.WorkspaceEdit{Changes: &changes},
},
})
}
}
return actions, nil
}
// hasMultipleFixableDiagnostics returns true if the file has at least 2 diagnostics
// matching the given error codes. Checks all diagnostic sources (semantic,
// syntactic, suggestion, declaration) to match ProvideDiagnostics.
func hasMultipleFixableDiagnostics(ctx context.Context, program *compiler.Program, file *ast.SourceFile, errorCodes []int32) bool {
allDiags := getAllDiagnostics(ctx, program, file)
count := 0
for _, d := range allDiags {
if containsErrorCode(errorCodes, d.Code()) {
count++
if count >= 2 {
return true
}
}
}
return false
}
// codeActionKindContains returns true if the requested kind equals or is a
// hierarchical parent of actionKind, using '.' as the separator. This matches
// the semantics of VS Code's HierarchicalKind.contains.
func codeActionKindContains(requestedKind, actionKind lsproto.CodeActionKind) bool {
return requestedKind == actionKind ||
requestedKind == "" ||
strings.HasPrefix(string(actionKind), string(requestedKind)+".")
}
// isFixAllKind returns true if the requested kind matches source.fixAll
func isFixAllKind(kind lsproto.CodeActionKind) bool {
return codeActionKindContains(kind, lsproto.CodeActionKindSourceFixAll)
}
// wantsQuickFixes returns true if the Only filter is nil/empty (meaning all kinds are wanted)
// or explicitly includes the quickfix kind.
func wantsQuickFixes(only *[]lsproto.CodeActionKind) bool {
if only == nil || len(*only) == 0 {
return true
}
for _, kind := range *only {
if codeActionKindContains(kind, lsproto.CodeActionKindQuickFix) {
return true
}
}
return false
}
// createFixAllAction creates a source.fixAll code action that applies all auto-fixable
// code fixes across the file.
func (l *LanguageService) createFixAllAction(
ctx context.Context,
program *compiler.Program,
file *ast.SourceFile,
uri lsproto.DocumentUri,
) (*lsproto.CommandOrCodeAction, error) {
kind := lsproto.CodeActionKindSourceFixAll
lspChanges := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
for _, provider := range codeFixProviders {
if provider.GetAllCodeActions == nil {
continue
}
fixContext := &CodeFixContext{
SourceFile: file,
Program: program,
LS: l,
}
combined, err := provider.GetAllCodeActions(ctx, fixContext)
if err != nil {
return nil, err
}
if combined != nil && len(combined.Changes) > 0 {
lspChanges[uri] = append(lspChanges[uri], combined.Changes...)
}
}
if len(lspChanges) == 0 {
return nil, nil
}
return &lsproto.CommandOrCodeAction{
CodeAction: &lsproto.CodeAction{
Title: diagnostics.Fix_All.Localize(locale.FromContext(ctx)),
Kind: &kind,
Edit: &lsproto.WorkspaceEdit{Changes: &lspChanges},
},
}, nil
}
// getOrganizeImportsActionTitle returns the appropriate title for the given organize imports kind
func getOrganizeImportsActionTitle(ctx context.Context, kind lsproto.CodeActionKind) string {
loc := locale.FromContext(ctx)
switch kind {
case lsproto.CodeActionKindSourceRemoveUnusedImports:
return diagnostics.Remove_Unused_Imports.Localize(loc)
case lsproto.CodeActionKindSourceSortImports:
return diagnostics.Sort_Imports.Localize(loc)
default:
return diagnostics.Organize_Imports.Localize(loc)
}
}
// getOrganizeImportsActionsForKind returns the organize imports code action kinds that should be
// returned for the given requested kind.
func getOrganizeImportsActionsForKind(requestedKind lsproto.CodeActionKind) []lsproto.CodeActionKind {
organizeImportsKinds := []lsproto.CodeActionKind{
lsproto.CodeActionKindSourceOrganizeImports,
lsproto.CodeActionKindSourceRemoveUnusedImports,
lsproto.CodeActionKindSourceSortImports,
}
var result []lsproto.CodeActionKind
for _, organizeKind := range organizeImportsKinds {
if codeActionKindContains(requestedKind, organizeKind) {
result = append(result, organizeKind)
}
}
if slices.Contains(result, requestedKind) {
return []lsproto.CodeActionKind{requestedKind}
}
return result
}
// createOrganizeImportsAction creates the organize imports code action
func (l *LanguageService) createOrganizeImportsAction(
ctx context.Context,
program *compiler.Program,
file *ast.SourceFile,
kind lsproto.CodeActionKind,
) *lsproto.CommandOrCodeAction {
title := getOrganizeImportsActionTitle(ctx, kind)
changes := l.OrganizeImports(
ctx,
file,
program,
kind,
)
if len(changes) == 0 {
return &lsproto.CommandOrCodeAction{
CodeAction: &lsproto.CodeAction{
Title: title,
Kind: &kind,
Edit: &lsproto.WorkspaceEdit{Changes: &map[lsproto.DocumentUri][]*lsproto.TextEdit{}},
},
}
}
lspChanges := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
for fileName, edits := range changes {
fileURI := lsconv.FileNameToDocumentURI(fileName)
lspChanges[fileURI] = edits
}
return &lsproto.CommandOrCodeAction{
CodeAction: &lsproto.CodeAction{
Title: title,
Kind: &kind,
Edit: &lsproto.WorkspaceEdit{Changes: &lspChanges},
},
}
}
// containsErrorCode checks if the error code is in the list
func containsErrorCode(codes []int32, code int32) bool {
return slices.Contains(codes, code)
}
// convertToLSPCodeAction converts an internal CodeAction to an LSP CodeAction
func convertToLSPCodeAction(action *CodeAction, diag *lsproto.Diagnostic, uri lsproto.DocumentUri) lsproto.CommandOrCodeAction {
kind := lsproto.CodeActionKindQuickFix
changes := map[lsproto.DocumentUri][]*lsproto.TextEdit{
uri: action.Changes,
}
diagnostics := []*lsproto.Diagnostic{diag}
return lsproto.CommandOrCodeAction{
CodeAction: &lsproto.CodeAction{
Title: action.Description,
Kind: &kind,
Edit: &lsproto.WorkspaceEdit{Changes: &changes},
Diagnostics: &diagnostics,
},
}
}

View File

@@ -0,0 +1,236 @@
package ls
import (
"context"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/ls/autoimport"
"github.com/microsoft/typescript-go/internal/ls/change"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
)
const fixClassIncorrectlyImplementsInterfaceFixID = "fixClassIncorrectlyImplementsInterface"
var fixClassIncorrectlyImplementsInterfaceErrorCodes = []int32{
diagnostics.Class_0_incorrectly_implements_interface_1.Code(),
diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass.Code(),
}
var FixClassIncorrectlyImplementsInterfaceProvider = &CodeFixProvider{
ErrorCodes: fixClassIncorrectlyImplementsInterfaceErrorCodes,
GetCodeActions: getCodeActionsToFixClassIncorrectlyImplementsInterface,
FixIds: []string{fixClassIncorrectlyImplementsInterfaceFixID},
GetAllCodeActions: getAllCodeActionsToFixClassIncorrectlyImplementsInterface,
}
func getCodeActionsToFixClassIncorrectlyImplementsInterface(context context.Context, fixContext *CodeFixContext) ([]*CodeAction, error) {
classDeclaration := getClass(fixContext.SourceFile, fixContext.Span)
if classDeclaration == nil {
return nil, nil
}
implementsTypes := ast.GetImplementsTypeNodes(classDeclaration)
locale := locale.FromContext(context)
typeChecker, done := fixContext.Program.GetTypeCheckerForFile(context, fixContext.SourceFile)
defer done()
var actions []*CodeAction
for _, implementedTypeNode := range implementsTypes {
changeTracker := change.NewTracker(context, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters)
importAdder, err := createImportAdder(context, fixContext, typeChecker)
if err != nil {
return nil, err
}
addChanges(context, fixContext, changeTracker, importAdder, typeChecker, classDeclaration, implementedTypeNode)
changes := getChanges(changeTracker, importAdder, fixContext.SourceFile)
if len(changes) == 0 {
continue
}
actions = append(actions, &CodeAction{
Description: diagnostics.Implement_interface_0.Localize(locale, scanner.GetTextOfNode(implementedTypeNode)),
Changes: changes,
FixID: fixClassIncorrectlyImplementsInterfaceFixID,
FixAllDescription: diagnostics.Implement_all_unimplemented_interfaces.Localize(locale),
})
}
return actions, nil
}
func getAllCodeActionsToFixClassIncorrectlyImplementsInterface(context context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) {
typeChecker, done := fixContext.Program.GetTypeCheckerForFile(context, fixContext.SourceFile)
defer done()
changeTracker := change.NewTracker(context, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters)
importAdder, err := createImportAdder(context, fixContext, typeChecker)
if err != nil {
return nil, err
}
seenClassDeclarations := collections.Set[*ast.Node]{}
for _, diag := range getAllDiagnostics(context, fixContext.Program, fixContext.SourceFile) {
if containsErrorCode(fixClassIncorrectlyImplementsInterfaceErrorCodes, diag.Code()) {
classDeclaration := getClass(fixContext.SourceFile, core.NewTextRange(diag.Pos(), diag.End()))
if classDeclaration == nil {
continue
}
if seenClassDeclarations.AddIfAbsent(classDeclaration) {
implementsTypes := ast.GetImplementsTypeNodes(classDeclaration)
for _, implementedTypeNode := range implementsTypes {
addChanges(context, fixContext, changeTracker, importAdder, typeChecker, classDeclaration, implementedTypeNode)
}
}
}
}
changes := getChanges(changeTracker, importAdder, fixContext.SourceFile)
if len(changes) == 0 {
return nil, nil
}
return &CombinedCodeActions{
Description: diagnostics.Implement_all_unimplemented_interfaces.Localize(locale.FromContext(context)),
Changes: changes,
}, nil
}
func addChanges(context context.Context, fixContext *CodeFixContext, changeTracker *change.Tracker, importAdder autoimport.ImportAdder, typeChecker *checker.Checker, classDeclaration *ast.Node, implementedTypeNode *ast.Node) {
missingMemberFixer := newMissingMemberFixer(changeTracker, fixContext.Program, typeChecker, fixContext.LS.UserPreferences(), importAdder, locale.FromContext(context))
constructor := getConstructor(classDeclaration)
implementedType := typeChecker.GetTypeAtLocation(implementedTypeNode)
classType := typeChecker.GetTypeAtLocation(classDeclaration)
if typeChecker.GetNumberIndexType(classType) == nil {
member := missingMemberFixer.createIndexSignatureDeclarationFromType(classDeclaration, implementedType, typeChecker.GetNumberType())
if member != nil {
insertInterfaceMemberNode(changeTracker, fixContext.SourceFile, classDeclaration, constructor, member)
}
}
if typeChecker.GetStringIndexType(classType) == nil {
member := missingMemberFixer.createIndexSignatureDeclarationFromType(classDeclaration, implementedType, typeChecker.GetStringType())
if member != nil {
insertInterfaceMemberNode(changeTracker, fixContext.SourceFile, classDeclaration, constructor, member)
}
}
missingMembers := getMissingMembers(typeChecker, classDeclaration, []*checker.Type{implementedType})
for _, member := range missingMembers {
memberNodes := missingMemberFixer.createMemberFromSymbol(member, classDeclaration, fixContext.SourceFile, nil /*body*/, preserveOptionalFlagsAll)
for _, memberNode := range memberNodes {
insertInterfaceMemberNode(changeTracker, fixContext.SourceFile, classDeclaration, constructor, memberNode)
}
}
}
func getChanges(changeTracker *change.Tracker, importAdder autoimport.ImportAdder, sourceFile *ast.SourceFile) []*lsproto.TextEdit {
fileChanges := changeTracker.GetChanges()[sourceFile.FileName()]
if importAdder != nil && importAdder.HasFixes() {
fileChanges = append(fileChanges, importAdder.Edits()...)
}
return fileChanges
}
func insertInterfaceMemberNode(changeTracker *change.Tracker, sourceFile *ast.SourceFile, classDeclaration *ast.Node, constructor *ast.Node, member *ast.Node) {
if constructor == nil {
changeTracker.InsertMemberAtStart(sourceFile, classDeclaration, member)
} else {
changeTracker.InsertNodeAfter(sourceFile, constructor, member)
}
}
func getClass(sourceFile *ast.SourceFile, span core.TextRange) *ast.Node {
token := astnav.GetTokenAtPosition(sourceFile, span.Pos())
if token == nil {
return nil
}
return ast.GetContainingClass(token)
}
func getConstructor(classDeclaration *ast.Node) *ast.Node {
if classDeclaration == nil || classDeclaration.MemberList() == nil {
return nil
}
for _, member := range classDeclaration.MemberList().Nodes {
if member != nil && ast.IsConstructorDeclaration(member) {
return member
}
}
return nil
}
func getMissingMembers(typeChecker *checker.Checker, classDeclaration *ast.Node, implementedTypes []*checker.Type) []*ast.Symbol {
inheritedMembers := getInheritedMembers(typeChecker, classDeclaration)
seenMembers := make(map[string]*ast.Symbol)
var classMembers ast.SymbolTable
if classDeclaration.Symbol() != nil {
classMembers = classDeclaration.Symbol().Members
}
var missingMembers []*ast.Symbol
for _, implementedType := range implementedTypes {
for _, symbol := range typeChecker.GetPropertiesOfType(implementedType) {
if symbol == nil {
continue
}
if classMembers != nil && classMembers[symbol.Name] != nil {
continue
}
if inheritedMembers[symbol.Name] != nil || seenMembers[symbol.Name] != nil {
continue
}
flags := checker.GetDeclarationModifierFlagsFromSymbol(symbol)
if flags&ast.ModifierFlagsPrivate == 0 {
seenMembers[symbol.Name] = symbol
missingMembers = append(missingMembers, symbol)
}
}
}
return missingMembers
}
func getInheritedMembers(typeChecker *checker.Checker, classDeclaration *ast.Node) ast.SymbolTable {
typeNode := ast.GetClassExtendsHeritageElement(classDeclaration)
if typeNode == nil {
return ast.SymbolTable{}
}
baseType := typeChecker.GetTypeAtLocation(typeNode.AsNode())
if baseType == nil {
return ast.SymbolTable{}
}
inheritedMembers := make(ast.SymbolTable)
for _, symbol := range typeChecker.GetPropertiesOfType(baseType) {
if symbol == nil {
continue
}
flags := checker.GetDeclarationModifierFlagsFromSymbol(symbol)
if flags&ast.ModifierFlagsPrivate == 0 {
inheritedMembers[symbol.Name] = symbol
}
}
return inheritedMembers
}
func createImportAdder(context context.Context, fixContext *CodeFixContext, typeChecker *checker.Checker) (autoimport.ImportAdder, error) {
view, err := fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
if err != nil {
return nil, err
}
if view == nil {
return nil, nil
}
return autoimport.NewImportAdder(context, fixContext.Program, typeChecker, fixContext.SourceFile, view, fixContext.LS.FormatOptions(), fixContext.LS.converters, fixContext.LS.UserPreferences()), nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,452 @@
package ls
import (
"context"
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/ls/autoimport"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/tspath"
)
var importFixErrorCodes = []int32{
diagnostics.Cannot_find_name_0.Code(),
diagnostics.Cannot_find_name_0_Did_you_mean_1.Code(),
diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0.Code(),
diagnostics.Cannot_find_name_0_Did_you_mean_the_static_member_1_0.Code(),
diagnostics.Cannot_find_namespace_0.Code(),
diagnostics.X_0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.Code(),
diagnostics.X_0_only_refers_to_a_type_but_is_being_used_as_a_value_here.Code(),
diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer.Code(),
diagnostics.X_0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type.Code(),
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slashjquery.Code(),
diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_1_or_later.Code(),
diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_include_dom.Code(),
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_save_dev_types_Slashjest_or_npm_i_save_dev_types_Slashmocha_and_then_add_jest_or_mocha_to_the_types_field_in_your_tsconfig.Code(),
diagnostics.Cannot_find_name_0_Did_you_mean_to_write_this_in_an_async_function.Code(),
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slashjquery_and_then_add_jquery_to_the_types_field_in_your_tsconfig.Code(),
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_save_dev_types_Slashjest_or_npm_i_save_dev_types_Slashmocha.Code(),
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashnode.Code(),
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashnode_and_then_add_node_to_the_types_field_in_your_tsconfig.Code(),
diagnostics.Cannot_find_namespace_0_Did_you_mean_1.Code(),
diagnostics.Cannot_extend_an_interface_0_Did_you_mean_implements.Code(),
diagnostics.This_JSX_tag_requires_0_to_be_in_scope_but_it_could_not_be_found.Code(),
}
const (
importFixID = "fixMissingImport"
)
// ImportFixProvider is the CodeFixProvider for import-related fixes
var ImportFixProvider = &CodeFixProvider{
ErrorCodes: importFixErrorCodes,
GetCodeActions: getImportCodeActions,
FixIds: []string{importFixID},
GetAllCodeActions: getAllImportCodeActions,
}
type fixInfo struct {
fix *autoimport.Fix
symbolName string
errorIdentifierText string
isJsxNamespaceFix bool
}
func getImportCodeActions(ctx context.Context, fixContext *CodeFixContext) ([]*CodeAction, error) {
info, err := getFixInfos(ctx, fixContext, fixContext.ErrorCode, fixContext.Span.Pos())
if err != nil {
return nil, err
}
if len(info) == 0 {
return nil, nil
}
var actions []*CodeAction
for _, fixInfo := range info {
edits, description := fixInfo.fix.Edits(
ctx,
fixContext.SourceFile,
fixContext.Program.Options(),
fixContext.LS.FormatOptions(),
fixContext.LS.converters,
fixContext.LS.UserPreferences(),
)
actions = append(actions, &CodeAction{
Description: description,
Changes: edits,
FixID: importFixID,
FixAllDescription: diagnostics.Add_all_missing_imports.Localize(locale.FromContext(ctx)),
})
}
return actions, nil
}
func getAllImportCodeActions(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) {
if tspath.IsDynamicFileName(fixContext.SourceFile.FileName()) {
return nil, nil
}
allDiagnostics := fixContext.Program.GetSemanticDiagnostics(ctx, fixContext.SourceFile)
var importDiags []*ast.Diagnostic
for _, diag := range allDiagnostics {
if containsErrorCode(importFixErrorCodes, diag.Code()) {
importDiags = append(importDiags, diag)
}
}
if len(importDiags) == 0 {
return nil, nil
}
view, err := fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
if err != nil {
return nil, err
}
if view == nil {
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
}
ch, done := fixContext.Program.GetTypeChecker(ctx)
defer done()
importAdder := autoimport.NewImportAdder(
ctx,
fixContext.Program,
ch,
fixContext.SourceFile,
view,
fixContext.LS.FormatOptions(),
fixContext.LS.converters,
fixContext.LS.UserPreferences(),
)
for _, diag := range importDiags {
if err := addImportFromDiagnostic(ctx, importAdder, diag, fixContext); err != nil {
return nil, err
}
}
if !importAdder.HasFixes() {
return nil, nil
}
return &CombinedCodeActions{
Description: diagnostics.Add_all_missing_imports.Localize(locale.FromContext(ctx)),
Changes: importAdder.Edits(),
}, nil
}
// addImportFromDiagnostic finds the best import fix for a diagnostic and adds it to the adder.
func addImportFromDiagnostic(ctx context.Context, importAdder autoimport.ImportAdder, diag *ast.Diagnostic, fixContext *CodeFixContext) error {
diagFixContext := &CodeFixContext{
SourceFile: fixContext.SourceFile,
Span: core.NewTextRange(diag.Pos(), diag.End()),
ErrorCode: diag.Code(),
Program: fixContext.Program,
LS: fixContext.LS,
}
infos, err := getFixInfos(ctx, diagFixContext, diag.Code(), diag.Pos())
if err != nil {
return err
}
if len(infos) > 0 {
importAdder.AddImportFix(infos[0].fix)
}
return nil
}
func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int32, pos int) ([]*fixInfo, error) {
// Can't compute import fixes for dynamic/untitled files since they don't have real file paths
if tspath.IsDynamicFileName(fixContext.SourceFile.FileName()) {
return nil, nil
}
symbolToken := astnav.GetTokenAtPosition(fixContext.SourceFile, pos)
var view *autoimport.View
var info []*fixInfo
if errorCode == diagnostics.X_0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.Code() {
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
info = getFixesInfoForUMDImport(ctx, fixContext, symbolToken, view)
} else if !ast.IsIdentifier(symbolToken) {
return nil, nil
} else if errorCode == diagnostics.X_0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type.Code() {
ch, done := fixContext.Program.GetTypeChecker(ctx)
defer done()
compilerOptions := fixContext.Program.Options()
symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
var allTypeOnlyFixes []*fixInfo
for _, sn := range symbolNames {
if !sn.isTypeOnly {
continue
}
fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, sn.name, fixContext.Program)
if fix != nil {
allTypeOnlyFixes = append(allTypeOnlyFixes, &fixInfo{fix: fix, symbolName: sn.name, errorIdentifierText: symbolToken.Text()})
}
}
// For JSX opening tags, there can be separate type-only errors for both the tag name
// identifier and the JSX namespace identifier. When both produce valid fixes, we
// disambiguate using the diagnostic message, which quotes the symbol name in single
// quotes (e.g., "'React' cannot be used as a value..."). If filtering yields nothing
// (e.g., due to localization), fall back to returning all candidates.
diagnosticMessage := ""
if fixContext.Diagnostic != nil {
diagnosticMessage = fixContext.Diagnostic.Message.AsString()
}
if len(allTypeOnlyFixes) > 1 && diagnosticMessage != "" {
for _, fi := range allTypeOnlyFixes {
if strings.Contains(diagnosticMessage, "'"+fi.symbolName+"'") {
info = append(info, fi)
}
}
}
if len(info) == 0 {
info = allTypeOnlyFixes
}
return info, nil
} else {
var err error
view, err = fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
if err != nil {
return nil, err
}
if view != nil {
info = getFixesInfoForNonUMDImport(ctx, fixContext, symbolToken, view)
}
}
// Sort fixes by preference
if view == nil {
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
}
return sortFixInfo(info, fixContext, view), nil
}
func getFixesInfoForUMDImport(ctx context.Context, fixContext *CodeFixContext, token *ast.Node, view *autoimport.View) []*fixInfo {
ch, done := fixContext.Program.GetTypeChecker(ctx)
defer done()
umdSymbol := getUmdSymbol(token, ch)
if umdSymbol == nil {
return nil
}
export := autoimport.SymbolToExport(umdSymbol, ch)
isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(token)
var result []*fixInfo
for _, fix := range view.GetFixes(ctx, export, false, isValidTypeOnlyUseSite, nil) {
errorIdentifierText := ""
if ast.IsIdentifier(token) {
errorIdentifierText = token.Text()
}
result = append(result, &fixInfo{
fix: fix,
symbolName: umdSymbol.Name,
errorIdentifierText: errorIdentifierText,
})
}
return result
}
func getUmdSymbol(token *ast.Node, ch *checker.Checker) *ast.Symbol {
// try the identifier to see if it is the umd symbol
var umdSymbol *ast.Symbol
if ast.IsIdentifier(token) {
umdSymbol = ch.GetResolvedSymbol(token)
}
if isUMDExportSymbol(umdSymbol) {
return umdSymbol
}
// The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`.
parent := token.Parent
if (ast.IsJsxOpeningLikeElement(parent) && parent.TagName() == token) ||
ast.IsJsxOpeningFragment(parent) {
var location *ast.Node
if ast.IsJsxOpeningLikeElement(parent) {
location = token
} else {
location = parent
}
jsxNamespace := ch.GetJsxNamespace(parent)
parentSymbol := ch.ResolveName(jsxNamespace, location, ast.SymbolFlagsValue, false /* excludeGlobals */)
if isUMDExportSymbol(parentSymbol) {
return parentSymbol
}
}
return nil
}
func isUMDExportSymbol(symbol *ast.Symbol) bool {
return symbol != nil && len(symbol.Declarations) > 0 &&
symbol.Declarations[0] != nil &&
ast.IsNamespaceExportDeclaration(symbol.Declarations[0])
}
func getFixesInfoForNonUMDImport(ctx context.Context, fixContext *CodeFixContext, symbolToken *ast.Node, view *autoimport.View) []*fixInfo {
ch, done := fixContext.Program.GetTypeChecker(ctx)
defer done()
compilerOptions := fixContext.Program.Options()
isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(symbolToken)
symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
var allInfo []*fixInfo
// Compute usage position for JSDoc import type fixes
usagePosition := fixContext.LS.converters.PositionToLineAndCharacter(fixContext.SourceFile, core.TextPos(scanner.GetTokenPosOfNode(symbolToken, fixContext.SourceFile, false)))
for _, sn := range symbolNames {
// Type-only imports are handled by the promotion code path, not the auto-import path.
if sn.isTypeOnly {
continue
}
symbolName := sn.name
// "default" is a keyword and not a legal identifier for the import
if symbolName == "default" {
continue
}
isJSXTagName := symbolName == symbolToken.Text() && ast.IsJsxTagName(symbolToken)
queryKind := autoimport.QueryKindExactMatch
if isJSXTagName {
queryKind = autoimport.QueryKindCaseInsensitiveMatch
}
exports := view.Search(symbolName, queryKind)
for _, export := range exports {
if isJSXTagName && !(export.Name() == symbolName || export.IsRenameable()) {
continue
}
fixes := view.GetFixes(ctx, export, isJSXTagName, isValidTypeOnlyUseSite, &usagePosition)
for _, fix := range fixes {
allInfo = append(allInfo, &fixInfo{
fix: fix,
symbolName: symbolName,
isJsxNamespaceFix: symbolName != symbolToken.Text(),
})
}
}
}
return allInfo
}
func getTypeOnlyPromotionFix(ctx context.Context, sourceFile *ast.SourceFile, symbolToken *ast.Node, symbolName string, program *compiler.Program) *autoimport.Fix {
ch, done := program.GetTypeChecker(ctx)
defer done()
// Get the symbol at the token location
symbol := ch.ResolveName(symbolName, symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */)
if symbol == nil {
return nil
}
// Get the type-only alias declaration
typeOnlyAliasDeclaration := ch.GetTypeOnlyAliasDeclaration(symbol)
if typeOnlyAliasDeclaration == nil || ast.GetSourceFileOfNode(typeOnlyAliasDeclaration) != sourceFile {
return nil
}
return &autoimport.Fix{
AutoImportFix: &lsproto.AutoImportFix{
Kind: lsproto.AutoImportFixKindPromoteTypeOnly,
},
TypeOnlyAliasDeclaration: typeOnlyAliasDeclaration,
}
}
type symbolNameInfo struct {
name string
isTypeOnly bool // whether the symbol currently resolves to a type-only import
}
func getSymbolNamesToImport(sourceFile *ast.SourceFile, ch *checker.Checker, symbolToken *ast.Node, compilerOptions *core.CompilerOptions) []symbolNameInfo {
parent := symbolToken.Parent
if (ast.IsJsxOpeningLikeElement(parent) || ast.IsJsxClosingElement(parent)) &&
parent.TagName() == symbolToken &&
jsxModeNeedsExplicitImport(compilerOptions.Jsx) {
jsxNamespace := ch.GetJsxNamespace(sourceFile.AsNode())
if needsJsxNamespaceFix(jsxNamespace, symbolToken, ch) {
var result []symbolNameInfo
if !scanner.IsIntrinsicJsxName(symbolToken.Text()) {
compSymbol := ch.ResolveName(symbolToken.Text(), symbolToken, ast.SymbolFlagsValue, false /* excludeGlobals */)
if compSymbol == nil {
result = append(result, symbolNameInfo{name: symbolToken.Text()})
} else if ch.GetTypeOnlyAliasDeclaration(compSymbol) != nil {
result = append(result, symbolNameInfo{name: symbolToken.Text(), isTypeOnly: true})
}
}
nsIsTypeOnly := false
if nsSymbol := ch.ResolveName(jsxNamespace, symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */); nsSymbol != nil {
nsIsTypeOnly = ch.GetTypeOnlyAliasDeclaration(nsSymbol) != nil
}
result = append(result, symbolNameInfo{name: jsxNamespace, isTypeOnly: nsIsTypeOnly})
return result
}
}
tokenIsTypeOnly := false
if sym := ch.ResolveName(symbolToken.Text(), symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */); sym != nil {
tokenIsTypeOnly = ch.GetTypeOnlyAliasDeclaration(sym) != nil
}
return []symbolNameInfo{{name: symbolToken.Text(), isTypeOnly: tokenIsTypeOnly}}
}
func needsJsxNamespaceFix(jsxNamespace string, symbolToken *ast.Node, ch *checker.Checker) bool {
if scanner.IsIntrinsicJsxName(symbolToken.Text()) {
return true
}
namespaceSymbol := ch.ResolveName(jsxNamespace, symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */)
if namespaceSymbol == nil {
return true
}
if slices.ContainsFunc(namespaceSymbol.Declarations, ast.IsTypeOnlyImportOrExportDeclaration) {
return (namespaceSymbol.Flags & ast.SymbolFlagsValue) == 0
}
return false
}
func jsxModeNeedsExplicitImport(jsx core.JsxEmit) bool {
return jsx == core.JsxEmitReact || jsx == core.JsxEmitReactNative
}
func sortFixInfo(fixes []*fixInfo, fixContext *CodeFixContext, view *autoimport.View) []*fixInfo {
if len(fixes) == 0 {
return fixes
}
// Create a copy to avoid modifying the original
sorted := make([]*fixInfo, len(fixes))
copy(sorted, fixes)
// Sort by:
// 1. JSX namespace fixes last
// 2. Fix comparison using view.CompareFixes
slices.SortFunc(sorted, func(a, b *fixInfo) int {
// JSX namespace fixes should come last
if cmp := core.CompareBooleans(a.isJsxNamespaceFix, b.isJsxNamespaceFix); cmp != 0 {
return cmp
}
return view.CompareFixesForSorting(a.fix, b.fix)
})
return sorted
}

View File

@@ -0,0 +1,498 @@
package ls
import (
"strconv"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/ls/autoimport"
"github.com/microsoft/typescript-go/internal/ls/change"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/nodebuilder"
)
type preserveOptionalFlags int
const (
preserveOptionalFlagsMethod preserveOptionalFlags = 1 << iota
preserveOptionalFlagsProperty
preserveOptionalFlagsAll = preserveOptionalFlagsMethod | preserveOptionalFlagsProperty
)
type missingMemberFixer struct {
changeTracker *change.Tracker
typeChecker *checker.Checker
program *compiler.Program
preferences lsutil.UserPreferences
importAdder autoimport.ImportAdder
locale locale.Locale
}
func newMissingMemberFixer(changeTracker *change.Tracker, program *compiler.Program, typeChecker *checker.Checker, preferences lsutil.UserPreferences, importAdder autoimport.ImportAdder, locale locale.Locale) *missingMemberFixer {
return &missingMemberFixer{
changeTracker: changeTracker,
typeChecker: typeChecker,
program: program,
preferences: preferences,
importAdder: importAdder,
locale: locale,
}
}
func (f *missingMemberFixer) createNodeBuilder() (*checker.NodeBuilder, map[*ast.IdentifierNode]*ast.Symbol) {
idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol)
nodeBuilder := checker.NewNodeBuilderEx(f.typeChecker, f.changeTracker.EmitContext, idToSymbol)
return nodeBuilder, idToSymbol
}
func (f *missingMemberFixer) createMemberFromSymbol(symbol *ast.Symbol, enclosingDeclaration *ast.Node, sourceFile *ast.SourceFile, body *ast.FunctionBody, preserveOptional preserveOptionalFlags) []*ast.Node {
declarations := symbol.Declarations
declaration := core.FirstOrNil(declarations)
quotePreference := lsutil.GetQuotePreference(sourceFile, f.preferences)
ambient := enclosingDeclaration.Flags&ast.NodeFlagsAmbient != 0
optional := symbol.Flags&ast.SymbolFlagsOptional != 0
kind := ast.KindPropertySignature
if declaration != nil {
kind = declaration.Kind
}
declarationName := createDeclarationName(f.changeTracker.NodeFactory, f.typeChecker, symbol, declaration)
modifiers := f.createModifiers(symbol, declaration)
flags := nodebuilder.FlagsNoTruncation
if quotePreference == lsutil.QuotePreferenceSingle {
flags |= nodebuilder.FlagsUseSingleQuotesForStringLiteralType
}
t := f.typeChecker.GetWidenedType(f.typeChecker.GetTypeOfSymbolAtLocation(symbol, enclosingDeclaration))
var nodes []*ast.Node
switch kind {
case ast.KindPropertySignature, ast.KindPropertyDeclaration:
nodeBuilder, idToSymbol := f.createNodeBuilder()
typeNode := f.createTypeNode(t, enclosingDeclaration, flags, nodeBuilder, idToSymbol)
var questionToken *ast.TokenNode
if optional && preserveOptional&preserveOptionalFlagsProperty != 0 {
questionToken = f.changeTracker.NodeFactory.NewToken(ast.KindQuestionToken)
}
return append(nodes, f.changeTracker.NodeFactory.NewPropertyDeclaration(modifiers, createPropertyName(f.changeTracker.NodeFactory, declarationName, quotePreference), questionToken, typeNode, nil /*initializer*/))
case ast.KindGetAccessor, ast.KindSetAccessor:
nodeBuilder, idToSymbol := f.createNodeBuilder()
accessors := ast.GetAllAccessorDeclarations(symbol.Declarations, declaration)
var orderedAccessors []*ast.Node
if accessors.SecondAccessor == nil {
orderedAccessors = append(orderedAccessors, accessors.FirstAccessor)
} else {
orderedAccessors = append(orderedAccessors, accessors.FirstAccessor, accessors.SecondAccessor)
}
for _, accessor := range orderedAccessors {
if ast.IsGetAccessorDeclaration(accessor) {
nodes = append(
nodes,
f.changeTracker.NodeFactory.NewGetAccessorDeclaration(
modifiers, createPropertyName(f.changeTracker.NodeFactory, declarationName, quotePreference),
nil /*typeParameters*/, nil /*parameters*/, f.createTypeNode(t, enclosingDeclaration, flags, nodeBuilder, idToSymbol), nil /*fullSignature*/, f.createBody(body, ambient, quotePreference),
),
)
}
if ast.IsSetAccessorDeclaration(accessor) {
parameter := checker.GetSetAccessorValueParameter(accessor)
if parameter == nil {
panic("Expected set accessor to have a parameter.")
}
nodes = append(
nodes, f.changeTracker.NodeFactory.NewSetAccessorDeclaration(
modifiers, createPropertyName(f.changeTracker.NodeFactory, declarationName, quotePreference),
nil /*typeParameters*/, createDummyParameters(f.changeTracker.NodeFactory, 1, []string{parameter.Name().Text()}, []*ast.TypeNode{f.createTypeNode(t, enclosingDeclaration, flags, nodeBuilder, idToSymbol)}, 1, ast.IsInJSFile(enclosingDeclaration)),
nil /*type*/, nil /*fullSignature*/, f.createBody(body, ambient, quotePreference),
),
)
}
}
return nodes
case ast.KindMethodSignature, ast.KindMethodDeclaration:
signatures := f.getCallSignatures(t)
preserveOptional := optional && preserveOptional&preserveOptionalFlagsMethod != 0
if len(signatures) == 0 {
return nil
}
if len(declarations) == 1 {
method := f.createSignatureDeclarationFromSignature(core.FirstOrNil(signatures), ast.KindMethodDeclaration, sourceFile, enclosingDeclaration, f.createBody(body, ambient, quotePreference), modifiers, declarationName, preserveOptional)
if method != nil {
nodes = append(nodes, method)
}
return nodes
}
for _, signature := range signatures {
if signature.Declaration() != nil && signature.Declaration().Flags&ast.NodeFlagsAmbient != 0 {
continue
}
method := f.createSignatureDeclarationFromSignature(signature, ast.KindMethodDeclaration, sourceFile, enclosingDeclaration, nil, modifiers, declarationName, preserveOptional)
if method != nil {
nodes = append(nodes, method)
}
}
if ambient {
return nodes
}
if len(declarations) > len(signatures) {
signature := f.typeChecker.GetSignatureFromDeclaration(core.LastOrNil(declarations))
method := f.createSignatureDeclarationFromSignature(signature, ast.KindMethodDeclaration, sourceFile, enclosingDeclaration, f.createBody(body, ambient, quotePreference), modifiers, declarationName, preserveOptional)
if method != nil {
nodes = append(nodes, method)
}
} else {
method := f.createSignatureDeclarationFromSignatures(signatures, declarationName, preserveOptional, modifiers, quotePreference, body, enclosingDeclaration)
if method != nil {
nodes = append(nodes, method)
}
}
return nodes
}
return nil
}
func (f *missingMemberFixer) getCallSignatures(t *checker.Type) []*checker.Signature {
if t.IsUnion() {
return core.FlatMap(t.Types(), f.typeChecker.GetCallSignatures)
}
return f.typeChecker.GetCallSignatures(t)
}
func (f *missingMemberFixer) createTypeNode(t *checker.Type, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, nodeBuilder *checker.NodeBuilder, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) *ast.TypeNode {
return f.importTypeNode(nodeBuilder.TypeToTypeNode(t, enclosingDeclaration, flags, nodebuilder.InternalFlagsNone, nil /*tracker*/), idToSymbol)
}
func (f *missingMemberFixer) createModifiers(symbol *ast.Symbol, declaration *ast.Node) *ast.ModifierList {
modifierFlags := ast.ModifierFlagsNone
if declaration != nil {
effective := checker.GetDeclarationModifierFlagsFromSymbol(symbol)
modifierFlags = effective & ast.ModifierFlagsStatic
if effective&ast.ModifierFlagsPublic != 0 {
modifierFlags |= ast.ModifierFlagsPublic
} else if effective&ast.ModifierFlagsProtected != 0 {
modifierFlags |= ast.ModifierFlagsProtected
}
if ast.IsAutoAccessorPropertyDeclaration(declaration) {
modifierFlags |= ast.ModifierFlagsAccessor
}
}
if f.shouldAddOverrideKeyword(declaration) {
modifierFlags |= ast.ModifierFlagsOverride
}
if modifierFlags == ast.ModifierFlagsNone {
return nil
}
return f.changeTracker.NodeFactory.NewModifierList(ast.CreateModifiersFromModifierFlags(modifierFlags, f.changeTracker.NodeFactory.NewModifier))
}
func (f *missingMemberFixer) shouldAddOverrideKeyword(declaration *ast.Node) bool {
return declaration != nil && f.program.Options().NoImplicitOverride.IsTrue() && ast.HasAbstractModifier(declaration)
}
func (f *missingMemberFixer) createSignatureDeclarationFromSignature(signature *checker.Signature, kind ast.Kind, sourceFile *ast.SourceFile, enclosingDeclaration *ast.Node, body *ast.FunctionBody, modifiers *ast.ModifierList, name *ast.PropertyName, optional bool) *ast.Node {
quotePreference := lsutil.GetQuotePreference(sourceFile, f.preferences)
flags := nodebuilder.FlagsNoTruncation | nodebuilder.FlagsSuppressAnyReturnType | nodebuilder.FlagsAllowEmptyTuple
if quotePreference == lsutil.QuotePreferenceSingle {
flags |= nodebuilder.FlagsUseSingleQuotesForStringLiteralType
}
nodeBuilder, idToSymbol := f.createNodeBuilder()
signatureDeclaration := nodeBuilder.SignatureToSignatureDeclaration(signature, kind, enclosingDeclaration, flags, nodebuilder.InternalFlagsAllowUnresolvedNames, nil /*tracker*/)
if signatureDeclaration == nil {
return nil
}
isJS := ast.IsInJSFile(enclosingDeclaration)
parameters := signatureDeclaration.ParameterList()
typeParameters := core.IfElse(isJS, nil, signatureDeclaration.TypeParameterList())
typeNode := core.IfElse(isJS, nil, signatureDeclaration.Type())
if typeParameters != nil && len(typeParameters.Nodes) > 0 {
nodes := make([]*ast.Node, 0, len(typeParameters.Nodes))
for _, tp := range typeParameters.Nodes {
if tp == nil {
continue
}
if ast.IsTypeParameterDeclaration(tp) {
typeParameter := tp.AsTypeParameterDeclaration()
constraint := typeParameter.Constraint
if constraint != nil {
constraint = f.importTypeNode(constraint, idToSymbol)
}
defaultType := typeParameter.DefaultType
if defaultType != nil {
defaultType = f.importTypeNode(defaultType, idToSymbol)
}
nodes = append(nodes,
f.changeTracker.NodeFactory.UpdateTypeParameterDeclaration(typeParameter, typeParameter.Modifiers(), typeParameter.Name(), constraint, typeParameter.Expression, defaultType))
} else {
nodes = append(nodes, tp)
}
}
typeParameters = f.changeTracker.NodeFactory.NewNodeList(nodes)
}
if parameters != nil {
nodes := make([]*ast.Node, 0, len(parameters.Nodes))
for _, p := range parameters.Nodes {
if p == nil {
continue
}
parameter := p.AsParameterDeclaration()
parameterTypeNode := parameter.Type
if parameterTypeNode != nil {
parameterTypeNode = f.importTypeNode(parameterTypeNode, idToSymbol)
}
nodes = append(nodes,
f.changeTracker.NodeFactory.UpdateParameterDeclaration(parameter, parameter.Modifiers(), parameter.DotDotDotToken, parameter.Name(), core.IfElse(isJS, nil, parameter.QuestionToken), parameterTypeNode, parameter.Initializer))
}
parameters = f.changeTracker.NodeFactory.NewNodeList(nodes)
}
if typeNode != nil {
typeNode = f.importTypeNode(typeNode, idToSymbol)
}
var questionToken *ast.TokenNode
if optional {
questionToken = f.changeTracker.NodeFactory.NewToken(ast.KindQuestionToken)
}
switch kind {
case ast.KindFunctionExpression:
fn := signatureDeclaration.AsFunctionExpression()
return f.changeTracker.NodeFactory.UpdateFunctionExpression(fn, modifiers, fn.AsteriskToken, core.IfElse(name != nil && ast.IsIdentifier(name), name, nil), typeParameters, parameters, typeNode, fn.FullSignature, core.OrElse(body, fn.Body))
case ast.KindArrowFunction:
fn := signatureDeclaration.AsArrowFunction()
return f.changeTracker.NodeFactory.UpdateArrowFunction(fn, modifiers, typeParameters, parameters, typeNode, fn.FullSignature, fn.EqualsGreaterThanToken, core.OrElse(body, fn.Body))
case ast.KindMethodDeclaration:
method := signatureDeclaration.AsMethodDeclaration()
methodName := core.IfElse(name == nil, f.changeTracker.NodeFactory.NewIdentifier(""), createPropertyName(f.changeTracker.NodeFactory, name, quotePreference))
return f.changeTracker.NodeFactory.UpdateMethodDeclaration(method, modifiers, method.AsteriskToken, methodName, questionToken, typeParameters, parameters, typeNode, method.FullSignature, body)
case ast.KindFunctionDeclaration:
fn := signatureDeclaration.AsFunctionDeclaration()
return f.changeTracker.NodeFactory.UpdateFunctionDeclaration(fn, modifiers, fn.AsteriskToken, core.IfElse(name != nil && ast.IsIdentifier(name), name, nil), typeParameters, parameters, typeNode, fn.FullSignature, core.OrElse(body, fn.Body))
}
return nil
}
func (f *missingMemberFixer) createSignatureDeclarationFromSignatures(signatures []*checker.Signature, name *ast.PropertyName, optional bool, modifiers *ast.ModifierList, quotePreference lsutil.QuotePreference, body *ast.FunctionBody, enclosingDeclaration *ast.Node) *ast.Node {
if len(signatures) == 0 {
return nil
}
nodeBuilder, idToSymbol := f.createNodeBuilder()
maxArgsSignature := signatures[0]
minArgumentCount := signatures[0].MinArgumentCount()
hasRestParameter := false
for _, signature := range signatures {
minArgumentCount = min(minArgumentCount, signature.MinArgumentCount())
if signature.HasRestParameter() {
hasRestParameter = true
}
if len(signature.Parameters()) >= len(maxArgsSignature.Parameters()) && (!signature.HasRestParameter() || maxArgsSignature.HasRestParameter()) {
maxArgsSignature = signature
}
}
maxNonRestArgs := len(maxArgsSignature.Parameters()) - core.IfElse(maxArgsSignature.HasRestParameter(), 1, 0)
parameterNames := make([]string, 0, len(maxArgsSignature.Parameters()))
for _, symbol := range maxArgsSignature.Parameters() {
parameterNames = append(parameterNames, symbol.Name)
}
parameters := createDummyParameters(f.changeTracker.NodeFactory, maxNonRestArgs, parameterNames, nil /*types*/, minArgumentCount, ast.IsInJSFile(enclosingDeclaration))
if hasRestParameter {
restParameterName := "rest"
if maxNonRestArgs < len(parameterNames) && parameterNames[maxNonRestArgs] != "" {
restParameterName = parameterNames[maxNonRestArgs]
}
var questionToken *ast.QuestionToken
if maxNonRestArgs >= minArgumentCount {
questionToken = f.changeTracker.NodeFactory.NewToken(ast.KindQuestionToken)
}
parameters.Nodes = append(parameters.Nodes, f.changeTracker.NodeFactory.NewParameterDeclaration(
nil /*modifiers*/, f.changeTracker.NodeFactory.NewToken(ast.KindDotDotDotToken),
f.changeTracker.NodeFactory.NewIdentifier(restParameterName), questionToken,
f.changeTracker.NodeFactory.NewArrayTypeNode(f.changeTracker.NodeFactory.NewKeywordTypeNode(ast.KindUnknownKeyword)), nil, /*initializer*/
))
}
methodName := core.IfElse(name == nil, f.changeTracker.NodeFactory.NewIdentifier(""), createPropertyName(f.changeTracker.NodeFactory, name, quotePreference))
return f.changeTracker.NodeFactory.NewMethodDeclaration(
modifiers, nil /*asteriskToken*/, methodName, core.IfElse(optional, f.changeTracker.NodeFactory.NewToken(ast.KindQuestionToken), nil),
nil /*typeParameters*/, parameters, f.getReturnTypeFromSignatures(signatures, enclosingDeclaration, nodeBuilder, idToSymbol),
nil /*fullSignature*/, f.createBody(body, false /*ambient*/, quotePreference),
)
}
func (f *missingMemberFixer) getReturnTypeFromSignatures(signatures []*checker.Signature, enclosingDeclaration *ast.Node, nodeBuilder *checker.NodeBuilder, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) *ast.TypeNode {
if len(signatures) == 0 {
return nil
}
returnTypes := make([]*checker.Type, 0, len(signatures))
for _, signature := range signatures {
returnTypes = append(returnTypes, f.typeChecker.GetReturnTypeOfSignature(signature))
}
unionType := f.typeChecker.GetUnionType(returnTypes)
return f.importTypeNode(nodeBuilder.TypeToTypeNode(unionType, enclosingDeclaration, nodebuilder.FlagsNoTruncation, nodebuilder.InternalFlagsAllowUnresolvedNames, nil /*typeArguments*/), idToSymbol)
}
func (f *missingMemberFixer) importTypeNode(typeNode *ast.TypeNode, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) *ast.TypeNode {
if typeNode == nil || f.importAdder == nil {
return typeNode
}
importedTypeNode, symbols := autoimport.TryGetAutoImportableReferenceFromTypeNode(typeNode, idToSymbol)
if importedTypeNode != nil {
for _, symbol := range symbols {
f.importAdder.AddImportFromExportedSymbol(symbol, true /*isValidTypeOnlyUseSite*/)
}
return importedTypeNode
}
seen := make(map[*ast.Symbol]bool)
for _, symbol := range idToSymbol {
if symbol == nil || seen[symbol] {
continue
}
seen[symbol] = true
f.importAdder.AddImportFromExportedSymbol(symbol, true /*isValidTypeOnlyUseSite*/)
}
return typeNode
}
func (f *missingMemberFixer) createIndexSignatureDeclarationFromType(classDeclaration *ast.Node, implementedType *checker.Type, keyType *checker.Type) *ast.Node {
indexInfo := f.typeChecker.GetIndexInfoOfType(implementedType, keyType)
if indexInfo == nil {
return nil
}
builder := checker.NewNodeBuilder(f.typeChecker, f.changeTracker.EmitContext)
return builder.IndexInfoToIndexSignatureDeclaration(indexInfo, classDeclaration, nodebuilder.FlagsNone, nodebuilder.InternalFlagsNone, nil)
}
func (f *missingMemberFixer) createBody(body *ast.FunctionBody, ambient bool, quotePreference lsutil.QuotePreference) *ast.FunctionBody {
if ambient {
return nil
}
body = f.changeTracker.NodeFactory.DeepCloneNode(body)
if body == nil {
return f.createStubbedMethodBody(quotePreference)
}
return body
}
func (f *missingMemberFixer) createStubbedMethodBody(quotePreference lsutil.QuotePreference) *ast.FunctionBody {
tokenFlags := ast.TokenFlagsNone
if quotePreference == lsutil.QuotePreferenceSingle {
tokenFlags = ast.TokenFlagsSingleQuote
}
return f.changeTracker.NodeFactory.NewBlock(f.changeTracker.NodeFactory.NewNodeList([]*ast.Node{
f.changeTracker.NodeFactory.NewThrowStatement(
f.changeTracker.NodeFactory.NewNewExpression(
f.changeTracker.NodeFactory.NewIdentifier("Error"), nil /*typeArguments*/, f.changeTracker.NodeFactory.NewNodeList([]*ast.Node{
f.changeTracker.NodeFactory.NewStringLiteral(diagnostics.Method_not_implemented.Localize(f.locale), tokenFlags),
}),
),
),
}), true /*multiLine*/)
}
func createDummyParameters(factory *ast.NodeFactory, argCount int, names []string, types []*ast.TypeNode, minArgumentCount int, inJS bool) *ast.ParameterList {
parameters := make([]*ast.Node, 0, argCount)
parameterNameCounts := make(map[string]int)
for i := range argCount {
parameterName := ""
if i < len(names) && names[i] != "" {
parameterName = names[i]
} else {
parameterName = "arg" + strconv.Itoa(i)
}
count := parameterNameCounts[parameterName]
parameterNameCounts[parameterName] = count + 1
if count > 0 {
parameterName += strconv.Itoa(count)
}
var questionToken *ast.QuestionToken
if i >= minArgumentCount {
questionToken = factory.NewToken(ast.KindQuestionToken)
}
var typeNode *ast.TypeNode
if inJS {
typeNode = nil
} else if i < len(types) && types[i] != nil {
typeNode = types[i]
} else {
typeNode = factory.NewKeywordTypeNode(ast.KindUnknownKeyword)
}
parameters = append(parameters,
factory.NewParameterDeclaration(nil /*modifiers*/, nil /*dotDotDotToken*/, factory.NewIdentifier(parameterName), questionToken, typeNode, nil /*initializer*/))
}
return factory.NewNodeList(parameters)
}
func createDeclarationName(factory *ast.NodeFactory, typeChecker *checker.Checker, symbol *ast.Symbol, declaration *ast.Node) *ast.PropertyName {
if symbol != nil && symbol.CheckFlags&ast.CheckFlagsMapped != 0 {
nameType := typeChecker.GetNameTypeOfSymbol(symbol)
if nameType != nil && checker.IsTypeUsableAsPropertyName(nameType) {
return factory.NewIdentifier(checker.GetPropertyNameFromType(nameType))
}
}
if declaration != nil && declaration.Name() != nil {
return declaration.Name().Clone(factory)
}
if symbol != nil {
return factory.NewIdentifier(symbol.Name)
}
return nil
}
func createPropertyName(factory *ast.NodeFactory, node *ast.Node, quotePreference lsutil.QuotePreference) *ast.PropertyName {
if ast.IsIdentifier(node) && node.Text() == "constructor" {
tokenFlags := ast.TokenFlagsNone
if quotePreference == lsutil.QuotePreferenceSingle {
tokenFlags = ast.TokenFlagsSingleQuote
}
return factory.NewComputedPropertyName(factory.NewStringLiteral(node.Text(), tokenFlags))
}
return factory.DeepCloneNode(node)
}

View File

@@ -0,0 +1,207 @@
package ls
import (
"context"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
)
func (l *LanguageService) ProvideCodeLenses(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.CodeLensResponse, error) {
_, file := l.getProgramAndFile(documentURI)
userPrefs := l.UserPreferences().CodeLens
if !userPrefs.ReferencesCodeLensEnabled.IsTrue() && !userPrefs.ImplementationsCodeLensEnabled.IsTrue() {
return lsproto.CodeLensResponse{}, nil
}
// Keeps track of the last symbol to avoid duplicating code lenses across overloads.
var lastSymbol *ast.Symbol
var result []*lsproto.CodeLens
var visit func(node *ast.Node) bool
visit = func(node *ast.Node) bool {
if ctx.Err() != nil {
return true
}
if currentSymbol := node.Symbol(); lastSymbol != currentSymbol {
lastSymbol = currentSymbol
if userPrefs.ReferencesCodeLensEnabled.IsTrue() && isValidReferenceLensNode(node, userPrefs) {
result = append(result, l.newCodeLensForNode(documentURI, file, node, lsproto.CodeLensKindReferences))
}
if userPrefs.ImplementationsCodeLensEnabled.IsTrue() && isValidImplementationsCodeLensNode(node, userPrefs) {
result = append(result, l.newCodeLensForNode(documentURI, file, node, lsproto.CodeLensKindImplementations))
}
}
savedLastSymbol := lastSymbol
node.ForEachChild(visit)
lastSymbol = savedLastSymbol
return false
}
visit(file.AsNode())
return lsproto.CodeLensResponse{
CodeLenses: &result,
}, nil
}
func (l *LanguageService) ResolveCodeLens(ctx context.Context, codeLens *lsproto.CodeLens, showLocationsCommandName *string, orchestrator CrossProjectOrchestrator) (*lsproto.CodeLens, error) {
uri := codeLens.Data.Uri
textDoc := lsproto.TextDocumentIdentifier{Uri: uri}
locale := locale.FromContext(ctx)
var locs []lsproto.Location
var lensTitle string
switch codeLens.Data.Kind {
case lsproto.CodeLensKindReferences:
referencesResp, err := l.ProvideReferences(ctx, &lsproto.ReferenceParams{
TextDocument: textDoc,
Position: codeLens.Range.Start,
Context: &lsproto.ReferenceContext{
// Don't include the declaration in the references count.
IncludeDeclaration: false,
},
}, orchestrator)
if err != nil {
return nil, err
}
if referencesResp.Locations != nil {
locs = *referencesResp.Locations
}
if len(locs) == 1 {
lensTitle = diagnostics.X_1_reference.Localize(locale)
} else {
lensTitle = diagnostics.X_0_references.Localize(locale, len(locs))
}
case lsproto.CodeLensKindImplementations:
implementations, err := l.provideImplementationsEx(
ctx,
&lsproto.ImplementationParams{
TextDocument: textDoc,
Position: codeLens.Range.Start,
},
// "Force" link support to be false so that we only get `Locations` back,
// and don't include the "current" node in the results.
symbolEntryTransformOptions{
requireLocationsResult: true,
dropOriginNodes: true,
},
orchestrator,
)
if err != nil {
return nil, err
}
if implementations.Locations != nil {
locs = *implementations.Locations
}
if len(locs) == 1 {
lensTitle = diagnostics.X_1_implementation.Localize(locale)
} else {
lensTitle = diagnostics.X_0_implementations.Localize(locale, len(locs))
}
}
cmd := &lsproto.Command{
Title: lensTitle,
}
if len(locs) > 0 && showLocationsCommandName != nil {
cmd.Command = *showLocationsCommandName
cmd.Arguments = &[]any{
uri,
codeLens.Range.Start,
locs,
}
}
codeLens.Command = cmd
return codeLens, nil
}
func (l *LanguageService) newCodeLensForNode(fileUri lsproto.DocumentUri, file *ast.SourceFile, node *ast.Node, kind lsproto.CodeLensKind) *lsproto.CodeLens {
nodeForRange := node
nodeName := node.Name()
if nodeName != nil {
nodeForRange = nodeName
}
pos := scanner.SkipTrivia(file.Text(), nodeForRange.Pos())
return &lsproto.CodeLens{
Range: lsproto.Range{
Start: l.converters.PositionToLineAndCharacter(file, core.TextPos(pos)),
End: l.converters.PositionToLineAndCharacter(file, core.TextPos(node.End())),
},
Data: &lsproto.CodeLensData{
Kind: kind,
Uri: fileUri,
},
}
}
func isValidImplementationsCodeLensNode(node *ast.Node, userPrefs lsutil.CodeLensUserPreferences) bool {
switch node.Kind {
// Always show on interfaces
case ast.KindInterfaceDeclaration:
// TODO: ast.KindTypeAliasDeclaration?
return true
// If configured, show on interface methods
case ast.KindMethodSignature:
return userPrefs.ImplementationsCodeLensShowOnInterfaceMethods.IsTrue() && node.Parent.Kind == ast.KindInterfaceDeclaration
// If configured, show on all class methods - but not private ones.
case ast.KindMethodDeclaration:
if userPrefs.ImplementationsCodeLensShowOnAllClassMethods.IsTrue() && node.Parent.Kind == ast.KindClassDeclaration {
return !ast.HasModifier(node, ast.ModifierFlagsPrivate) && node.Name().Kind != ast.KindPrivateIdentifier
}
fallthrough
// Always show on abstract classes/properties/methods
case ast.KindClassDeclaration, ast.KindConstructor,
ast.KindGetAccessor, ast.KindSetAccessor, ast.KindPropertyDeclaration:
return ast.HasModifier(node, ast.ModifierFlagsAbstract)
}
return false
}
func isValidReferenceLensNode(node *ast.Node, userPrefs lsutil.CodeLensUserPreferences) bool {
switch node.Kind {
case ast.KindFunctionDeclaration:
if userPrefs.ReferencesCodeLensShowOnAllFunctions.IsTrue() {
return true
}
fallthrough
case ast.KindVariableDeclaration:
return ast.GetCombinedModifierFlags(node)&ast.ModifierFlagsExport != 0
case ast.KindClassDeclaration, ast.KindInterfaceDeclaration, ast.KindTypeAliasDeclaration, ast.KindEnumDeclaration, ast.KindEnumMember:
return true
case ast.KindMethodDeclaration, ast.KindMethodSignature, ast.KindConstructor,
ast.KindGetAccessor, ast.KindSetAccessor,
ast.KindPropertyDeclaration, ast.KindPropertySignature:
// Don't show if child and parent have same start
// For https://github.com/microsoft/vscode/issues/90396
// !!!
switch node.Parent.Kind {
case ast.KindClassDeclaration, ast.KindInterfaceDeclaration, ast.KindTypeLiteral:
return true
}
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
package ls
const (
moduleSpecifierResolutionLimit = 100
moduleSpecifierResolutionCacheAttemptLimit = 1000
)

View File

@@ -0,0 +1,421 @@
package ls
import (
"context"
"fmt"
"iter"
"runtime/debug"
"sync"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/tspath"
)
type Project interface {
Id() tspath.Path
GetProgram() *compiler.Program
HasFile(fileName string) bool
}
type projectAndTextDocumentPosition struct {
project Project
ls *LanguageService
Uri lsproto.DocumentUri
Position lsproto.Position
forOriginalLocation bool
}
type response[Resp any] struct {
complete bool
result Resp
forOriginalLocation bool
}
type CrossProjectOrchestrator interface {
GetDefaultProject() Project
GetAllProjectsForInitialRequest() []Project
GetLanguageServiceForProjectWithFile(ctx context.Context, project Project, uri lsproto.DocumentUri) *LanguageService
GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) ([]Project, error)
GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.Path]) iter.Seq[Project]
}
func handleCrossProject[Req lsproto.HasTextDocumentPosition, Resp any](
defaultLs *LanguageService,
ctx context.Context,
params Req,
orchestrator CrossProjectOrchestrator,
symbolAndEntriesToResp func(*LanguageService, context.Context, Req, SymbolAndEntriesData, symbolEntryTransformOptions) (Resp, error),
combineResults func(iter.Seq[Resp]) Resp,
isRename bool,
implementations bool,
options symbolEntryTransformOptions,
) (Resp, error) {
var resp Resp
var err error
// Single project
if orchestrator == nil {
data, _ := defaultLs.provideSymbolsAndEntries(ctx, params.TextDocumentURI(), params.TextDocumentPosition(), isRename, implementations)
return symbolAndEntriesToResp(defaultLs, ctx, params, data, options)
}
defaultProject := orchestrator.GetDefaultProject()
allProjects := orchestrator.GetAllProjectsForInitialRequest()
var results collections.SyncMap[tspath.Path, *response[Resp]]
var defaultDefinition *nonLocalDefinition
canSearchProject := func(project Project) bool {
_, searched := results.Load(project.Id())
return !searched
}
wg := core.NewWorkGroup(false)
var errMu sync.Mutex
var enqueueItem func(item projectAndTextDocumentPosition)
var panicsOccured []string
var panicMu sync.Mutex
enqueueItem = func(item projectAndTextDocumentPosition) {
var response response[Resp]
if _, loaded := results.LoadOrStore(item.project.Id(), &response); loaded {
return
}
wg.Queue(func() {
if ctx.Err() != nil {
return
}
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
panicOccured := fmt.Sprintf("panic handling request: %v\n%s", r, string(stack))
panicMu.Lock()
panicsOccured = append(panicsOccured, panicOccured)
panicMu.Unlock()
}
}()
// Process the item
ls := item.ls
if ls == nil {
// Get it now
ls = orchestrator.GetLanguageServiceForProjectWithFile(ctx, item.project, item.Uri)
if ls == nil {
return
}
}
data, ok := ls.provideSymbolsAndEntries(ctx, item.Uri, item.Position, isRename, implementations)
if ctx.Err() != nil {
return
}
if ok {
for _, entry := range data.SymbolsAndEntries {
// Find the default definition that can be in another project
// Later we will use this load ancestor tree that references this location and expand search
if item.project == defaultProject && defaultDefinition == nil {
defaultDefinition = ls.getNonLocalDefinition(ctx, entry)
}
ls.forEachOriginalDefinitionLocation(ctx, entry, func(uri lsproto.DocumentUri, position lsproto.Position) {
// Get default configured project for this file
defProjects, errProjects := orchestrator.GetProjectsForFile(ctx, uri)
if errProjects != nil {
return
}
for _, defProject := range defProjects {
// Optimization: don't enqueue if will be discarded
if canSearchProject(defProject) {
enqueueItem(projectAndTextDocumentPosition{
project: defProject,
Uri: uri,
Position: position,
forOriginalLocation: true,
})
}
}
})
}
}
if result, errSearch := symbolAndEntriesToResp(ls, ctx, params, data, options); errSearch == nil {
response.complete = true
response.result = result
response.forOriginalLocation = item.forOriginalLocation
} else {
errMu.Lock()
defer errMu.Unlock()
if err == nil {
err = errSearch
}
}
})
}
// Initial set of projects and locations in the queue, starting with default project
enqueueItem(projectAndTextDocumentPosition{
project: defaultProject,
ls: defaultLs,
Uri: params.TextDocumentURI(),
Position: params.TextDocumentPosition(),
})
for _, project := range allProjects {
if project != defaultProject {
enqueueItem(projectAndTextDocumentPosition{
project: project,
// TODO!! symlinks need to change the URI
Uri: params.TextDocumentURI(),
Position: params.TextDocumentPosition(),
})
}
}
getResultsIterator := func() iter.Seq[Resp] {
return func(yield func(Resp) bool) {
var seenProjects collections.SyncSet[tspath.Path]
if response, loaded := results.Load(defaultProject.Id()); loaded && response.complete {
if !yield(response.result) {
return
}
}
seenProjects.Add(defaultProject.Id())
for _, project := range allProjects {
if seenProjects.AddIfAbsent(project.Id()) {
if response, loaded := results.Load(project.Id()); loaded && response.complete {
if !yield(response.result) {
return
}
}
}
}
// Prefer the searches from locations for default definition
results.Range(func(key tspath.Path, response *response[Resp]) bool {
if !response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete {
return yield(response.result)
}
return true
})
// Then the searches from original locations
results.Range(func(key tspath.Path, response *response[Resp]) bool {
if response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete {
return yield(response.result)
}
return true
})
}
}
// Outer loop - to complete work if more is added after completing existing queue
for {
// Process existing known projects first
wg.RunAndWait()
// No need to use mu here since we are not in parallel at this point
if panicsOccured != nil {
panic(fmt.Sprintf("Panics occurred during cross-project handling: %v", panicsOccured))
}
if ctx.Err() != nil {
return resp, ctx.Err()
}
if err != nil {
return resp, err
}
wg = core.NewWorkGroup(false)
hasMoreWork := false
if defaultDefinition != nil {
var requestedProjectTrees collections.Set[tspath.Path]
results.Range(func(key tspath.Path, response *response[Resp]) bool {
if response.complete {
requestedProjectTrees.Add(key)
}
return true
})
// Load more projects based on default definition found
for loadedProject := range orchestrator.GetProjectsLoadingProjectTree(ctx, &requestedProjectTrees) {
if ctx.Err() != nil {
return resp, ctx.Err()
}
// Can loop forever without this (enqueue here, dequeue above, repeat)
if !canSearchProject(loadedProject) || loadedProject.GetProgram() == nil {
continue
}
// Enqueue the project and location for further processing
if loadedProject.HasFile(defaultDefinition.TextDocumentURI().FileName()) {
enqueueItem(projectAndTextDocumentPosition{
project: loadedProject,
Uri: defaultDefinition.TextDocumentURI(),
Position: defaultDefinition.TextDocumentPosition(),
})
hasMoreWork = true
} else if sourcePos := defaultDefinition.GetSourcePosition(); sourcePos != nil && loadedProject.HasFile(sourcePos.TextDocumentURI().FileName()) {
enqueueItem(projectAndTextDocumentPosition{
project: loadedProject,
Uri: sourcePos.TextDocumentURI(),
Position: sourcePos.TextDocumentPosition(),
})
hasMoreWork = true
} else if generatedPos := defaultDefinition.GetGeneratedPosition(); generatedPos != nil && loadedProject.HasFile(generatedPos.TextDocumentURI().FileName()) {
enqueueItem(projectAndTextDocumentPosition{
project: loadedProject,
Uri: generatedPos.TextDocumentURI(),
Position: generatedPos.TextDocumentPosition(),
})
hasMoreWork = true
}
}
}
if !hasMoreWork {
break
}
}
if results.Size() > 1 {
resp = combineResults(getResultsIterator())
} else {
// Single result, return that directly
for value := range getResultsIterator() {
resp = value
break
}
}
return resp, nil
}
func combineLocationArray[T lsproto.HasLocation](
combined []T,
locations *[]T,
seen *collections.Set[lsproto.Location],
) []T {
for _, loc := range *locations {
if seen.AddIfAbsent(loc.GetLocation()) {
combined = append(combined, loc)
}
}
return combined
}
func combineResponseLocations[T lsproto.HasLocations](results iter.Seq[T]) *[]lsproto.Location {
var combined []lsproto.Location
var seenLocations collections.Set[lsproto.Location]
for resp := range results {
if locations := resp.GetLocations(); locations != nil {
combined = combineLocationArray(combined, locations, &seenLocations)
}
}
return &combined
}
func combineReferences(results iter.Seq[lsproto.ReferencesResponse]) lsproto.ReferencesResponse {
return lsproto.LocationsOrNull{Locations: combineResponseLocations(results)}
}
func combineVSReferences(results iter.Seq[lsproto.VSReferencesResponse]) lsproto.VSReferencesResponse {
var combined []*lsproto.VSReferenceItem
// Re-number IDs across projects to maintain unique IDs and correct definition references
nextId := int32(0)
for resp := range results {
if resp.VSReferenceItems == nil {
continue
}
// Map old IDs to new IDs for this batch
idMap := make(map[int32]int32)
for _, item := range *resp.VSReferenceItems {
oldId := item.VSId
newId := nextId
idMap[oldId] = newId
nextId++
newItem := *item
newItem.VSId = newId
if item.VSDefinitionId != nil {
newDefId := idMap[*item.VSDefinitionId]
newItem.VSDefinitionId = &newDefId
}
combined = append(combined, &newItem)
}
}
return lsproto.VSReferencesResponse{VSReferenceItems: &combined}
}
func combineImplementations(results iter.Seq[lsproto.ImplementationResponse]) lsproto.ImplementationResponse {
var combined []*lsproto.LocationLink
var seenLocations collections.Set[lsproto.Location]
for resp := range results {
if definitionLinks := resp.DefinitionLinks; definitionLinks != nil {
combined = combineLocationArray(combined, definitionLinks, &seenLocations)
} else if locations := resp.Locations; locations != nil {
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: combineResponseLocations(results)}
}
}
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &combined}
}
func combineRenameResponse(results iter.Seq[lsproto.RenameResponse]) lsproto.RenameResponse {
combined := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
seenChanges := make(map[lsproto.DocumentUri]*collections.Set[lsproto.Range])
var documentChanges []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
seenRenames := collections.Set[[2]lsproto.DocumentUri]{}
for resp := range results {
if resp.WorkspaceEdit != nil && resp.WorkspaceEdit.DocumentChanges != nil {
for _, change := range *resp.WorkspaceEdit.DocumentChanges {
switch {
case change.RenameFile != nil:
key := [2]lsproto.DocumentUri{change.RenameFile.OldUri, change.RenameFile.NewUri}
if seenRenames.AddIfAbsent(key) {
documentChanges = append(documentChanges, change)
}
default:
documentChanges = append(documentChanges, change)
}
}
}
if resp.WorkspaceEdit != nil && resp.WorkspaceEdit.Changes != nil {
for doc, changes := range *resp.WorkspaceEdit.Changes {
seenSet, ok := seenChanges[doc]
if !ok {
seenSet = &collections.Set[lsproto.Range]{}
seenChanges[doc] = seenSet
}
changesForDoc, exists := combined[doc]
if !exists {
changesForDoc = []*lsproto.TextEdit{}
}
for _, change := range changes {
if !seenSet.Has(change.Range) {
seenSet.Add(change.Range)
changesForDoc = append(changesForDoc, change)
}
}
combined[doc] = changesForDoc
}
}
}
if len(documentChanges) > 0 || len(combined) > 0 {
workspaceEdit := &lsproto.WorkspaceEdit{}
if len(documentChanges) > 0 {
workspaceEdit.DocumentChanges = &documentChanges
}
if len(combined) > 0 {
workspaceEdit.Changes = &combined
}
return lsproto.RenameResponse{
WorkspaceEdit: workspaceEdit,
}
}
return lsproto.RenameResponse{}
}
func combineIncomingCalls(results iter.Seq[lsproto.CallHierarchyIncomingCallsResponse]) lsproto.CallHierarchyIncomingCallsResponse {
var combined []*lsproto.CallHierarchyIncomingCall
var seenCalls collections.Set[lsproto.Location]
for resp := range results {
if resp.CallHierarchyIncomingCalls != nil {
for _, call := range *resp.CallHierarchyIncomingCalls {
if seenCalls.AddIfAbsent(call.From.GetLocation()) {
combined = append(combined, call)
}
}
}
}
return lsproto.CallHierarchyIncomingCallsResponse{CallHierarchyIncomingCalls: &combined}
}

View File

@@ -0,0 +1,440 @@
package ls
import (
"context"
"slices"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
)
func (l *LanguageService) ProvideDefinition(
ctx context.Context,
documentURI lsproto.DocumentUri,
position lsproto.Position,
) (lsproto.DefinitionResponse, error) {
if l.UserPreferences().PreferGoToSourceDefinition {
return l.ProvideSourceDefinition(ctx, documentURI, position)
}
return l.provideDefinitionWorker(ctx, documentURI, position)
}
func (l *LanguageService) provideDefinitionWorker(
ctx context.Context,
documentURI lsproto.DocumentUri,
position lsproto.Position,
) (lsproto.DefinitionResponse, error) {
caps := lsproto.GetClientCapabilities(ctx)
clientSupportsLink := caps.TextDocument.Definition.LinkSupport
program, file := l.getProgramAndFile(documentURI)
pos := int(l.converters.LineAndCharacterToPosition(file, position))
node := astnav.GetTouchingPropertyName(file, pos)
reference := getReferenceAtPosition(file, pos, program)
if node.Kind == ast.KindSourceFile {
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil
}
originSelectionRange := l.createLspRangeFromNode(node, file)
if reference != nil && reference.file != nil {
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, []*ast.Node{}, reference), nil
}
c, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
if node.Kind == ast.KindOverrideKeyword {
if sym := getSymbolForOverriddenMember(c, node); sym != nil {
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, sym.Declarations, nil /*reference*/), nil
}
}
if ast.IsJumpStatementTarget(node) {
if label := getTargetLabel(node.Parent, node.Text()); label != nil {
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, []*ast.Node{label}, nil /*reference*/), nil
}
}
if node.Kind == ast.KindCaseKeyword || node.Kind == ast.KindDefaultKeyword && ast.IsDefaultClause(node.Parent) {
if stmt := ast.FindAncestor(node.Parent, ast.IsSwitchStatement); stmt != nil {
file := ast.GetSourceFileOfNode(stmt)
return l.createLocationFromFileAndRange(file, scanner.GetRangeOfTokenAtPosition(file, stmt.Pos())), nil
}
}
if node.Kind == ast.KindReturnKeyword || node.Kind == ast.KindYieldKeyword || node.Kind == ast.KindAwaitKeyword {
if fn := ast.FindAncestor(node, ast.IsFunctionLikeDeclaration); fn != nil {
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, []*ast.Node{fn}, nil /*reference*/), nil
}
}
declarations := getDeclarationsFromLocation(c, node)
calledDeclaration := tryGetSignatureDeclaration(c, node)
if calledDeclaration != nil && !(ast.IsJsxOpeningLikeElement(node.Parent) && isJsxConstructorLike(calledDeclaration)) {
symbol := c.GetSymbolAtLocation(getDeclarationNameForKeyword(node))
if symbol != nil && core.Some(c.GetRootSymbols(symbol), func(rootSymbol *ast.Symbol) bool {
return symbolMatchesSignature(rootSymbol, calledDeclaration)
}) {
if !ast.IsConstructorDeclaration(calledDeclaration) {
declarations = nil
} else {
declarations = core.Filter(slices.Clip(declarations), func(node *ast.Node) bool {
return node != calledDeclaration && (ast.IsClassDeclaration(node) || ast.IsClassExpression(node))
})
}
} else {
declarations = core.Filter(slices.Clip(declarations), func(node *ast.Node) bool { return node != calledDeclaration })
}
declarations = append(declarations, calledDeclaration)
}
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, declarations, reference), nil
}
func (l *LanguageService) ProvideTypeDefinition(
ctx context.Context,
documentURI lsproto.DocumentUri,
position lsproto.Position,
) (lsproto.TypeDefinitionResponse, error) {
caps := lsproto.GetClientCapabilities(ctx)
clientSupportsLink := caps.TextDocument.TypeDefinition.LinkSupport
program, file := l.getProgramAndFile(documentURI)
node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position)))
if node.Kind == ast.KindSourceFile {
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil
}
originSelectionRange := l.createLspRangeFromNode(node, file)
c, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
node = getDeclarationNameForKeyword(node)
if symbol := c.GetSymbolAtLocation(node); symbol != nil {
symbolType := getTypeOfSymbolAtLocation(c, symbol, node)
declarations := getDeclarationsFromType(symbolType)
if typeArgument := c.GetFirstTypeArgumentFromKnownType(symbolType); typeArgument != nil {
declarations = core.Concatenate(getDeclarationsFromType(typeArgument), declarations)
}
if len(declarations) != 0 {
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, declarations, nil /*reference*/), nil
}
if symbol.Flags&ast.SymbolFlagsValue == 0 && symbol.Flags&ast.SymbolFlagsType != 0 {
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, symbol.Declarations, nil /*reference*/), nil
}
}
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil
}
func getDeclarationNameForKeyword(node *ast.Node) *ast.Node {
if node.Kind >= ast.KindFirstKeyword && node.Kind <= ast.KindLastKeyword {
if ast.IsVariableDeclarationList(node.Parent) {
if decl := core.FirstOrNil(node.Parent.AsVariableDeclarationList().Declarations.Nodes); decl != nil && decl.Name() != nil {
return decl.Name()
}
} else if node.Parent.DeclarationData() != nil && node.Parent.Name() != nil && node.Pos() < node.Parent.Name().Pos() {
return node.Parent.Name()
}
}
return node
}
type fileRange struct {
fileName string
fileRange core.TextRange
}
func (l *LanguageService) createDefinitionLocations(
originSelectionRange lsproto.Range,
clientSupportsLink bool,
declarations []*ast.Node,
reference *refInfo,
) lsproto.DefinitionResponse {
locations := make([]*lsproto.LocationLink, 0)
locationRanges := collections.Set[fileRange]{}
if reference != nil {
targetRange := lsproto.Range{
Start: lsproto.Position{
Line: 0,
Character: 0,
},
End: lsproto.Position{
Line: 0,
Character: 0,
},
}
locations = append(locations, &lsproto.LocationLink{
OriginSelectionRange: &originSelectionRange,
TargetUri: lsconv.FileNameToDocumentURI(reference.fileName),
TargetRange: targetRange,
TargetSelectionRange: targetRange,
})
}
for _, decl := range declarations {
file := ast.GetSourceFileOfNode(decl)
fileName := file.FileName()
name := core.OrElse(ast.GetNameOfDeclaration(decl), decl)
var nameRange core.TextRange
if name.Kind == ast.KindEmptyStatement {
nameRange = core.NewTextRange(name.Pos(), name.Pos())
} else {
nameRange = createRangeFromNode(name, file)
}
if locationRanges.AddIfAbsent(fileRange{fileName, nameRange}) {
contextNode := core.OrElse(getContextNode(decl), decl)
contextRange := core.OrElse(toContextRange(&nameRange, file, contextNode), &nameRange)
targetSelectionLoc := l.getMappedLocation(fileName, nameRange)
targetLoc := l.getMappedLocation(fileName, *contextRange)
locations = append(locations, &lsproto.LocationLink{
OriginSelectionRange: &originSelectionRange,
TargetSelectionRange: targetSelectionLoc.Range,
TargetUri: targetLoc.Uri,
TargetRange: targetLoc.Range,
})
}
}
if clientSupportsLink {
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &locations}
}
return createLocationsFromLinks(locations)
}
func createLocationsFromLinks(links []*lsproto.LocationLink) lsproto.DefinitionResponse {
locations := core.Map(links, func(link *lsproto.LocationLink) lsproto.Location {
return lsproto.Location{
Uri: link.TargetUri,
Range: link.TargetSelectionRange,
}
})
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations}
}
func (l *LanguageService) createLocationFromFileAndRange(file *ast.SourceFile, textRange core.TextRange) lsproto.DefinitionResponse {
mappedLocation := l.getMappedLocation(file.FileName(), textRange)
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{
Location: &mappedLocation,
}
}
func getDeclarationsFromLocation(c *checker.Checker, node *ast.Node) []*ast.Node {
if ast.IsIdentifier(node) && ast.IsShorthandPropertyAssignment(node.Parent) {
// Because name in short-hand property assignment has two different meanings: property name and property value,
// using go-to-definition at such position should go to the variable declaration of the property value rather than
// go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition
// is performed at the location of property access, we would like to go to definition of the property in the short-hand
// assignment. This case and others are handled by the following code.
// and the contextual type's property declarations
shorthandSymbol := c.GetResolvedSymbol(node)
var declarations []*ast.Node
if shorthandSymbol != nil {
declarations = shorthandSymbol.Declarations
}
contextualDeclarations := getDeclarationsFromObjectLiteralElement(c, node)
return core.Concatenate(declarations, contextualDeclarations)
}
if ast.IsPropertyName(node) && ast.IsBindingElement(node.Parent) && ast.IsObjectBindingPattern(node.Parent.Parent) {
// If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the
// declaration of the symbol (which is itself), we should try to get to the original type of the
// ObjectBindingPattern and return the property declaration for the referenced property.
// For example:
// import('./foo').then(({ bar }) => undefined); => should navigate to the declaration in file "./foo"
//
// function bar<T>(onfulfilled: (value: T) => void) { }
// interface Test { prop1: number }
// bar<Test>(({ prop1 }) => {}); => should navigate to prop1 in Test
bindingEl := node.Parent.AsBindingElement()
if bindingEl.DotDotDotToken == nil && node == core.OrElse(bindingEl.PropertyName, node.Parent.Name()) {
if name, ok := ast.TryGetTextOfPropertyName(node); ok {
t := c.GetTypeAtLocation(node.Parent.Parent)
types := []*checker.Type{t}
if t.IsUnion() {
types = t.Types()
}
var result []*ast.Node
for _, unionType := range types {
if prop := c.GetPropertyOfType(unionType, name); prop != nil {
result = append(result, prop.Declarations...)
}
}
return result
}
}
}
node = getDeclarationNameForKeyword(node)
if symbol := c.GetSymbolAtLocation(node); symbol != nil {
if symbol.Flags&ast.SymbolFlagsClass != 0 && symbol.Flags&(ast.SymbolFlagsFunction|ast.SymbolFlagsVariable) == 0 && node.Kind == ast.KindConstructorKeyword {
if constructor := symbol.Members[ast.InternalSymbolNameConstructor]; constructor != nil {
symbol = constructor
}
}
if symbol.Flags&ast.SymbolFlagsAlias != 0 {
if resolved, ok := c.ResolveAlias(symbol); ok {
symbol = resolved
}
}
objectLiteralElementDeclarations := getDeclarationsFromObjectLiteralElement(c, node)
if len(objectLiteralElementDeclarations) > 0 {
return objectLiteralElementDeclarations
}
if len(symbol.Declarations) > 0 {
return symbol.Declarations
}
}
if indexInfos := c.GetIndexSignaturesAtLocation(node); len(indexInfos) != 0 {
return indexInfos
}
return nil
}
// getDeclarationsFromObjectLiteralElement returns declarations from the contextual type
// of an object literal element, if available.
func getDeclarationsFromObjectLiteralElement(c *checker.Checker, node *ast.Node) []*ast.Node {
element := getContainingObjectLiteralElement(node)
if element == nil {
return nil
}
contextualType := c.GetContextualType(element.Parent, checker.ContextFlagsNone)
if contextualType == nil {
return nil
}
properties := c.GetPropertySymbolsFromContextualType(element, contextualType, false /*unionSymbolOk*/)
if core.Some(properties, func(p *ast.Symbol) bool {
return p.ValueDeclaration != nil && ast.IsObjectLiteralExpression(p.ValueDeclaration.Parent) && ast.IsObjectLiteralElement(p.ValueDeclaration) && p.ValueDeclaration.Name() == node
}) {
if withoutNodeInferencesType := c.GetContextualType(element.Parent, checker.ContextFlagsIgnoreNodeInferences); withoutNodeInferencesType != nil {
if withoutNodeInferencesProperties := c.GetPropertySymbolsFromContextualType(element, withoutNodeInferencesType, false /*unionSymbolOk*/); len(withoutNodeInferencesProperties) > 0 {
properties = withoutNodeInferencesProperties
}
}
}
var result []*ast.Node
for _, prop := range properties {
result = append(result, prop.Declarations...)
}
return result
}
// Returns a CallLikeExpression where `node` is the target being invoked.
func getAncestorCallLikeExpression(node *ast.Node) *ast.Node {
target := ast.FindAncestor(node, func(n *ast.Node) bool {
return !ast.IsRightSideOfPropertyAccess(n)
})
callLike := target.Parent
if callLike != nil && ast.IsCallLikeExpression(callLike) && ast.GetInvokedExpression(callLike) == target {
return callLike
}
return nil
}
func tryGetSignatureDeclaration(typeChecker *checker.Checker, node *ast.Node) *ast.Node {
var signature *checker.Signature
callLike := getAncestorCallLikeExpression(node)
if callLike != nil {
signature = typeChecker.GetResolvedSignature(callLike)
}
// Don't go to a function type, go to the value having that type.
var declaration *ast.Node
if signature != nil && signature.Declaration() != nil {
declaration = signature.Declaration()
if ast.IsFunctionLike(declaration) && !ast.IsFunctionTypeNode(declaration) {
return declaration
}
}
return nil
}
func isJsxConstructorLike(node *ast.Node) bool {
switch {
case ast.IsConstructorDeclaration(node),
ast.IsConstructorTypeNode(node),
ast.IsCallSignatureDeclaration(node),
ast.IsConstructSignatureDeclaration(node):
return true
default:
return false
}
}
func symbolMatchesSignature(symbol *ast.Symbol, calledDeclaration *ast.Node) bool {
if symbol == nil || calledDeclaration == nil {
return false
}
calledSymbol := calledDeclaration.Symbol()
if symbol == calledSymbol || calledSymbol != nil && symbol == calledSymbol.Parent {
return true
}
parent := calledDeclaration.Parent
return parent != nil && (ast.IsAssignmentExpression(parent, false /*excludeCompoundAssignment*/) ||
!ast.IsCallLikeExpression(parent) && ast.CanHaveSymbol(parent) && symbol == parent.Symbol())
}
func getSymbolForOverriddenMember(typeChecker *checker.Checker, node *ast.Node) *ast.Symbol {
classElement := ast.FindAncestor(node, ast.IsClassElement)
if classElement == nil || classElement.Name() == nil {
return nil
}
baseDeclaration := ast.FindAncestor(classElement, ast.IsClassLike)
if baseDeclaration == nil {
return nil
}
baseTypeNode := ast.GetClassExtendsHeritageElement(baseDeclaration)
if baseTypeNode == nil {
return nil
}
expression := ast.SkipParentheses(baseTypeNode.Expression())
var base *ast.Symbol
if ast.IsClassExpression(expression) {
base = expression.Symbol()
} else {
base = typeChecker.GetSymbolAtLocation(expression)
}
if base == nil {
return nil
}
name := ast.GetTextOfPropertyName(classElement.Name())
if ast.HasStaticModifier(classElement) {
return typeChecker.GetPropertyOfType(typeChecker.GetTypeOfSymbol(base), name)
}
return typeChecker.GetPropertyOfType(typeChecker.GetDeclaredTypeOfSymbol(base), name)
}
func getTypeOfSymbolAtLocation(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) *checker.Type {
t := c.GetTypeOfSymbolAtLocation(symbol, node)
// If the type is just a function's inferred type, go-to-type should go to the return type instead since
// go-to-definition takes you to the function anyway.
if t.Symbol() == symbol || t.Symbol() != nil && symbol.ValueDeclaration != nil && ast.IsVariableDeclaration(symbol.ValueDeclaration) && symbol.ValueDeclaration.Initializer() == t.Symbol().ValueDeclaration {
sigs := c.GetCallSignatures(t)
if len(sigs) == 1 {
return c.GetReturnTypeOfSignature(sigs[0])
}
}
return t
}
func getDeclarationsFromType(t *checker.Type) []*ast.Node {
var result []*ast.Node
for _, t := range t.Distributed() {
if t.Symbol() != nil {
for _, decl := range t.Symbol().Declarations {
result = core.AppendIfUnique(result, decl)
}
}
}
return result
}

View File

@@ -0,0 +1,58 @@
package ls
import (
"context"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
)
// getAllDiagnostics collects all diagnostics for a file: syntactic, semantic,
// suggestion, and (when declarations are emitted) declaration diagnostics.
func getAllDiagnostics(ctx context.Context, program *compiler.Program, file *ast.SourceFile) []*ast.Diagnostic {
var diags []*ast.Diagnostic
diags = append(diags, program.GetSyntacticDiagnostics(ctx, file)...)
diags = append(diags, program.GetSemanticDiagnostics(ctx, file)...)
diags = append(diags, program.GetSuggestionDiagnostics(ctx, file)...)
if program.Options().GetEmitDeclarations() {
diags = append(diags, program.GetDeclarationDiagnostics(ctx, file)...)
}
return diags
}
func (l *LanguageService) ProvideDiagnostics(ctx context.Context, uri lsproto.DocumentUri) (lsproto.DocumentDiagnosticResponse, error) {
program, file := l.getProgramAndFile(uri)
if l.UserPreferences().EnableValidation.IsFalse() {
diagnostics := []*lsproto.Diagnostic{}
return lsproto.RelatedFullDocumentDiagnosticReportOrUnchangedDocumentDiagnosticReport{
FullDocumentDiagnosticReport: &lsproto.RelatedFullDocumentDiagnosticReport{
Items: diagnostics,
},
}, nil
}
diagnostics := getAllDiagnostics(ctx, program, file)
return lsproto.RelatedFullDocumentDiagnosticReportOrUnchangedDocumentDiagnosticReport{
FullDocumentDiagnosticReport: &lsproto.RelatedFullDocumentDiagnosticReport{
Items: l.toLSPDiagnostics(ctx, diagnostics),
},
}, nil
}
func (l *LanguageService) toLSPDiagnostics(ctx context.Context, diagnostics ...[]*ast.Diagnostic) []*lsproto.Diagnostic {
size := 0
for _, diagSlice := range diagnostics {
size += len(diagSlice)
}
lspDiagnostics := make([]*lsproto.Diagnostic, 0, size)
for _, diagSlice := range diagnostics {
for _, diag := range diagSlice {
lspDiagnostics = append(lspDiagnostics, lsconv.DiagnosticToLSPPull(ctx, l.converters, diag, l.UserPreferences().ReportStyleChecksAsWarnings.IsTrue()))
}
}
return lspDiagnostics
}

View File

@@ -0,0 +1,217 @@
package ls
import (
"strings"
"unicode/utf8"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/printer"
"github.com/microsoft/typescript-go/internal/stringutil"
)
var _ printer.EmitTextWriter = &displayPartsWriter{}
// displayPartsWriter implements EmitTextWriter and captures classified text runs
// for VS colorized labels, while also building a plain string.
// When vsCapability is false, only the plain string is built; runs are skipped.
type displayPartsWriter struct {
builder strings.Builder
runs []*lsproto.VSClassifiedTextRun
vsCapability bool
lastWritten string
}
func newDisplayPartsWriter(vsCapability bool) *displayPartsWriter {
return &displayPartsWriter{vsCapability: vsCapability}
}
func (w *displayPartsWriter) addRun(classification lsproto.ClassificationTypeName, text string) {
if text == "" {
return
}
if w.vsCapability {
w.runs = append(w.runs, &lsproto.VSClassifiedTextRun{
ClassificationTypeName: string(classification),
Text: text,
})
}
w.lastWritten = text
w.builder.WriteString(text)
}
// WriteClassified writes text with an explicit classification type.
func (w *displayPartsWriter) WriteClassified(text string, classification lsproto.ClassificationTypeName) {
w.addRun(classification, text)
}
// WriteFrom copies the accumulated content from another displayPartsWriter.
func (w *displayPartsWriter) WriteFrom(other *displayPartsWriter) {
w.builder.WriteString(other.String())
if w.vsCapability {
w.runs = append(w.runs, other.GetRuns()...)
}
if other.lastWritten != "" {
w.lastWritten = other.lastWritten
}
}
func (w *displayPartsWriter) GetRuns() []*lsproto.VSClassifiedTextRun {
return w.runs
}
func (w *displayPartsWriter) String() string {
return w.builder.String()
}
func (w *displayPartsWriter) Clear() {
w.lastWritten = ""
w.builder.Reset()
w.runs = nil
}
func (w displayPartsWriter) DecreaseIndent() {}
func (w displayPartsWriter) GetColumn() core.UTF16Offset { return 0 }
func (w displayPartsWriter) GetIndent() int { return 0 }
func (w displayPartsWriter) GetLine() int { return 0 }
func (w displayPartsWriter) GetTextPos() int {
return w.builder.Len()
}
func (w displayPartsWriter) HasTrailingComment() bool { return false }
func (w displayPartsWriter) HasTrailingWhitespace() bool {
if w.builder.Len() == 0 {
return false
}
ch, _ := utf8.DecodeLastRuneInString(w.lastWritten)
if ch == utf8.RuneError {
return false
}
return stringutil.IsWhiteSpaceLike(ch)
}
func (w displayPartsWriter) IncreaseIndent() {}
func (w displayPartsWriter) IsAtStartOfLine() bool { return false }
func (w *displayPartsWriter) RawWrite(s string) {
w.addRun(lsproto.ClassificationTypeNameText, s)
}
func (w *displayPartsWriter) Write(s string) {
w.addRun(lsproto.ClassificationTypeNameText, s)
}
func (w *displayPartsWriter) WriteComment(text string) {
// Strada's writeComment uses unknownWrite → SymbolDisplayPartKind.text → "text"
w.addRun(lsproto.ClassificationTypeNameText, text)
}
func (w *displayPartsWriter) WriteKeyword(text string) {
w.addRun(lsproto.ClassificationTypeNameKeyword, text)
}
func (w *displayPartsWriter) WriteLine() {
w.addRun(lsproto.ClassificationTypeNameWhiteSpace, " ")
}
func (w *displayPartsWriter) WriteLineForce(force bool) {
w.addRun(lsproto.ClassificationTypeNameWhiteSpace, " ")
}
func (w *displayPartsWriter) WriteLiteral(s string) {
// Strada's writeLiteral → SymbolDisplayPartKind.stringLiteral → "string"
w.addRun(lsproto.ClassificationTypeNameString, s)
}
func (w *displayPartsWriter) WriteOperator(text string) {
w.addRun(lsproto.ClassificationTypeNameOperator, text)
}
func (w *displayPartsWriter) WriteParameter(text string) {
w.addRun(lsproto.ClassificationTypeNameParameterName, text)
}
func (w *displayPartsWriter) WriteProperty(text string) {
w.addRun(lsproto.ClassificationTypeNamePropertyName, text)
}
func (w *displayPartsWriter) WritePunctuation(text string) {
w.addRun(lsproto.ClassificationTypeNamePunctuation, text)
}
func (w *displayPartsWriter) WriteSpace(text string) {
w.addRun(lsproto.ClassificationTypeNameWhiteSpace, text)
}
func (w *displayPartsWriter) WriteStringLiteral(text string) {
w.addRun(lsproto.ClassificationTypeNameString, text)
}
func (w *displayPartsWriter) WriteSymbol(text string, symbol *ast.Symbol) {
classification := classificationForSymbol(symbol)
w.addRun(classification, text)
}
func (w *displayPartsWriter) WriteTrailingSemicolon(text string) {
w.addRun(lsproto.ClassificationTypeNamePunctuation, text)
}
// classificationForSymbol determines the Roslyn classification type name based on a symbol's flags.
// Matches the Strada translation chain: displayPartKind() → GetClassificationName().
func classificationForSymbol(symbol *ast.Symbol) lsproto.ClassificationTypeName {
if symbol == nil {
return lsproto.ClassificationTypeNameText
}
flags := symbol.Flags
switch {
case flags&ast.SymbolFlagsVariable != 0:
if isFirstDeclarationOfSymbolParameter(symbol) {
return lsproto.ClassificationTypeNameParameterName
}
return lsproto.ClassificationTypeNameLocalName
case flags&ast.SymbolFlagsProperty != 0:
return lsproto.ClassificationTypeNamePropertyName
case flags&ast.SymbolFlagsGetAccessor != 0:
return lsproto.ClassificationTypeNamePropertyName
case flags&ast.SymbolFlagsSetAccessor != 0:
return lsproto.ClassificationTypeNamePropertyName
case flags&ast.SymbolFlagsEnumMember != 0:
return lsproto.ClassificationTypeNameFieldName
case flags&ast.SymbolFlagsFunction != 0:
return lsproto.ClassificationTypeNameMethodName
case flags&ast.SymbolFlagsClass != 0:
return lsproto.ClassificationTypeNameClassName
case flags&ast.SymbolFlagsInterface != 0:
return lsproto.ClassificationTypeNameInterfaceName
case flags&ast.SymbolFlagsEnum != 0:
return lsproto.ClassificationTypeNameEnumName
case flags&ast.SymbolFlagsModule != 0:
return lsproto.ClassificationTypeNameModuleName
case flags&ast.SymbolFlagsMethod != 0:
return lsproto.ClassificationTypeNameMethodName
case flags&ast.SymbolFlagsTypeParameter != 0:
return lsproto.ClassificationTypeNameTypeParameterName
case flags&ast.SymbolFlagsTypeAlias != 0:
return lsproto.ClassificationTypeNameIdentifier
case flags&ast.SymbolFlagsAlias != 0:
return lsproto.ClassificationTypeNameIdentifier
default:
return lsproto.ClassificationTypeNameText
}
}
// isFirstDeclarationOfSymbolParameter checks if the symbol's first declaration is a parameter.
func isFirstDeclarationOfSymbolParameter(symbol *ast.Symbol) bool {
declarations := symbol.Declarations
if len(declarations) == 0 {
return false
}
return declarations[0].Kind == ast.KindParameter
}

View File

@@ -0,0 +1,752 @@
package ls
import (
"context"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/stringutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
)
func (l *LanguageService) ProvideDocumentHighlights(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position) (lsproto.DocumentHighlightResponse, error) {
result, err := l.provideDocumentHighlightsWorker(ctx, documentUri, documentPosition, nil)
if err != nil {
return lsproto.DocumentHighlightsOrNull{}, err
}
// Extract highlights for the current file only.
var documentHighlights []*lsproto.DocumentHighlight
if result.MultiDocumentHighlights != nil {
for _, mh := range *result.MultiDocumentHighlights {
if mh.Uri == documentUri {
documentHighlights = append(documentHighlights, mh.Highlights...)
}
}
}
return lsproto.DocumentHighlightsOrNull{DocumentHighlights: &documentHighlights}, nil
}
func (l *LanguageService) ProvideMultiDocumentHighlights(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position, filesToSearch []lsproto.DocumentUri) (lsproto.CustomMultiDocumentHighlightResponse, error) {
return l.provideDocumentHighlightsWorker(ctx, documentUri, documentPosition, filesToSearch)
}
func (l *LanguageService) provideDocumentHighlightsWorker(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position, filesToSearch []lsproto.DocumentUri) (lsproto.MultiDocumentHighlightsOrNull, error) {
program, sourceFile := l.getProgramAndFile(documentUri)
position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition))
node := astnav.GetTouchingPropertyName(sourceFile, position)
// Cheap JSX check before resolving files to search.
if node.Parent != nil && (node.Parent.Kind == ast.KindJsxClosingElement || (node.Parent.Kind == ast.KindJsxOpeningElement && node.Parent.TagName() == node)) {
var openingElement, closingElement *ast.Node
if ast.IsJsxElement(node.Parent.Parent) {
openingElement = node.Parent.Parent.AsJsxElement().OpeningElement
closingElement = node.Parent.Parent.AsJsxElement().ClosingElement
}
var highlights []*lsproto.DocumentHighlight
kind := lsproto.DocumentHighlightKindRead
if openingElement != nil {
highlights = append(highlights, &lsproto.DocumentHighlight{
Range: l.createLspRangeFromNode(openingElement, sourceFile),
Kind: &kind,
})
}
if closingElement != nil {
highlights = append(highlights, &lsproto.DocumentHighlight{
Range: l.createLspRangeFromNode(closingElement, sourceFile),
Kind: &kind,
})
}
multiHighlights := []*lsproto.MultiDocumentHighlight{
{Uri: documentUri, Highlights: highlights},
}
return lsproto.MultiDocumentHighlightsOrNull{
MultiDocumentHighlights: &multiHighlights,
}, nil
}
// Resolve the source files to search, deduplicating by file name.
var sourceFiles []*ast.SourceFile
seenFiles := collections.NewSetWithSizeHint[string](len(filesToSearch))
for _, uri := range filesToSearch {
fileName := uri.FileName()
if !seenFiles.AddIfAbsent(fileName) {
continue
}
if sf := program.GetSourceFile(fileName); sf != nil {
sourceFiles = append(sourceFiles, sf)
}
}
if len(sourceFiles) == 0 {
sourceFiles = []*ast.SourceFile{sourceFile}
}
multiHighlights := l.getSemanticDocumentHighlights(ctx, position, node, program, sourceFiles)
if len(multiHighlights) == 0 {
// Fall back to syntactic highlights for the current file only.
syntacticHighlights := l.getSyntacticDocumentHighlights(node, sourceFile)
if len(syntacticHighlights) > 0 {
multiHighlights = []*lsproto.MultiDocumentHighlight{
{Uri: documentUri, Highlights: syntacticHighlights},
}
}
}
return lsproto.MultiDocumentHighlightsOrNull{MultiDocumentHighlights: &multiHighlights}, nil
}
func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFiles []*ast.SourceFile) []*lsproto.MultiDocumentHighlight {
options := refOptions{use: referenceUseNone}
referenceEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, sourceFiles, options)
if referenceEntries == nil {
return nil
}
// Group highlights by file
fileHighlights := make(map[string][]*lsproto.DocumentHighlight)
for _, entry := range referenceEntries {
for _, ref := range entry.references {
fileName, highlight := l.toDocumentHighlight(ref)
fileHighlights[fileName] = append(fileHighlights[fileName], highlight)
}
}
var result []*lsproto.MultiDocumentHighlight
for _, sf := range sourceFiles {
if highlights, ok := fileHighlights[sf.FileName()]; ok {
result = append(result, &lsproto.MultiDocumentHighlight{
Uri: lsconv.FileNameToDocumentURI(sf.FileName()),
Highlights: highlights,
})
}
}
return result
}
func (l *LanguageService) toDocumentHighlight(entry *ReferenceEntry) (string, *lsproto.DocumentHighlight) {
entry = l.resolveEntry(entry)
kind := lsproto.DocumentHighlightKindRead
if entry.kind == entryKindRange {
return entry.fileName, &lsproto.DocumentHighlight{
Range: l.getRangeOfEntry(entry),
Kind: &kind,
}
}
// Determine write access for node references.
if ast.IsWriteAccessForReference(entry.node) {
kind = lsproto.DocumentHighlightKindWrite
}
dh := &lsproto.DocumentHighlight{
Range: l.getRangeOfEntry(entry),
Kind: &kind,
}
return entry.fileName, dh
}
func (l *LanguageService) getSyntacticDocumentHighlights(node *ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight {
switch node.Kind {
case ast.KindIfKeyword, ast.KindElseKeyword:
if ast.IsIfStatement(node.Parent) {
return l.getIfElseOccurrences(node.Parent.AsIfStatement(), sourceFile)
}
return nil
case ast.KindReturnKeyword:
return l.useParent(node.Parent, ast.IsReturnStatement, getReturnOccurrences, sourceFile)
case ast.KindThrowKeyword:
return l.useParent(node.Parent, ast.IsThrowStatement, getThrowOccurrences, sourceFile)
case ast.KindTryKeyword, ast.KindCatchKeyword, ast.KindFinallyKeyword:
var tryStatement *ast.Node
if node.Kind == ast.KindCatchKeyword {
tryStatement = node.Parent.Parent
} else {
tryStatement = node.Parent
}
return l.useParent(tryStatement, ast.IsTryStatement, getTryCatchFinallyOccurrences, sourceFile)
case ast.KindSwitchKeyword:
return l.useParent(node.Parent, ast.IsSwitchStatement, getSwitchCaseDefaultOccurrences, sourceFile)
case ast.KindCaseKeyword, ast.KindDefaultKeyword:
if ast.IsDefaultClause(node.Parent) || ast.IsCaseClause(node.Parent) {
return l.useParent(node.Parent.Parent.Parent, ast.IsSwitchStatement, getSwitchCaseDefaultOccurrences, sourceFile)
}
return nil
case ast.KindBreakKeyword, ast.KindContinueKeyword:
return l.useParent(node.Parent, ast.IsBreakOrContinueStatement, getBreakOrContinueStatementOccurrences, sourceFile)
case ast.KindForKeyword, ast.KindWhileKeyword, ast.KindDoKeyword:
return l.useParent(node.Parent, func(n *ast.Node) bool {
return ast.IsIterationStatement(n, true)
}, getLoopBreakContinueOccurrences, sourceFile)
case ast.KindConstructorKeyword:
return l.getFromAllDeclarations(ast.IsConstructorDeclaration, []ast.Kind{ast.KindConstructorKeyword}, node, sourceFile)
case ast.KindGetKeyword, ast.KindSetKeyword:
return l.getFromAllDeclarations(ast.IsAccessor, []ast.Kind{ast.KindGetKeyword, ast.KindSetKeyword}, node, sourceFile)
case ast.KindAwaitKeyword:
return l.useParent(node.Parent, ast.IsAwaitExpression, getAsyncAndAwaitOccurrences, sourceFile)
case ast.KindAsyncKeyword:
return l.highlightSpans(getAsyncAndAwaitOccurrences(node, sourceFile), sourceFile)
case ast.KindYieldKeyword:
return l.highlightSpans(getYieldOccurrences(node, sourceFile), sourceFile)
case ast.KindInKeyword, ast.KindOutKeyword:
return nil
default:
if ast.IsModifierKind(node.Kind) && (ast.IsDeclaration(node.Parent) || ast.IsVariableStatement(node.Parent)) {
return l.highlightSpans(getModifierOccurrences(node.Kind, node.Parent, sourceFile), sourceFile)
}
return nil
}
}
func (l *LanguageService) useParent(node *ast.Node, nodeTest func(*ast.Node) bool, getNodes func(*ast.Node, *ast.SourceFile) []*ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight {
if nodeTest(node) {
return l.highlightSpans(getNodes(node, sourceFile), sourceFile)
}
return nil
}
func (l *LanguageService) highlightSpans(nodes []*ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight {
if len(nodes) == 0 {
return nil
}
var highlights []*lsproto.DocumentHighlight
kind := lsproto.DocumentHighlightKindRead
for _, node := range nodes {
if node != nil {
highlights = append(highlights, &lsproto.DocumentHighlight{
Range: l.createLspRangeFromNode(node, sourceFile),
Kind: &kind,
})
}
}
return highlights
}
func (l *LanguageService) getFromAllDeclarations(nodeTest func(*ast.Node) bool, keywords []ast.Kind, node *ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight {
return l.useParent(node.Parent, nodeTest, func(decl *ast.Node, sf *ast.SourceFile) []*ast.Node {
var symbolDecls []*ast.Node
if ast.CanHaveSymbol(decl) {
if symbol := decl.Symbol(); symbol != nil {
for _, d := range symbol.Declarations {
if nodeTest(d) {
outer:
for _, c := range getChildrenFromNonJSDocNode(d, sourceFile) {
for _, k := range keywords {
if c.Kind == k {
symbolDecls = append(symbolDecls, c)
break outer
}
}
}
}
}
}
}
return symbolDecls
}, sourceFile)
}
func (l *LanguageService) getIfElseOccurrences(ifStatement *ast.IfStatement, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight {
keywords := getIfElseKeywords(ifStatement, sourceFile)
kind := lsproto.DocumentHighlightKindRead
var highlights []*lsproto.DocumentHighlight
// We'd like to highlight else/ifs together if they are only separated by whitespace
// (i.e. the keywords are separated by no comments, no newlines).
for i := 0; i < len(keywords); i++ {
if keywords[i].Kind == ast.KindElseKeyword && i < len(keywords)-1 {
elseKeyword := keywords[i]
ifKeyword := keywords[i+1] // this *should* always be an 'if' keyword.
shouldCombine := true
// Avoid recalculating getStart() by iterating backwards.
ifTokenStart := scanner.GetTokenPosOfNode(ifKeyword, sourceFile, false)
if ifTokenStart < 0 {
ifTokenStart = ifKeyword.Pos()
}
for j := ifTokenStart - 1; j >= elseKeyword.End(); j-- {
if !stringutil.IsWhiteSpaceSingleLine(rune(sourceFile.Text()[j])) {
shouldCombine = false
break
}
}
if shouldCombine {
highlights = append(highlights, &lsproto.DocumentHighlight{
Range: l.createLspRangeFromBounds(scanner.SkipTrivia(sourceFile.Text(), elseKeyword.Pos()), ifKeyword.End(), sourceFile),
Kind: &kind,
})
i++ // skip the next keyword
continue
}
}
// Ordinary case: just highlight the keyword.
highlights = append(highlights, &lsproto.DocumentHighlight{
Range: l.createLspRangeFromNode(keywords[i], sourceFile),
Kind: &kind,
})
}
return highlights
}
func getIfElseKeywords(ifStatement *ast.IfStatement, sourceFile *ast.SourceFile) []*ast.Node {
// We may be at an if statement like those in the range below:
//
// ```
// if (...) {
// } else [|if (...) {}|]
// ````
//
// Traverse upwards through all parent if-statements linked by their else-branches.
for ast.IsIfStatement(ifStatement.Parent) {
// See if the parent's `else` is actually the current `if` statement.
parentingIf := ifStatement.Parent.AsIfStatement()
elseStatement := parentingIf.ElseStatement
if elseStatement != ifStatement.AsNode() {
break
}
ifStatement = parentingIf
}
var keywords []*ast.Node
// Traverse back down through the else branches, aggregating if/else keywords of if-statements.
for {
children := getChildrenFromNonJSDocNode(ifStatement.AsNode(), sourceFile)
if len(children) > 0 && children[0].Kind == ast.KindIfKeyword {
keywords = append(keywords, children[0])
}
// Generally the 'else' keyword is second-to-last, so traverse backwards.
for i := len(children) - 1; i >= 0; i-- {
if children[i].Kind == ast.KindElseKeyword {
keywords = append(keywords, children[i])
break
}
}
elseStatement := ifStatement.ElseStatement
if elseStatement == nil || !ast.IsIfStatement(elseStatement) {
break
}
ifStatement = elseStatement.AsIfStatement()
}
return keywords
}
func getReturnOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
funcNode := ast.FindAncestor(node.Parent, ast.IsFunctionLike)
if funcNode == nil {
return nil
}
var keywords []*ast.Node
body := funcNode.Body()
if body != nil {
ast.ForEachReturnStatement(body, func(ret *ast.Node) bool {
keyword := astnav.FindChildOfKind(ret, ast.KindReturnKeyword, sourceFile)
if keyword != nil {
keywords = append(keywords, keyword)
}
return false // continue traversal
})
// Get all throw statements not in a try block
throwStatements := aggregateOwnedThrowStatements(body, sourceFile)
for _, throw := range throwStatements {
keyword := astnav.FindChildOfKind(throw, ast.KindThrowKeyword, sourceFile)
if keyword != nil {
keywords = append(keywords, keyword)
}
}
}
return keywords
}
func aggregateOwnedThrowStatements(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
if ast.IsThrowStatement(node) {
return []*ast.Node{node}
}
if ast.IsTryStatement(node) {
// Exceptions thrown within a try block lacking a catch clause are "owned" in the current context.
statement := node.AsTryStatement()
tryBlock := statement.TryBlock
catchClause := statement.CatchClause
finallyBlock := statement.FinallyBlock
var result []*ast.Node
if catchClause != nil {
result = aggregateOwnedThrowStatements(catchClause, sourceFile)
} else if tryBlock != nil {
result = aggregateOwnedThrowStatements(tryBlock, sourceFile)
}
if finallyBlock != nil {
result = append(result, aggregateOwnedThrowStatements(finallyBlock, sourceFile)...)
}
return result
}
// Do not cross function boundaries.
if ast.IsFunctionLike(node) {
return nil
}
return flatMapChildren(node, sourceFile, aggregateOwnedThrowStatements)
}
func flatMapChildren[T any](node *ast.Node, sourceFile *ast.SourceFile, cb func(child *ast.Node, sourceFile *ast.SourceFile) []T) []T {
var result []T
node.ForEachChild(func(child *ast.Node) bool {
value := cb(child, sourceFile)
if value != nil {
result = append(result, value...)
}
return false // continue traversal
})
return result
}
func getThrowOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
owner := getThrowStatementOwner(node)
if owner == nil {
return nil
}
var keywords []*ast.Node
// Aggregate all throw statements "owned" by this owner.
throwStatements := aggregateOwnedThrowStatements(owner, sourceFile)
for _, throw := range throwStatements {
keyword := astnav.FindChildOfKind(throw, ast.KindThrowKeyword, sourceFile)
if keyword != nil {
keywords = append(keywords, keyword)
}
}
// If the "owner" is a function, then we equate 'return' and 'throw' statements in their
// ability to "jump out" of the function, and include occurrences for both
if ast.IsFunctionBlock(owner) {
ast.ForEachReturnStatement(owner, func(ret *ast.Node) bool {
keyword := astnav.FindChildOfKind(ret, ast.KindReturnKeyword, sourceFile)
if keyword != nil {
keywords = append(keywords, keyword)
}
return false // continue traversal
})
}
return keywords
}
// For lack of a better name, this function takes a throw statement and returns the
// nearest ancestor that is a try-block (whose try statement has a catch clause),
// function-block, or source file.
func getThrowStatementOwner(throwStatement *ast.Node) *ast.Node {
child := throwStatement
for child.Parent != nil {
parent := child.Parent
if ast.IsFunctionBlock(parent) || parent.Kind == ast.KindSourceFile {
return parent
}
// A throw-statement is only owned by a try-statement if the try-statement has
// a catch clause, and if the throw-statement occurs within the try block.
if ast.IsTryStatement(parent) {
tryStatement := parent.AsTryStatement()
if tryStatement.TryBlock == child && tryStatement.CatchClause != nil {
return child
}
}
child = parent
}
return nil
}
func getTryCatchFinallyOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
tryStatement := node.AsTryStatement()
var keywords []*ast.Node
token := lsutil.GetFirstToken(node, sourceFile)
if token != nil && token.Kind == ast.KindTryKeyword {
keywords = append(keywords, token)
}
if tryStatement.CatchClause != nil {
if catchToken := astnav.FindChildOfKind(node, ast.KindCatchKeyword, sourceFile); catchToken != nil {
keywords = append(keywords, catchToken)
}
}
if tryStatement.FinallyBlock != nil {
if finallyKeyword := astnav.FindChildOfKind(node, ast.KindFinallyKeyword, sourceFile); finallyKeyword != nil {
keywords = append(keywords, finallyKeyword)
}
}
return keywords
}
func getSwitchCaseDefaultOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
switchStatement := node.AsSwitchStatement()
var keywords []*ast.Node
token := lsutil.GetFirstToken(node, sourceFile)
if token.Kind == ast.KindSwitchKeyword {
keywords = append(keywords, token)
}
clauses := switchStatement.CaseBlock.AsCaseBlock().Clauses
for _, clause := range clauses.Nodes {
clauseToken := lsutil.GetFirstToken(clause.AsNode(), sourceFile)
if clauseToken.Kind == ast.KindCaseKeyword || clauseToken.Kind == ast.KindDefaultKeyword {
keywords = append(keywords, clauseToken)
}
breakAndContinueStatements := aggregateAllBreakAndContinueStatements(clause, sourceFile)
for _, statement := range breakAndContinueStatements {
if statement.Kind == ast.KindBreakStatement && ownsBreakOrContinueStatement(switchStatement.AsNode(), statement) {
keywords = append(keywords, lsutil.GetFirstToken(statement, sourceFile))
}
}
}
return keywords
}
func aggregateAllBreakAndContinueStatements(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
if ast.IsBreakOrContinueStatement(node) {
return []*ast.Node{node}
}
if ast.IsFunctionLike(node) {
return nil
}
return flatMapChildren(node, sourceFile, aggregateAllBreakAndContinueStatements)
}
func ownsBreakOrContinueStatement(owner *ast.Node, statement *ast.Node) bool {
actualOwner := getBreakOrContinueOwner(statement)
if actualOwner == nil {
return false
}
return actualOwner == owner
}
func getBreakOrContinueOwner(statement *ast.Node) *ast.Node {
return ast.FindAncestorOrQuit(statement, func(node *ast.Node) ast.FindAncestorResult {
switch node.Kind {
case ast.KindSwitchStatement:
if statement.Kind == ast.KindContinueStatement {
return ast.FindAncestorFalse
}
fallthrough
case ast.KindForStatement,
ast.KindForInStatement,
ast.KindForOfStatement,
ast.KindWhileStatement,
ast.KindDoStatement:
// If the statement is labeled, check if the node is labeled by the statement's label.
if statement.Label() == nil || isLabeledBy(node, statement.Label().Text()) {
return ast.FindAncestorTrue
}
return ast.FindAncestorFalse
default:
// Don't cross function boundaries.
if ast.IsFunctionLike(node) {
return ast.FindAncestorQuit
}
return ast.FindAncestorFalse
}
})
}
// Whether or not a 'node' is preceded by a label of the given string.
// Note: 'node' cannot be a SourceFile.
func isLabeledBy(node *ast.Node, labelName string) bool {
return ast.FindAncestorOrQuit(node.Parent, func(owner *ast.Node) ast.FindAncestorResult {
if !ast.IsLabeledStatement(owner) {
return ast.FindAncestorQuit
}
if owner.Label().Text() == labelName {
return ast.FindAncestorTrue
}
return ast.FindAncestorFalse
}) != nil
}
func getBreakOrContinueStatementOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
if owner := getBreakOrContinueOwner(node); owner != nil {
switch owner.Kind {
case ast.KindForStatement, ast.KindForInStatement, ast.KindForOfStatement, ast.KindDoStatement, ast.KindWhileStatement:
return getLoopBreakContinueOccurrences(owner, sourceFile)
case ast.KindSwitchStatement:
return getSwitchCaseDefaultOccurrences(owner, sourceFile)
}
}
return nil
}
func getLoopBreakContinueOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
var keywords []*ast.Node
token := lsutil.GetFirstToken(node, sourceFile)
if token.Kind == ast.KindForKeyword || token.Kind == ast.KindDoKeyword || token.Kind == ast.KindWhileKeyword {
keywords = append(keywords, token)
if node.Kind == ast.KindDoStatement {
loopTokens := getChildrenFromNonJSDocNode(node, sourceFile)
for i := len(loopTokens) - 1; i >= 0; i-- {
if loopTokens[i].Kind == ast.KindWhileKeyword {
keywords = append(keywords, loopTokens[i])
break
}
}
}
}
breakAndContinueStatements := aggregateAllBreakAndContinueStatements(node, sourceFile)
for _, statement := range breakAndContinueStatements {
token := lsutil.GetFirstToken(statement, sourceFile)
if ownsBreakOrContinueStatement(node, statement) && (token.Kind == ast.KindBreakKeyword || token.Kind == ast.KindContinueKeyword) {
keywords = append(keywords, token)
}
}
return keywords
}
func getAsyncAndAwaitOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
fun := ast.GetContainingFunction(node)
if fun == nil {
return nil
}
var keywords []*ast.Node
for _, modifier := range fun.ModifierNodes() {
if modifier.Kind == ast.KindAsyncKeyword {
keywords = append(keywords, modifier)
}
}
fun.ForEachChild(func(child *ast.Node) bool {
traverseWithoutCrossingFunction(child, sourceFile, func(child *ast.Node) {
if ast.IsAwaitExpression(child) {
token := lsutil.GetFirstToken(child, sourceFile)
if token.Kind == ast.KindAwaitKeyword {
keywords = append(keywords, token)
}
}
})
return false // continue traversal
})
return keywords
}
func getYieldOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
parentFunc := ast.FindAncestor(node.Parent, ast.IsFunctionLike)
if parentFunc == nil {
return nil
}
var keywords []*ast.Node
parentFunc.ForEachChild(func(child *ast.Node) bool {
traverseWithoutCrossingFunction(child, sourceFile, func(child *ast.Node) {
if ast.IsYieldExpression(child) {
token := lsutil.GetFirstToken(child, sourceFile)
if token.Kind == ast.KindYieldKeyword {
keywords = append(keywords, token)
}
}
})
return false // continue traversal
})
return keywords
}
func traverseWithoutCrossingFunction(node *ast.Node, sourceFile *ast.SourceFile, cb func(*ast.Node)) {
cb(node)
if !ast.IsFunctionLike(node) && !ast.IsClassLike(node) && !ast.IsInterfaceDeclaration(node) && !ast.IsModuleDeclaration(node) && !ast.IsTypeAliasDeclaration(node) && !ast.IsTypeNode(node) {
node.ForEachChild(func(child *ast.Node) bool {
traverseWithoutCrossingFunction(child, sourceFile, cb)
return false // continue traversal
})
}
}
func getModifierOccurrences(kind ast.Kind, node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
var result []*ast.Node
nodesToSearch := getNodesToSearchForModifier(node, ast.ModifierToFlag(kind))
for _, n := range nodesToSearch {
modifier := findModifier(n, kind)
if modifier != nil {
result = append(result, modifier)
}
}
return result
}
func getNodesToSearchForModifier(declaration *ast.Node, modifierFlag ast.ModifierFlags) []*ast.Node {
var result []*ast.Node
container := declaration.Parent
if container == nil {
return nil
}
// Types of node whose children might have modifiers.
switch container.Kind {
case ast.KindModuleBlock, ast.KindSourceFile, ast.KindBlock, ast.KindCaseClause, ast.KindDefaultClause:
// Container is either a class declaration or the declaration is a classDeclaration
if (modifierFlag&ast.ModifierFlagsAbstract) != 0 && ast.IsClassDeclaration(declaration) {
return append(append(result, declaration.Members()...), declaration)
} else {
return append(result, container.Statements()...)
}
case ast.KindConstructor, ast.KindMethodDeclaration, ast.KindFunctionDeclaration:
// Parameters and, if inside a class, also class members
result = append(result, container.Parameters()...)
if ast.IsClassLike(container.Parent) {
result = append(result, container.Parent.Members()...)
}
return result
case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindTypeLiteral:
nodes := container.Members()
result = append(result, nodes...)
// If we're an accessibility modifier, we're in an instance member and should search
// the constructor's parameter list for instance members as well.
if (modifierFlag & (ast.ModifierFlagsAccessibilityModifier | ast.ModifierFlagsReadonly)) != 0 {
var constructor *ast.Node
for _, member := range nodes {
if ast.IsConstructorDeclaration(member) {
constructor = member
break
}
}
if constructor != nil {
result = append(result, constructor.Parameters()...)
}
} else if (modifierFlag & ast.ModifierFlagsAbstract) != 0 {
result = append(result, container)
}
return result
default:
// Syntactically invalid positions or unsupported containers
return nil
}
}
func findModifier(node *ast.Node, kind ast.Kind) *ast.Node {
for _, modifier := range node.ModifierNodes() {
if modifier.Kind == kind {
return modifier
}
}
return nil
}

View File

@@ -0,0 +1,382 @@
package ls
import (
"context"
"slices"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/change"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/modulespecifiers"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
type pathUpdater func(path string) (string, bool)
type toImport struct {
newFileName string
updated bool
}
func (l *LanguageService) GetEditsForFileRename(ctx context.Context, oldURI lsproto.DocumentUri, newURI lsproto.DocumentUri) []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile {
program := l.GetProgram()
oldPath := oldURI.FileName()
newPath := newURI.FileName()
oldToNew := l.createPathUpdater(oldPath, newPath)
changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters)
l.updateTsconfigFiles(program, changeTracker, oldToNew, oldPath, newPath)
l.updateImportsForFileRename(program, changeTracker, oldToNew)
var documentChanges []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
// When renaming e.g. `foo.d.css.ts` -> `bar.d.css.ts`, also rename `foo.css` -> `bar.css` if it exists.
if tspath.IsDeclarationFileName(oldPath) && tspath.IsDeclarationFileName(newPath) {
dtsExt := tspath.GetDeclarationFileExtension(oldPath)
originalExtensions := tspath.GetPossibleOriginalInputExtensionForExtension(dtsExt)
for _, ext := range originalExtensions {
oldOriginalPath := tspath.ChangeFullExtension(oldPath, ext)
if l.host.FileExists(oldOriginalPath) {
newDtsExt := tspath.GetDeclarationFileExtension(oldPath)
newOriginalExtensions := tspath.GetPossibleOriginalInputExtensionForExtension(newDtsExt)
if slices.Contains(newOriginalExtensions, ext) {
newOriginalPath := tspath.ChangeFullExtension(newPath, ext)
documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{
RenameFile: &lsproto.RenameFile{
OldUri: lsconv.FileNameToDocumentURI(oldOriginalPath),
NewUri: lsconv.FileNameToDocumentURI(newOriginalPath),
},
})
}
}
}
}
for fileName, edits := range changeTracker.GetChanges() {
uri := lsconv.FileNameToDocumentURI(fileName)
lspEdits := make([]lsproto.TextEditOrAnnotatedTextEditOrSnippetTextEdit, 0, len(edits))
for _, edit := range edits {
lspEdits = append(lspEdits, lsproto.TextEditOrAnnotatedTextEditOrSnippetTextEdit{
TextEdit: edit,
})
}
documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{
TextDocumentEdit: &lsproto.TextDocumentEdit{
TextDocument: lsproto.OptionalVersionedTextDocumentIdentifier{Uri: uri},
Edits: lspEdits,
},
})
}
return documentChanges
}
func (l *LanguageService) createPathUpdater(oldPath string, newPath string) pathUpdater {
compareOptions := tspath.ComparePathsOptions{UseCaseSensitiveFileNames: l.UseCaseSensitiveFileNames()}
return func(path string) (string, bool) {
if tspath.ComparePaths(path, oldPath, compareOptions) == 0 {
return newPath, true
}
if tspath.StartsWithDirectory(path, oldPath, l.UseCaseSensitiveFileNames()) {
return newPath + path[len(oldPath):], true
}
return "", false
}
}
func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeTracker *change.Tracker, oldToNew pathUpdater, oldPath string, newPath string) {
commandLine := program.CommandLine()
if commandLine == nil || commandLine.ConfigFile == nil {
return
}
configFile := commandLine.ConfigFile.SourceFile
if configFile == nil {
return
}
configDir := tspath.GetDirectoryPath(configFile.FileName())
jsonObjectLiteral := getTsConfigObjectLiteralExpression(configFile)
if jsonObjectLiteral == nil {
return
}
forEachObjectProperty(jsonObjectLiteral, func(property *ast.PropertyAssignment, propertyName string) {
switch propertyName {
case "files", "include", "exclude":
foundExactMatch := updatePathsProperty(configFile, configDir, property, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames())
if foundExactMatch || propertyName != "include" || !ast.IsArrayLiteralExpression(property.Initializer) {
return
}
if oldSpec, isDefault := commandLine.GetMatchedIncludeSpec(oldPath); oldSpec != "" && !isDefault {
if newSpec, _ := commandLine.GetMatchedIncludeSpec(newPath); newSpec == "" {
elements := property.Initializer.Elements()
if len(elements) > 0 {
changeTracker.InsertNodeAfter(
configFile,
elements[len(elements)-1],
changeTracker.NodeFactory.NewStringLiteral(relativePathFromDirectory(configDir, newPath, l.UseCaseSensitiveFileNames()), ast.TokenFlagsNone),
)
}
}
}
case "compilerOptions":
if !ast.IsObjectLiteralExpression(property.Initializer) {
return
}
forEachObjectProperty(property.Initializer.AsObjectLiteralExpression(), func(property *ast.PropertyAssignment, propertyName string) {
option := tsoptions.CommandLineCompilerOptionsMap.Get(propertyName)
if option != nil {
elementOption := option.Elements()
if option.IsFilePath || (option.Kind == tsoptions.CommandLineOptionTypeList && elementOption != nil && elementOption.IsFilePath) {
updatePathsProperty(configFile, configDir, property, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames())
return
}
}
if propertyName != "paths" || !ast.IsObjectLiteralExpression(property.Initializer) {
return
}
forEachObjectProperty(property.Initializer.AsObjectLiteralExpression(), func(pathsProperty *ast.PropertyAssignment, _ string) {
if !ast.IsArrayLiteralExpression(pathsProperty.Initializer) {
return
}
for _, element := range pathsProperty.Initializer.Elements() {
tryUpdateConfigString(configFile, configDir, element, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames())
}
})
})
}
})
}
func updatePathsProperty(configFile *ast.SourceFile, configDir string, property *ast.PropertyAssignment, changeTracker *change.Tracker, oldToNew pathUpdater, converters *lsconv.Converters, useCaseSensitiveFileNames bool) bool {
elements := []*ast.Node{property.Initializer}
if ast.IsArrayLiteralExpression(property.Initializer) {
elements = property.Initializer.Elements()
}
foundExactMatch := false
for _, element := range elements {
foundExactMatch = tryUpdateConfigString(configFile, configDir, element, changeTracker, oldToNew, converters, useCaseSensitiveFileNames) || foundExactMatch
}
return foundExactMatch
}
func tryUpdateConfigString(configFile *ast.SourceFile, configDir string, element *ast.Node, changeTracker *change.Tracker, oldToNew pathUpdater, converters *lsconv.Converters, useCaseSensitiveFileNames bool) bool {
if !ast.IsStringLiteral(element) {
return false
}
elementFileName := tspath.NormalizePath(tspath.CombinePaths(configDir, element.Text()))
updated, ok := oldToNew(elementFileName)
if !ok {
return false
}
changeTracker.ReplaceRangeWithText(configFile, lsproto.Range{
Start: converters.PositionToLineAndCharacter(configFile, core.TextPos(scanner.GetTokenPosOfNode(element, configFile, false)+1)),
End: converters.PositionToLineAndCharacter(configFile, core.TextPos(element.End()-1)),
}, relativePathFromDirectory(configDir, updated, useCaseSensitiveFileNames))
return true
}
func (l *LanguageService) updateRelativePath(oldToNew pathUpdater, oldImportFromPath, newImportFromPath, relativeSpecifier string) string {
oldAbsolute := tspath.NormalizePath(tspath.CombinePaths(tspath.GetDirectoryPath(oldImportFromPath), relativeSpecifier))
newAbsolute, ok := oldToNew(oldAbsolute)
if !ok {
newAbsolute = oldAbsolute
}
return relativeImportPathFromDirectory(tspath.GetDirectoryPath(newImportFromPath), newAbsolute, l.UseCaseSensitiveFileNames())
}
func (l *LanguageService) updateImportsForFileRename(program *compiler.Program, changeTracker *change.Tracker, oldToNew pathUpdater) {
allFiles := program.GetSourceFiles()
checker, done := program.GetTypeChecker(context.Background())
defer done()
moduleSpecifierPreferences := l.UserPreferences().ModuleSpecifierPreferences()
for _, sourceFile := range allFiles {
oldFileName := sourceFile.FileName()
newFromOld, fileMoved := oldToNew(sourceFile.FileName())
newImportFromPath := sourceFile.FileName()
if fileMoved {
newImportFromPath = newFromOld
}
for _, ref := range sourceFile.ReferencedFiles {
if !tspath.IsExternalModuleNameRelative(ref.FileName) {
continue
}
updated := l.updateRelativePath(oldToNew, oldFileName, newImportFromPath, ref.FileName)
if updated != ref.FileName {
changeTracker.ReplaceRangeWithText(sourceFile, l.converters.ToLSPRange(sourceFile, ref.TextRange), updated)
}
}
for _, importStringLiteral := range sourceFile.Imports() {
updated := l.getUpdatedImportSpecifier(program, checker, sourceFile, importStringLiteral, oldToNew, newImportFromPath, fileMoved, moduleSpecifierPreferences)
if updated != "" && updated != importStringLiteral.Text() {
changeTracker.ReplaceRangeWithText(sourceFile, l.converters.ToLSPRange(sourceFile, createStringTextRange(sourceFile, importStringLiteral)), updated)
}
}
}
}
// We assume the source file did not move to a different program.
func (l *LanguageService) getUpdatedImportSpecifier(
program *compiler.Program,
checker *checker.Checker,
sourceFile *ast.SourceFile, // old importing source file
importLiteral *ast.StringLiteralLike,
oldToNew pathUpdater,
newImportFromPath string,
importingSourceFileMoved bool,
userPreferences modulespecifiers.UserPreferences,
) string {
importedModuleSymbol := checker.GetSymbolAtLocation(importLiteral)
if isAmbientModuleSymbol(importedModuleSymbol) {
return ""
}
target := getSourceFileToImport(program, sourceFile, importLiteral, oldToNew)
if target == nil {
// First fall back: try every file in the program to see if any of them would match the import specifier, and if so, obtain the updated specifier for that file.
if updated := getUpdatedImportSpecifierFromMovedSourceFiles(program, sourceFile, importLiteral, oldToNew, newImportFromPath, userPreferences); updated != "" && updated != importLiteral.Text() {
return updated
}
// Fall back to a regular path update for unresolved module.
if tspath.IsExternalModuleNameRelative(importLiteral.Text()) {
return l.updateRelativePath(oldToNew, sourceFile.FileName(), newImportFromPath, importLiteral.Text())
}
return ""
}
// Optimization: neither the importing or imported file changed.
if !target.updated && !(importingSourceFileMoved && tspath.IsExternalModuleNameRelative(importLiteral.Text())) {
return ""
}
updated := modulespecifiers.UpdateModuleSpecifier(
program.Options(),
program,
sourceFile,
newImportFromPath,
importLiteral.Text(),
target.newFileName,
userPreferences,
modulespecifiers.ModuleSpecifierOptions{
OverrideImportMode: program.GetModeForUsageLocation(sourceFile, importLiteral),
},
)
return updated
}
func getSourceFileToImport(
program *compiler.Program,
sourceFile *ast.SourceFile,
importLiteral *ast.StringLiteralLike,
oldToNew pathUpdater,
) *toImport {
if resolved := program.GetResolvedModuleFromModuleSpecifier(sourceFile, importLiteral); resolved != nil && resolved.ResolvedFileName != "" {
oldFileName := resolved.ResolvedFileName
if newFileName, ok := oldToNew(oldFileName); ok {
return &toImport{newFileName: newFileName, updated: true}
}
return &toImport{newFileName: oldFileName, updated: false}
}
return nil
}
// As a fall back for unresolved modules, we'll check all files in the program to see if any of them would match
// the import specifier, and if so, we'll obtain the updated specifier for that file.
func getUpdatedImportSpecifierFromMovedSourceFiles(program *compiler.Program, sourceFile *ast.SourceFile, importLiteral *ast.StringLiteralLike, oldToNew pathUpdater, importingSourceFileName string, userPreferences modulespecifiers.UserPreferences) string {
resolutionMode := program.GetModeForUsageLocation(sourceFile, importLiteral)
for _, candidate := range program.GetSourceFiles() {
newFileName, ok := oldToNew(candidate.FileName())
if !ok {
continue
}
oldSpecifier := modulespecifiers.UpdateModuleSpecifier(
program.Options(),
program,
sourceFile,
importingSourceFileName,
importLiteral.Text(),
candidate.FileName(),
userPreferences,
modulespecifiers.ModuleSpecifierOptions{
OverrideImportMode: resolutionMode,
},
)
if oldSpecifier != importLiteral.Text() {
continue
}
return modulespecifiers.UpdateModuleSpecifier(
program.Options(),
program,
sourceFile,
importingSourceFileName,
importLiteral.Text(),
newFileName,
userPreferences,
modulespecifiers.ModuleSpecifierOptions{
OverrideImportMode: resolutionMode,
},
)
}
return ""
}
func createStringTextRange(sourceFile *ast.SourceFile, node *ast.LiteralLikeNode) core.TextRange {
return core.NewTextRange(scanner.GetTokenPosOfNode(node, sourceFile, false)+1, node.End()-1)
}
func getTsConfigObjectLiteralExpression(tsConfigSourceFile *ast.SourceFile) *ast.ObjectLiteralExpression {
if tsConfigSourceFile != nil && tsConfigSourceFile.Statements != nil && len(tsConfigSourceFile.Statements.Nodes) > 0 {
expression := tsConfigSourceFile.Statements.Nodes[0].Expression()
if ast.IsObjectLiteralExpression(expression) {
return expression.AsObjectLiteralExpression()
}
}
return nil
}
func forEachObjectProperty(objectLiteral *ast.ObjectLiteralExpression, cb func(property *ast.PropertyAssignment, propertyName string)) {
if objectLiteral == nil {
return
}
for _, property := range objectLiteral.Properties.Nodes {
if !ast.IsPropertyAssignment(property) {
continue
}
if name, ok := ast.TryGetTextOfPropertyName(property.Name()); ok {
cb(property.AsPropertyAssignment(), name)
}
}
}
func relativePathFromDirectory(fromDirectory string, to string, useCaseSensitiveFileNames bool) string {
return tspath.GetRelativePathFromDirectory(fromDirectory, to, tspath.ComparePathsOptions{UseCaseSensitiveFileNames: useCaseSensitiveFileNames})
}
func relativeImportPathFromDirectory(fromDirectory string, to string, useCaseSensitiveFileNames bool) string {
return tspath.EnsurePathIsNonModuleName(relativePathFromDirectory(fromDirectory, to, useCaseSensitiveFileNames))
}
func isAmbientModuleSymbol(symbol *ast.Symbol) bool {
if symbol == nil {
return false
}
return slices.ContainsFunc(symbol.Declarations, ast.IsModuleWithStringLiteralName)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,568 @@
package ls
import (
"cmp"
"context"
"slices"
"strings"
"unicode"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/debug"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/printer"
"github.com/microsoft/typescript-go/internal/scanner"
)
func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.FoldingRangeResponse, error) {
_, sourceFile := l.getProgramAndFile(documentURI)
res := l.addNodeOutliningSpans(ctx, sourceFile)
res = append(res, l.addRegionOutliningSpans(ctx, sourceFile)...)
if lsproto.GetClientCapabilities(ctx).TextDocument.FoldingRange.LineFoldingOnly {
res = l.adjustFoldingEnd(res, sourceFile)
}
slices.SortFunc(res, func(a, b *lsproto.FoldingRange) int {
if c := cmp.Compare(a.StartLine, b.StartLine); c != 0 {
return c
}
return cmp.Compare(*a.StartCharacter, *b.StartCharacter)
})
return lsproto.FoldingRangesOrNull{FoldingRanges: &res}, nil
}
// adjustFoldingEnd adjusts the end line of folding ranges when the client signals lineFoldingOnly.
// This mirrors the behavior of VS Code's built-in TypeScript extension (workaround for vscode#47240).
// When lineFoldingOnly is true, we hide lines from startLine+1 to endLine. And to keep closing
// brackets/braces visible, we subtract 1 from endLine when the range ends with a closing pair character.
func (l *LanguageService) adjustFoldingEnd(ranges []*lsproto.FoldingRange, sourceFile *ast.SourceFile) []*lsproto.FoldingRange {
sourceText := sourceFile.Text()
result := make([]*lsproto.FoldingRange, 0, len(ranges))
for _, r := range ranges {
if r.EndCharacter != nil && *r.EndCharacter > 0 {
endOffset := int(l.converters.LineAndCharacterToPosition(sourceFile, lsproto.Position{
Line: r.EndLine,
Character: *r.EndCharacter,
}))
if endOffset > 0 && endOffset <= len(sourceText) {
foldEndChar := sourceText[endOffset-1]
if foldEndChar == '}' || foldEndChar == ']' || foldEndChar == ')' || foldEndChar == '`' || foldEndChar == '>' {
if r.EndLine > r.StartLine {
r.EndLine--
}
}
}
}
result = append(result, r)
}
return result
}
func (l *LanguageService) addNodeOutliningSpans(ctx context.Context, sourceFile *ast.SourceFile) []*lsproto.FoldingRange {
depthRemaining := 40
current := 0
statements := sourceFile.Statements
n := len(statements.Nodes)
foldingRange := make([]*lsproto.FoldingRange, 0, 40)
for current < n {
for current < n && !ast.IsAnyImportSyntax(statements.Nodes[current]) {
foldingRange = append(foldingRange, visitNode(ctx, statements.Nodes[current], depthRemaining, sourceFile, l)...)
current++
}
if current == n {
break
}
firstImport := current
for current < n && ast.IsAnyImportSyntax(statements.Nodes[current]) {
foldingRange = append(foldingRange, visitNode(ctx, statements.Nodes[current], depthRemaining, sourceFile, l)...)
current++
}
lastImport := current - 1
if lastImport != firstImport {
foldingRangeKind := lsproto.FoldingRangeKindImports
foldingRange = append(foldingRange, createFoldingRangeFromBounds(
ctx,
astnav.GetStartOfNode(astnav.FindChildOfKind(statements.Nodes[firstImport],
ast.KindImportKeyword, sourceFile), sourceFile, false /*includeJSDoc*/),
statements.Nodes[lastImport].End(),
foldingRangeKind,
sourceFile,
l,
))
}
}
// Visit the EOF Token so that comments which aren't attached to statements are included.
foldingRange = append(foldingRange, visitNode(ctx, sourceFile.EndOfFileToken, depthRemaining, sourceFile, l)...)
return foldingRange
}
func (l *LanguageService) addRegionOutliningSpans(ctx context.Context, sourceFile *ast.SourceFile) []*lsproto.FoldingRange {
regions := make([]*lsproto.FoldingRange, 0, 40)
out := make([]*lsproto.FoldingRange, 0, 40)
lineStarts := scanner.GetECMALineStarts(sourceFile)
for _, currentLineStart := range lineStarts {
lineEnd := getLineEndOfPosition(sourceFile, int(currentLineStart))
lineText := sourceFile.Text()[currentLineStart:lineEnd]
result := parseRegionDelimiter(lineText)
if result == nil || isInComment(sourceFile, int(currentLineStart), astnav.GetTokenAtPosition(sourceFile, int(currentLineStart))) != nil {
continue
}
if result.isStart {
commentStart := l.createLspPosition(strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//")+int(currentLineStart), sourceFile)
foldingRangeKindRegion := lsproto.FoldingRangeKindRegion
region := &lsproto.FoldingRange{
StartLine: commentStart.Line,
StartCharacter: &commentStart.Character,
Kind: &foldingRangeKindRegion,
}
if supportsCollapsedText(ctx) {
collapsedText := "#region"
if result.name != "" {
collapsedText = result.name
}
region.CollapsedText = &collapsedText
}
// Our spans start out with some initial data.
// On every `#endregion`, we'll come back to these `FoldingRange`s
// and fill in their EndLine/EndCharacter.
regions = append(regions, region)
} else {
if len(regions) > 0 {
region := regions[len(regions)-1]
regions = regions[:len(regions)-1]
endingPosition := l.createLspPosition(lineEnd, sourceFile)
region.EndLine = endingPosition.Line
region.EndCharacter = &endingPosition.Character
out = append(out, region)
}
}
}
return out
}
func visitNode(ctx context.Context, n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange {
if n.Flags&ast.NodeFlagsReparsed != 0 || depthRemaining == 0 || ctx.Err() != nil {
return nil
}
foldingRange := make([]*lsproto.FoldingRange, 0, 40)
if (!ast.IsBinaryExpression(n) && ast.IsDeclaration(n)) || ast.IsVariableStatement(n) || ast.IsReturnStatement(n) || ast.IsCallOrNewExpression(n) || n.Kind == ast.KindEndOfFile {
foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(ctx, n, sourceFile, l)...)
}
if ast.IsFunctionLike(n) && n.Parent != nil && ast.IsBinaryExpression(n.Parent) && n.Parent.AsBinaryExpression().Left != nil && ast.IsPropertyAccessExpression(n.Parent.AsBinaryExpression().Left) {
foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(ctx, n.Parent.AsBinaryExpression().Left, sourceFile, l)...)
}
if ast.IsBlock(n) {
statements := n.AsBlock().Statements
if statements != nil {
foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, statements.End(), sourceFile, l)...)
}
}
if ast.IsModuleBlock(n) {
statements := n.AsModuleBlock().Statements
if statements != nil {
foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, statements.End(), sourceFile, l)...)
}
}
if ast.IsClassLike(n) || ast.IsInterfaceDeclaration(n) {
var members *ast.NodeList
if ast.IsClassDeclaration(n) {
members = n.AsClassDeclaration().Members
} else if ast.IsClassExpression(n) {
members = n.AsClassExpression().Members
} else {
members = n.AsInterfaceDeclaration().Members
}
if members != nil {
foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, members.End(), sourceFile, l)...)
}
}
span := getOutliningSpanForNode(ctx, n, sourceFile, l)
if span != nil {
foldingRange = append(foldingRange, span)
}
depthRemaining--
if ast.IsCallExpression(n) {
depthRemaining++
expressionNodes := visitNode(ctx, n.Expression(), depthRemaining, sourceFile, l)
if expressionNodes != nil {
foldingRange = append(foldingRange, expressionNodes...)
}
depthRemaining--
for _, arg := range n.Arguments() {
if arg != nil {
foldingRange = append(foldingRange, visitNode(ctx, arg, depthRemaining, sourceFile, l)...)
}
}
typeArguments := n.TypeArguments()
for _, typeArg := range typeArguments {
if typeArg != nil {
foldingRange = append(foldingRange, visitNode(ctx, typeArg, depthRemaining, sourceFile, l)...)
}
}
} else if ast.IsIfStatement(n) && n.AsIfStatement().ElseStatement != nil && ast.IsIfStatement(n.AsIfStatement().ElseStatement) {
// Consider an 'else if' to be on the same depth as the 'if'.
ifStatement := n.AsIfStatement()
expressionNodes := visitNode(ctx, n.Expression(), depthRemaining, sourceFile, l)
if expressionNodes != nil {
foldingRange = append(foldingRange, expressionNodes...)
}
thenNode := visitNode(ctx, ifStatement.ThenStatement, depthRemaining, sourceFile, l)
if thenNode != nil {
foldingRange = append(foldingRange, thenNode...)
}
depthRemaining++
elseNode := visitNode(ctx, ifStatement.ElseStatement, depthRemaining, sourceFile, l)
if elseNode != nil {
foldingRange = append(foldingRange, elseNode...)
}
depthRemaining--
} else {
visit := func(node *ast.Node) bool {
childNode := visitNode(ctx, node, depthRemaining, sourceFile, l)
if childNode != nil {
foldingRange = append(foldingRange, childNode...)
}
return false
}
n.ForEachChild(visit)
}
depthRemaining++
return foldingRange
}
func addOutliningForLeadingCommentsForNode(ctx context.Context, n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange {
if ast.IsJsxText(n) {
return nil
}
return addOutliningForLeadingCommentsForPos(ctx, n.Pos(), sourceFile, l)
}
func addOutliningForLeadingCommentsForPos(ctx context.Context, pos int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange {
p := &printer.EmitContext{}
foldingRange := make([]*lsproto.FoldingRange, 0, 40)
firstSingleLineCommentStart := -1
lastSingleLineCommentEnd := -1
singleLineCommentCount := 0
foldingRangeKindComment := lsproto.FoldingRangeKindComment
combineAndAddMultipleSingleLineComments := func() *lsproto.FoldingRange {
// Only outline spans of two or more consecutive single line comments
if singleLineCommentCount > 1 {
return createFoldingRangeFromBounds(ctx, firstSingleLineCommentStart, lastSingleLineCommentEnd, foldingRangeKindComment, sourceFile, l)
}
return nil
}
sourceText := sourceFile.Text()
for comment := range scanner.GetLeadingCommentRanges(&printer.NewNodeFactory(p).NodeFactory, sourceText, pos) {
commentPos := comment.Pos()
commentEnd := comment.End()
if ctx.Err() != nil {
return nil
}
switch comment.Kind {
case ast.KindSingleLineCommentTrivia:
// never fold region delimiters into single-line comment regions
commentText := sourceText[commentPos:commentEnd]
if parseRegionDelimiter(commentText) != nil {
comments := combineAndAddMultipleSingleLineComments()
if comments != nil {
foldingRange = append(foldingRange, comments)
}
singleLineCommentCount = 0
break
}
// For single line comments, combine consecutive ones (2 or more) into
// a single span from the start of the first till the end of the last
if singleLineCommentCount == 0 {
firstSingleLineCommentStart = commentPos
}
lastSingleLineCommentEnd = commentEnd
singleLineCommentCount++
break
case ast.KindMultiLineCommentTrivia:
comments := combineAndAddMultipleSingleLineComments()
if comments != nil {
foldingRange = append(foldingRange, comments)
}
foldingRange = append(foldingRange, createFoldingRangeFromBounds(ctx, commentPos, commentEnd, foldingRangeKindComment, sourceFile, l))
singleLineCommentCount = 0
break
default:
debug.AssertNever(comment.Kind)
}
}
addedComments := combineAndAddMultipleSingleLineComments()
if addedComments != nil {
foldingRange = append(foldingRange, addedComments)
}
return foldingRange
}
type regionDelimiterResult struct {
isStart bool
name string
}
func parseRegionDelimiter(lineText string) *regionDelimiterResult {
// We trim the leading whitespace and // without the regex since the
// multiple potential whitespace matches can make for some gnarly backtracking behavior
lineText = strings.TrimLeftFunc(lineText, unicode.IsSpace)
if !strings.HasPrefix(lineText, "//") {
return nil
}
lineText = strings.TrimSpace(lineText[2:])
lineText = strings.TrimSuffix(lineText, "\r")
if !strings.HasPrefix(lineText, "#") {
return nil
}
lineText = lineText[1:]
isStart := true
if strings.HasPrefix(lineText, "end") {
isStart = false
lineText = lineText[3:]
}
if !strings.HasPrefix(lineText, "region") {
return nil
}
lineText = lineText[6:]
return &regionDelimiterResult{
isStart: isStart,
name: strings.TrimSpace(lineText),
}
}
func getOutliningSpanForNode(ctx context.Context, n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
switch n.Kind {
case ast.KindBlock:
if ast.IsFunctionLike(n.Parent) {
return functionSpan(ctx, n.Parent, n, sourceFile, l)
}
// Check if the block is standalone, or 'attached' to some parent statement.
// If the latter, we want to collapse the block, but consider its hint span
// to be the entire span of the parent.
switch n.Parent.Kind {
case ast.KindDoStatement, ast.KindForInStatement, ast.KindForOfStatement, ast.KindForStatement, ast.KindIfStatement, ast.KindWhileStatement, ast.KindWithStatement, ast.KindCatchClause:
return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l)
case ast.KindTryStatement:
// Could be the try-block, or the finally-block.
tryStatement := n.Parent.AsTryStatement()
if tryStatement.TryBlock == n {
return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l)
} else if tryStatement.FinallyBlock == n {
if span := spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l); span != nil {
return span
}
}
fallthrough
default:
// Block was a standalone block. In this case we want to only collapse
// the span of the block, independent of any parent span.
return createFoldingRange(ctx, l.createLspRangeFromNode(n, sourceFile), "", "")
}
case ast.KindModuleBlock:
return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l)
case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration, ast.KindCaseBlock, ast.KindTypeLiteral, ast.KindObjectBindingPattern:
return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l)
case ast.KindTupleType:
return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsTupleTypeNode(n.Parent) /*useFullStart*/, sourceFile, l)
case ast.KindCaseClause, ast.KindDefaultClause:
return spanForNodeArray(ctx, n.AsCaseOrDefaultClause().Statements, sourceFile, l)
case ast.KindObjectLiteralExpression:
return spanForNode(ctx, n, ast.KindOpenBraceToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l)
case ast.KindArrayLiteralExpression:
return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l)
case ast.KindJsxElement, ast.KindJsxFragment:
return spanForJSXElement(ctx, n, sourceFile, l)
case ast.KindJsxSelfClosingElement, ast.KindJsxOpeningElement:
return spanForJSXAttributes(ctx, n, sourceFile, l)
case ast.KindTemplateExpression, ast.KindNoSubstitutionTemplateLiteral:
return spanForTemplateLiteral(ctx, n, sourceFile, l)
case ast.KindArrayBindingPattern:
return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsBindingElement(n.Parent) /*useFullStart*/, sourceFile, l)
case ast.KindArrowFunction:
return spanForArrowFunction(ctx, n, sourceFile, l)
case ast.KindCallExpression:
return spanForCallExpression(ctx, n, sourceFile, l)
case ast.KindParenthesizedExpression:
return spanForParenthesizedExpression(ctx, n, sourceFile, l)
case ast.KindNamedImports, ast.KindNamedExports, ast.KindImportAttributes:
return spanForImportExportElements(ctx, n, sourceFile, l)
}
return nil
}
func spanForImportExportElements(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
var elements *ast.NodeList
switch node.Kind {
case ast.KindNamedImports:
elements = node.AsNamedImports().Elements
case ast.KindNamedExports:
elements = node.AsNamedExports().Elements
case ast.KindImportAttributes:
elements = node.AsImportAttributes().Attributes
}
if elements == nil || len(elements.Nodes) == 0 {
return nil
}
openToken := astnav.FindChildOfKind(node, ast.KindOpenBraceToken, sourceFile)
closeToken := astnav.FindChildOfKind(node, ast.KindCloseBraceToken, sourceFile)
if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) {
return nil
}
return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, false /*useFullStart*/, l)
}
func spanForParenthesizedExpression(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
start := astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/)
if printer.PositionsAreOnSameLine(start, node.End(), sourceFile) {
return nil
}
textRange := l.createLspRangeFromBounds(start, node.End(), sourceFile)
return createFoldingRange(ctx, textRange, "", "")
}
func spanForCallExpression(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
if node.AsCallExpression().Arguments == nil || len(node.AsCallExpression().Arguments.Nodes) == 0 {
return nil
}
openToken := astnav.FindChildOfKind(node, ast.KindOpenParenToken, sourceFile)
closeToken := astnav.FindChildOfKind(node, ast.KindCloseParenToken, sourceFile)
if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) {
return nil
}
return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, true /*useFullStart*/, l)
}
func spanForArrowFunction(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
arrowFunctionNode := node.AsArrowFunction()
if ast.IsBlock(arrowFunctionNode.Body) || ast.IsParenthesizedExpression(arrowFunctionNode.Body) || printer.PositionsAreOnSameLine(arrowFunctionNode.Body.Pos(), arrowFunctionNode.Body.End(), sourceFile) {
return nil
}
textRange := l.createLspRangeFromBounds(arrowFunctionNode.Body.Pos(), arrowFunctionNode.Body.End(), sourceFile)
return createFoldingRange(ctx, textRange, "", "")
}
func spanForTemplateLiteral(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
if node.Kind == ast.KindNoSubstitutionTemplateLiteral && len(node.Text()) == 0 {
return nil
}
return createFoldingRangeFromBounds(ctx, astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l)
}
func spanForJSXElement(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
if node.Kind == ast.KindJsxElement {
jsxElement := node.AsJsxElement()
textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxElement.OpeningElement, sourceFile, false /*includeJSDoc*/), jsxElement.ClosingElement.End(), sourceFile)
tagName := scanner.GetTextOfNode(jsxElement.OpeningElement.TagName())
bannerText := "<" + tagName + ">...</" + tagName + ">"
return createFoldingRange(ctx, textRange, "", bannerText)
}
// JsxFragment
jsxFragment := node.AsJsxFragment()
textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxFragment.OpeningFragment, sourceFile, false /*includeJSDoc*/), jsxFragment.ClosingFragment.End(), sourceFile)
return createFoldingRange(ctx, textRange, "", "<>...</>")
}
func spanForJSXAttributes(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
var attributes *ast.JsxAttributesNode
if node.Kind == ast.KindJsxSelfClosingElement {
attributes = node.AsJsxSelfClosingElement().Attributes
} else {
attributes = node.AsJsxOpeningElement().Attributes
}
if len(attributes.Properties()) == 0 {
return nil
}
return createFoldingRangeFromBounds(ctx, astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l)
}
func spanForNodeArray(ctx context.Context, statements *ast.NodeList, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
if statements != nil && len(statements.Nodes) != 0 {
return createFoldingRange(ctx, l.createLspRangeFromBounds(statements.Pos(), statements.End(), sourceFile), "", "")
}
return nil
}
func spanForNode(ctx context.Context, node *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
closeBrace := ast.KindCloseBraceToken
if open != ast.KindOpenBraceToken {
closeBrace = ast.KindCloseBracketToken
}
openToken := astnav.FindChildOfKind(node, open, sourceFile)
closeToken := astnav.FindChildOfKind(node, closeBrace, sourceFile)
if openToken != nil && closeToken != nil {
return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, useFullStart, l)
}
return nil
}
func rangeBetweenTokens(ctx context.Context, openToken *ast.Node, closeToken *ast.Node, sourceFile *ast.SourceFile, useFullStart bool, l *LanguageService) *lsproto.FoldingRange {
var textRange lsproto.Range
if useFullStart {
textRange = l.createLspRangeFromBounds(openToken.Pos(), closeToken.End(), sourceFile)
} else {
textRange = l.createLspRangeFromBounds(astnav.GetStartOfNode(openToken, sourceFile, false /*includeJSDoc*/), closeToken.End(), sourceFile)
}
return createFoldingRange(ctx, textRange, "", "")
}
func supportsCollapsedText(ctx context.Context) bool {
return lsproto.GetClientCapabilities(ctx).TextDocument.FoldingRange.FoldingRange.CollapsedText
}
func createFoldingRange(ctx context.Context, textRange lsproto.Range, foldingRangeKind lsproto.FoldingRangeKind, collapsedText string) *lsproto.FoldingRange {
var kind *lsproto.FoldingRangeKind
if foldingRangeKind != "" {
kind = &foldingRangeKind
}
result := &lsproto.FoldingRange{
StartLine: textRange.Start.Line,
StartCharacter: &textRange.Start.Character,
EndLine: textRange.End.Line,
EndCharacter: &textRange.End.Character,
Kind: kind,
}
if collapsedText != "" && supportsCollapsedText(ctx) {
result.CollapsedText = &collapsedText
}
return result
}
func createFoldingRangeFromBounds(ctx context.Context, pos int, end int, foldingRangeKind lsproto.FoldingRangeKind, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
return createFoldingRange(ctx, l.createLspRangeFromBounds(pos, end, sourceFile), foldingRangeKind, "")
}
func functionSpan(ctx context.Context, node *ast.Node, body *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
openToken := tryGetFunctionOpenToken(node, body, sourceFile)
closeToken := astnav.FindChildOfKind(body, ast.KindCloseBraceToken, sourceFile)
if openToken != nil && closeToken != nil {
return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, true /*useFullStart*/, l)
}
return nil
}
func tryGetFunctionOpenToken(node *ast.SignatureDeclaration, body *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
if isNodeArrayMultiLine(node.Parameters(), sourceFile) {
openParenToken := astnav.FindChildOfKind(node, ast.KindOpenParenToken, sourceFile)
if openParenToken != nil {
return openParenToken
}
}
return astnav.FindChildOfKind(body, ast.KindOpenBraceToken, sourceFile)
}
func isNodeArrayMultiLine(list []*ast.Node, sourceFile *ast.SourceFile) bool {
if len(list) == 0 {
return false
}
return !printer.PositionsAreOnSameLine(list[0].Pos(), list[len(list)-1].End(), sourceFile)
}

View File

@@ -0,0 +1,181 @@
package ls
import (
"context"
"iter"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/format"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
)
func (l *LanguageService) toLSProtoTextEdits(file *ast.SourceFile, changes []core.TextChange) []*lsproto.TextEdit {
result := make([]*lsproto.TextEdit, 0, len(changes))
for _, c := range changes {
result = append(result, &lsproto.TextEdit{
NewText: c.NewText,
Range: l.createLspRangeFromBounds(c.Pos(), c.End(), file),
})
}
return result
}
func (l *LanguageService) ProvideFormatDocument(
ctx context.Context,
documentURI lsproto.DocumentUri,
options *lsproto.FormattingOptions,
) (lsproto.DocumentFormattingResponse, error) {
if l.UserPreferences().EnableFormatting.IsFalse() {
return lsproto.TextEditsOrNull{}, nil
}
_, file := l.getProgramAndFile(documentURI)
formatOpts := lsutil.FromLSFormatOptions(l.FormatOptions(), options)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForDocument(
ctx,
file,
formatOpts,
))
return lsproto.TextEditsOrNull{TextEdits: &edits}, nil
}
func (l *LanguageService) ProvideFormatDocumentRange(
ctx context.Context,
documentURI lsproto.DocumentUri,
options *lsproto.FormattingOptions,
r lsproto.Range,
) (lsproto.DocumentRangeFormattingResponse, error) {
if l.UserPreferences().EnableFormatting.IsFalse() {
return lsproto.TextEditsOrNull{}, nil
}
_, file := l.getProgramAndFile(documentURI)
formatOpts := lsutil.FromLSFormatOptions(l.FormatOptions(), options)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForRange(
ctx,
file,
formatOpts,
l.converters.FromLSPRange(file, r),
))
return lsproto.TextEditsOrNull{TextEdits: &edits}, nil
}
func (l *LanguageService) ProvideFormatDocumentOnType(
ctx context.Context,
documentURI lsproto.DocumentUri,
options *lsproto.FormattingOptions,
position lsproto.Position,
character string,
) (lsproto.DocumentOnTypeFormattingResponse, error) {
if l.UserPreferences().EnableFormatting.IsFalse() {
return lsproto.TextEditsOrNull{}, nil
}
_, file := l.getProgramAndFile(documentURI)
formatOpts := lsutil.FromLSFormatOptions(l.FormatOptions(), options)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsAfterKeystroke(
ctx,
file,
formatOpts,
int(l.converters.LineAndCharacterToPosition(file, position)),
character,
))
return lsproto.TextEditsOrNull{TextEdits: &edits}, nil
}
func (l *LanguageService) getFormattingEditsForRange(
ctx context.Context,
file *ast.SourceFile,
options lsutil.FormatCodeSettings,
r core.TextRange,
) []core.TextChange {
ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter)
return format.FormatSelection(ctx, file, r.Pos(), r.End())
}
func (l *LanguageService) getFormattingEditsForDocument(
ctx context.Context,
file *ast.SourceFile,
options lsutil.FormatCodeSettings,
) []core.TextChange {
ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter)
return format.FormatDocument(ctx, file)
}
func (l *LanguageService) getFormattingEditsAfterKeystroke(
ctx context.Context,
file *ast.SourceFile,
options lsutil.FormatCodeSettings,
position int,
key string,
) []core.TextChange {
ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter)
tokenAtPosition := astnav.GetTokenAtPosition(file, position)
if isInComment(file, position, tokenAtPosition) == nil {
switch key {
case "{":
return format.FormatOnOpeningCurly(ctx, file, position)
case "}":
return format.FormatOnClosingCurly(ctx, file, position)
case ";":
return format.FormatOnSemicolon(ctx, file, position)
case "\n":
return format.FormatOnEnter(ctx, file, position)
default:
return nil
}
}
return nil
}
// Unlike the TS implementation, this function *will not* compute default values for
// `precedingToken` and `tokenAtPosition`.
// It is the caller's responsibility to call `astnav.GetTokenAtPosition` to compute a default `tokenAtPosition`,
// or `astnav.FindPrecedingToken` to compute a default `precedingToken`.
func getRangeOfEnclosingComment(
file *ast.SourceFile,
position int,
precedingToken *ast.Node,
tokenAtPosition *ast.Node,
) *ast.CommentRange {
jsdoc := ast.FindAncestor(tokenAtPosition, (*ast.Node).IsJSDoc)
if jsdoc != nil {
tokenAtPosition = jsdoc.Parent
}
tokenStart := astnav.GetStartOfNode(tokenAtPosition, file, false /*includeJSDoc*/)
if tokenStart <= position && position < tokenAtPosition.End() {
return nil
}
// Between two consecutive tokens, all comments are either trailing on the former
// or leading on the latter (and none are in both lists).
var trailingRangesOfPreviousToken iter.Seq[ast.CommentRange]
if precedingToken != nil {
trailingRangesOfPreviousToken = scanner.GetTrailingCommentRanges(&ast.NodeFactory{}, file.Text(), precedingToken.End())
}
leadingRangesOfNextToken := getLeadingCommentRangesOfNode(tokenAtPosition, file)
commentRanges := core.ConcatenateSeq(trailingRangesOfPreviousToken, leadingRangesOfNextToken)
for commentRange := range commentRanges {
// The end marker of a single-line comment does not include the newline character.
// In the following case where the cursor is at `^`, we are inside a comment:
//
// // asdf ^\n
//
// But for closed multi-line comments, we don't want to be inside the comment in the following case:
//
// /* asdf */^
//
// Internally, we represent the end of the comment prior to the newline and at the '/', respectively.
//
// However, unterminated multi-line comments lack a `/`, end at the end of the file, and *do* contain their end.
//
if commentRange.ContainsExclusive(position) ||
position == commentRange.End() &&
(commentRange.Kind == ast.KindSingleLineCommentTrivia || position == len(file.Text())) {
return &commentRange
}
}
return nil
}

View File

@@ -0,0 +1,135 @@
package ls
import (
"context"
"testing"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/parser"
)
// Test for issue: Panic Handling textDocument/onTypeFormatting
// This reproduces the panic when pressing enter in an empty file
func TestGetFormattingEditsAfterKeystroke_EmptyFile(t *testing.T) {
t.Parallel()
// Create an empty file
text := ""
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/index.ts",
Path: "/index.ts",
}, text, core.ScriptKindTS)
// Create language service with nil program (we're only testing the formatting function)
langService := &LanguageService{}
// Test formatting after keystroke with newline character at position 0
ctx := context.Background()
options := lsutil.GetDefaultFormatCodeSettings()
// This should not panic
edits := langService.getFormattingEditsAfterKeystroke(
ctx,
sourceFile,
options,
0, // position
"\n",
)
// Should return nil or empty edits, not panic
_ = edits
}
// Test with a simple statement
func TestGetFormattingEditsAfterKeystroke_SimpleStatement(t *testing.T) {
t.Parallel()
// Create a file with a simple statement
text := "const x = 1"
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/index.ts",
Path: "/index.ts",
}, text, core.ScriptKindTS)
// Create language service with nil program
langService := &LanguageService{}
// Test formatting after keystroke with newline character at end of statement
ctx := context.Background()
options := lsutil.GetDefaultFormatCodeSettings()
// This should not panic
edits := langService.getFormattingEditsAfterKeystroke(
ctx,
sourceFile,
options,
len(text), // position at end of file
"\n",
)
// Should return nil or empty edits, not panic
_ = edits
}
// Test for issue: Crash in range formatting when requested on a line that is different from the containing function
// This reproduces the panic when formatting a range inside a function body
func TestGetFormattingEditsForRange_FunctionBody(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
text string
startPos int
endPos int
}{
{
name: "return statement in function",
text: "function foo() {\n return (1 + 2);\n}",
startPos: 21, // Start of "return"
endPos: 38, // End of ");"
},
{
name: "function with newline after keyword",
text: "function\nf() {\n}",
startPos: 9, // After "function\n"
endPos: 13, // Inside or after function
},
{
name: "empty function body",
text: "function f() {\n \n}",
startPos: 15, // Inside body
endPos: 17, // Inside body
},
{
name: "after function closing brace",
text: "function f() {\n}",
startPos: 15, // After closing brace
endPos: 15,
},
}
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)
langService := &LanguageService{}
ctx := context.Background()
options := lsutil.GetDefaultFormatCodeSettings()
// This should not panic
edits := langService.getFormattingEditsForRange(
ctx,
sourceFile,
options,
core.NewTextRange(tc.startPos, tc.endPos),
)
// Should not panic
_ = edits // Just ensuring no panic
})
}
}

View File

@@ -0,0 +1,25 @@
package ls
import (
"github.com/microsoft/typescript-go/internal/ls/autoimport"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/sourcemap"
)
type Host interface {
UseCaseSensitiveFileNames() bool
ReadFile(path string) (contents string, ok bool)
Converters() *lsconv.Converters
GetPreferences(activeFile string) lsutil.UserPreferences
GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo
AutoImportRegistry() *autoimport.Registry
// Used for module specifier completions.
// ! Do not use for anything else, as this violates the principle that
// the host is a snapshot-in-time.
ReadDirectory(currentDir string, path string, extensions []string, excludes []string, includes []string, depth int) []string
GetDirectories(path string) []string
DirectoryExists(path string) bool
FileExists(path string) bool
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,767 @@
package ls
import (
"context"
"slices"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/debug"
)
type ImpExpKind int32
const (
ImpExpKindUnknown ImpExpKind = iota
ImpExpKindImport
ImpExpKindExport
)
type ImportExportSymbol struct {
kind ImpExpKind
symbol *ast.Symbol
exportInfo *ExportInfo
}
type ExportKind int
const (
ExportKindNamed ExportKind = 0
ExportKindDefault ExportKind = 1
ExportKindExportEquals ExportKind = 2
ExportKindUMD ExportKind = 3
ExportKindModule ExportKind = 4
)
type ExportInfo struct {
exportingModuleSymbol *ast.Symbol
exportKind ExportKind
}
type LocationAndSymbol struct {
importLocation *ast.Node
importSymbol *ast.Symbol
}
type ImportsResult struct {
importSearches []LocationAndSymbol
singleReferences []*ast.Node
indirectUsers []*ast.SourceFile
}
type ImportTracker func(exportSymbol *ast.Symbol, exportInfo *ExportInfo, isForRename bool) *ImportsResult
type ModuleReferenceKind int32
const (
ModuleReferenceKindImport ModuleReferenceKind = iota
ModuleReferenceKindReference
ModuleReferenceKindImplicit
)
// ModuleReference represents a reference to a module, either via import, <reference>, or implicit reference
type ModuleReference struct {
kind ModuleReferenceKind
literal *ast.Node // for import and implicit kinds (StringLiteralLike)
referencingFile *ast.SourceFile
ref *ast.FileReference // for reference kind
}
// Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily.
func createImportTracker(ctx context.Context, program *compiler.Program, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string], checker *checker.Checker) ImportTracker {
allDirectImports := getDirectImportsMap(ctx, program, sourceFiles, checker)
return func(exportSymbol *ast.Symbol, exportInfo *ExportInfo, isForRename bool) *ImportsResult {
directImports, indirectUsers := getImportersForExport(sourceFiles, sourceFilesSet, allDirectImports, exportInfo, checker)
importSearches, singleReferences := getSearchesFromDirectImports(directImports, exportSymbol, exportInfo.exportKind, checker, isForRename)
return &ImportsResult{importSearches, singleReferences, indirectUsers}
}
}
// Returns a map from a module symbol to all import statements that directly reference the module
func getDirectImportsMap(ctx context.Context, program *compiler.Program, sourceFiles []*ast.SourceFile, checker *checker.Checker) map[*ast.Symbol][]*ast.Node {
result := make(map[*ast.Symbol][]*ast.Node)
for _, sourceFile := range sourceFiles {
if ctx.Err() != nil {
return result
}
forEachImport(program, sourceFile, func(importDecl *ast.Node, moduleSpecifier *ast.Node) {
if moduleSymbol := checker.GetSymbolAtLocation(moduleSpecifier); moduleSymbol != nil {
result[moduleSymbol] = append(result[moduleSymbol], importDecl)
}
})
}
return result
}
// Calls `action` for each import, re-export, or require() in a file
func forEachImport(program *compiler.Program, sourceFile *ast.SourceFile, action func(importStatement *ast.Node, imported *ast.Node)) {
var implicitImports []*ast.LiteralLikeNode
_, jsxSpecifier := program.GetJSXRuntimeImportSpecifier(sourceFile.Path())
if jsxSpecifier != nil {
implicitImports = append(implicitImports, jsxSpecifier)
}
importHelpersSpecifier := program.GetImportHelpersImportSpecifier(sourceFile.Path())
if importHelpersSpecifier != nil {
implicitImports = append(implicitImports, importHelpersSpecifier)
}
if sourceFile.ExternalModuleIndicator != nil || len(sourceFile.Imports())+len(implicitImports) != 0 {
for _, i := range sourceFile.Imports() {
action(ast.ImportFromModuleSpecifier(i), i)
}
for _, i := range implicitImports {
action(ast.ImportFromModuleSpecifier(i), i)
}
} else {
forEachPossibleImportOrExportStatement(sourceFile.AsNode(), func(node *ast.Node) bool {
switch node.Kind {
case ast.KindExportDeclaration, ast.KindImportDeclaration, ast.KindJSImportDeclaration:
if specifier := node.ModuleSpecifier(); specifier != nil && ast.IsStringLiteral(specifier) {
action(node, specifier)
}
case ast.KindImportEqualsDeclaration:
if isExternalModuleImportEquals(node) {
action(node, node.AsImportEqualsDeclaration().ModuleReference.Expression())
}
}
return false
})
}
}
func forEachPossibleImportOrExportStatement(sourceFileLike *ast.Node, action func(statement *ast.Node) bool) bool {
for _, statement := range getStatementsOfSourceFileLike(sourceFileLike) {
if action(statement) || isAmbientModuleDeclaration(statement) && forEachPossibleImportOrExportStatement(statement, action) {
return true
}
}
return false
}
func getSourceFileLikeForImportDeclaration(node *ast.Node) *ast.Node {
if ast.IsCallExpression(node) || ast.IsJSDocImportTag(node) {
return ast.GetSourceFileOfNode(node).AsNode()
}
parent := node.Parent
if ast.IsSourceFile(parent) {
return parent
}
debug.Assert(ast.IsModuleBlock(parent) && isAmbientModuleDeclaration(parent.Parent))
return parent.Parent
}
func isAmbientModuleDeclaration(node *ast.Node) bool {
return ast.IsModuleDeclaration(node) && ast.IsStringLiteral(node.Name())
}
func getStatementsOfSourceFileLike(node *ast.Node) []*ast.Node {
if ast.IsSourceFile(node) {
return node.Statements()
}
if body := node.Body(); body != nil {
return body.Statements()
}
return nil
}
func getImportersForExport(
sourceFiles []*ast.SourceFile,
sourceFilesSet *collections.Set[string],
allDirectImports map[*ast.Symbol][]*ast.Node,
exportInfo *ExportInfo,
checker *checker.Checker,
) ([]*ast.Node, []*ast.SourceFile) {
var directImports []*ast.Node
var indirectUserDeclarations []*ast.Node
markSeenDirectImport := nodeSeenTracker()
markSeenIndirectUser := nodeSeenTracker()
isAvailableThroughGlobal := isSourceFileWithGlobalExports(exportInfo.exportingModuleSymbol.ValueDeclaration)
getDirectImports := func(moduleSymbol *ast.Symbol) []*ast.Node {
return allDirectImports[moduleSymbol]
}
// Adds a module and all of its transitive dependencies as possible indirect users
var addIndirectUser func(*ast.Node, bool)
addIndirectUser = func(sourceFileLike *ast.Node, addTransitiveDependencies bool) {
// When isAvailableThroughGlobal, getIndirectUsers already returns all source files,
// so indirectUserDeclarations is never consulted. Nothing to do here.
if isAvailableThroughGlobal {
return
}
if !markSeenIndirectUser(sourceFileLike) {
return
}
indirectUserDeclarations = append(indirectUserDeclarations, sourceFileLike)
if !addTransitiveDependencies {
return
}
moduleSymbol := checker.GetMergedSymbol(sourceFileLike.Symbol())
if moduleSymbol == nil {
return
}
debug.Assert(moduleSymbol.Flags&ast.SymbolFlagsModule != 0)
for _, directImport := range getDirectImports(moduleSymbol) {
if !ast.IsImportTypeNode(directImport) {
addIndirectUser(getSourceFileLikeForImportDeclaration(directImport), true /*addTransitiveDependencies*/)
}
}
}
isExported := func(node *ast.Node, stopAtAmbientModule bool) bool {
for node != nil && !(stopAtAmbientModule && isAmbientModuleDeclaration(node)) {
if ast.HasSyntacticModifier(node, ast.ModifierFlagsExport) {
return true
}
node = node.Parent
}
return false
}
handleImportCall := func(importCall *ast.Node) {
top := ast.FindAncestor(importCall, isAmbientModuleDeclaration)
if top == nil {
top = ast.GetSourceFileOfNode(importCall).AsNode()
}
addIndirectUser(top, isExported(importCall, true /*stopAtAmbientModule*/))
}
handleNamespaceImport := func(importDeclaration *ast.Node, name *ast.Node, isReExport bool, alreadyAddedDirect bool) {
if exportInfo.exportKind == ExportKindExportEquals {
// This is a direct import, not import-as-namespace.
if !alreadyAddedDirect {
directImports = append(directImports, importDeclaration)
}
} else if !isAvailableThroughGlobal {
sourceFileLike := getSourceFileLikeForImportDeclaration(importDeclaration)
debug.Assert(ast.IsSourceFile(sourceFileLike) || ast.IsModuleDeclaration(sourceFileLike))
addIndirectUser(sourceFileLike, isReExport || findNamespaceReExports(sourceFileLike, name, checker))
}
}
var handleDirectImports func(*ast.Symbol)
handleDirectImports = func(exportingModuleSymbol *ast.Symbol) {
theseDirectImports := getDirectImports(exportingModuleSymbol)
for _, direct := range theseDirectImports {
if !markSeenDirectImport(direct) {
continue
}
// !!! cancellation
switch direct.Kind {
case ast.KindCallExpression:
if ast.IsImportCall(direct) {
handleImportCall(direct)
} else if !isAvailableThroughGlobal {
parent := direct.Parent
if exportInfo.exportKind == ExportKindExportEquals && ast.IsVariableDeclaration(parent) {
name := parent.Name()
if ast.IsIdentifier(name) {
directImports = append(directImports, name)
}
}
}
case ast.KindIdentifier:
// Nothing
case ast.KindImportEqualsDeclaration:
handleNamespaceImport(direct, direct.Name(), ast.HasSyntacticModifier(direct, ast.ModifierFlagsExport), false /*alreadyAddedDirect*/)
case ast.KindImportDeclaration, ast.KindJSImportDeclaration, ast.KindJSDocImportTag:
directImports = append(directImports, direct)
if importClause := direct.ImportClause(); importClause != nil {
if namedBindings := importClause.AsImportClause().NamedBindings; namedBindings != nil && ast.IsNamespaceImport(namedBindings) {
handleNamespaceImport(direct, namedBindings.Name(), false /*isReExport*/, true /*alreadyAddedDirect*/)
break
}
}
if !isAvailableThroughGlobal && ast.IsDefaultImport(direct) {
addIndirectUser(getSourceFileLikeForImportDeclaration(direct), false)
// Add a check for indirect uses to handle synthetic default imports
}
case ast.KindExportDeclaration:
exportClause := direct.AsExportDeclaration().ExportClause
if exportClause == nil {
// This is `export * from "foo"`, so imports of this module may import the export too.
handleDirectImports(getContainingModuleSymbol(direct, checker))
} else if ast.IsNamespaceExport(exportClause) {
// `export * as foo from "foo"` add to indirect uses
addIndirectUser(getSourceFileLikeForImportDeclaration(direct), true /*addTransitiveDependencies*/)
} else {
// This is `export { foo } from "foo"` and creates an alias symbol, so recursive search will get handle re-exports.
directImports = append(directImports, direct)
}
case ast.KindImportType:
// Only check for typeof import('xyz')
if !isAvailableThroughGlobal && direct.AsImportTypeNode().IsTypeOf && direct.AsImportTypeNode().Qualifier == nil && isExported(direct, false) {
addIndirectUser(ast.GetSourceFileOfNode(direct).AsNode(), true /*addTransitiveDependencies*/)
}
directImports = append(directImports, direct)
default:
debug.FailBadSyntaxKind(direct, "Unexpected import kind.")
}
}
}
getIndirectUsers := func() []*ast.SourceFile {
if isAvailableThroughGlobal {
// It has `export as namespace`, so anything could potentially use it.
return sourceFiles
}
// Module augmentations may use this module's exports without importing it.
for _, decl := range exportInfo.exportingModuleSymbol.Declarations {
if ast.IsExternalModuleAugmentation(decl) && sourceFilesSet.Has(ast.GetSourceFileOfNode(decl).FileName()) {
addIndirectUser(decl, false)
}
}
// This may return duplicates (if there are multiple module declarations in a single source file, all importing the same thing as a namespace), but `State.markSearchedSymbol` will handle that.
return core.Map(indirectUserDeclarations, ast.GetSourceFileOfNode)
}
handleDirectImports(exportInfo.exportingModuleSymbol)
return directImports, getIndirectUsers()
}
func getContainingModuleSymbol(importer *ast.Node, checker *checker.Checker) *ast.Symbol {
return checker.GetMergedSymbol(getSourceFileLikeForImportDeclaration(importer).Symbol())
}
// Returns 'true' is the namespace 'name' is re-exported from this module, and 'false' if it is only used locally
func findNamespaceReExports(sourceFileLike *ast.Node, name *ast.Node, checker *checker.Checker) bool {
namespaceImportSymbol := checker.GetSymbolAtLocation(name)
return forEachPossibleImportOrExportStatement(sourceFileLike, func(statement *ast.Node) bool {
if !ast.IsExportDeclaration(statement) {
return false
}
exportClause := statement.AsExportDeclaration().ExportClause
moduleSpecifier := statement.ModuleSpecifier()
return moduleSpecifier == nil && exportClause != nil && ast.IsNamedExports(exportClause) && core.Some(exportClause.Elements(), func(element *ast.Node) bool {
return checker.GetExportSpecifierLocalTargetSymbol(element) == namespaceImportSymbol
})
})
}
func getSearchesFromDirectImports(
directImports []*ast.Node,
exportSymbol *ast.Symbol,
exportKind ExportKind,
checker *checker.Checker,
isForRename bool,
) ([]LocationAndSymbol, []*ast.Node) {
var importSearches []LocationAndSymbol
var singleReferences []*ast.Node
addSearch := func(location *ast.Node, symbol *ast.Symbol) {
importSearches = append(importSearches, LocationAndSymbol{location, symbol})
}
isNameMatch := func(name string) bool {
// Use name of "default" even in `export =` case because we may have allowSyntheticDefaultImports
return name == exportSymbol.Name || exportKind != ExportKindNamed && name == ast.InternalSymbolNameDefault
}
// `import x = require("./x")` or `import * as x from "./x"`.
// An `export =` may be imported by this syntax, so it may be a direct import.
// If it's not a direct import, it will be in `indirectUsers`, so we don't have to do anything here.
handleNamespaceImportLike := func(importName *ast.Node) {
// Don't rename an import that already has a different name than the export.
if exportKind == ExportKindExportEquals && (!isForRename || isNameMatch(importName.Text())) {
addSearch(importName, checker.GetSymbolAtLocation(importName))
}
}
searchForNamedImport := func(namedBindings *ast.Node) {
if namedBindings == nil {
return
}
for _, element := range namedBindings.Elements() {
name := element.Name()
propertyName := element.PropertyName()
if !isNameMatch(core.OrElse(propertyName, name).Text()) {
continue
}
if propertyName != nil {
// This is `import { foo as bar } from "./a"` or `export { foo as bar } from "./a"`. `foo` isn't a local in the file, so just add it as a single reference.
singleReferences = append(singleReferences, propertyName)
// If renaming `{ foo as bar }`, don't touch `bar`, just `foo`.
// But do rename `foo` in ` { default as foo }` if that's the original export name.
if !isForRename || name.Text() == exportSymbol.Name {
// Search locally for `bar`.
addSearch(name, checker.GetSymbolAtLocation(name))
}
} else {
var localSymbol *ast.Symbol
if ast.IsExportSpecifier(element) && element.PropertyName() != nil {
localSymbol = checker.GetExportSpecifierLocalTargetSymbol(element)
} else {
localSymbol = checker.GetSymbolAtLocation(name)
}
addSearch(name, localSymbol)
}
}
}
handleImport := func(decl *ast.Node) {
if ast.IsImportEqualsDeclaration(decl) {
if isExternalModuleImportEquals(decl) {
handleNamespaceImportLike(decl.Name())
}
return
}
if ast.IsIdentifier(decl) {
handleNamespaceImportLike(decl)
return
}
if ast.IsImportTypeNode(decl) {
if qualifier := decl.AsImportTypeNode().Qualifier; qualifier != nil {
firstIdentifier := ast.GetFirstIdentifier(qualifier)
if firstIdentifier.Text() == ast.SymbolName(exportSymbol) {
singleReferences = append(singleReferences, firstIdentifier)
}
} else if exportKind == ExportKindExportEquals {
singleReferences = append(singleReferences, decl.AsImportTypeNode().Argument.AsLiteralTypeNode().Literal)
}
return
}
// Ignore if there's a grammar error
if !ast.IsStringLiteral(decl.ModuleSpecifier()) {
return
}
if ast.IsExportDeclaration(decl) {
if exportClause := decl.AsExportDeclaration().ExportClause; exportClause != nil && ast.IsNamedExports(exportClause) {
searchForNamedImport(exportClause)
}
return
}
if importClause := decl.ImportClause(); importClause != nil {
if namedBindings := importClause.AsImportClause().NamedBindings; namedBindings != nil {
switch namedBindings.Kind {
case ast.KindNamespaceImport:
handleNamespaceImportLike(namedBindings.Name())
case ast.KindNamedImports:
// 'default' might be accessed as a named import `{ default as foo }`.
if exportKind == ExportKindNamed || exportKind == ExportKindDefault {
searchForNamedImport(namedBindings)
}
}
}
// `export =` might be imported by a default import if `--allowSyntheticDefaultImports` is on, so this handles both ExportKind.Default and ExportKind.ExportEquals.
// If a default import has the same name as the default export, allow to rename it.
// Given `import f` and `export default function f`, we will rename both, but for `import g` we will rename just that.
if name := importClause.Name(); name != nil && (exportKind == ExportKindDefault || exportKind == ExportKindExportEquals) && (!isForRename || name.Text() == symbolNameNoDefault(exportSymbol)) {
defaultImportAlias := checker.GetSymbolAtLocation(name)
addSearch(name, defaultImportAlias)
}
}
}
for _, decl := range directImports {
handleImport(decl)
}
return importSearches, singleReferences
}
func getImportOrExportSymbol(node *ast.Node, symbol *ast.Symbol, checker *checker.Checker, comingFromExport bool) *ImportExportSymbol {
exportInfo := func(symbol *ast.Symbol, kind ExportKind) *ImportExportSymbol {
if exportInfo := getExportInfo(symbol, kind, checker); exportInfo != nil {
return &ImportExportSymbol{
kind: ImpExpKindExport,
symbol: symbol,
exportInfo: exportInfo,
}
}
return nil
}
getExport := func() *ImportExportSymbol {
getExportAssignmentExport := func(ex *ast.Node) *ImportExportSymbol {
// Get the symbol for the `export =` node; its parent is the module it's the export of.
if ex.Symbol().Parent == nil {
return nil
}
exportKind := core.IfElse(ex.AsExportAssignment().IsExportEquals, ExportKindExportEquals, ExportKindDefault)
return &ImportExportSymbol{
kind: ImpExpKindExport,
symbol: symbol,
exportInfo: &ExportInfo{
exportingModuleSymbol: ex.Symbol().Parent,
exportKind: exportKind,
},
}
}
// Not meant for use with export specifiers or export assignment.
getExportKindForDeclaration := func(node *ast.Node) ExportKind {
if ast.HasSyntacticModifier(node, ast.ModifierFlagsDefault) {
return ExportKindDefault
}
return ExportKindNamed
}
getSpecialPropertyExport := func(node *ast.Node, useLhsSymbol bool) *ImportExportSymbol {
var kind ExportKind
switch ast.GetAssignmentDeclarationKind(node) {
case ast.JSDeclarationKindExportsProperty:
kind = ExportKindNamed
case ast.JSDeclarationKindModuleExports:
kind = ExportKindExportEquals
default:
return nil
}
sym := symbol
if useLhsSymbol {
sym = node.Symbol()
}
if sym == nil {
return nil
}
return exportInfo(sym, kind)
}
parent := node.Parent
grandparent := parent.Parent
if symbol.ExportSymbol != nil {
if ast.IsPropertyAccessExpression(parent) {
// When accessing an export of a JS module, there's no alias. The symbol will still be flagged as an export even though we're at the use.
// So check that we are at the declaration.
if ast.IsBinaryExpression(grandparent) && slices.Contains(symbol.Declarations, parent) {
return getSpecialPropertyExport(grandparent, false /*useLhsSymbol*/)
}
return nil
}
return exportInfo(symbol.ExportSymbol, getExportKindForDeclaration(parent))
} else {
exportNode := getExportNode(parent, node)
switch {
case exportNode != nil && (ast.HasSyntacticModifier(exportNode, ast.ModifierFlagsExport) || ast.IsImplicitlyExportedJSDocDeclaration(exportNode)):
if ast.IsImportEqualsDeclaration(exportNode) && exportNode.AsImportEqualsDeclaration().ModuleReference == node {
// We're at `Y` in `export import X = Y`. This is not the exported symbol, the left-hand-side is. So treat this as an import statement.
if comingFromExport {
return nil
}
lhsSymbol := checker.GetSymbolAtLocation(exportNode.Name())
return &ImportExportSymbol{
kind: ImpExpKindImport,
symbol: lhsSymbol,
}
}
return exportInfo(symbol, getExportKindForDeclaration(exportNode))
case ast.IsNamespaceExport(parent):
return exportInfo(symbol, ExportKindNamed)
case ast.IsExportAssignment(parent):
return getExportAssignmentExport(parent)
case ast.IsExportAssignment(grandparent):
return getExportAssignmentExport(grandparent)
case ast.IsBinaryExpression(parent):
return getSpecialPropertyExport(parent, true /*useLhsSymbol*/)
case ast.IsBinaryExpression(grandparent):
return getSpecialPropertyExport(grandparent, true /*useLhsSymbol*/)
case ast.IsJSDocTypedefTag(parent) || ast.IsJSDocCallbackTag(parent):
return exportInfo(symbol, ExportKindNamed)
}
}
return nil
}
getImport := func() *ImportExportSymbol {
if !isNodeImport(node) {
return nil
}
// JS destructuring from `require(...)` is import-like for references, but the binding element
// itself is still a local variable symbol rather than an alias.
var importedSymbol *ast.Symbol
if symbol.Flags&ast.SymbolFlagsAlias != 0 {
importedSymbol = checker.GetImmediateAliasedSymbol(symbol)
} else {
importedSymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker)
}
if importedSymbol == nil {
return nil
}
// Search on the local symbol in the exporting module, not the exported symbol.
importedSymbol = skipExportSpecifierSymbol(importedSymbol, checker)
if importedSymbol == nil {
return nil
}
// Similarly, skip past the symbol for 'export ='
if importedSymbol.Name == "export=" {
importedSymbol = getExportEqualsLocalSymbol(importedSymbol, checker)
if importedSymbol == nil {
return nil
}
}
// If the import has a different name than the export, do not continue searching.
// If `importedName` is undefined, do continue searching as the export is anonymous.
// (All imports returned from this function will be ignored anyway if we are in rename and this is a not a named export.)
importedName := symbolNameNoDefault(importedSymbol)
if importedName == "" || importedName == ast.InternalSymbolNameDefault || importedName == symbol.Name {
return &ImportExportSymbol{
kind: ImpExpKindImport,
symbol: importedSymbol,
}
}
return nil
}
result := getExport()
if result == nil && !comingFromExport {
result = getImport()
}
return result
}
func getExportInfo(exportSymbol *ast.Symbol, exportKind ExportKind, c *checker.Checker) *ExportInfo {
// Parent can be nil if an `export` is not at the top-level (which is a compile error).
if exportSymbol.Parent != nil {
exportingModuleSymbol := c.GetMergedSymbol(exportSymbol.Parent)
// `export` may appear in a namespace. In that case, just rely on global search.
if checker.IsExternalModuleSymbol(exportingModuleSymbol) {
return &ExportInfo{
exportingModuleSymbol: exportingModuleSymbol,
exportKind: exportKind,
}
}
}
return nil
}
// If a reference is a class expression, the exported node would be its parent.
// If a reference is a variable declaration, the exported node would be the variable statement.
func getExportNode(parent *ast.Node, node *ast.Node) *ast.Node {
var declaration *ast.Node
switch {
case ast.IsVariableDeclaration(parent):
declaration = parent
case ast.IsBindingElement(parent):
declaration = ast.WalkUpBindingElementsAndPatterns(parent)
}
if declaration != nil {
if parent.Name() == node && !ast.IsCatchClause(declaration.Parent) && ast.IsVariableStatement(declaration.Parent.Parent) {
return declaration.Parent.Parent
}
return nil
}
return parent
}
func isNodeImport(node *ast.Node) bool {
parent := node.Parent
switch parent.Kind {
case ast.KindImportEqualsDeclaration:
return parent.Name() == node && isExternalModuleImportEquals(parent)
case ast.KindImportSpecifier:
// For a rename import `{ foo as bar }`, don't search for the imported symbol. Just find local uses of `bar`.
return parent.PropertyName() == nil
case ast.KindImportClause, ast.KindNamespaceImport:
debug.Assert(parent.Name() == node)
return true
case ast.KindBindingElement:
return ast.IsInJSFile(node) && ast.IsVariableDeclarationInitializedToBareOrAccessedRequire(parent.Parent.Parent)
}
return false
}
func isExternalModuleImportEquals(node *ast.Node) bool {
moduleReference := node.AsImportEqualsDeclaration().ModuleReference
return ast.IsExternalModuleReference(moduleReference) && moduleReference.Expression().Kind == ast.KindStringLiteral
}
// If at an export specifier, go to the symbol it refers to. */
func skipExportSpecifierSymbol(symbol *ast.Symbol, checker *checker.Checker) *ast.Symbol {
// For `export { foo } from './bar", there's nothing to skip, because it does not create a new alias. But `export { foo } does.
for _, declaration := range symbol.Declarations {
switch {
case ast.IsExportSpecifier(declaration) && declaration.PropertyName() == nil && declaration.Parent.Parent.ModuleSpecifier() == nil:
return core.OrElse(checker.GetExportSpecifierLocalTargetSymbol(declaration), symbol)
case ast.IsPropertyAccessExpression(declaration) && ast.IsModuleExportsAccessExpression(declaration.Expression()) && !ast.IsPrivateIdentifier(declaration.Name()):
// Export of form 'module.exports.propName = expr';
return checker.GetSymbolAtLocation(declaration)
case ast.IsShorthandPropertyAssignment(declaration) && ast.IsBinaryExpression(declaration.Parent.Parent) && ast.GetAssignmentDeclarationKind(declaration.Parent.Parent) == ast.JSDeclarationKindModuleExports:
return checker.GetExportSpecifierLocalTargetSymbol(declaration.Name())
}
}
return symbol
}
func getExportEqualsLocalSymbol(importedSymbol *ast.Symbol, checker *checker.Checker) *ast.Symbol {
if importedSymbol.Flags&ast.SymbolFlagsAlias != 0 {
return checker.GetImmediateAliasedSymbol(importedSymbol)
}
decl := importedSymbol.ValueDeclaration
debug.Assert(decl != nil)
switch {
case ast.IsExportAssignment(decl):
return decl.Expression().Symbol()
case ast.IsBinaryExpression(decl):
return decl.AsBinaryExpression().Right.Symbol()
case ast.IsSourceFile(decl):
return decl.Symbol()
}
return nil
}
func symbolNameNoDefault(symbol *ast.Symbol) string {
if symbol.Name != ast.InternalSymbolNameDefault {
return symbol.Name
}
for _, decl := range symbol.Declarations {
name := ast.GetNameOfDeclaration(decl)
if name != nil && ast.IsIdentifier(name) {
return name.Text()
}
}
return ""
}
// findModuleReferences finds all references to a module symbol across the given source files.
// This includes import statements, <reference> directives, and implicit references (e.g., JSX runtime imports).
func findModuleReferences(program *compiler.Program, sourceFiles []*ast.SourceFile, searchModuleSymbol *ast.Symbol, checker *checker.Checker) []ModuleReference {
refs := []ModuleReference{}
for _, referencingFile := range sourceFiles {
searchSourceFile := searchModuleSymbol.ValueDeclaration
if searchSourceFile != nil && searchSourceFile.Kind == ast.KindSourceFile {
// Check <reference path> directives
for _, ref := range referencingFile.ReferencedFiles {
if program.GetSourceFileFromReference(referencingFile, ref) == searchSourceFile.AsSourceFile() {
refs = append(refs, ModuleReference{
kind: ModuleReferenceKindReference,
referencingFile: referencingFile,
ref: ref,
})
}
}
// Check <reference types> directives
for _, ref := range referencingFile.TypeReferenceDirectives {
referenced := program.GetResolvedTypeReferenceDirectiveFromTypeReferenceDirective(ref, referencingFile)
if referenced != nil && referenced.ResolvedFileName == searchSourceFile.AsSourceFile().FileName() {
refs = append(refs, ModuleReference{
kind: ModuleReferenceKindReference,
referencingFile: referencingFile,
ref: ref,
})
}
}
}
// Check all imports (including require() calls)
forEachImport(program, referencingFile, func(importDecl *ast.Node, moduleSpecifier *ast.Node) {
moduleSymbol := checker.GetSymbolAtLocation(moduleSpecifier)
if moduleSymbol == searchModuleSymbol {
if ast.NodeIsSynthesized(importDecl) {
refs = append(refs, ModuleReference{
kind: ModuleReferenceKindImplicit,
literal: moduleSpecifier,
referencingFile: referencingFile,
})
} else {
refs = append(refs, ModuleReference{
kind: ModuleReferenceKindImport,
literal: moduleSpecifier,
})
}
}
})
}
return refs
}

View File

@@ -0,0 +1,927 @@
package ls
import (
"context"
"slices"
"strings"
"unicode"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/debug"
"github.com/microsoft/typescript-go/internal/evaluator"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/nodebuilder"
"github.com/microsoft/typescript-go/internal/printer"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/stringutil"
)
func (l *LanguageService) ProvideInlayHint(
ctx context.Context,
params *lsproto.InlayHintParams,
) (lsproto.InlayHintResponse, error) {
userPreferences := l.UserPreferences()
inlayHintPreferences := userPreferences.InlayHints
if !isAnyInlayHintEnabled(inlayHintPreferences) {
return lsproto.InlayHintsOrNull{InlayHints: nil}, nil
}
program, file := l.getProgramAndFile(params.TextDocument.Uri)
quotePreference := lsutil.GetQuotePreference(file, userPreferences)
checker, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
inlayHintState := &inlayHintState{
ctx: ctx,
span: l.converters.FromLSPRange(file, params.Range),
preferences: inlayHintPreferences,
quotePreference: quotePreference,
file: file,
checker: checker,
converters: l.converters,
}
inlayHintState.visit(file.AsNode())
return lsproto.InlayHintsOrNull{InlayHints: &inlayHintState.result}, nil
}
type inlayHintState struct {
ctx context.Context
span core.TextRange
preferences lsutil.InlayHintsPreferences
quotePreference lsutil.QuotePreference
file *ast.SourceFile
checker *checker.Checker
converters *lsconv.Converters
result []*lsproto.InlayHint
}
func (s *inlayHintState) visit(node *ast.Node) bool {
if node == nil || node.End()-node.Pos() == 0 || node.Flags&ast.NodeFlagsReparsed != 0 {
return false
}
switch node.Kind {
case ast.KindModuleDeclaration, ast.KindClassDeclaration, ast.KindInterfaceDeclaration,
ast.KindFunctionDeclaration, ast.KindClassExpression, ast.KindFunctionExpression,
ast.KindMethodDeclaration, ast.KindArrowFunction:
if s.ctx.Err() != nil {
return true
}
}
if !s.span.Intersects(node.Loc) {
return false
}
if ast.IsTypeNode(node) && !ast.IsExpressionWithTypeArguments(node) {
return false
}
if s.preferences.IncludeInlayVariableTypeHints.IsTrue() && ast.IsVariableDeclaration(node) {
s.visitVariableLikeDeclaration(node)
} else if s.preferences.IncludeInlayPropertyDeclarationTypeHints.IsTrue() && ast.IsPropertyDeclaration(node) {
s.visitVariableLikeDeclaration(node)
} else if s.preferences.IncludeInlayEnumMemberValueHints.IsTrue() && ast.IsEnumMember(node) {
s.visitEnumMember(node)
} else if shouldShowParameterNameHints(s.preferences) && (ast.IsCallExpression(node) || ast.IsNewExpression(node)) {
s.visitCallOrNewExpression(node)
} else {
if s.preferences.IncludeInlayFunctionParameterTypeHints.IsTrue() &&
ast.IsFunctionLikeDeclaration(node) &&
ast.HasContextSensitiveParameters(node) {
s.visitFunctionLikeForParameterType(node)
}
if s.preferences.IncludeInlayFunctionLikeReturnTypeHints.IsTrue() &&
isSignatureSupportingReturnAnnotation(node) {
s.visitFunctionDeclarationLikeForReturnType(node)
}
}
return node.ForEachChild(s.visit)
}
// FunctionDeclaration | MethodDeclaration | GetAccessor | FunctionExpression | ArrowFunction
func (s *inlayHintState) visitFunctionDeclarationLikeForReturnType(decl *ast.FunctionLikeDeclaration) {
if ast.IsArrowFunction(decl) {
if astnav.FindChildOfKind(decl, ast.KindOpenParenToken, s.file) == nil {
return
}
}
typeAnnotation := decl.Type()
if typeAnnotation != nil || decl.Body() == nil {
return
}
signature := s.checker.GetSignatureFromDeclaration(decl)
if signature == nil {
return
}
typePredicate := s.checker.GetTypePredicateOfSignature(signature)
if typePredicate != nil && typePredicate.Type() != nil {
hintParts := s.typePredicateToInlayHintParts(typePredicate)
s.addTypeHints(hintParts, s.getTypeAnnotationPosition(decl))
return
}
returnType := s.checker.GetReturnTypeOfSignature(signature)
if isModuleReferenceType(returnType) {
return
}
hintParts := s.typeToInlayHintParts(returnType)
s.addTypeHints(hintParts, s.getTypeAnnotationPosition(decl))
}
func (s *inlayHintState) visitCallOrNewExpression(expr *ast.CallOrNewExpression) {
args := expr.Arguments()
if len(args) == 0 {
return
}
signature := s.checker.GetResolvedSignature(expr)
if signature == nil {
return
}
signatureParamPos := 0
for _, originalArg := range args {
arg := ast.SkipParentheses(originalArg)
if shouldShowLiteralParameterNameHintsOnly(s.preferences) && !isHintableLiteral(arg) {
signatureParamPos++
continue
}
spreadArgs := 0
if ast.IsSpreadElement(arg) {
spreadType := s.checker.GetTypeAtLocation(arg.Expression())
if spreadType.IsTupleType() {
elementFlags := spreadType.Target().AsTupleType().ElementFlags()
fixedLength := spreadType.Target().AsTupleType().FixedLength()
if fixedLength == 0 {
continue
}
firstOptionalIndex := slices.IndexFunc(elementFlags, func(f checker.ElementFlags) bool {
return f&checker.ElementFlagsRequired == 0
})
requiredArgs := core.IfElse(firstOptionalIndex < 0, fixedLength, firstOptionalIndex)
if requiredArgs > 0 {
spreadArgs = requiredArgs
}
}
}
identifierInfo := s.getParameterIdentifierInfoAtPosition(signature, signatureParamPos)
signatureParamPos = signatureParamPos + core.IfElse(spreadArgs > 0, spreadArgs, 1)
if identifierInfo == nil {
return
}
parameter := identifierInfo.parameter
parameterName := identifierInfo.name
isFirstVariadicArgument := identifierInfo.isRestParameter
parameterNameNotSameAsArgument := s.preferences.IncludeInlayParameterNameHintsWhenArgumentMatchesName.IsTrue() ||
!identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName)
if !parameterNameNotSameAsArgument && !isFirstVariadicArgument {
continue
}
if s.leadingCommentsContainsParameterName(arg, parameterName) {
continue
}
s.addParameterHints(
parameterName,
parameter,
astnav.GetStartOfNode(originalArg, s.file, false /*includeJSDoc*/),
isFirstVariadicArgument,
)
}
}
func (s *inlayHintState) visitEnumMember(member *ast.EnumMemberNode) {
if member.Initializer() != nil {
return
}
enumValue := s.checker.GetConstantValue(member)
if enumValue != nil {
s.addEnumMemberValueHints(evaluator.AnyToString(enumValue), member.End())
}
}
func (s *inlayHintState) visitVariableLikeDeclaration(decl *ast.VariableOrPropertyDeclaration) {
if decl.Initializer() == nil &&
!(ast.IsPropertyDeclaration(decl) && s.checker.GetTypeAtLocation(decl).Flags()&checker.TypeFlagsAny == 0) ||
ast.IsBindingPattern(decl.Name()) || (ast.IsVariableDeclaration(decl) && !isHintableDeclaration(decl)) {
return
}
typeAnnotation := decl.Type()
if typeAnnotation != nil {
return
}
declarationType := s.checker.GetTypeAtLocation(decl)
if isModuleReferenceType(declarationType) {
return
}
hintParts := s.typeToInlayHintParts(declarationType)
var hintText string
if hintParts.String != nil {
hintText = *hintParts.String
} else if hintParts.InlayHintLabelParts != nil {
var b strings.Builder
for _, part := range *hintParts.InlayHintLabelParts {
b.WriteString(part.Value)
}
hintText = b.String()
}
if !s.preferences.IncludeInlayVariableTypeHintsWhenTypeMatchesName.IsTrue() &&
!ast.IsComputedPropertyName(decl.Name()) &&
stringutil.EquateStringCaseInsensitive(decl.Name().Text(), hintText) {
return
}
s.addTypeHints(hintParts, decl.Name().End())
}
func (s *inlayHintState) visitFunctionLikeForParameterType(node *ast.FunctionLikeDeclaration) {
signature := s.checker.GetSignatureFromDeclaration(node)
if signature == nil {
return
}
pos := 0
for _, param := range node.Parameters() {
if isHintableDeclaration(param) {
var symbol *ast.Symbol
if ast.IsThisParameter(param) {
symbol = signature.ThisParameter()
} else {
symbol = signature.Parameters()[pos]
}
s.addParameterTypeHint(param, symbol)
}
if ast.IsThisParameter(param) {
continue
}
pos++
}
}
func (s *inlayHintState) addParameterTypeHint(node *ast.ParameterDeclarationNode, symbol *ast.Symbol) {
typeAnnotation := node.Type()
if typeAnnotation != nil || symbol == nil {
return
}
typeHints := s.getParameterDeclarationTypeHints(symbol)
if typeHints == nil {
return
}
var pos int
if node.QuestionToken() != nil {
pos = node.QuestionToken().End()
} else {
pos = node.Name().End()
}
s.addTypeHints(*typeHints, pos)
}
func (s *inlayHintState) getParameterDeclarationTypeHints(symbol *ast.Symbol) *lsproto.StringOrInlayHintLabelParts {
valueDeclaration := symbol.ValueDeclaration
if valueDeclaration == nil || !ast.IsParameterDeclaration(valueDeclaration) {
return nil
}
signatureParamType := s.checker.GetTypeOfSymbolAtLocation(symbol, valueDeclaration)
if isModuleReferenceType(signatureParamType) {
return nil
}
return new(s.typeToInlayHintParts(signatureParamType))
}
func (s *inlayHintState) typeToInlayHintParts(t *checker.Type) lsproto.StringOrInlayHintLabelParts {
flags := nodebuilder.FlagsIgnoreErrors | nodebuilder.FlagsAllowUniqueESSymbolType |
nodebuilder.FlagsUseAliasDefinedOutsideCurrentScope
idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol)
// !!! Avoid type node reuse so we collect identifier symbols.
typeNode := s.checker.TypeToTypeNode(t, nil /*enclosingDeclaration*/, flags, idToSymbol)
debug.Assert(typeNode != nil, "should always get typenode")
return lsproto.StringOrInlayHintLabelParts{
InlayHintLabelParts: new(s.getInlayHintLabelParts(typeNode, idToSymbol)),
}
}
func (s *inlayHintState) typePredicateToInlayHintParts(typePredicate *checker.TypePredicate) lsproto.StringOrInlayHintLabelParts {
flags := nodebuilder.FlagsIgnoreErrors | nodebuilder.FlagsAllowUniqueESSymbolType |
nodebuilder.FlagsUseAliasDefinedOutsideCurrentScope
idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol)
// !!! Avoid type node reuse so we collect identifier symbols.
typeNode := s.checker.TypePredicateToTypePredicateNode(typePredicate, nil /*enclosingDeclaration*/, flags, idToSymbol)
debug.Assert(typeNode != nil, "should always get typePredicateNode")
return lsproto.StringOrInlayHintLabelParts{
InlayHintLabelParts: new(s.getInlayHintLabelParts(typeNode, idToSymbol)),
}
}
func (s *inlayHintState) addTypeHints(hint lsproto.StringOrInlayHintLabelParts, position int) {
if hint.String != nil {
hint.String = new(": " + *hint.String)
} else {
hint.InlayHintLabelParts = new(append([]*lsproto.InlayHintLabelPart{{Value: ": "}}, *hint.InlayHintLabelParts...))
}
s.result = append(s.result, &lsproto.InlayHint{
Label: hint,
Position: s.converters.PositionToLineAndCharacter(s.file, core.TextPos(position)),
Kind: new(lsproto.InlayHintKindType),
PaddingLeft: new(true),
})
}
func (s *inlayHintState) addEnumMemberValueHints(text string, position int) {
s.result = append(s.result, &lsproto.InlayHint{
Label: lsproto.StringOrInlayHintLabelParts{
String: new("= " + text),
},
Position: s.converters.PositionToLineAndCharacter(s.file, core.TextPos(position)),
PaddingLeft: new(true),
})
}
func (s *inlayHintState) addParameterHints(text string, parameter *ast.IdentifierNode, position int, isFirstVariadicArgument bool) {
hintText := core.IfElse(isFirstVariadicArgument, "...", "") + text
displayParts := []*lsproto.InlayHintLabelPart{
s.getNodeDisplayPart(hintText, parameter),
{
Value: ":",
},
}
labelParts := lsproto.StringOrInlayHintLabelParts{InlayHintLabelParts: &displayParts}
s.result = append(s.result, &lsproto.InlayHint{
Label: labelParts,
Position: s.converters.PositionToLineAndCharacter(s.file, core.TextPos(position)),
Kind: new(lsproto.InlayHintKindParameter),
PaddingRight: new(true),
})
}
func shouldShowParameterNameHints(preferences lsutil.InlayHintsPreferences) bool {
return (preferences.IncludeInlayParameterNameHints == lsutil.IncludeInlayParameterNameHintsLiterals ||
preferences.IncludeInlayParameterNameHints == lsutil.IncludeInlayParameterNameHintsAll)
}
func shouldShowLiteralParameterNameHintsOnly(preferences lsutil.InlayHintsPreferences) bool {
return preferences.IncludeInlayParameterNameHints == lsutil.IncludeInlayParameterNameHintsLiterals
}
// node is FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration | GetAccessor
func isSignatureSupportingReturnAnnotation(node *ast.Node) bool {
return ast.IsArrowFunction(node) || ast.IsFunctionExpression(node) || ast.IsFunctionDeclaration(node) ||
ast.IsMethodDeclaration(node) || ast.IsGetAccessorDeclaration(node)
}
func isHintableDeclaration(node *ast.VariableOrParameterDeclaration) bool {
if (ast.IsPartOfParameterDeclaration(node) || ast.IsVariableDeclaration(node) && ast.IsVarConst(node)) &&
node.Initializer() != nil {
initializer := ast.SkipParentheses(node.Initializer())
return !(isHintableLiteral(initializer) || ast.IsNewExpression(initializer) ||
ast.IsObjectLiteralExpression(initializer) || ast.IsAssertionExpression(initializer))
}
return true
}
func isHintableLiteral(node *ast.Node) bool {
switch node.Kind {
case ast.KindPrefixUnaryExpression:
operand := node.AsPrefixUnaryExpression().Operand
return ast.IsLiteralExpression(operand) || ast.IsIdentifier(operand) && ast.IsInfinityOrNaNString(operand.Text())
case ast.KindTrueKeyword, ast.KindFalseKeyword, ast.KindNullKeyword,
ast.KindNoSubstitutionTemplateLiteral, ast.KindTemplateExpression:
return true
case ast.KindIdentifier:
name := node.Text()
return name == "undefined" || ast.IsInfinityOrNaNString(name)
}
return ast.IsLiteralExpression(node)
}
func isModuleReferenceType(t *checker.Type) bool {
symbol := t.Symbol()
return symbol != nil && symbol.Flags&ast.SymbolFlagsModule != 0
}
func (s *inlayHintState) getInlayHintLabelParts(node *ast.Node, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) []*lsproto.InlayHintLabelPart {
var parts []*lsproto.InlayHintLabelPart
var visitForDisplayParts func(node *ast.Node)
var visitDisplayPartList func(nodes []*ast.Node, separator string)
var visitParametersAndTypeParameters func(node *ast.SignatureDeclaration)
visitForDisplayParts = func(node *ast.Node) {
if node == nil {
return
}
tokenString := scanner.TokenToString(node.Kind)
if tokenString != "" {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: tokenString})
return
}
if ast.IsLiteralExpression(node) {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: s.getLiteralText(node)})
return
}
switch node.Kind {
case ast.KindIdentifier:
identifierText := node.Text()
var name *ast.Node
if symbol := idToSymbol[node]; symbol != nil && len(symbol.Declarations) != 0 {
name = ast.GetNameOfDeclaration(symbol.Declarations[0])
}
if name != nil {
parts = append(parts, s.getNodeDisplayPart(identifierText, name))
} else {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: identifierText})
}
case ast.KindQualifiedName:
visitForDisplayParts(node.AsQualifiedName().Left)
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "."})
visitForDisplayParts(node.AsQualifiedName().Right)
case ast.KindTypePredicate:
if node.AsTypePredicateNode().AssertsModifier != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "asserts "})
}
visitForDisplayParts(node.AsTypePredicateNode().ParameterName)
if node.Type() != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " is "})
visitForDisplayParts(node.Type())
}
case ast.KindTypeReference:
visitForDisplayParts(node.AsTypeReferenceNode().TypeName)
if len(node.TypeArguments()) > 0 {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
visitDisplayPartList(node.TypeArguments(), ",")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
}
case ast.KindTypeParameter:
if len(node.ModifierNodes()) > 0 {
visitDisplayPartList(node.ModifierNodes(), "")
}
visitForDisplayParts(node.Name())
if node.AsTypeParameterDeclaration().Constraint != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " extends "})
visitForDisplayParts(node.AsTypeParameterDeclaration().Constraint)
}
if node.AsTypeParameterDeclaration().DefaultType != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " = "})
visitForDisplayParts(node.AsTypeParameterDeclaration().DefaultType)
}
case ast.KindParameter:
if len(node.ModifierNodes()) > 0 {
visitDisplayPartList(node.ModifierNodes(), " ")
}
if node.AsParameterDeclaration().DotDotDotToken != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "..."})
}
visitForDisplayParts(node.Name())
if node.QuestionToken() != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
}
if node.Type() != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
visitForDisplayParts(node.Type())
}
case ast.KindConstructorType:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "new "})
visitParametersAndTypeParameters(node)
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " => "})
visitForDisplayParts(node.Type())
case ast.KindTypeQuery:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "typeof "})
visitForDisplayParts(node.AsTypeQueryNode().ExprName)
if len(node.TypeArguments()) > 0 {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
visitDisplayPartList(node.TypeArguments(), ", ")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
}
case ast.KindTypeLiteral:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "{"})
if len(node.Members()) > 0 {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
visitDisplayPartList(node.Members(), "; ")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
}
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "}"})
case ast.KindArrayType:
visitForDisplayParts(node.AsArrayTypeNode().ElementType)
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "[]"})
case ast.KindTupleType:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
visitDisplayPartList(node.Elements(), ", ")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
case ast.KindNamedTupleMember:
if node.AsNamedTupleMember().DotDotDotToken != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "..."})
}
visitForDisplayParts(node.Name())
if node.QuestionToken() != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
}
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
visitForDisplayParts(node.Type())
case ast.KindOptionalType:
visitForDisplayParts(node.Type())
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
case ast.KindRestType:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "..."})
visitForDisplayParts(node.Type())
case ast.KindUnionType:
if node.AsUnionTypeNode().Types != nil {
visitDisplayPartList(node.AsUnionTypeNode().Types.Nodes, " | ")
}
case ast.KindIntersectionType:
if node.AsIntersectionTypeNode().Types != nil {
visitDisplayPartList(node.AsIntersectionTypeNode().Types.Nodes, " & ")
}
case ast.KindConditionalType:
visitForDisplayParts(node.AsConditionalTypeNode().CheckType)
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " extends "})
visitForDisplayParts(node.AsConditionalTypeNode().ExtendsType)
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " ? "})
visitForDisplayParts(node.AsConditionalTypeNode().TrueType)
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " : "})
visitForDisplayParts(node.AsConditionalTypeNode().FalseType)
case ast.KindInferType:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "infer "})
visitForDisplayParts(node.AsInferTypeNode().TypeParameter)
case ast.KindParenthesizedType:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "("})
visitForDisplayParts(node.Type())
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ")"})
case ast.KindTypeOperator:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: scanner.TokenToString(node.AsTypeOperatorNode().Operator)})
visitForDisplayParts(node.Type())
case ast.KindIndexedAccessType:
visitForDisplayParts(node.AsIndexedAccessTypeNode().ObjectType)
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
visitForDisplayParts(node.AsIndexedAccessTypeNode().IndexType)
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
case ast.KindMappedType:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "{ "})
if node.AsMappedTypeNode().ReadonlyToken != nil {
if node.AsMappedTypeNode().ReadonlyToken.Kind == ast.KindPlusToken {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "+"})
} else if node.AsMappedTypeNode().ReadonlyToken.Kind == ast.KindMinusToken {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "-"})
}
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "readonly "})
}
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
visitForDisplayParts(node.AsMappedTypeNode().TypeParameter)
if node.AsMappedTypeNode().NameType != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " as "})
visitForDisplayParts(node.AsMappedTypeNode().NameType)
}
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
if node.QuestionToken() != nil {
if node.QuestionToken().Kind == ast.KindPlusToken {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "+"})
} else if node.QuestionToken().Kind == ast.KindMinusToken {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "-"})
}
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
}
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
if node.Type() != nil {
visitForDisplayParts(node.Type())
}
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "; }"})
case ast.KindLiteralType:
visitForDisplayParts(node.AsLiteralTypeNode().Literal)
case ast.KindFunctionType:
visitParametersAndTypeParameters(node)
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " => "})
visitForDisplayParts(node.Type())
case ast.KindImportType:
if node.AsImportTypeNode().IsTypeOf {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "typeof "})
}
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "import("})
visitForDisplayParts(node.AsImportTypeNode().Argument)
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ")"})
if node.AsImportTypeNode().Qualifier != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "."})
visitForDisplayParts(node.AsImportTypeNode().Qualifier)
}
if len(node.TypeArguments()) > 0 {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
visitDisplayPartList(node.TypeArguments(), ", ")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
}
case ast.KindPropertySignature:
if len(node.ModifierNodes()) > 0 {
visitDisplayPartList(node.ModifierNodes(), " ")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
}
visitForDisplayParts(node.Name())
if node.PostfixToken() != nil {
parts = append(
parts,
&lsproto.InlayHintLabelPart{
Value: scanner.TokenToString(node.PostfixToken().Kind),
},
)
}
if node.Type() != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
visitForDisplayParts(node.Type())
}
case ast.KindIndexSignature:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
visitDisplayPartList(node.Parameters(), ", ")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
if node.Type() != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
visitForDisplayParts(node.Type())
}
case ast.KindMethodSignature:
if len(node.ModifierNodes()) > 0 {
visitDisplayPartList(node.ModifierNodes(), " ")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
}
visitForDisplayParts(node.Name())
if node.PostfixToken() != nil {
parts = append(
parts,
&lsproto.InlayHintLabelPart{
Value: scanner.TokenToString(node.PostfixToken().Kind),
},
)
}
visitParametersAndTypeParameters(node)
if node.Type() != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
visitForDisplayParts(node.Type())
}
case ast.KindCallSignature:
visitParametersAndTypeParameters(node)
if node.Type() != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
visitForDisplayParts(node.Type())
}
case ast.KindConstructSignature:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "new "})
visitParametersAndTypeParameters(node)
if node.Type() != nil {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
visitForDisplayParts(node.Type())
}
case ast.KindArrayBindingPattern:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
visitDisplayPartList(node.Elements(), ", ")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
case ast.KindObjectBindingPattern:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "{"})
if len(node.Elements()) > 0 {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
visitDisplayPartList(node.Elements(), ", ")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
}
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "}"})
case ast.KindBindingElement:
visitForDisplayParts(node.Name())
case ast.KindPrefixUnaryExpression:
parts = append(
parts,
&lsproto.InlayHintLabelPart{
Value: scanner.TokenToString(node.AsPrefixUnaryExpression().Operator),
},
)
visitForDisplayParts(node.AsPrefixUnaryExpression().Operand)
case ast.KindTemplateLiteralType:
visitForDisplayParts(node.AsTemplateLiteralTypeNode().Head)
for _, span := range node.AsTemplateLiteralTypeNode().TemplateSpans.Nodes {
visitForDisplayParts(span)
}
case ast.KindTemplateHead:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: s.getLiteralText(node)})
case ast.KindTemplateLiteralTypeSpan:
visitForDisplayParts(node.Type())
visitForDisplayParts(node.AsTemplateLiteralTypeSpan().Literal)
case ast.KindTemplateMiddle, ast.KindTemplateTail:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: s.getLiteralText(node)})
case ast.KindThisType:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "this"})
case ast.KindComputedPropertyName:
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
visitForDisplayParts(node.Expression())
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
case ast.KindPropertyAccessExpression:
visitForDisplayParts(node.Expression())
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "."})
visitForDisplayParts(node.Name())
case ast.KindElementAccessExpression:
visitForDisplayParts(node.Expression())
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
visitForDisplayParts(node.AsElementAccessExpression().ArgumentExpression)
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
default:
debug.FailBadSyntaxKind(node)
}
}
visitDisplayPartList = func(nodes []*ast.Node, separator string) {
for i, n := range nodes {
if i > 0 {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: separator})
}
visitForDisplayParts(n)
}
}
visitParametersAndTypeParameters = func(node *ast.SignatureDeclaration) {
if len(node.TypeParameters()) > 0 {
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
visitDisplayPartList(node.TypeParameters(), ", ")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
}
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "("})
visitDisplayPartList(node.Parameters(), ", ")
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ")"})
}
visitForDisplayParts(node)
return parts
}
func (s *inlayHintState) getNodeDisplayPart(text string, node *ast.Node) *lsproto.InlayHintLabelPart {
file := ast.GetSourceFileOfNode(node)
pos := astnav.GetStartOfNode(node, file, false /*includeJSDoc*/)
end := node.End()
return &lsproto.InlayHintLabelPart{
Value: text,
Location: &lsproto.Location{
Uri: lsconv.FileNameToDocumentURI(file.FileName()),
Range: s.converters.ToLSPRange(file, core.NewTextRange(pos, end)),
},
}
}
func (s *inlayHintState) getLiteralText(node *ast.LiteralLikeNode) string {
switch node.Kind {
case ast.KindStringLiteral:
if s.quotePreference == lsutil.QuotePreferenceSingle {
return `'` + printer.EscapeString(node.Text(), printer.QuoteCharSingleQuote) + `'`
}
return `"` + printer.EscapeString(node.Text(), printer.QuoteCharDoubleQuote) + `"`
case ast.KindTemplateHead, ast.KindTemplateMiddle, ast.KindTemplateTail:
rawText := node.RawText()
if rawText == "" {
rawText = printer.EscapeString(node.Text(), printer.QuoteCharBacktick)
}
switch node.Kind {
case ast.KindTemplateHead:
return "`" + rawText + "${"
case ast.KindTemplateMiddle:
return "}" + rawText + "${"
case ast.KindTemplateTail:
return "}" + rawText + "`"
}
}
return node.Text()
}
type parameterInfo struct {
parameter *ast.IdentifierNode
name string
isRestParameter bool
}
func (s *inlayHintState) getParameterIdentifierInfoAtPosition(signature *checker.Signature, pos int) *parameterInfo {
parameters := signature.Parameters()
paramCount := len(parameters) - core.IfElse(signature.HasRestParameter(), 1, 0)
if pos < paramCount {
param := parameters[pos]
paramId := getParameterDeclarationIdentifier(param)
if paramId == nil {
return nil
}
return &parameterInfo{
parameter: paramId,
name: paramId.Text(),
isRestParameter: false,
}
}
var restParameter *ast.Symbol
var restId *ast.IdentifierNode
if paramCount < len(parameters) {
restParameter = parameters[paramCount]
restId = getParameterDeclarationIdentifier(restParameter)
}
if restId == nil {
return nil
}
restType := s.checker.GetTypeOfSymbol(restParameter)
if restType.IsTupleType() {
associatedNames := make([]*ast.Node, 0, len(restType.Target().AsTupleType().ElementInfos()))
for _, elementInfo := range restType.Target().AsTupleType().ElementInfos() {
labeledElement := elementInfo.LabeledDeclaration()
associatedNames = append(associatedNames, labeledElement)
}
index := pos - paramCount
if index < len(associatedNames) {
associatedName := associatedNames[index]
if associatedName != nil {
debug.Assert(ast.IsIdentifier(associatedName.Name()))
var isRestTupleElement bool
if ast.IsNamedTupleMember(associatedName) {
isRestTupleElement = associatedName.AsNamedTupleMember().DotDotDotToken != nil
} else {
isRestTupleElement = associatedName.AsParameterDeclaration().DotDotDotToken != nil
}
return &parameterInfo{
parameter: associatedName.Name(),
name: associatedName.Name().Text(),
isRestParameter: isRestTupleElement,
}
}
}
return nil
}
if pos == paramCount {
return &parameterInfo{
parameter: restId,
name: restParameter.Name,
isRestParameter: true,
}
}
return nil
}
func getParameterDeclarationIdentifier(symbol *ast.Symbol) *ast.IdentifierNode {
if symbol.ValueDeclaration != nil && ast.IsParameterDeclaration(symbol.ValueDeclaration) && ast.IsIdentifier(symbol.ValueDeclaration.Name()) {
return symbol.ValueDeclaration.Name()
}
return nil
}
func identifierOrAccessExpressionPostfixMatchesParameterName(expr *ast.Expression, parameterName string) bool {
if ast.IsIdentifier(expr) {
return expr.Text() == parameterName
}
if ast.IsPropertyAccessExpression(expr) {
return expr.Name().Text() == parameterName
}
return false
}
func (s *inlayHintState) leadingCommentsContainsParameterName(node *ast.Node, name string) bool {
if !scanner.IsIdentifierText(name, s.file.LanguageVariant) {
return false
}
ranges := getLeadingCommentRangesOfNode(node, s.file)
fileText := s.file.Text()
for r := range ranges {
commentText := strings.TrimFunc(fileText[r.Pos():r.End()], func(r rune) bool {
return unicode.IsSpace(r) || r == '/' || r == '*'
})
if commentText == name {
return true
}
}
return false
}
func (s *inlayHintState) getTypeAnnotationPosition(decl *ast.FunctionLikeDeclaration) int {
closeParenToken := astnav.FindChildOfKind(decl, ast.KindCloseParenToken, s.file)
if closeParenToken != nil {
return closeParenToken.End()
}
return decl.ParameterList().End()
}
func isAnyInlayHintEnabled(preferences lsutil.InlayHintsPreferences) bool {
return preferences.IncludeInlayParameterNameHints != lsutil.IncludeInlayParameterNameHintsNone ||
preferences.IncludeInlayFunctionParameterTypeHints.IsTrue() ||
preferences.IncludeInlayVariableTypeHints.IsTrue() ||
preferences.IncludeInlayPropertyDeclarationTypeHints.IsTrue() ||
preferences.IncludeInlayFunctionLikeReturnTypeHints.IsTrue() ||
preferences.IncludeInlayEnumMemberValueHints.IsTrue()
}

View File

@@ -0,0 +1,161 @@
package ls
import (
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
)
// JSDocTagInfo mirrors Strada's `JSDocTagInfo`, but renders the tag's text as a
// plain string instead of `SymbolDisplayPart[]`.
type JSDocTagInfo struct {
Name string
Text string
}
// GetSymbolDocumentationComment renders a symbol's documentation comment as plain text.
// It backs the API's Symbol.getDocumentationComment and mirrors Strada's
// getJsDocCommentsFromDeclarations: comments are gathered from each unique declaration,
// deduplicated, and joined with line breaks. Like Strada, it does not resolve aliases —
// consumers resolve aliases themselves (via getAliasedSymbol) and re-query if desired.
func (l *LanguageService) GetSymbolDocumentationComment(c *checker.Checker, symbol *ast.Symbol) string {
if symbol == nil {
return ""
}
var parts []string
var seen collections.Set[*ast.Node]
for _, decl := range symbol.Declarations {
if decl == nil {
continue
}
if !seen.AddIfAbsent(decl) {
continue
}
if doc := l.getDocumentationFromDeclaration(c, symbol, decl, decl, lsproto.MarkupKindPlainText, true /*commentOnly*/); doc != "" && !slices.Contains(parts, doc) {
parts = append(parts, doc)
}
}
return strings.Join(parts, "\n")
}
// GetSymbolJSDocTags collects a symbol's JSDoc tags. It backs the API's Symbol.getJsDocTags
// and mirrors Strada's getJsDocTagsFromDeclarations, except each tag's text is rendered as a
// plain string rather than SymbolDisplayPart[]. Tags with no text have an empty Text field.
func (l *LanguageService) GetSymbolJSDocTags(symbol *ast.Symbol) []JSDocTagInfo {
if symbol == nil {
return nil
}
var infos []JSDocTagInfo
var seen collections.Set[*ast.Node]
for _, decl := range symbol.Declarations {
if decl == nil {
continue
}
if !seen.AddIfAbsent(decl) {
continue
}
tags := declarationJSDocTags(decl)
// Skip comments containing @typedef/@callback since they're not associated with a
// particular declaration, unless they also carry @param/@return (treated as local docs).
hasTypedef := core.Some(tags, func(t *ast.Node) bool {
return t.Kind == ast.KindJSDocTypedefTag || t.Kind == ast.KindJSDocCallbackTag
})
hasParamOrReturn := core.Some(tags, func(t *ast.Node) bool {
return t.Kind == ast.KindJSDocParameterTag || t.Kind == ast.KindJSDocReturnTag
})
if hasTypedef && !hasParamOrReturn {
continue
}
for _, tag := range tags {
infos = append(infos, JSDocTagInfo{Name: tag.TagName().Text(), Text: getJSDocTagText(tag)})
}
}
return infos
}
// declarationJSDocTags returns the JSDoc tags associated with a declaration, walking the
// JSDoc comment location chain like the checker's getAllJSDocTags.
func declarationJSDocTags(node *ast.Node) []*ast.Node {
if node.Flags&ast.NodeFlagsJSDoc == 0 {
for current := node; current != nil; current = ast.GetNextJSDocCommentLocation(current) {
jsdocs := current.JSDoc(nil)
if len(jsdocs) == 0 {
continue
}
lastJSDoc := jsdocs[len(jsdocs)-1].AsJSDoc()
if lastJSDoc.Tags != nil {
return lastJSDoc.Tags.Nodes
}
}
}
return nil
}
// getJSDocTagText renders the text of a single JSDoc tag as a plain string, mirroring
// Strada's getCommentDisplayParts collapsed from SymbolDisplayPart[] to a string.
func getJSDocTagText(tag *ast.Node) string {
comment := scanner.GetTextOfJSDocComment(tag.CommentList())
addComment := func(s string) string {
if comment == "" {
return s
}
return s + " " + comment
}
switch tag.Kind {
case ast.KindJSDocThrowsTag:
if te := tag.AsJSDocThrowsTag().TypeExpression; te != nil {
return addComment(scanner.GetTextOfNode(te))
}
return comment
case ast.KindJSDocImplementsTag:
return addComment(scanner.GetTextOfNode(tag.AsJSDocImplementsTag().ClassName))
case ast.KindJSDocAugmentsTag:
return addComment(scanner.GetTextOfNode(tag.AsJSDocAugmentsTag().ClassName))
case ast.KindJSDocTemplateTag:
templateTag := tag.AsJSDocTemplateTag()
var b strings.Builder
if templateTag.Constraint != nil {
b.WriteString(scanner.GetTextOfNode(templateTag.Constraint))
}
if templateTag.TypeParameters != nil {
for i, tp := range templateTag.TypeParameters.Nodes {
if i == 0 && b.Len() != 0 {
b.WriteString(" ")
}
if i != 0 {
b.WriteString(", ")
}
b.WriteString(scanner.GetTextOfNode(tp))
}
}
if comment != "" {
if b.Len() != 0 {
b.WriteString(" ")
}
b.WriteString(comment)
}
return b.String()
case ast.KindJSDocTypeTag:
return addComment(scanner.GetTextOfNode(tag.AsJSDocTypeTag().TypeExpression))
case ast.KindJSDocSatisfiesTag:
return addComment(scanner.GetTextOfNode(tag.AsJSDocSatisfiesTag().TypeExpression))
case ast.KindJSDocSeeTag:
if ne := tag.AsJSDocSeeTag().NameExpression; ne != nil {
return addComment(scanner.GetTextOfNode(ne))
}
return comment
case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag:
if name := tag.Name(); name != nil {
return addComment(scanner.GetTextOfNode(name))
}
return comment
default:
return comment
}
}

View File

@@ -0,0 +1,594 @@
package ls
import (
"context"
"fmt"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/format"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/parser"
"github.com/microsoft/typescript-go/internal/stringutil"
)
type docCommentTemplate struct {
newText string
}
type commentOwnerInfo struct {
commentOwner *ast.Node
parameters []*ast.ParameterDeclarationNode
hasReturn bool
}
func (l *LanguageService) getJSDocSnippetCompletion(ctx context.Context, file *ast.SourceFile, position int) *CompletionList {
if l.UserPreferences().EnableJSDocCompletions.IsFalse() {
return nil
}
if !isPotentiallyValidJSDocSnippetCompletionPosition(file, position) {
return nil
}
newLine := l.FormatOptions().NewLineCharacter
if newLine == "" {
newLine = "\n"
}
template := getDocCommentTemplateAtPosition(file, position, l.UserPreferences().GenerateReturnInDocTemplate.IsTrue(), newLine)
if template == nil {
return nil
}
insertText := template.newText
var insertTextFormat *lsproto.InsertTextFormat
if clientSupportsItemSnippet(ctx) {
insertText = templateToSnippet(insertText, newLine)
insertTextFormat = new(lsproto.InsertTextFormatSnippet)
}
editRange := l.getJSDocSnippetCompletionRange(ctx, file, position, insertText)
var commitCharacters *[]string
if clientSupportsItemCommitCharacters(ctx) {
commitCharacters = &[]string{}
}
item := &CompletionItem{
CompletionItem: &lsproto.CompletionItem{
Label: "/** */",
Kind: new(lsproto.CompletionItemKindText),
Detail: new(diagnostics.JSDoc_comment.Localize(locale.FromContext(ctx))),
SortText: new("\x00"),
InsertTextFormat: insertTextFormat,
TextEdit: editRange,
CommitCharacters: commitCharacters,
},
}
return &CompletionList{
IsIncomplete: false,
Items: []*CompletionItem{item},
}
}
func isPotentiallyValidJSDocSnippetCompletionPosition(file *ast.SourceFile, position int) bool {
text := file.Text()
lineStart := format.GetLineStartPositionForPosition(position, file)
prefix := text[lineStart:position]
if !isJSDocSnippetPrefix(prefix) {
return false
}
lineEnd := getLineEndOfPosition(file, position)
suffix := text[position:lineEnd]
return isJSDocSnippetSuffix(suffix)
}
func (l *LanguageService) getJSDocSnippetCompletionRange(ctx context.Context, file *ast.SourceFile, position int, newText string) *lsproto.TextEditOrInsertReplaceEdit {
text := file.Text()
lineStart := format.GetLineStartPositionForPosition(position, file)
prefix := text[lineStart:position]
start := position
if prefixStart, ok := getJSDocSnippetPrefixStart(prefix); ok {
start = lineStart + prefixStart
}
lineEnd := getLineEndOfPosition(file, position)
suffix := text[position:lineEnd]
end := position
if suffixEnd, ok := getJSDocSnippetSuffixEnd(suffix); ok {
end += suffixEnd
}
replacementRange := l.createLspRangeFromBounds(start, end, file)
if clientSupportsItemInsertReplace(ctx) {
return &lsproto.TextEditOrInsertReplaceEdit{
InsertReplaceEdit: &lsproto.InsertReplaceEdit{
NewText: newText,
Insert: replacementRange,
Replace: replacementRange,
},
}
}
return &lsproto.TextEditOrInsertReplaceEdit{
TextEdit: &lsproto.TextEdit{
NewText: newText,
Range: replacementRange,
},
}
}
func getDocCommentTemplateAtPosition(sourceFile *ast.SourceFile, position int, generateReturnInDocTemplate bool, newLine string) *docCommentTemplate {
tokenAtPos := astnav.GetTokenAtPosition(sourceFile, position)
if tokenAtPos == nil {
return nil
}
existingDocComment := ast.FindAncestor(tokenAtPos, ast.IsJSDoc)
docCommentEnd, hasDocCommentAtPosition, hasClosingDocCommentAtPosition := getDocCommentEndAtPosition(sourceFile, position)
isInEmptyDocComment := existingDocComment != nil || hasDocCommentAtPosition
if isNonEmptyJSDoc(existingDocComment) && hasDocCommentAtPosition && !hasClosingDocCommentAtPosition {
reparseText := sourceFile.Text()[:position] + " */" + sourceFile.Text()[position:]
reparse := parser.ParseSourceFile(sourceFile.ParseOptions(), reparseText, sourceFile.ScriptKind)
return getDocCommentTemplateAtPosition(reparse, position, generateReturnInDocTemplate, newLine)
}
if isNonEmptyJSDoc(existingDocComment) {
return nil
}
if existingDocComment == nil && hasDocCommentAtPosition {
tokenAtPos = astnav.GetTokenAtPosition(sourceFile, skipWhitespace(sourceFile.Text(), docCommentEnd))
if tokenAtPos == nil {
return nil
}
}
tokenStart := astnav.GetStartOfNode(tokenAtPos, sourceFile, false /*includeJSDoc*/)
if !isInEmptyDocComment && tokenStart < position {
return nil
}
commentOwnerInfo := getCommentOwnerInfo(tokenAtPos, generateReturnInDocTemplate)
if commentOwnerInfo == nil {
return nil
}
commentOwner := commentOwnerInfo.commentOwner
lastJSDoc := core.LastOrNil(commentOwner.JSDoc(sourceFile))
if commentOwnerStart := astnav.GetStartOfNode(commentOwner, sourceFile, false /*includeJSDoc*/); commentOwnerStart < position ||
lastJSDoc != nil && existingDocComment != nil && lastJSDoc != existingDocComment {
return nil
}
indentation := getIndentationStringAtPosition(sourceFile, position)
tags := parameterDocComments(commentOwnerInfo.parameters, ast.IsSourceFileJS(sourceFile), indentation, newLine)
if commentOwnerInfo.hasReturn {
tags += returnsDocComment(indentation, newLine)
}
if tags != "" && !hasJSDocTags(commentOwner, sourceFile) {
preamble := "/**" + newLine + indentation + " * "
endLine := ""
if tokenStart == position {
endLine = newLine + indentation
}
return &docCommentTemplate{newText: preamble + newLine + tags + indentation + " */" + endLine}
}
return &docCommentTemplate{newText: "/** */"}
}
func getDocCommentEndAtPosition(file *ast.SourceFile, position int) (end int, ok bool, hasClosing bool) {
text := file.Text()
lineStart := format.GetLineStartPositionForPosition(position, file)
lineEnd := getLineEndOfPosition(file, position)
prefix := text[lineStart:position]
suffix := text[position:lineEnd]
if !strings.HasSuffix(trimRightSingleLineWhitespace(prefix), "/**") {
return 0, false, false
}
suffixEnd, hasClosing := getJSDocSnippetSuffixEnd(suffix)
return position + suffixEnd, true, hasClosing
}
func skipWhitespace(text string, position int) int {
for position < len(text) {
ch, size := stringutil.DecodeJSStringRune(text[position:])
if size == 0 {
break
}
if !stringutil.IsWhiteSpaceLike(ch) {
break
}
position += size
}
return position
}
func getCommentOwnerInfo(tokenAtPos *ast.Node, generateReturnInDocTemplate bool) *commentOwnerInfo {
for node := tokenAtPos; node != nil; node = node.Parent {
info, quit := getCommentOwnerInfoWorker(node, generateReturnInDocTemplate)
if info != nil || quit {
return info
}
}
return nil
}
func getCommentOwnerInfoWorker(commentOwner *ast.Node, generateReturnInDocTemplate bool) (*commentOwnerInfo, bool) {
if commentOwner == nil {
return nil, false
}
switch commentOwner.Kind {
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindMethodDeclaration, ast.KindConstructor, ast.KindMethodSignature, ast.KindArrowFunction:
return &commentOwnerInfo{commentOwner: commentOwner, parameters: commentOwner.Parameters(), hasReturn: hasReturn(commentOwner, generateReturnInDocTemplate)}, false
case ast.KindPropertyAssignment:
return getCommentOwnerInfoWorker(commentOwner.AsPropertyAssignment().Initializer, generateReturnInDocTemplate)
case ast.KindClassDeclaration, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration, ast.KindEnumMember, ast.KindTypeAliasDeclaration:
return &commentOwnerInfo{commentOwner: commentOwner}, false
case ast.KindPropertySignature:
if typeNode := commentOwner.AsPropertySignatureDeclaration().Type; typeNode != nil && ast.IsFunctionTypeNode(typeNode) {
return &commentOwnerInfo{commentOwner: commentOwner, parameters: typeNode.Parameters(), hasReturn: hasReturn(typeNode, generateReturnInDocTemplate)}, false
}
return &commentOwnerInfo{commentOwner: commentOwner}, false
case ast.KindVariableStatement:
declarations := commentOwner.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes
if len(declarations) == 1 {
if initializer := declarations[0].AsVariableDeclaration().Initializer; initializer != nil {
if host := getRightHandSideOfAssignment(initializer); host != nil {
return &commentOwnerInfo{commentOwner: commentOwner, parameters: host.Parameters(), hasReturn: hasReturn(host, generateReturnInDocTemplate)}, false
}
}
}
return &commentOwnerInfo{commentOwner: commentOwner}, false
case ast.KindSourceFile:
return nil, true
case ast.KindModuleDeclaration:
if commentOwner.Parent.Kind == ast.KindModuleDeclaration {
return nil, false
}
return &commentOwnerInfo{commentOwner: commentOwner}, false
case ast.KindExpressionStatement:
return getCommentOwnerInfoWorker(commentOwner.AsExpressionStatement().Expression, generateReturnInDocTemplate)
case ast.KindBinaryExpression:
binaryExpression := commentOwner.AsBinaryExpression()
if ast.GetAssignmentDeclarationKind(commentOwner) == ast.JSDeclarationKindNone {
return nil, true
}
if ast.IsFunctionLike(binaryExpression.Right) {
return &commentOwnerInfo{commentOwner: commentOwner, parameters: binaryExpression.Right.Parameters(), hasReturn: hasReturn(binaryExpression.Right, generateReturnInDocTemplate)}, false
}
return &commentOwnerInfo{commentOwner: commentOwner}, false
case ast.KindPropertyDeclaration:
if initializer := commentOwner.AsPropertyDeclaration().Initializer; initializer != nil && ast.IsFunctionExpressionOrArrowFunction(initializer) {
return &commentOwnerInfo{commentOwner: commentOwner, parameters: initializer.Parameters(), hasReturn: hasReturn(initializer, generateReturnInDocTemplate)}, false
}
}
return nil, false
}
func hasReturn(node *ast.Node, generateReturnInDocTemplate bool) bool {
if !generateReturnInDocTemplate {
return false
}
if ast.IsFunctionTypeNode(node) {
return true
}
if ast.IsArrowFunction(node) {
if body := node.Body(); body != nil && ast.IsExpression(body) {
return true
}
}
return ast.IsFunctionLikeDeclaration(node) && node.Body() != nil && ast.IsBlock(node.Body()) && ast.ForEachReturnStatement(node.Body(), func(*ast.Node) bool {
return true
})
}
func getRightHandSideOfAssignment(rightHandSide *ast.Node) *ast.Node {
if rightHandSide == nil {
return nil
}
for rightHandSide.Kind == ast.KindParenthesizedExpression {
rightHandSide = rightHandSide.AsParenthesizedExpression().Expression
}
switch rightHandSide.Kind {
case ast.KindFunctionExpression, ast.KindArrowFunction:
return rightHandSide
case ast.KindClassExpression:
return core.Find(rightHandSide.Members(), ast.IsConstructorDeclaration)
default:
return nil
}
}
func parameterDocComments(parameters []*ast.ParameterDeclarationNode, isJavaScriptFile bool, indentation, newLine string) string {
var b strings.Builder
for i, parameter := range parameters {
paramName := fmt.Sprintf("param%d", i)
if ast.IsIdentifier(parameter.Name()) {
paramName = parameter.Name().Text()
}
paramType := ""
if isJavaScriptFile {
if parameter.AsParameterDeclaration().DotDotDotToken != nil {
paramType = "{...any} "
} else {
paramType = "{any} "
}
}
b.WriteString(indentation)
b.WriteString(" * @param ")
b.WriteString(paramType)
b.WriteString(paramName)
b.WriteString(newLine)
}
return b.String()
}
func returnsDocComment(indentation, newLine string) string {
return indentation + " * @returns" + newLine
}
func getIndentationStringAtPosition(sourceFile *ast.SourceFile, position int) string {
text := sourceFile.Text()
lineStart := format.GetLineStartPositionForPosition(position, sourceFile)
pos := lineStart
for pos < position {
ch, size := stringutil.DecodeJSStringRune(text[pos:])
if size == 0 {
break
}
if !stringutil.IsWhiteSpaceSingleLine(ch) {
break
}
pos += size
}
return text[lineStart:pos]
}
func isNonEmptyJSDoc(jsdoc *ast.Node) bool {
if jsdoc == nil {
return false
}
data := jsdoc.AsJSDoc()
return data.Comment != nil && len(data.Comment.Nodes) > 0 || data.Tags != nil && len(data.Tags.Nodes) > 0
}
func hasJSDocTags(node *ast.Node, file *ast.SourceFile) bool {
jsdocs := node.JSDoc(file)
if len(jsdocs) == 0 {
return false
}
tags := jsdocs[len(jsdocs)-1].AsJSDoc().Tags
return tags != nil && len(tags.Nodes) > 0
}
func templateToSnippet(template string, newLine string) string {
if template == "/** */" {
return "/**" + newLine + " * $0" + newLine + " */"
}
snippetIndex := 1
template = escapeSnippetText(template)
template = stripJSDocTemplateIndentation(template, newLine)
return transformJSDocTemplateLines(template, newLine, &snippetIndex)
}
func stripJSDocTemplateIndentation(template string, newLine string) string {
lines := strings.Split(template, newLine)
for i, line := range lines {
trimmed := strings.TrimLeft(line, " \t")
if strings.HasPrefix(trimmed, "/") {
lines[i] = trimmed
} else if strings.HasPrefix(trimmed, "*") {
lines[i] = " " + trimmed
}
}
return strings.Join(lines, newLine)
}
func transformJSDocTemplateLines(template string, newLine string, snippetIndex *int) string {
lines := strings.Split(template, newLine)
for i, line := range lines {
if i > 0 && strings.HasPrefix(lines[i-1], "/**") && lineHasOnlyJSDocAsterisk(line) {
lines[i] = line + "$0"
continue
}
if transformed, ok := transformJSDocParamLine(line, snippetIndex); ok {
lines[i] = transformed
continue
}
if transformed, ok := transformJSDocReturnsLine(line, snippetIndex); ok {
lines[i] = transformed
}
}
return strings.Join(lines, newLine)
}
func lineHasOnlyJSDocAsterisk(line string) bool {
line = strings.TrimLeft(line, " \t")
return strings.HasPrefix(line, "*") && isOnlySpacesOrTabs(line[1:])
}
func transformJSDocParamLine(line string, snippetIndex *int) (string, bool) {
prefix := ""
rest := line
if strings.HasPrefix(rest, " ") {
prefix = " "
rest = rest[1:]
}
if !strings.HasPrefix(rest, "* @param") {
return "", false
}
rest = rest[len("* @param"):]
if !startsWithSingleLineWhitespace(rest) {
return "", false
}
rest = strings.TrimLeft(rest, " \t")
var typeText string
if strings.HasPrefix(rest, "{") {
closeBrace := strings.IndexByte(rest, '}')
if closeBrace < 0 {
return "", false
}
typeText = " " + rest[:closeBrace+1]
rest = rest[closeBrace+1:]
if !startsWithSingleLineWhitespace(rest) {
return "", false
}
rest = strings.TrimLeft(rest, " \t")
}
paramName, rest, ok := scanNonWhitespace(rest)
if !ok || !isOnlySpacesOrTabs(rest) {
return "", false
}
out := prefix + "* @param "
if typeText == " {any}" || typeText == " {*}" {
out += fmt.Sprintf("{${%d:*}} ", *snippetIndex)
*snippetIndex++
} else if typeText != "" {
out += typeText + " "
}
out += fmt.Sprintf("%s ${%d}", paramName, *snippetIndex)
*snippetIndex++
return out, true
}
func transformJSDocReturnsLine(line string, snippetIndex *int) (string, bool) {
prefix := ""
rest := line
if strings.HasPrefix(rest, " ") {
prefix = " "
rest = rest[1:]
}
if !strings.HasPrefix(rest, "* @returns") || !isOnlySpacesOrTabs(rest[len("* @returns"):]) {
return "", false
}
text := fmt.Sprintf("%s* @returns ${%d}", prefix, *snippetIndex)
*snippetIndex++
return text, true
}
func scanNonWhitespace(text string) (word string, rest string, ok bool) {
if text == "" {
return "", "", false
}
for i := 0; i < len(text); {
ch, size := stringutil.DecodeJSStringRune(text[i:])
if size == 0 || stringutil.IsWhiteSpaceLike(ch) {
if i == 0 {
return "", "", false
}
return text[:i], text[i:], true
}
i += size
}
return text, "", true
}
func isJSDocSnippetPrefix(prefix string) bool {
trimmed := trimRightSingleLineWhitespace(prefix)
if strings.HasSuffix(trimmed, "/**") {
return true
}
start := skipSingleLineWhitespace(prefix, 0)
if start >= len(trimmed) || trimmed[start] != '/' {
return false
}
if start+3 > len(trimmed) {
return false
}
for i := start + 1; i < len(trimmed); i++ {
if trimmed[i] != '*' {
return false
}
}
return len(trimmed)-start >= 3
}
func getJSDocSnippetPrefixStart(prefix string) (int, bool) {
trimmed := trimRightSingleLineWhitespace(prefix)
for i := len(trimmed) - 1; i >= 0 && trimmed[i] == '*'; i-- {
if i > 0 && trimmed[i-1] == '/' {
return i - 1, true
}
}
if strings.HasSuffix(trimmed, "/") {
return len(trimmed) - 1, true
}
return 0, false
}
func isJSDocSnippetSuffix(suffix string) bool {
trimmed := trimRightSingleLineWhitespace(suffix[skipSingleLineWhitespace(suffix, 0):])
if trimmed == "" {
return true
}
if !strings.HasSuffix(trimmed, "/") {
return false
}
for i := range len(trimmed) - 1 {
if trimmed[i] != '*' {
return false
}
}
return true
}
func getJSDocSnippetSuffixEnd(suffix string) (int, bool) {
pos := skipSingleLineWhitespace(suffix, 0)
for pos < len(suffix) && suffix[pos] == '*' {
pos++
}
if pos < len(suffix) && suffix[pos] == '/' {
return pos + 1, true
}
return 0, false
}
func trimRightSingleLineWhitespace(text string) string {
end := 0
for pos := 0; pos < len(text); {
ch, size := stringutil.DecodeJSStringRune(text[pos:])
if size == 0 {
break
}
pos += size
if !stringutil.IsWhiteSpaceSingleLine(ch) {
end = pos
}
}
return text[:end]
}
func skipSingleLineWhitespace(text string, pos int) int {
for pos < len(text) {
ch, size := stringutil.DecodeJSStringRune(text[pos:])
if size == 0 || !stringutil.IsWhiteSpaceSingleLine(ch) {
break
}
pos += size
}
return pos
}
func isOnlySingleLineWhitespace(text string) bool {
return skipSingleLineWhitespace(text, 0) == len(text)
}
func startsWithSingleLineWhitespace(text string) bool {
if text == "" {
return false
}
ch, size := stringutil.DecodeJSStringRune(text)
return size != 0 && stringutil.IsWhiteSpaceSingleLine(ch)
}
func isOnlySpacesOrTabs(text string) bool {
for i := range len(text) {
if text[i] != ' ' && text[i] != '\t' {
return false
}
}
return true
}

View File

@@ -0,0 +1,132 @@
package ls
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/ls/autoimport"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/sourcemap"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)
type LanguageService struct {
projectPath tspath.Path
host Host
activeConfig lsutil.UserPreferences
program *compiler.Program
converters *lsconv.Converters
documentPositionMappers map[string]*sourcemap.DocumentPositionMapper
}
func NewLanguageService(
projectPath tspath.Path,
program *compiler.Program,
host Host,
activeFile string,
) *LanguageService {
return &LanguageService{
projectPath: projectPath,
host: host,
program: program,
converters: host.Converters(),
activeConfig: host.GetPreferences(activeFile),
documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{},
}
}
func (l *LanguageService) toPath(fileName string) tspath.Path {
return tspath.ToPath(fileName, l.program.GetCurrentDirectory(), l.UseCaseSensitiveFileNames())
}
func (l *LanguageService) GetProgram() *compiler.Program {
return l.program
}
func (l *LanguageService) UserPreferences() lsutil.UserPreferences {
return l.activeConfig
}
func (l *LanguageService) FormatOptions() lsutil.FormatCodeSettings {
return l.activeConfig.FormatCodeSettings
}
func (l *LanguageService) tryGetProgramAndFile(fileName string) (*compiler.Program, *ast.SourceFile) {
program := l.GetProgram()
file := program.GetSourceFile(fileName)
return program, file
}
func (l *LanguageService) getProgramAndFile(documentURI lsproto.DocumentUri) (*compiler.Program, *ast.SourceFile) {
fileName := documentURI.FileName()
program, file := l.tryGetProgramAndFile(fileName)
if file == nil {
panic("file not found: " + fileName)
}
return program, file
}
func (l *LanguageService) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper {
d, ok := l.documentPositionMappers[fileName]
if !ok {
d = sourcemap.GetDocumentPositionMapper(l, fileName)
l.documentPositionMappers[fileName] = d
}
return d
}
func (l *LanguageService) ReadFile(fileName string) (string, bool) {
return l.host.ReadFile(fileName)
}
func (l *LanguageService) UseCaseSensitiveFileNames() bool {
return l.host.UseCaseSensitiveFileNames()
}
func (l *LanguageService) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo {
return l.host.GetECMALineInfo(fileName)
}
// getPreparedAutoImportView returns an auto-import view for the given file if the registry is prepared
// to provide up-to-date auto-imports for it. If not, it returns ErrNeedsAutoImports.
// If auto-imports are disabled via user preferences, it returns (nil, nil).
func (l *LanguageService) getPreparedAutoImportView(fromFile *ast.SourceFile) (*autoimport.View, error) {
if l.UserPreferences().IncludeCompletionsForModuleExports.IsFalse() {
return nil, nil
}
registry := l.host.AutoImportRegistry()
if !registry.IsPreparedForImportingFile(fromFile.FileName(), l.projectPath, l.UserPreferences()) {
return nil, ErrNeedsAutoImports
}
view := autoimport.NewView(registry, fromFile, l.projectPath, l.program, l.UserPreferences().ModuleSpecifierPreferences())
return view, nil
}
// getCurrentAutoImportView returns an auto-import view for the given file, based on the current state
// of the auto-import registry, which may or may not be up-to-date.
func (l *LanguageService) getCurrentAutoImportView(fromFile *ast.SourceFile) *autoimport.View {
return autoimport.NewView(
l.host.AutoImportRegistry(),
fromFile,
l.projectPath,
l.program,
l.UserPreferences().ModuleSpecifierPreferences(),
)
}
// Used for module specifier completions.
func (l *LanguageService) DirectoryExists(path string) bool {
return l.host.DirectoryExists(path)
}
// Used for module specifier completions.
func (l *LanguageService) ReadDirectory(path string, extensions []string, includes []string) []string {
return l.host.ReadDirectory(l.program.GetCurrentDirectory(), path, extensions, nil /*excludes*/, includes, vfsmatch.UnlimitedDepth)
}
func (l *LanguageService) GetDirectories(path string) []string {
return l.host.GetDirectories(path)
}

View File

@@ -0,0 +1,107 @@
package ls
import (
"context"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/debug"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
)
// allow the client to match more than valid tag names. This allows linked editing when typing is in progress or tag name is incomplete
var jsxTagWordPattern = new("[a-zA-Z0-9:\\-\\._$]*")
func (l *LanguageService) ProvideLinkedEditingRange(ctx context.Context, params *lsproto.LinkedEditingRangeParams) (lsproto.LinkedEditingRangeResponse, error) {
_, sourceFile := l.getProgramAndFile(params.TextDocument.Uri)
position := l.converters.LineAndCharacterToPosition(sourceFile, params.Position)
token := astnav.FindPrecedingToken(sourceFile, int(position))
if token == nil || token.Parent.Kind == ast.KindSourceFile {
return lsproto.LinkedEditingRangeResponse{}, nil
}
if ast.IsJsxFragment(token.Parent.Parent) {
fragment := token.Parent.Parent.AsJsxFragment()
openFragment := fragment.OpeningFragment
closeFragment := fragment.ClosingFragment
if openFragment.Flags&ast.NodeFlagsThisNodeOrAnySubNodesHasError != 0 || closeFragment.Flags&ast.NodeFlagsThisNodeOrAnySubNodesHasError != 0 {
return lsproto.LinkedEditingRangeResponse{}, nil
}
openPos := core.TextPos(astnav.GetStartOfNode(openFragment.AsNode(), sourceFile, false) + len("<"))
closePos := core.TextPos(astnav.GetStartOfNode(closeFragment.AsNode(), sourceFile, false) + len("</"))
// only allows linked editing right after opening bracket: <| ></| >
if (position != openPos) && (position != closePos) {
return lsproto.LinkedEditingRangeResponse{}, nil
}
openLineChar := l.converters.PositionToLineAndCharacter(sourceFile, openPos)
closeLineChar := l.converters.PositionToLineAndCharacter(sourceFile, closePos)
return lsproto.LinkedEditingRangeResponse{
LinkedEditingRanges: &lsproto.LinkedEditingRanges{
Ranges: []lsproto.Range{
{Start: openLineChar, End: openLineChar}, // only return start position for opening tag since the length of a fragment is always 3 and it is unlikely user will type in the middle of a fragment tag
{Start: closeLineChar, End: closeLineChar},
},
WordPattern: jsxTagWordPattern,
},
}, nil
} else {
// determines if the cursor is in an element tag
tag := ast.FindAncestor(token.Parent, func(n *ast.Node) bool {
if ast.IsJsxOpeningElement(n) || ast.IsJsxClosingElement(n) {
return true
}
return false
})
if tag == nil {
return lsproto.LinkedEditingRangeResponse{}, nil
}
debug.Assert(ast.IsJsxOpeningElement(tag) || ast.IsJsxClosingElement(tag), "tag should be opening or closing element")
jsxElement := tag.Parent.AsJsxElement()
openTag := jsxElement.OpeningElement
closeTag := jsxElement.ClosingElement
openTagNameStart := astnav.GetStartOfNode(openTag.TagName().AsNode(), sourceFile, false)
openTagNameEnd := openTag.TagName().End()
closeTagNameStart := astnav.GetStartOfNode(closeTag.TagName().AsNode(), sourceFile, false)
closeTagNameEnd := closeTag.TagName().End()
// do not return linked cursors if tags are not well-formed
if openTagNameStart == astnav.GetStartOfNode(openTag.AsNode(), sourceFile, false) || closeTagNameStart == astnav.GetStartOfNode(closeTag.AsNode(), sourceFile, false) ||
openTagNameEnd == openTag.End() || closeTagNameEnd == closeTag.End() {
return lsproto.LinkedEditingRangeResponse{}, nil
}
// only return linked cursors if the cursor is within a tag name
positionInt := int(position)
if !(openTagNameStart <= positionInt && positionInt <= openTagNameEnd || closeTagNameStart <= positionInt && positionInt <= closeTagNameEnd) {
return lsproto.LinkedEditingRangeResponse{}, nil
}
// only return linked cursors if text in both tags is identical
openingTagText := scanner.GetTextOfNode(openTag.TagName().AsNode())
if openingTagText != scanner.GetTextOfNode(closeTag.TagName().AsNode()) {
return lsproto.LinkedEditingRangeResponse{}, nil
}
return lsproto.LinkedEditingRangeResponse{
LinkedEditingRanges: &lsproto.LinkedEditingRanges{
Ranges: []lsproto.Range{
{
Start: l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(openTagNameStart)),
End: l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(openTagNameEnd)),
},
{
Start: l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(closeTagNameStart)),
End: l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(closeTagNameEnd)),
},
},
WordPattern: jsxTagWordPattern,
},
}, nil
}
}

View File

@@ -0,0 +1,356 @@
package lsconv
import (
"context"
"fmt"
"net/url"
"slices"
"strings"
"unicode/utf16"
"unicode/utf8"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/diagnosticwriter"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/tspath"
)
type Converters struct {
getLineMap func(fileName string) *LSPLineMap
positionEncoding lsproto.PositionEncodingKind
}
type Script interface {
FileName() string
Text() string
}
func NewConverters(positionEncoding lsproto.PositionEncodingKind, getLineMap func(fileName string) *LSPLineMap) *Converters {
return &Converters{
getLineMap: getLineMap,
positionEncoding: positionEncoding,
}
}
func (c *Converters) ToLSPRange(script Script, textRange core.TextRange) lsproto.Range {
return lsproto.Range{
Start: c.PositionToLineAndCharacter(script, core.TextPos(textRange.Pos())),
End: c.PositionToLineAndCharacter(script, core.TextPos(textRange.End())),
}
}
func (c *Converters) FromLSPRange(script Script, textRange lsproto.Range) core.TextRange {
return core.NewTextRange(
int(c.LineAndCharacterToPosition(script, textRange.Start)),
int(c.LineAndCharacterToPosition(script, textRange.End)),
)
}
func (c *Converters) FromLSPTextChange(script Script, change *lsproto.TextDocumentContentChangePartial) core.TextChange {
return core.TextChange{
TextRange: c.FromLSPRange(script, change.Range),
NewText: change.Text,
}
}
func (c *Converters) ToLSPLocation(script Script, rng core.TextRange) lsproto.Location {
return lsproto.Location{
Uri: FileNameToDocumentURI(script.FileName()),
Range: c.ToLSPRange(script, rng),
}
}
func LanguageKindToScriptKind(languageID lsproto.LanguageKind) core.ScriptKind {
switch languageID {
case "typescript":
return core.ScriptKindTS
case "typescriptreact":
return core.ScriptKindTSX
case "javascript":
return core.ScriptKindJS
case "javascriptreact":
return core.ScriptKindJSX
case "json":
return core.ScriptKindJSON
default:
return core.ScriptKindUnknown
}
}
// https://github.com/microsoft/vscode-uri/blob/edfdccd976efaf4bb8fdeca87e97c47257721729/src/uri.ts#L455
var extraEscapeReplacer = strings.NewReplacer(
":", "%3A",
"/", "%2F",
"?", "%3F",
"#", "%23",
"[", "%5B",
"]", "%5D",
"@", "%40",
"!", "%21",
"$", "%24",
"&", "%26",
"'", "%27",
"(", "%28",
")", "%29",
"*", "%2A",
"+", "%2B",
",", "%2C",
";", "%3B",
"=", "%3D",
" ", "%20",
)
func FileNameToDocumentURI(fileName string) lsproto.DocumentUri {
if bundled.IsBundled(fileName) {
return lsproto.DocumentUri(fileName)
}
if tspath.IsDynamicFileName(fileName) {
scheme, rest, ok := strings.Cut(fileName[2:], "/")
if !ok {
panic("invalid file name: " + fileName)
}
authority, path, ok := strings.Cut(rest, "/")
if !ok {
panic("invalid file name: " + fileName)
}
if authority == "ts-nul-authority" {
return lsproto.DocumentUri(scheme + ":" + path)
}
return lsproto.DocumentUri(scheme + "://" + authority + "/" + path)
}
volume, fileName, _ := tspath.SplitVolumePath(fileName)
if volume != "" {
volume = "/" + extraEscapeReplacer.Replace(volume)
}
fileName = strings.TrimPrefix(fileName, "//")
parts := strings.Split(fileName, "/")
for i, part := range parts {
parts[i] = extraEscapeReplacer.Replace(url.PathEscape(part))
}
return lsproto.DocumentUri("file://" + volume + strings.Join(parts, "/"))
}
func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter lsproto.Position) core.TextPos {
// UTF-8/16 0-indexed line and character to UTF-8 offset
lineMap := c.getLineMap(script.FileName())
line := core.TextPos(lineAndCharacter.Line)
char := core.TextPos(lineAndCharacter.Character)
textLen := core.TextPos(len(script.Text()))
// Clamp line to valid range.
if int(line) >= len(lineMap.LineStarts) {
return textLen
}
start := lineMap.LineStarts[line]
// Determine the end of this line (start of next line, or end of text).
var lineEnd core.TextPos
if int(line)+1 < len(lineMap.LineStarts) {
lineEnd = lineMap.LineStarts[int(line)+1]
} else {
lineEnd = textLen
}
if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 {
return max(start, min(start+char, lineEnd))
}
// Scan from line start counting UTF-16 code units to find the byte position.
// Uses DecodeRuneInString (not range + RuneLen) so that invalid UTF-8 bytes
// advance by their actual size (1) rather than RuneLen(RuneError) == 3.
// This matches the approach in scanner.ComputePositionOfLineAndUTF16Character.
var utf16Char core.TextPos
pos := int(start)
end := int(lineEnd)
text := script.Text()
for pos < end {
r, size := utf8.DecodeRuneInString(text[pos:])
u16Len := core.TextPos(utf16.RuneLen(r))
if utf16Char+u16Len > char {
break
}
utf16Char += u16Len
pos += size
}
return core.TextPos(pos)
}
func (c *Converters) PositionToLineAndCharacter(script Script, position core.TextPos) lsproto.Position {
// UTF-8 offset to UTF-8/16 0-indexed line and character
position = max(0, min(position, core.TextPos(len(script.Text()))))
lineMap := c.getLineMap(script.FileName())
line, isLineStart := slices.BinarySearch(lineMap.LineStarts, position)
if !isLineStart {
line--
}
line = max(0, min(line, len(lineMap.LineStarts)-1))
// The current line ranges from lineMap.LineStarts[line] (or 0) to lineMap.LineStarts[line+1] (or len(text)).
start := lineMap.LineStarts[line]
var character core.TextPos
if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 {
character = position - start
} else {
// We need to rescan the text as UTF-16 to find the character offset.
for _, r := range script.Text()[start:position] {
character += core.TextPos(utf16.RuneLen(r))
}
}
return lsproto.Position{
Line: uint32(line),
Character: uint32(character),
}
}
type diagnosticOptions struct {
reportStyleChecksAsWarnings bool
relatedInformation bool
tagValueSet []lsproto.DiagnosticTag
visualStudio bool
}
// DiagnosticToLSPPull converts a diagnostic for pull diagnostics (textDocument/diagnostic)
func DiagnosticToLSPPull(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic, reportStyleChecksAsWarnings bool) *lsproto.Diagnostic {
clientCaps := lsproto.GetClientCapabilities(ctx)
clientDiagnosticCaps := clientCaps.TextDocument.Diagnostic
return diagnosticToLSP(ctx, converters, diagnostic, diagnosticOptions{
reportStyleChecksAsWarnings: reportStyleChecksAsWarnings, // !!! get through context UserPreferences
relatedInformation: clientDiagnosticCaps.RelatedInformation,
tagValueSet: clientDiagnosticCaps.TagSupport.ValueSet,
visualStudio: clientCaps.VSSupportsVisualStudioExtensions,
})
}
// DiagnosticToLSPPush converts a diagnostic for push diagnostics (textDocument/publishDiagnostics)
func DiagnosticToLSPPush(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
clientCaps := lsproto.GetClientCapabilities(ctx)
clientDiagnosticCaps := clientCaps.TextDocument.PublishDiagnostics
return diagnosticToLSP(ctx, converters, diagnostic, diagnosticOptions{
relatedInformation: clientDiagnosticCaps.RelatedInformation,
tagValueSet: clientDiagnosticCaps.TagSupport.ValueSet,
visualStudio: clientCaps.VSSupportsVisualStudioExtensions,
})
}
// https://github.com/microsoft/vscode/blob/93e08afe0469712706ca4e268f778cfadf1a43ef/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts#L40C7-L40C29
var styleCheckDiagnostics = collections.NewSetFromItems(
diagnostics.X_0_is_declared_but_never_used.Code(),
diagnostics.X_0_is_declared_but_its_value_is_never_read.Code(),
diagnostics.Property_0_is_declared_but_its_value_is_never_read.Code(),
diagnostics.All_imports_in_import_declaration_are_unused.Code(),
diagnostics.Unreachable_code_detected.Code(),
diagnostics.Unused_label.Code(),
diagnostics.Fallthrough_case_in_switch.Code(),
diagnostics.Not_all_code_paths_return_a_value.Code(),
)
func diagnosticToLSP(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic, opts diagnosticOptions) *lsproto.Diagnostic {
locale := locale.FromContext(ctx)
var severity lsproto.DiagnosticSeverity
switch diagnostic.Category() {
case diagnostics.CategorySuggestion:
severity = lsproto.DiagnosticSeverityHint
case diagnostics.CategoryMessage:
severity = lsproto.DiagnosticSeverityInformation
case diagnostics.CategoryWarning:
severity = lsproto.DiagnosticSeverityWarning
default:
severity = lsproto.DiagnosticSeverityError
}
if opts.reportStyleChecksAsWarnings && severity == lsproto.DiagnosticSeverityError && styleCheckDiagnostics.Has(diagnostic.Code()) {
severity = lsproto.DiagnosticSeverityWarning
}
var relatedInformation []*lsproto.DiagnosticRelatedInformation
if opts.relatedInformation {
relatedInformation = make([]*lsproto.DiagnosticRelatedInformation, 0, len(diagnostic.RelatedInformation()))
for _, related := range diagnostic.RelatedInformation() {
relatedInformation = append(relatedInformation, &lsproto.DiagnosticRelatedInformation{
Location: lsproto.Location{
Uri: FileNameToDocumentURI(related.File().FileName()),
Range: converters.ToLSPRange(related.File(), related.Loc()),
},
Message: related.Localize(locale),
})
}
}
var tags []lsproto.DiagnosticTag
if len(opts.tagValueSet) > 0 && (diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated()) {
tags = make([]lsproto.DiagnosticTag, 0, 2)
if diagnostic.ReportsUnnecessary() && slices.Contains(opts.tagValueSet, lsproto.DiagnosticTagUnnecessary) {
tags = append(tags, lsproto.DiagnosticTagUnnecessary)
}
if diagnostic.ReportsDeprecated() && slices.Contains(opts.tagValueSet, lsproto.DiagnosticTagDeprecated) {
tags = append(tags, lsproto.DiagnosticTagDeprecated)
}
}
// For diagnostics without a file (e.g., program diagnostics), use a zero range
var lspRange lsproto.Range
if diagnostic.File() != nil {
lspRange = converters.ToLSPRange(diagnostic.File(), diagnostic.Loc())
}
var code *lsproto.IntegerOrString
var source *string
if opts.visualStudio {
code = &lsproto.IntegerOrString{
String: new(fmt.Sprintf("TS%d", diagnostic.Code())),
}
} else {
code = &lsproto.IntegerOrString{
Integer: new(diagnostic.Code()),
}
source = new("ts")
}
return &lsproto.Diagnostic{
Range: lspRange,
Code: code,
Severity: &severity,
Message: lsproto.StringOrMarkupContent{String: new(messageChainToString(diagnostic, locale))},
Source: source,
RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation),
Tags: ptrToSliceIfNonEmpty(tags),
}
}
func messageChainToString(diagnostic *ast.Diagnostic, locale locale.Locale) string {
if len(diagnostic.MessageChain()) == 0 {
return diagnostic.Localize(locale)
}
var b strings.Builder
diagnosticwriter.WriteFlattenedASTDiagnosticMessage(&b, diagnostic, "\n", locale)
return b.String()
}
func ptrToSliceIfNonEmpty[T any](s []T) *[]T {
if len(s) == 0 {
return nil
}
return &s
}

View File

@@ -0,0 +1,329 @@
package lsconv_test
import (
"bytes"
"encoding/binary"
"fmt"
"os/exec"
"testing"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"gotest.tools/v3/assert"
)
func TestDocumentURIToFileName(t *testing.T) {
t.Parallel()
tests := []struct {
uri lsproto.DocumentUri
fileName string
}{
{"file:///path/to/file.ts", "/path/to/file.ts"},
{"file://server/share/file.ts", "//server/share/file.ts"},
{"file:///d%3A/work/tsgo932/lib/utils.ts", "d:/work/tsgo932/lib/utils.ts"},
{"file:///D%3A/work/tsgo932/lib/utils.ts", "d:/work/tsgo932/lib/utils.ts"},
{"file:///d%3A/work/tsgo932/app/%28test%29/comp/comp-test.tsx", "d:/work/tsgo932/app/(test)/comp/comp-test.tsx"},
{"file:///path/to/file.ts#section", "/path/to/file.ts"},
{"file:///c:/test/me", "c:/test/me"},
{"file://shares/files/c%23/p.cs", "//shares/files/c#/p.cs"},
{"file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins/c%23/plugin.json", "c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json"},
{"file:///c:/test %25/path", "c:/test %/path"},
// {"file:?q", "/"},
{"file:///_:/path", "/_:/path"},
{"file:///users/me/c%23-projects/", "/users/me/c#-projects/"},
{"file://localhost/c%24/GitDevelopment/express", "//localhost/c$/GitDevelopment/express"},
{"file:///c%3A/test%20with%20%2525/c%23code", "c:/test with %25/c#code"},
{"untitled:Untitled-1", "^/untitled/ts-nul-authority/Untitled-1"},
{"untitled:Untitled-1#fragment", "^/untitled/ts-nul-authority/Untitled-1#fragment"},
{"untitled:c:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt"},
{"untitled:C:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/C:/Users/jrieken/Code/abc.txt"},
{"untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts", "^/untitled/wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts"},
}
for _, test := range tests {
t.Run(string(test.uri), func(t *testing.T) {
t.Parallel()
assert.Equal(t, test.uri.FileName(), test.fileName)
})
}
}
func TestFileNameToDocumentURI(t *testing.T) {
t.Parallel()
tests := []struct {
fileName string
uri lsproto.DocumentUri
}{
{"/path/to/file.ts", "file:///path/to/file.ts"},
{"//server/share/file.ts", "file://server/share/file.ts"},
{"d:/work/tsgo932/lib/utils.ts", "file:///d%3A/work/tsgo932/lib/utils.ts"},
{"d:/work/tsgo932/lib/utils.ts", "file:///d%3A/work/tsgo932/lib/utils.ts"},
{"d:/work/tsgo932/app/(test)/comp/comp-test.tsx", "file:///d%3A/work/tsgo932/app/%28test%29/comp/comp-test.tsx"},
{"/path/to/file.ts", "file:///path/to/file.ts"},
{"c:/test/me", "file:///c%3A/test/me"},
{"//shares/files/c#/p.cs", "file://shares/files/c%23/p.cs"},
{"c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json", "file:///c%3A/Source/Z%C3%BCrich%20or%20Zurich%20%28%CB%88zj%CA%8A%C9%99r%C9%AAk%2C/Code/resources/app/plugins/c%23/plugin.json"},
{"c:/test %/path", "file:///c%3A/test%20%25/path"},
{"/", "file:///"},
{"/_:/path", "file:///_%3A/path"},
{"/users/me/c#-projects/", "file:///users/me/c%23-projects/"},
{"//localhost/c$/GitDevelopment/express", "file://localhost/c%24/GitDevelopment/express"},
{"c:/test with %25/c#code", "file:///c%3A/test%20with%20%2525/c%23code"},
{"^/untitled/ts-nul-authority/Untitled-1", "untitled:Untitled-1"},
{"^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt", "untitled:c:/Users/jrieken/Code/abc.txt"},
{"^/untitled/ts-nul-authority///wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts", "untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts"},
}
for _, test := range tests {
t.Run(test.fileName, func(t *testing.T) {
t.Parallel()
assert.Equal(t, lsconv.FileNameToDocumentURI(test.fileName), test.uri)
})
}
}
type testScript struct {
name string
text string
}
func (s *testScript) FileName() string { return s.name }
func (s *testScript) Text() string { return s.text }
func newTestConverters(text string) (*lsconv.Converters, *testScript) {
script := &testScript{name: "test.ts", text: text}
lineMap := lsconv.ComputeLSPLineStarts(text)
conv := lsconv.NewConverters(lsproto.PositionEncodingKindUTF16, func(_ string) *lsconv.LSPLineMap {
return lineMap
})
return conv, script
}
// TestConvertersInvalidUTF8 verifies behavior on text containing invalid UTF-8
// sequences (e.g. lone continuation bytes). Node's TextDecoder substitutes such
// bytes with U+FFFD, so the JS-reference test cannot cover this; we assert the
// expected Go-side behavior directly. Each invalid byte advances the byte
// position by 1 and the UTF-16 character by 1 (RuneError = 1 code unit).
func TestConvertersInvalidUTF8(t *testing.T) {
t.Parallel()
// Text with invalid UTF-8 byte 0x80 (continuation byte without start byte).
// Old code used utf8.RuneLen(RuneError)==3, overshooting the byte offset.
text := "a\x80b\ncd"
conv, script := newTestConverters(text)
// (line, char) → byte position. Each row asserts both directions where the
// position lies on a character boundary.
mappings := []struct {
line, char uint32
bytePos core.TextPos
}{
{0, 0, 0}, // 'a'
{0, 1, 1}, // invalid byte 0x80
{0, 2, 2}, // 'b'
{0, 3, 3}, // newline (line end)
{1, 0, 4}, // 'c'
{1, 1, 5}, // 'd'
{1, 2, 6}, // EOF
}
for _, m := range mappings {
lc := lsproto.Position{Line: m.line, Character: m.char}
assert.Equal(t, conv.LineAndCharacterToPosition(script, lc), m.bytePos,
fmt.Sprintf("LineAndCharacterToPosition(%d,%d)", m.line, m.char))
assert.Equal(t, conv.PositionToLineAndCharacter(script, m.bytePos), lc,
fmt.Sprintf("PositionToLineAndCharacter(%d)", m.bytePos))
}
// Byte-by-byte round-trip across the entire text.
for bytePos := core.TextPos(0); bytePos <= core.TextPos(len(text)); bytePos++ {
lc := conv.PositionToLineAndCharacter(script, bytePos)
rt := conv.LineAndCharacterToPosition(script, lc)
assert.Equal(t, rt, bytePos, fmt.Sprintf("round-trip byte %d", bytePos))
}
}
// jsReferenceScript is a Node.js script that, given a list of UTF-8 byte buffers,
// computes the authoritative mapping between (line, character in UTF-16 code units)
// and UTF-8 byte offsets.
//
// To avoid any string round-tripping at the protocol boundary, the inputs are sent
// as raw bytes: the test writes a length-prefixed binary stream to stdin
// ([uint32 little-endian count][uint32 LE len][bytes]...[uint32 LE len][bytes]).
// Node reads the buffers and decodes each with TextDecoder('utf-8') — which is
// essentially what tsserver / sys.ts does when reading file contents from disk
// (read as Buffer, decode as UTF-8 to a JS string with real UTF-16 semantics).
//
// For each input buffer, Node walks the underlying UTF-8 bytes (NOT the decoded
// string) to identify codepoint boundaries: every byte is the start of a codepoint
// unless it's a UTF-8 continuation byte (0b10xxxxxx). At each boundary it records
// the UTF-8 byte offset and the corresponding UTF-16 code unit offset (in the
// decoded JS string) and (line, char) using the LSP line-break rules
// (\n, \r, \r\n only).
//
// Output is JSON on stdout: [ [ { bytePos, line, char }, ... ], ... ]
const jsReferenceScript = `
const inChunks = [];
process.stdin.on('data', c => inChunks.push(c));
process.stdin.on('end', () => {
const buf = Buffer.concat(inChunks);
let off = 0;
const readU32 = () => { const v = buf.readUInt32LE(off); off += 4; return v; };
const n = readU32();
const buffers = [];
for (let i = 0; i < n; i++) {
const len = readU32();
buffers.push(buf.subarray(off, off + len));
off += len;
}
const decoder = new TextDecoder('utf-8', { fatal: true });
const out = buffers.map(bytes => {
// Decode the raw UTF-8 bytes to a JS string (this is what sys.ts does with file contents).
const text = decoder.decode(bytes);
// LSP line starts in the *decoded* JS string: \\n, \\r, \\r\\n only.
const lineStartsJs = [0];
for (let i = 0; i < text.length; i++) {
const c = text.charCodeAt(i);
if (c === 13) {
if (i + 1 < text.length && text.charCodeAt(i + 1) === 10) i++;
lineStartsJs.push(i + 1);
} else if (c === 10) {
lineStartsJs.push(i + 1);
}
}
// Walk the original UTF-8 byte buffer to find codepoint boundaries. Inputs are
// valid UTF-8, so we advance bytePos by the sequence length of each lead byte
// and jsIdx by the corresponding UTF-16 code unit count (1 for BMP, 2 for
// surrogate pair) of the codepoint at jsIdx in the decoded string.
const boundaries = [{ bytePos: 0, jsIdx: 0 }];
let bytePos = 0, jsIdx = 0;
while (bytePos < bytes.length) {
const seq = utf8SeqLen(bytes[bytePos]);
const cp = text.codePointAt(jsIdx);
bytePos += seq;
jsIdx += cp > 0xFFFF ? 2 : 1;
boundaries.push({ bytePos, jsIdx });
}
return boundaries.map(({ bytePos, jsIdx }) => {
let lo = 0, hi = lineStartsJs.length - 1;
while (lo < hi) {
const mid = (lo + hi + 1) >> 1;
if (lineStartsJs[mid] <= jsIdx) lo = mid;
else hi = mid - 1;
}
return { bytePos, line: lo, char: jsIdx - lineStartsJs[lo] };
});
});
process.stdout.write(JSON.stringify(out));
});
function utf8SeqLen(b) {
if (b < 0x80) return 1;
if ((b & 0xE0) === 0xC0) return 2;
if ((b & 0xF0) === 0xE0) return 3;
if ((b & 0xF8) === 0xF0) return 4;
throw new Error('invalid UTF-8 lead byte 0x' + b.toString(16));
}
`
type jsTuple struct {
BytePos int `json:"bytePos"`
Line int `json:"line"`
Char int `json:"char"`
}
func runJSReference(t *testing.T, texts []string) [][]jsTuple {
t.Helper()
if _, err := exec.LookPath("node"); err != nil {
t.Skipf("node not available: %v", err)
}
// Build a length-prefixed binary stream of the raw UTF-8 bytes:
// [uint32 LE count] then for each: [uint32 LE length][bytes].
var in bytes.Buffer
var u32 [4]byte
binary.LittleEndian.PutUint32(u32[:], uint32(len(texts)))
in.Write(u32[:])
for _, s := range texts {
binary.LittleEndian.PutUint32(u32[:], uint32(len(s)))
in.Write(u32[:])
in.WriteString(s)
}
cmd := exec.Command("node", "-e", jsReferenceScript)
cmd.Stdin = &in
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
t.Fatalf("node failed: %v\nstderr: %s", err, stderr.String())
}
var out [][]jsTuple
assert.NilError(t, json.Unmarshal(stdout.Bytes(), &out))
return out
}
// TestConvertersAgainstJSReference cross-checks the Go UTF-16 conversions against
// authoritative results computed by Node.js using real UTF-16 string semantics.
func TestConvertersAgainstJSReference(t *testing.T) {
t.Parallel()
cases := []struct {
name string
text string
}{
{"empty", ""},
{"ascii", "hello\nworld"},
{"ascii_crlf", "hello\r\nworld\r\n!"},
{"ascii_cr_only", "a\rb\rc"},
{"trailing_newline", "abc\n"},
{"bmp_em_dash", "ab\u2014cd\nef"},
{"bmp_multi", "α\nβ\nγδε\nzz"},
{"supplementary_emoji", "x\U0001F600y\nz"}, // 😀 is 4 UTF-8 bytes, 2 UTF-16 units
{"supplementary_at_lineend", "ab\U0001F600\ncd\U0001F60A"},
{"supplementary_only", "\U0001F600\U0001F601\U0001F602"},
{"mixed", "α — \U0001F600\r\nβ\nγ\r"},
{"long_mixed_ws", " \tαβ\n\t\U0001F600 end\n"},
{"zwj_emoji", "\U0001F468\u200D\U0001F4BB\nnext"},
{"only_newlines", "\n\n\r\n\r"},
}
texts := make([]string, len(cases))
for i, c := range cases {
texts[i] = c.text
}
refs := runJSReference(t, texts)
assert.Equal(t, len(refs), len(cases))
for i, c := range cases {
ref := refs[i]
t.Run(c.name, func(t *testing.T) {
t.Parallel()
conv, script := newTestConverters(c.text)
for _, tup := range ref {
bytePos := core.TextPos(tup.BytePos)
expectedLC := lsproto.Position{Line: uint32(tup.Line), Character: uint32(tup.Char)}
gotLC := conv.PositionToLineAndCharacter(script, bytePos)
assert.Equal(t, gotLC, expectedLC,
fmt.Sprintf("PositionToLineAndCharacter(%d) mismatch in %q", bytePos, c.text))
gotPos := conv.LineAndCharacterToPosition(script, expectedLC)
assert.Equal(t, gotPos, bytePos,
fmt.Sprintf("LineAndCharacterToPosition(%d,%d) mismatch in %q", tup.Line, tup.Char, c.text))
}
})
}
}

View File

@@ -0,0 +1,71 @@
package lsconv
import (
"cmp"
"slices"
"strings"
"unicode/utf8"
"github.com/microsoft/typescript-go/internal/core"
)
type LSPLineStarts []core.TextPos
type LSPLineMap struct {
LineStarts LSPLineStarts
AsciiOnly bool // TODO(jakebailey): collect ascii-only info per line
}
func ComputeLSPLineStarts(text string) *LSPLineMap {
// This is like core.ComputeLineStarts, but only considers "\n", "\r", and "\r\n" as line breaks,
// and reports when the text is ASCII-only.
lineStarts := make([]core.TextPos, 0, strings.Count(text, "\n")+1)
asciiOnly := true
textLen := core.TextPos(len(text))
var pos core.TextPos
var lineStart core.TextPos
for pos < textLen {
b := text[pos]
if b < utf8.RuneSelf {
pos++
switch b {
case '\r':
if pos < textLen && text[pos] == '\n' {
pos++
}
fallthrough
case '\n':
lineStarts = append(lineStarts, lineStart)
lineStart = pos
}
} else {
_, size := utf8.DecodeRuneInString(text[pos:])
pos += core.TextPos(size)
asciiOnly = false
}
}
lineStarts = append(lineStarts, lineStart)
return &LSPLineMap{
LineStarts: lineStarts,
AsciiOnly: asciiOnly,
}
}
func (lm *LSPLineMap) ComputeIndexOfLineStart(targetPos core.TextPos) int {
// port of computeLineOfPosition(lineStarts: readonly number[], position: number, lowerBound?: number): number {
lineNumber, ok := slices.BinarySearchFunc(lm.LineStarts, targetPos, func(p, t core.TextPos) int {
return cmp.Compare(int(p), int(t))
})
if !ok && lineNumber > 0 {
// If the actual position was not found, the binary search returns where the target line start would be inserted
// if the target was in the slice.
// e.g. if the line starts at [5, 10, 23, 80] and the position requested was 20
// then the search will return (3, false).
//
// We want the index of the previous line start, so we subtract 1.
lineNumber = lineNumber - 1
}
return lineNumber
}

View File

@@ -0,0 +1,104 @@
package lsutil
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/scanner"
)
func PositionIsASICandidate(pos int, context *ast.Node, file *ast.SourceFile) bool {
contextAncestor := ast.FindAncestorOrQuit(context, func(ancestor *ast.Node) ast.FindAncestorResult {
if ancestor.End() != pos {
return ast.FindAncestorQuit
}
return ast.ToFindAncestorResult(SyntaxMayBeASICandidate(ancestor.Kind))
})
return contextAncestor != nil && NodeIsASICandidate(contextAncestor, file)
}
func SyntaxMayBeASICandidate(kind ast.Kind) bool {
return SyntaxRequiresTrailingCommaOrSemicolonOrASI(kind) ||
SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind) ||
SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind) ||
SyntaxRequiresTrailingSemicolonOrASI(kind)
}
func SyntaxRequiresTrailingCommaOrSemicolonOrASI(kind ast.Kind) bool {
return kind == ast.KindCallSignature ||
kind == ast.KindConstructSignature ||
kind == ast.KindIndexSignature ||
kind == ast.KindPropertySignature ||
kind == ast.KindMethodSignature
}
func SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind ast.Kind) bool {
return kind == ast.KindFunctionDeclaration ||
kind == ast.KindConstructor ||
kind == ast.KindMethodDeclaration ||
kind == ast.KindGetAccessor ||
kind == ast.KindSetAccessor
}
func SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind ast.Kind) bool {
return kind == ast.KindModuleDeclaration
}
func SyntaxRequiresTrailingSemicolonOrASI(kind ast.Kind) bool {
return kind == ast.KindVariableStatement ||
kind == ast.KindExpressionStatement ||
kind == ast.KindDoStatement ||
kind == ast.KindContinueStatement ||
kind == ast.KindBreakStatement ||
kind == ast.KindReturnStatement ||
kind == ast.KindThrowStatement ||
kind == ast.KindDebuggerStatement ||
kind == ast.KindPropertyDeclaration ||
kind == ast.KindTypeAliasDeclaration ||
kind == ast.KindImportDeclaration ||
kind == ast.KindImportEqualsDeclaration ||
kind == ast.KindExportDeclaration ||
kind == ast.KindNamespaceExportDeclaration ||
kind == ast.KindExportAssignment
}
func NodeIsASICandidate(node *ast.Node, file *ast.SourceFile) bool {
lastToken := GetLastToken(node, file)
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
return false
}
if SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) {
if lastToken != nil && lastToken.Kind == ast.KindCommaToken {
return false
}
} else if SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(node.Kind) {
lastChild := GetLastChild(node, file)
if lastChild != nil && ast.IsModuleBlock(lastChild) {
return false
}
} else if SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(node.Kind) {
lastChild := GetLastChild(node, file)
if lastChild != nil && ast.IsFunctionBlock(lastChild) {
return false
}
} else if !SyntaxRequiresTrailingSemicolonOrASI(node.Kind) {
return false
}
// See comment in parser's `parseDoStatement`
if node.Kind == ast.KindDoStatement {
return true
}
topNode := ast.FindAncestor(node, func(ancestor *ast.Node) bool { return ancestor.Parent == nil })
nextToken := astnav.FindNextToken(node, topNode, file)
if nextToken == nil || nextToken.Kind == ast.KindCloseBraceToken {
return true
}
startLine := scanner.GetECMALineOfPosition(file, node.End())
endLine := scanner.GetECMALineOfPosition(file, astnav.GetStartOfNode(nextToken, file, false /*includeJSDoc*/))
return startLine != endLine
}

View File

@@ -0,0 +1,130 @@
package lsutil
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/scanner"
)
// Replaces last(node.getChildren(sourceFile))
func GetLastChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
lastChildNode := GetLastVisitedChild(node, sourceFile)
if ast.IsJSDocSingleCommentNode(node) && lastChildNode == nil {
return nil
}
var tokenStartPos int
if lastChildNode != nil {
tokenStartPos = lastChildNode.End()
} else {
tokenStartPos = node.Pos()
}
var lastToken *ast.Node
scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos)
for startPos := tokenStartPos; startPos < node.End(); {
tokenKind := scanner.Token()
tokenFullStart := scanner.TokenFullStart()
tokenEnd := scanner.TokenEnd()
lastToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node, scanner.TokenFlags())
startPos = tokenEnd
scanner.Scan()
}
return core.IfElse(lastToken != nil, lastToken, lastChildNode)
}
func GetLastToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
if node == nil {
return nil
}
if ast.IsTokenKind(node.Kind) || ast.IsIdentifier(node) {
return nil
}
AssertHasRealPosition(node)
lastChild := GetLastChild(node, sourceFile)
if lastChild == nil {
return nil
}
if lastChild.Kind < ast.KindFirstNode {
return lastChild
} else {
return GetLastToken(lastChild, sourceFile)
}
}
// Gets the last visited child of the given node.
// NOTE: This doesn't include unvisited tokens; for this, use `getLastChild` or `getLastToken`.
func GetLastVisitedChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
var lastChild *ast.Node
visitNode := func(n *ast.Node, _ *ast.NodeVisitor) *ast.Node {
if n != nil && n.Flags&ast.NodeFlagsReparsed == 0 {
lastChild = n
}
return n
}
visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
if nodeList != nil && len(nodeList.Nodes) > 0 {
for i := len(nodeList.Nodes) - 1; i >= 0; i-- {
if nodeList.Nodes[i].Flags&ast.NodeFlagsReparsed == 0 {
lastChild = nodeList.Nodes[i]
break
}
}
}
return nodeList
}
astnav.VisitEachChildAndJSDoc(node, sourceFile, visitNode, visitNodeList)
return lastChild
}
func GetFirstToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
if ast.IsIdentifier(node) || ast.IsTokenKind(node.Kind) {
return nil
}
AssertHasRealPosition(node)
var firstChild *ast.Node
node.ForEachChild(func(n *ast.Node) bool {
if n == nil || node.Flags&ast.NodeFlagsReparsed != 0 {
return false
}
firstChild = n
return true
})
var tokenEndPosition int
if firstChild != nil {
tokenEndPosition = firstChild.Pos()
} else {
tokenEndPosition = node.End()
}
scanner := scanner.GetScannerForSourceFile(sourceFile, node.Pos())
var firstToken *ast.Node
if node.Pos() < tokenEndPosition {
tokenKind := scanner.Token()
tokenFullStart := scanner.TokenFullStart()
tokenEnd := scanner.TokenEnd()
firstToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node, scanner.TokenFlags())
}
if firstToken != nil {
return firstToken
}
if firstChild == nil {
return nil
}
if firstChild.Kind < ast.KindFirstNode {
return firstChild
}
return GetFirstToken(firstChild, sourceFile)
}
func AssertHasRealPosition(node *ast.Node) {
if ast.PositionIsSynthesized(node.Pos()) || ast.PositionIsSynthesized(node.End()) {
panic("Node must have a real position for this operation.")
}
}

View File

@@ -0,0 +1,196 @@
package lsutil
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/scanner"
)
// PositionBelongsToNode returns true if the position belongs to the node.
// Assumes `candidate.Pos() <= position` holds.
func PositionBelongsToNode(candidate *ast.Node, position int, file *ast.SourceFile) bool {
if candidate.Pos() > position {
panic("Expected candidate.pos <= position")
}
return position < candidate.End() || !IsCompletedNode(candidate, file)
}
func IsCompletedNode(n *ast.Node, sourceFile *ast.SourceFile) bool {
if n == nil || ast.NodeIsMissing(n) {
return false
}
switch n.Kind {
case ast.KindClassDeclaration,
ast.KindInterfaceDeclaration,
ast.KindEnumDeclaration,
ast.KindObjectLiteralExpression,
ast.KindObjectBindingPattern,
ast.KindTypeLiteral,
ast.KindBlock,
ast.KindModuleBlock,
ast.KindCaseBlock,
ast.KindNamedImports,
ast.KindNamedExports:
return nodeEndsWith(n, ast.KindCloseBraceToken, sourceFile)
case ast.KindCatchClause:
return IsCompletedNode(n.AsCatchClause().Block, sourceFile)
case ast.KindNewExpression:
if n.ArgumentList() == nil {
return true
}
fallthrough
case ast.KindCallExpression,
ast.KindParenthesizedExpression,
ast.KindParenthesizedType:
return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile)
case ast.KindFunctionType,
ast.KindConstructorType:
return IsCompletedNode(n.Type(), sourceFile)
case ast.KindConstructor,
ast.KindGetAccessor,
ast.KindSetAccessor,
ast.KindFunctionDeclaration,
ast.KindFunctionExpression,
ast.KindMethodDeclaration,
ast.KindMethodSignature,
ast.KindConstructSignature,
ast.KindCallSignature,
ast.KindArrowFunction:
if n.Body() != nil {
return IsCompletedNode(n.Body(), sourceFile)
}
if n.Type() != nil {
return IsCompletedNode(n.Type(), sourceFile)
}
// Even though type parameters can be unclosed, we can get away with
// having at least a closing paren.
return hasChildOfKind(n, ast.KindCloseParenToken, sourceFile)
case ast.KindModuleDeclaration:
return n.Body() != nil && IsCompletedNode(n.Body(), sourceFile)
case ast.KindIfStatement:
if n.AsIfStatement().ElseStatement != nil {
return IsCompletedNode(n.AsIfStatement().ElseStatement, sourceFile)
}
return IsCompletedNode(n.AsIfStatement().ThenStatement, sourceFile)
case ast.KindExpressionStatement:
return IsCompletedNode(n.Expression(), sourceFile) ||
hasChildOfKind(n, ast.KindSemicolonToken, sourceFile)
case ast.KindArrayLiteralExpression,
ast.KindArrayBindingPattern,
ast.KindElementAccessExpression,
ast.KindComputedPropertyName,
ast.KindTupleType:
return nodeEndsWith(n, ast.KindCloseBracketToken, sourceFile)
case ast.KindIndexSignature:
if n.AsIndexSignatureDeclaration().Type != nil {
return IsCompletedNode(n.AsIndexSignatureDeclaration().Type, sourceFile)
}
return hasChildOfKind(n, ast.KindCloseBracketToken, sourceFile)
case ast.KindCaseClause,
ast.KindDefaultClause:
// there is no such thing as terminator token for CaseClause/DefaultClause so for simplicity always consider them non-completed
return false
case ast.KindForStatement,
ast.KindForInStatement,
ast.KindForOfStatement,
ast.KindWhileStatement:
return IsCompletedNode(n.Statement(), sourceFile)
case ast.KindDoStatement:
// rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')';
if hasChildOfKind(n, ast.KindWhileKeyword, sourceFile) {
return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile)
}
return IsCompletedNode(n.Statement(), sourceFile)
case ast.KindTypeQuery:
return IsCompletedNode(n.AsTypeQueryNode().ExprName, sourceFile)
case ast.KindTypeOfExpression,
ast.KindDeleteExpression,
ast.KindVoidExpression,
ast.KindYieldExpression,
ast.KindSpreadElement:
return IsCompletedNode(n.Expression(), sourceFile)
case ast.KindTaggedTemplateExpression:
return IsCompletedNode(n.AsTaggedTemplateExpression().Template, sourceFile)
case ast.KindTemplateExpression:
if n.AsTemplateExpression().TemplateSpans == nil {
return false
}
lastSpan := core.LastOrNil(n.AsTemplateExpression().TemplateSpans.Nodes)
return IsCompletedNode(lastSpan, sourceFile)
case ast.KindTemplateSpan:
return ast.NodeIsPresent(n.AsTemplateSpan().Literal)
case ast.KindExportDeclaration,
ast.KindImportDeclaration:
return ast.NodeIsPresent(n.ModuleSpecifier())
case ast.KindPrefixUnaryExpression:
return IsCompletedNode(n.AsPrefixUnaryExpression().Operand, sourceFile)
case ast.KindBinaryExpression:
return IsCompletedNode(n.AsBinaryExpression().Right, sourceFile)
case ast.KindConditionalExpression:
return IsCompletedNode(n.AsConditionalExpression().WhenFalse, sourceFile)
default:
return true
}
}
// Checks if node ends with 'expectedLastToken'.
// If child at position 'length - 1' is 'SemicolonToken' it is skipped and 'expectedLastToken' is compared with child at position 'length - 2'.
func nodeEndsWith(n *ast.Node, expectedLastToken ast.Kind, sourceFile *ast.SourceFile) bool {
lastChildNode := GetLastVisitedChild(n, sourceFile)
var lastNodeAndTokens []*ast.Node
var tokenStartPos int
if lastChildNode != nil {
lastNodeAndTokens = []*ast.Node{lastChildNode}
tokenStartPos = lastChildNode.End()
} else {
tokenStartPos = n.Pos()
}
scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos)
for startPos := tokenStartPos; startPos < n.End(); {
tokenKind := scanner.Token()
tokenFullStart := scanner.TokenFullStart()
tokenEnd := scanner.TokenEnd()
token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, n, scanner.TokenFlags())
lastNodeAndTokens = append(lastNodeAndTokens, token)
startPos = tokenEnd
scanner.Scan()
}
if len(lastNodeAndTokens) == 0 {
return false
}
lastChild := lastNodeAndTokens[len(lastNodeAndTokens)-1]
if lastChild.Kind == expectedLastToken {
return true
} else if lastChild.Kind == ast.KindSemicolonToken && len(lastNodeAndTokens) > 1 {
return lastNodeAndTokens[len(lastNodeAndTokens)-2].Kind == expectedLastToken
}
return false
}
func hasChildOfKind(containingNode *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) bool {
return astnav.FindChildOfKind(containingNode, kind, sourceFile) != nil
}

View File

@@ -0,0 +1,141 @@
package lsutil
import (
"strings"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/printer"
)
type IndentStyle int
const (
IndentStyleNone IndentStyle = iota
IndentStyleBlock
IndentStyleSmart
)
func parseIndentStyle(v any) IndentStyle {
switch s := v.(type) {
case string:
switch strings.ToLower(s) {
case "none":
return IndentStyleNone
case "block":
return IndentStyleBlock
case "smart":
return IndentStyleSmart
}
case float64:
return IndentStyle(int(s))
case int:
return IndentStyle(s)
}
return IndentStyleSmart
}
type SemicolonPreference string
const (
SemicolonPreferenceIgnore SemicolonPreference = "ignore"
SemicolonPreferenceInsert SemicolonPreference = "insert"
SemicolonPreferenceRemove SemicolonPreference = "remove"
)
func parseSemicolonPreference(v any) SemicolonPreference {
if s, ok := v.(string); ok {
switch strings.ToLower(s) {
case "ignore":
return SemicolonPreferenceIgnore
case "insert":
return SemicolonPreferenceInsert
case "remove":
return SemicolonPreferenceRemove
}
}
return SemicolonPreferenceIgnore
}
type EditorSettings struct {
BaseIndentSize int `raw:"baseIndentSize" config:"format.baseIndentSize"`
IndentSize int `raw:"indentSize" config:"format.indentSize"`
TabSize int `raw:"tabSize" config:"format.tabSize"`
NewLineCharacter string `raw:"newLineCharacter" config:"format.newLineCharacter"`
ConvertTabsToSpaces core.Tristate `raw:"convertTabsToSpaces" config:"format.convertTabsToSpaces"`
IndentStyle IndentStyle `raw:"indentStyle" config:"format.indentStyle"`
TrimTrailingWhitespace core.Tristate `raw:"trimTrailingWhitespace" config:"format.trimTrailingWhitespace"`
}
type FormatCodeSettings struct {
EditorSettings
InsertSpaceAfterCommaDelimiter core.Tristate `raw:"insertSpaceAfterCommaDelimiter" config:"format.insertSpaceAfterCommaDelimiter"`
InsertSpaceAfterSemicolonInForStatements core.Tristate `raw:"insertSpaceAfterSemicolonInForStatements" config:"format.insertSpaceAfterSemicolonInForStatements"`
InsertSpaceBeforeAndAfterBinaryOperators core.Tristate `raw:"insertSpaceBeforeAndAfterBinaryOperators" config:"format.insertSpaceBeforeAndAfterBinaryOperators"`
InsertSpaceAfterConstructor core.Tristate `raw:"insertSpaceAfterConstructor" config:"format.insertSpaceAfterConstructor"`
InsertSpaceAfterKeywordsInControlFlowStatements core.Tristate `raw:"insertSpaceAfterKeywordsInControlFlowStatements" config:"format.insertSpaceAfterKeywordsInControlFlowStatements"`
InsertSpaceAfterFunctionKeywordForAnonymousFunctions core.Tristate `raw:"insertSpaceAfterFunctionKeywordForAnonymousFunctions" config:"format.insertSpaceAfterFunctionKeywordForAnonymousFunctions"`
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"`
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"`
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"`
InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingEmptyBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces"`
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"`
InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"`
InsertSpaceAfterTypeAssertion core.Tristate `raw:"insertSpaceAfterTypeAssertion" config:"format.insertSpaceAfterTypeAssertion"`
InsertSpaceBeforeFunctionParenthesis core.Tristate `raw:"insertSpaceBeforeFunctionParenthesis" config:"format.insertSpaceBeforeFunctionParenthesis"`
PlaceOpenBraceOnNewLineForFunctions core.Tristate `raw:"placeOpenBraceOnNewLineForFunctions" config:"format.placeOpenBraceOnNewLineForFunctions"`
PlaceOpenBraceOnNewLineForControlBlocks core.Tristate `raw:"placeOpenBraceOnNewLineForControlBlocks" config:"format.placeOpenBraceOnNewLineForControlBlocks"`
InsertSpaceBeforeTypeAnnotation core.Tristate `raw:"insertSpaceBeforeTypeAnnotation" config:"format.insertSpaceBeforeTypeAnnotation"`
IndentMultiLineObjectLiteralBeginningOnBlankLine core.Tristate `raw:"indentMultiLineObjectLiteralBeginningOnBlankLine" config:"format.indentMultiLineObjectLiteralBeginningOnBlankLine"`
Semicolons SemicolonPreference `raw:"semicolons" config:"format.semicolons"`
IndentSwitchCase core.Tristate `raw:"indentSwitchCase" config:"format.indentSwitchCase"`
}
func FromLSFormatOptions(f FormatCodeSettings, opt *lsproto.FormattingOptions) FormatCodeSettings {
updatedSettings := f
updatedSettings.TabSize = int(opt.TabSize)
updatedSettings.IndentSize = int(opt.TabSize)
updatedSettings.ConvertTabsToSpaces = core.BoolToTristate(opt.InsertSpaces)
if opt.TrimTrailingWhitespace != nil {
updatedSettings.TrimTrailingWhitespace = core.BoolToTristate(*opt.TrimTrailingWhitespace)
}
return updatedSettings
}
func (settings FormatCodeSettings) ToLSFormatOptions() *lsproto.FormattingOptions {
trimTrailingWhitespace := settings.TrimTrailingWhitespace.IsTrue()
return &lsproto.FormattingOptions{
TabSize: uint32(settings.TabSize),
InsertSpaces: settings.ConvertTabsToSpaces.IsTrue(),
TrimTrailingWhitespace: &trimTrailingWhitespace,
}
}
func GetDefaultFormatCodeSettings() FormatCodeSettings {
return FormatCodeSettings{
EditorSettings: EditorSettings{
IndentSize: printer.GetDefaultIndentSize(),
TabSize: printer.GetDefaultIndentSize(),
NewLineCharacter: "\n",
ConvertTabsToSpaces: core.TSTrue,
IndentStyle: IndentStyleSmart,
TrimTrailingWhitespace: core.TSTrue,
},
InsertSpaceAfterConstructor: core.TSFalse,
InsertSpaceAfterCommaDelimiter: core.TSTrue,
InsertSpaceAfterSemicolonInForStatements: core.TSTrue,
InsertSpaceBeforeAndAfterBinaryOperators: core.TSTrue,
InsertSpaceAfterKeywordsInControlFlowStatements: core.TSTrue,
InsertSpaceAfterFunctionKeywordForAnonymousFunctions: core.TSFalse,
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: core.TSFalse,
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: core.TSFalse,
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: core.TSTrue,
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: core.TSFalse,
InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: core.TSFalse,
InsertSpaceBeforeFunctionParenthesis: core.TSFalse,
PlaceOpenBraceOnNewLineForFunctions: core.TSFalse,
PlaceOpenBraceOnNewLineForControlBlocks: core.TSFalse,
Semicolons: SemicolonPreferenceIgnore,
IndentSwitchCase: core.TSTrue,
}
}

View File

@@ -0,0 +1,695 @@
package lsutil
import (
"cmp"
"math"
"strings"
"unicode"
"unicode/utf8"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/stringutil"
"github.com/microsoft/typescript-go/internal/tspath"
"golang.org/x/text/unicode/norm"
)
// FilterImportDeclarations filters out non-import declarations from a list of statements.
func FilterImportDeclarations(statements []*ast.Statement) []*ast.Statement {
return core.Filter(statements, func(stmt *ast.Statement) bool {
return stmt.Kind == ast.KindImportDeclaration
})
}
// GetDetectionLists returns the lists of comparers and type orders to test for organize imports detection.
func GetDetectionLists(preferences UserPreferences) (comparersToTest []func(a, b string) int, typeOrdersToTest []OrganizeImportsTypeOrder) {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
comparersToTest = []func(a, b string) int{getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort)}
} else if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
comparersToTest = []func(a, b string) int{getOrganizeImportsStringComparer(preferences, preferences.OrganizeImportsIgnoreCase.IsTrue())}
} else {
comparersToTest = []func(a, b string) int{
getOrganizeImportsStringComparer(preferences, true),
getOrganizeImportsStringComparer(preferences, false),
}
}
if preferences.OrganizeImportsTypeOrder != OrganizeImportsTypeOrderAuto {
typeOrdersToTest = []OrganizeImportsTypeOrder{preferences.OrganizeImportsTypeOrder}
} else {
typeOrdersToTest = []OrganizeImportsTypeOrder{
OrganizeImportsTypeOrderLast,
OrganizeImportsTypeOrderInline,
OrganizeImportsTypeOrderFirst,
}
}
return comparersToTest, typeOrdersToTest
}
func ResolveOrganizeImportsSort(preferences UserPreferences) OrganizeImportsSort {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
return preferences.OrganizeImportsSort
}
if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode {
switch preferences.OrganizeImportsIgnoreCase {
case core.TSTrue:
return OrganizeImportsSortNaturalIgnoreCase
case core.TSFalse:
return OrganizeImportsSortNatural
default:
return OrganizeImportsSortAuto
}
}
switch preferences.OrganizeImportsIgnoreCase {
case core.TSTrue:
return OrganizeImportsSortOrdinalIgnoreCase
case core.TSFalse:
return OrganizeImportsSortOrdinal
default:
return OrganizeImportsSortAuto
}
}
func getOrganizeImportsOrdinalStringComparer(ignoreCase bool) func(a, b string) int {
if ignoreCase {
return stringutil.CompareStringsCaseInsensitiveEslintCompatible
}
return stringutil.CompareStringsCaseSensitive
}
func getOrganizeImportsNaturalStringComparer(caseSensitive bool) func(a, b string) int {
return func(a, b string) int {
return compareOrganizeImportsNaturalStrings(a, b, caseSensitive)
}
}
func getOrganizeImportsUnicodeStringComparer(ignoreCase bool, preferences UserPreferences) func(a, b string) int {
caseFirst := preferences.OrganizeImportsCaseFirst
numeric := preferences.OrganizeImportsNumericCollation.IsTrue()
accents := !preferences.OrganizeImportsAccentCollation.IsFalse()
return func(a, b string) int {
return compareOrganizeImportsUnicodeStrings(a, b, ignoreCase, caseFirst, numeric, accents)
}
}
func compareOrganizeImportsNaturalStrings(a string, b string, caseSensitive bool) int {
if cmp := compareStringsNumeric(naturalCollationKey(a), naturalCollationKey(b)); cmp != 0 {
return cmp
}
if caseSensitive {
if cmp := compareOrganizeImportsCaseUpperFirst(a, b); cmp != 0 {
return cmp
}
}
return strings.Compare(a, b)
}
func compareOrganizeImportsUnicodeStrings(a string, b string, ignoreCase bool, caseFirst OrganizeImportsCaseFirst, numeric bool, accents bool) int {
if cmp := compareOrganizeImportsUnicodeKeys(naturalCollationKey(a), naturalCollationKey(b), numeric); cmp != 0 {
return cmp
}
if accents {
if cmp := compareOrganizeImportsUnicodeKeys(strings.ToLower(a), strings.ToLower(b), numeric); cmp != 0 {
return cmp
}
}
if !ignoreCase {
if cmp := compareOrganizeImportsCase(a, b, caseFirst); cmp != 0 {
return cmp
}
}
return strings.Compare(a, b)
}
func naturalCollationKey(s string) string {
return strings.ToLower(removeDiacritics(s))
}
func removeDiacritics(s string) string {
return strings.Map(func(r rune) rune {
if unicode.Is(unicode.Mn, r) {
return -1
}
return r
}, norm.NFD.String(s))
}
func compareOrganizeImportsUnicodeKeys(a string, b string, numeric bool) int {
if numeric {
return compareStringsNumeric(a, b)
}
return strings.Compare(a, b)
}
func compareStringsNumeric(a string, b string) int {
for len(a) > 0 && len(b) > 0 {
if isASCIIDigit(a[0]) && isASCIIDigit(b[0]) {
aRunEnd := asciiDigitRunEnd(a)
bRunEnd := asciiDigitRunEnd(b)
if cmp := compareNumericText(a[:aRunEnd], b[:bRunEnd]); cmp != 0 {
return cmp
}
a = a[aRunEnd:]
b = b[bRunEnd:]
continue
}
aRune, aSize := utf8.DecodeRuneInString(a)
bRune, bSize := utf8.DecodeRuneInString(b)
if aRune != bRune {
return cmp.Compare(aRune, bRune)
}
a = a[aSize:]
b = b[bSize:]
}
return cmp.Compare(len(a), len(b))
}
func isASCIIDigit(ch byte) bool {
return ch >= '0' && ch <= '9'
}
func asciiDigitRunEnd(s string) int {
i := 0
for i < len(s) && isASCIIDigit(s[i]) {
i++
}
return i
}
func compareNumericText(a string, b string) int {
aDigits := strings.TrimLeft(a, "0")
bDigits := strings.TrimLeft(b, "0")
if aDigits == "" {
aDigits = "0"
}
if bDigits == "" {
bDigits = "0"
}
if len(aDigits) != len(bDigits) {
return cmp.Compare(len(aDigits), len(bDigits))
}
if cmp := strings.Compare(aDigits, bDigits); cmp != 0 {
return cmp
}
return strings.Compare(a, b)
}
func compareOrganizeImportsCaseUpperFirst(a string, b string) int {
return compareOrganizeImportsCase(a, b, OrganizeImportsCaseFirstUpper)
}
func compareOrganizeImportsCase(a string, b string, caseFirst OrganizeImportsCaseFirst) int {
aRunes := []rune(a)
bRunes := []rune(b)
minLen := min(len(aRunes), len(bRunes))
for i := range minLen {
aUpper := unicode.IsUpper(aRunes[i])
bUpper := unicode.IsUpper(bRunes[i])
if aUpper != bUpper {
switch caseFirst {
case OrganizeImportsCaseFirstUpper:
if aUpper {
return -1
}
return 1
case OrganizeImportsCaseFirstLower:
if !aUpper {
return -1
}
return 1
default:
if aUpper {
return 1
}
return -1
}
}
}
return cmp.Compare(len(aRunes), len(bRunes))
}
func getOrganizeImportsPresetStringComparer(sort OrganizeImportsSort) func(a, b string) int {
switch sort {
case OrganizeImportsSortOrdinalIgnoreCase:
return getOrganizeImportsOrdinalStringComparer(true)
case OrganizeImportsSortNatural:
return getOrganizeImportsNaturalStringComparer(true)
case OrganizeImportsSortNaturalIgnoreCase:
return getOrganizeImportsNaturalStringComparer(false)
default:
return getOrganizeImportsOrdinalStringComparer(false)
}
}
func getOrganizeImportsStringComparer(preferences UserPreferences, ignoreCase bool) func(a, b string) int {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
return getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort)
}
if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode {
return getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences)
}
return getOrganizeImportsOrdinalStringComparer(ignoreCase)
}
func getModuleSpecifierExpression(declaration *ast.Statement) *ast.Expression {
switch declaration.Kind {
case ast.KindImportEqualsDeclaration:
importEquals := declaration.AsImportEqualsDeclaration()
if importEquals.ModuleReference.Kind == ast.KindExternalModuleReference {
return importEquals.ModuleReference.Expression()
}
return nil
case ast.KindImportDeclaration:
return declaration.ModuleSpecifier()
case ast.KindVariableStatement:
declarations := declaration.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes
if len(declarations) > 0 {
initializer := declarations[0].Initializer()
if initializer != nil && initializer.Kind == ast.KindCallExpression {
callExpr := initializer.AsCallExpression()
if len(callExpr.Arguments.Nodes) > 0 {
return callExpr.Arguments.Nodes[0]
}
}
}
return nil
default:
return nil
}
}
// GetExternalModuleName returns the module name from a module specifier expression.
func GetExternalModuleName(specifier *ast.Expression) string {
if specifier != nil && ast.IsStringLiteralLike(specifier.AsNode()) {
return specifier.Text()
}
return ""
}
// CompareModuleSpecifiers compares two module specifiers using the given comparer.
func CompareModuleSpecifiers(m1 *ast.Expression, m2 *ast.Expression, comparer func(a, b string) int) int {
name1 := GetExternalModuleName(m1)
name2 := GetExternalModuleName(m2)
if cmp := core.CompareBooleans(name1 == "", name2 == ""); cmp != 0 {
return cmp
}
if cmp := core.CompareBooleans(tspath.IsExternalModuleNameRelative(name1), tspath.IsExternalModuleNameRelative(name2)); cmp != 0 {
return cmp
}
return comparer(name1, name2)
}
func compareImportKind(s1 *ast.Statement, s2 *ast.Statement) int {
return cmp.Compare(getImportKindOrder(s1), getImportKindOrder(s2))
}
// getImportKindOrder returns the sort order for different import kinds:
// 1. Side-effect imports
// 2. Type-only imports
// 3. Namespace imports
// 4. Default imports
// 5. Named imports
// 6. ImportEqualsDeclarations
// 7. Require variable statements
const (
importKindOrderSideEffect = 0
importKindOrderTypeOnly = 1
importKindOrderNamespace = 2
importKindOrderDefault = 3
importKindOrderNamed = 4
importKindOrderImportEquals = 5
importKindOrderRequire = 6
importKindOrderUnknown = 7
)
func getImportKindOrder(s1 *ast.Statement) int {
switch s1.Kind {
case ast.KindImportDeclaration:
importDecl := s1.AsImportDeclaration()
if importDecl.ImportClause == nil {
return importKindOrderSideEffect
}
importClause := importDecl.ImportClause.AsImportClause()
if importClause.IsTypeOnly() {
return importKindOrderTypeOnly
}
if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamespaceImport {
return importKindOrderNamespace
}
if importClause.Name() != nil {
return importKindOrderDefault
}
return importKindOrderNamed
case ast.KindImportEqualsDeclaration:
return importKindOrderImportEquals
case ast.KindVariableStatement:
return importKindOrderRequire
default:
return importKindOrderUnknown
}
}
// CompareImportsOrRequireStatements compares two import or require statements.
func CompareImportsOrRequireStatements(s1 *ast.Statement, s2 *ast.Statement, comparer func(a, b string) int) int {
if cmp := CompareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer); cmp != 0 {
return cmp
}
return compareImportKind(s1, s2)
}
func compareImportOrExportSpecifiers(s1 *ast.Node, s2 *ast.Node, comparer func(a, b string) int, preferences UserPreferences) int {
typeOrder := preferences.OrganizeImportsTypeOrder
s1Name := s1.Name().Text()
s2Name := s2.Name().Text()
switch typeOrder {
case OrganizeImportsTypeOrderFirst:
if cmp := core.CompareBooleans(s2.IsTypeOnly(), s1.IsTypeOnly()); cmp != 0 {
return cmp
}
return comparer(s1Name, s2Name)
case OrganizeImportsTypeOrderInline:
return comparer(s1Name, s2Name)
default: // OrganizeImportsTypeOrderLast
if cmp := core.CompareBooleans(s1.IsTypeOnly(), s2.IsTypeOnly()); cmp != 0 {
return cmp
}
return comparer(s1Name, s2Name)
}
}
// GetNamedImportSpecifierComparer returns a comparer function for sorting import specifiers.
func GetNamedImportSpecifierComparer(preferences UserPreferences, comparer func(a, b string) int) func(s1, s2 *ast.Node) int {
if comparer == nil {
ignoreCase := false
if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue()
}
comparer = getOrganizeImportsStringComparer(preferences, ignoreCase)
}
return func(s1, s2 *ast.Node) int {
return compareImportOrExportSpecifiers(s1, s2, comparer, preferences)
}
}
// GetImportSpecifierInsertionIndex returns the index at which to insert a new import specifier.
func GetImportSpecifierInsertionIndex(sortedImports []*ast.Node, newImport *ast.Node, comparer func(s1, s2 *ast.Node) int) int {
return core.FirstResult(core.BinarySearchUniqueFunc(sortedImports, func(mid int, value *ast.Node) int {
return comparer(value, newImport)
}))
}
// GetImportDeclarationInsertIndex returns the index at which to insert a new import declaration.
func GetImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int {
return core.FirstResult(core.BinarySearchUniqueFunc(sortedImports, func(mid int, value *ast.Statement) int {
return comparer(value, newImport)
}))
}
// GetOrganizeImportsStringComparerWithDetection returns a string comparer based on detecting the order of import statements by the module specifier
func GetOrganizeImportsStringComparerWithDetection(originalImportDecls []*ast.Statement, preferences UserPreferences) (comparer func(a, b string) int, isSorted bool) {
result, sorted := DetectModuleSpecifierCaseBySort([][]*ast.Statement{originalImportDecls}, getComparers(preferences))
return result, sorted
}
func getComparers(preferences UserPreferences) []func(a string, b string) int {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto || !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
ignoreCase := false
if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue()
}
return []func(a, b string) int{getOrganizeImportsStringComparer(preferences, ignoreCase)}
}
return []func(a, b string) int{
getOrganizeImportsStringComparer(preferences, true),
getOrganizeImportsStringComparer(preferences, false),
}
}
type namedImportSortResult struct {
namedImportComparer func(a, b string) int
typeOrder OrganizeImportsTypeOrder
isSorted bool
}
// DetectNamedImportOrganizationBySort detects the order of named imports throughout the file by considering the named imports in each statement as a group
func DetectNamedImportOrganizationBySort(
originalGroups []*ast.Statement,
comparersToTest []func(a, b string) int,
typesToTest []OrganizeImportsTypeOrder,
) (comparer func(a, b string) int, typeOrder OrganizeImportsTypeOrder, found bool) {
result := detectNamedImportOrganizationBySort(originalGroups, comparersToTest, typesToTest)
if result == nil {
return nil, OrganizeImportsTypeOrderLast, false
}
return result.namedImportComparer, result.typeOrder, true
}
func detectNamedImportOrganizationBySort(
originalGroups []*ast.Statement,
comparersToTest []func(a, b string) int,
typesToTest []OrganizeImportsTypeOrder,
) *namedImportSortResult {
var bothNamedImports bool
var importDeclsWithNamed []*ast.Statement
for _, imp := range originalGroups {
if imp.AsImportDeclaration().ImportClause == nil {
continue
}
clause := imp.AsImportDeclaration().ImportClause.AsImportClause()
if clause.NamedBindings == nil || clause.NamedBindings.Kind != ast.KindNamedImports {
continue
}
namedImports := clause.NamedBindings.AsNamedImports()
if len(namedImports.Elements.Nodes) == 0 {
continue
}
if !bothNamedImports {
hasTypeOnly := false
hasRegular := false
for _, elem := range namedImports.Elements.Nodes {
if elem.IsTypeOnly() {
hasTypeOnly = true
} else {
hasRegular = true
}
}
if hasTypeOnly && hasRegular {
bothNamedImports = true
}
}
importDeclsWithNamed = append(importDeclsWithNamed, imp)
}
if len(importDeclsWithNamed) == 0 {
return nil
}
namedImportsByDecl := make([][]*ast.Statement, 0, len(importDeclsWithNamed))
for _, imp := range importDeclsWithNamed {
clause := imp.AsImportDeclaration().ImportClause.AsImportClause()
namedImports := clause.NamedBindings.AsNamedImports()
namedImportsByDecl = append(namedImportsByDecl, namedImports.Elements.Nodes)
}
if !bothNamedImports || len(typesToTest) == 0 {
namesList := make([][]string, len(namedImportsByDecl))
for i, imports := range namedImportsByDecl {
names := make([]string, len(imports))
for j, imp := range imports {
names[j] = imp.Name().Text()
}
namesList[i] = names
}
sortState := detectCaseSensitivityBySort(namesList, comparersToTest)
typeOrder := OrganizeImportsTypeOrderLast
if len(typesToTest) == 1 {
typeOrder = typesToTest[0]
}
return &namedImportSortResult{
namedImportComparer: sortState.comparer,
typeOrder: typeOrder,
isSorted: sortState.isSorted,
}
}
bestDiff := map[OrganizeImportsTypeOrder]int{
OrganizeImportsTypeOrderFirst: math.MaxInt,
OrganizeImportsTypeOrderLast: math.MaxInt,
OrganizeImportsTypeOrderInline: math.MaxInt,
}
bestComparer := map[OrganizeImportsTypeOrder]func(a, b string) int{
OrganizeImportsTypeOrderFirst: comparersToTest[0],
OrganizeImportsTypeOrderLast: comparersToTest[0],
OrganizeImportsTypeOrderInline: comparersToTest[0],
}
for _, curComparer := range comparersToTest {
currDiff := map[OrganizeImportsTypeOrder]int{
OrganizeImportsTypeOrderFirst: 0,
OrganizeImportsTypeOrderLast: 0,
OrganizeImportsTypeOrderInline: 0,
}
for _, importDecl := range namedImportsByDecl {
for _, typeOrder := range typesToTest {
prefs := UserPreferences{OrganizeImportsTypeOrder: typeOrder}
diff := measureSortedness(importDecl, func(n1, n2 *ast.Node) int {
return compareImportOrExportSpecifiers(n1, n2, curComparer, prefs)
})
currDiff[typeOrder] = currDiff[typeOrder] + diff
}
}
for _, typeOrder := range typesToTest {
if currDiff[typeOrder] < bestDiff[typeOrder] {
bestDiff[typeOrder] = currDiff[typeOrder]
bestComparer[typeOrder] = curComparer
}
}
}
for _, bestTypeOrder := range typesToTest {
isBest := true
for _, testTypeOrder := range typesToTest {
if bestDiff[testTypeOrder] < bestDiff[bestTypeOrder] {
isBest = false
break
}
}
if isBest {
return &namedImportSortResult{
namedImportComparer: bestComparer[bestTypeOrder],
typeOrder: bestTypeOrder,
isSorted: bestDiff[bestTypeOrder] == 0,
}
}
}
return &namedImportSortResult{
namedImportComparer: bestComparer[OrganizeImportsTypeOrderLast],
typeOrder: OrganizeImportsTypeOrderLast,
isSorted: bestDiff[OrganizeImportsTypeOrderLast] == 0,
}
}
type caseSensitivityDetectionResult struct {
comparer func(a, b string) int
isSorted bool
}
// DetectModuleSpecifierCaseBySort detects the order of module specifiers based on import statements throughout the module/file
func DetectModuleSpecifierCaseBySort(importDeclsByGroup [][]*ast.Statement, comparersToTest []func(a, b string) int) (comparer func(a, b string) int, isSorted bool) {
moduleSpecifiersByGroup := make([][]string, 0, len(importDeclsByGroup))
for _, importGroup := range importDeclsByGroup {
moduleNames := make([]string, 0, len(importGroup))
for _, decl := range importGroup {
if expr := getModuleSpecifierExpression(decl); expr != nil {
moduleNames = append(moduleNames, GetExternalModuleName(expr))
} else {
moduleNames = append(moduleNames, "")
}
}
moduleSpecifiersByGroup = append(moduleSpecifiersByGroup, moduleNames)
}
result := detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest)
return result.comparer, result.isSorted
}
func detectCaseSensitivityBySort(originalGroups [][]string, comparersToTest []func(a, b string) int) caseSensitivityDetectionResult {
var bestComparer func(a, b string) int
bestDiff := math.MaxInt
for _, curComparer := range comparersToTest {
diffOfCurrentComparer := 0
for _, listToSort := range originalGroups {
if len(listToSort) <= 1 {
continue
}
diff := measureSortedness(listToSort, curComparer)
diffOfCurrentComparer += diff
}
if diffOfCurrentComparer < bestDiff {
bestDiff = diffOfCurrentComparer
bestComparer = curComparer
}
}
if bestComparer == nil && len(comparersToTest) > 0 {
bestComparer = comparersToTest[0]
}
return caseSensitivityDetectionResult{
comparer: bestComparer,
isSorted: bestDiff == 0,
}
}
func measureSortedness[T any](arr []T, comparer func(a, b T) int) int {
i := 0
for j := range len(arr) - 1 {
if comparer(arr[j], arr[j+1]) > 0 {
i++
}
}
return i
}
// GetNamedImportSpecifierComparerWithDetection returns a specifier comparer based on detecting the existing sort order within a single import statement
func GetNamedImportSpecifierComparerWithDetection(importDecl *ast.Node, sourceFile *ast.SourceFile, preferences UserPreferences) (specifierComparer func(s1, s2 *ast.Node) int, isSorted core.Tristate) {
comparersToTest, typeOrdersToTest := GetDetectionLists(preferences)
var importStmt *ast.Statement
if importDecl.Kind == ast.KindImportDeclaration {
importStmt = importDecl
}
specifierComparer = GetNamedImportSpecifierComparer(preferences, comparersToTest[0])
isSorted = core.TSUnknown
if (ResolveOrganizeImportsSort(preferences) == OrganizeImportsSortAuto || preferences.OrganizeImportsTypeOrder == OrganizeImportsTypeOrderAuto) && importStmt != nil {
detectFromDecl := detectNamedImportOrganizationBySort([]*ast.Statement{importStmt}, comparersToTest, typeOrdersToTest)
if detectFromDecl != nil {
isSorted = core.BoolToTristate(detectFromDecl.isSorted)
specifierComparer = GetNamedImportSpecifierComparer(
UserPreferences{OrganizeImportsTypeOrder: detectFromDecl.typeOrder},
detectFromDecl.namedImportComparer,
)
} else if sourceFile != nil {
allImports := FilterImportDeclarations(sourceFile.Statements.Nodes)
detectFromFile := detectNamedImportOrganizationBySort(allImports, comparersToTest, typeOrdersToTest)
if detectFromFile != nil {
isSorted = core.BoolToTristate(detectFromFile.isSorted)
specifierComparer = GetNamedImportSpecifierComparer(
UserPreferences{OrganizeImportsTypeOrder: detectFromFile.typeOrder},
detectFromFile.namedImportComparer,
)
}
}
}
return specifierComparer, isSorted
}

View File

@@ -0,0 +1,438 @@
package lsutil
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
)
type ScriptElementKind int
const (
ScriptElementKindUnknown ScriptElementKind = iota
ScriptElementKindWarning
// predefined type (void) or keyword (class)
ScriptElementKindKeyword
// top level script node
ScriptElementKindScriptElement
// module foo {}
ScriptElementKindModuleElement
// class X {}
ScriptElementKindClassElement
// var x = class X {}
ScriptElementKindLocalClassElement
// interface Y {}
ScriptElementKindInterfaceElement
// type T = ...
ScriptElementKindTypeElement
// enum E {}
ScriptElementKindEnumElement
ScriptElementKindEnumMemberElement
// Inside module and script only.
// const v = ...
ScriptElementKindVariableElement
// Inside function.
ScriptElementKindLocalVariableElement
// using foo = ...
ScriptElementKindVariableUsingElement
// await using foo = ...
ScriptElementKindVariableAwaitUsingElement
// Inside module and script only.
// function f() {}
ScriptElementKindFunctionElement
// Inside function.
ScriptElementKindLocalFunctionElement
// class X { [public|private]* foo() {} }
ScriptElementKindMemberFunctionElement
// class X { [public|private]* [get|set] foo:number; }
ScriptElementKindMemberGetAccessorElement
ScriptElementKindMemberSetAccessorElement
// class X { [public|private]* foo:number; }
// interface Y { foo:number; }
ScriptElementKindMemberVariableElement
// class X { [public|private]* accessor foo: number; }
ScriptElementKindMemberAccessorVariableElement
// class X { constructor() { } }
// class X { static { } }
ScriptElementKindConstructorImplementationElement
// interface Y { ():number; }
ScriptElementKindCallSignatureElement
// interface Y { []:number; }
ScriptElementKindIndexSignatureElement
// interface Y { new():Y; }
ScriptElementKindConstructSignatureElement
// function foo(*Y*: string)
ScriptElementKindParameterElement
ScriptElementKindTypeParameterElement
ScriptElementKindPrimitiveType
ScriptElementKindLabel
ScriptElementKindAlias
ScriptElementKindConstElement
ScriptElementKindLetElement
ScriptElementKindDirectory
ScriptElementKindExternalModuleName
// String literal
ScriptElementKindString
// Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}"
ScriptElementKindLink
// Jsdoc @link: in `{@link C link text}`, the entity name "C"
ScriptElementKindLinkName
// Jsdoc @link: in `{@link C link text}`, the link text "link text"
ScriptElementKindLinkText
)
type ScriptElementKindModifier uint32
const (
ScriptElementKindModifierNone ScriptElementKindModifier = 0
ScriptElementKindModifierPublic ScriptElementKindModifier = 1 << iota
ScriptElementKindModifierPrivate
ScriptElementKindModifierProtected
ScriptElementKindModifierExported
ScriptElementKindModifierAmbient
ScriptElementKindModifierStatic
ScriptElementKindModifierAbstract
ScriptElementKindModifierOptional
ScriptElementKindModifierDeprecated
ScriptElementKindModifierDts
ScriptElementKindModifierTs
ScriptElementKindModifierTsx
ScriptElementKindModifierJs
ScriptElementKindModifierJsx
ScriptElementKindModifierJson
ScriptElementKindModifierDmts
ScriptElementKindModifierMts
ScriptElementKindModifierMjs
ScriptElementKindModifierDcts
ScriptElementKindModifierCts
ScriptElementKindModifierCjs
)
var scriptElementKindModifierNames = []struct {
flag ScriptElementKindModifier
name string
}{
{ScriptElementKindModifierPublic, "public"},
{ScriptElementKindModifierPrivate, "private"},
{ScriptElementKindModifierProtected, "protected"},
{ScriptElementKindModifierExported, "export"},
{ScriptElementKindModifierAmbient, "declare"},
{ScriptElementKindModifierStatic, "static"},
{ScriptElementKindModifierAbstract, "abstract"},
{ScriptElementKindModifierOptional, "optional"},
{ScriptElementKindModifierDeprecated, "deprecated"},
{ScriptElementKindModifierDts, ".d.ts"},
{ScriptElementKindModifierTs, ".ts"},
{ScriptElementKindModifierTsx, ".tsx"},
{ScriptElementKindModifierJs, ".js"},
{ScriptElementKindModifierJsx, ".jsx"},
{ScriptElementKindModifierJson, ".json"},
{ScriptElementKindModifierDmts, ".d.mts"},
{ScriptElementKindModifierMts, ".mts"},
{ScriptElementKindModifierMjs, ".mjs"},
{ScriptElementKindModifierDcts, ".d.cts"},
{ScriptElementKindModifierCts, ".cts"},
{ScriptElementKindModifierCjs, ".cjs"},
}
func (m ScriptElementKindModifier) Strings() collections.Set[string] {
result := collections.Set[string]{}
for _, entry := range scriptElementKindModifierNames {
if m&entry.flag != 0 {
result.Add(entry.name)
}
}
return result
}
var FileExtensionKindModifiers = ScriptElementKindModifierDts |
ScriptElementKindModifierTs |
ScriptElementKindModifierTsx |
ScriptElementKindModifierJs |
ScriptElementKindModifierJsx |
ScriptElementKindModifierJson |
ScriptElementKindModifierDmts |
ScriptElementKindModifierMts |
ScriptElementKindModifierMjs |
ScriptElementKindModifierDcts |
ScriptElementKindModifierCts |
ScriptElementKindModifierCjs
func GetSymbolKind(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind {
result := getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location)
if result != ScriptElementKindUnknown {
return result
}
flags := symbol.CombinedLocalAndExportSymbolFlags()
if flags&ast.SymbolFlagsClass != 0 {
decl := ast.GetDeclarationOfKind(symbol, ast.KindClassExpression)
if decl != nil {
return ScriptElementKindLocalClassElement
}
return ScriptElementKindClassElement
}
if flags&ast.SymbolFlagsEnum != 0 {
return ScriptElementKindEnumElement
}
if flags&ast.SymbolFlagsTypeAlias != 0 {
return ScriptElementKindTypeElement
}
if flags&ast.SymbolFlagsInterface != 0 {
return ScriptElementKindInterfaceElement
}
if flags&ast.SymbolFlagsTypeParameter != 0 {
return ScriptElementKindTypeParameterElement
}
if flags&ast.SymbolFlagsEnumMember != 0 {
return ScriptElementKindEnumMemberElement
}
if flags&ast.SymbolFlagsAlias != 0 {
return ScriptElementKindAlias
}
if flags&ast.SymbolFlagsModule != 0 {
return ScriptElementKindModuleElement
}
return ScriptElementKindUnknown
}
func getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind {
var roots []*ast.Symbol
if typeChecker != nil {
roots = typeChecker.GetRootSymbols(symbol)
} else {
roots = []*ast.Symbol{symbol}
}
// If this is a method from a mapped type, leave as a method so long as it still has a call signature, as opposed to e.g.
// `{ [K in keyof I]: number }`.
if len(roots) == 1 &&
roots[0].Flags&ast.SymbolFlagsMethod != 0 &&
(typeChecker == nil || len(typeChecker.GetCallSignatures(typeChecker.GetNonNullableType(typeChecker.GetTypeOfSymbolAtLocation(symbol, location)))) > 0) {
return ScriptElementKindMemberFunctionElement
}
if typeChecker != nil {
if typeChecker.IsUndefinedSymbol(symbol) {
return ScriptElementKindVariableElement
}
if typeChecker.IsArgumentsSymbol(symbol) {
return ScriptElementKindLocalVariableElement
}
if location.Kind == ast.KindThisKeyword && ast.IsExpression(location) ||
ast.IsThisInTypeQuery(location) {
return ScriptElementKindParameterElement
}
}
flags := symbol.CombinedLocalAndExportSymbolFlags()
if flags&ast.SymbolFlagsVariable != 0 {
if isFirstDeclarationOfSymbolParameter(symbol) {
return ScriptElementKindParameterElement
} else if symbol.ValueDeclaration != nil && ast.IsVarConst(symbol.ValueDeclaration) {
return ScriptElementKindConstElement
} else if symbol.ValueDeclaration != nil && ast.IsVarUsing(symbol.ValueDeclaration) {
return ScriptElementKindVariableUsingElement
} else if symbol.ValueDeclaration != nil && ast.IsVarAwaitUsing(symbol.ValueDeclaration) {
return ScriptElementKindVariableAwaitUsingElement
} else if core.Some(symbol.Declarations, ast.IsLet) {
return ScriptElementKindLetElement
}
if isLocalVariableOrFunction(symbol) {
return ScriptElementKindLocalVariableElement
}
return ScriptElementKindVariableElement
}
if flags&ast.SymbolFlagsFunction != 0 {
if isLocalVariableOrFunction(symbol) {
return ScriptElementKindLocalFunctionElement
}
return ScriptElementKindFunctionElement
}
// FIXME: getter and setter use the same symbol. And it is rare to use only setter without getter, so in most cases the symbol always has getter flag.
// So, even when the location is just on the declaration of setter, this function returns getter.
if flags&ast.SymbolFlagsGetAccessor != 0 {
return ScriptElementKindMemberGetAccessorElement
}
if flags&ast.SymbolFlagsSetAccessor != 0 {
return ScriptElementKindMemberSetAccessorElement
}
if flags&ast.SymbolFlagsMethod != 0 {
return ScriptElementKindMemberFunctionElement
}
if flags&ast.SymbolFlagsConstructor != 0 {
return ScriptElementKindConstructorImplementationElement
}
if flags&ast.SymbolFlagsSignature != 0 {
return ScriptElementKindIndexSignatureElement
}
if flags&ast.SymbolFlagsProperty != 0 {
if typeChecker != nil && flags&ast.SymbolFlagsTransient != 0 && symbol.CheckFlags&ast.CheckFlagsSynthetic != 0 {
// If union property is result of union of non method (property/accessors/variables), it is labeled as property
var unionPropertyKind ScriptElementKind
for _, rootSymbol := range roots {
if rootSymbol.Flags&(ast.SymbolFlagsPropertyOrAccessor|ast.SymbolFlagsVariable) != 0 {
unionPropertyKind = ScriptElementKindMemberVariableElement
break
}
}
if unionPropertyKind == ScriptElementKindUnknown {
// If this was union of all methods,
// make sure it has call signatures before we can label it as method.
typeOfUnionProperty := typeChecker.GetTypeOfSymbolAtLocation(symbol, location)
if len(typeChecker.GetCallSignatures(typeOfUnionProperty)) > 0 {
return ScriptElementKindMemberFunctionElement
}
return ScriptElementKindMemberVariableElement
}
return unionPropertyKind
}
return ScriptElementKindMemberVariableElement
}
return ScriptElementKindUnknown
}
func isFirstDeclarationOfSymbolParameter(symbol *ast.Symbol) bool {
var declaration *ast.Node
if len(symbol.Declarations) > 0 {
declaration = symbol.Declarations[0]
}
result := ast.FindAncestorOrQuit(declaration, func(n *ast.Node) ast.FindAncestorResult {
if ast.IsParameterDeclaration(n) {
return ast.FindAncestorTrue
}
if ast.IsBindingElement(n) || ast.IsObjectBindingPattern(n) || ast.IsArrayBindingPattern(n) {
return ast.FindAncestorFalse
}
return ast.FindAncestorQuit
})
return result != nil
}
func isLocalVariableOrFunction(symbol *ast.Symbol) bool {
if symbol.Parent != nil {
return false // This is exported symbol
}
for _, decl := range symbol.Declarations {
// Function expressions are local
if decl.Kind == ast.KindFunctionExpression {
return true
}
if decl.Kind != ast.KindVariableDeclaration && decl.Kind != ast.KindFunctionDeclaration {
continue
}
// If the parent is not source file or module block, it is a local variable.
parent := decl.Parent
for ; !ast.IsFunctionBlock(parent); parent = parent.Parent {
// Reached source file or module block
if parent.Kind == ast.KindSourceFile || parent.Kind == ast.KindModuleBlock {
break
}
}
if ast.IsFunctionBlock(parent) {
// Parent is in function block.
return true
}
}
return false
}
func GetSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) ScriptElementKindModifier {
if symbol == nil {
return ScriptElementKindModifierNone
}
modifiers := getNormalizedSymbolModifiers(typeChecker, symbol)
if symbol.Flags&ast.SymbolFlagsAlias != 0 && typeChecker != nil {
resolvedSymbol := typeChecker.GetAliasedSymbol(symbol)
if resolvedSymbol != symbol {
modifiers |= getNormalizedSymbolModifiers(typeChecker, resolvedSymbol)
}
}
if symbol.Flags&ast.SymbolFlagsOptional != 0 {
modifiers |= ScriptElementKindModifierOptional
}
return modifiers
}
func getNormalizedSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) ScriptElementKindModifier {
var modifierSet ScriptElementKindModifier
if len(symbol.Declarations) > 0 {
declaration := symbol.Declarations[0]
declarations := symbol.Declarations[1:]
// omit deprecated flag if some declarations are not deprecated
var excludeFlags ast.ModifierFlags
if len(declarations) > 0 &&
isDeprecatedDeclaration(typeChecker, declaration) && // !!! include jsdoc node flags
core.Some(declarations, func(d *ast.Node) bool { return !isDeprecatedDeclaration(typeChecker, d) }) {
excludeFlags = ast.ModifierFlagsDeprecated
} else {
excludeFlags = ast.ModifierFlagsNone
}
modifierSet = getNodeModifiers(typeChecker, declaration, excludeFlags)
}
return modifierSet
}
func isDeprecatedDeclaration(typeChecker *checker.Checker, declaration *ast.Node) bool {
if typeChecker != nil {
return typeChecker.IsDeprecatedDeclaration(declaration)
}
return ast.IsDeprecatedDeclaration(declaration)
}
func getNodeModifiers(typeChecker *checker.Checker, node *ast.Node, excludeFlags ast.ModifierFlags) ScriptElementKindModifier {
var result ScriptElementKindModifier
var flags ast.ModifierFlags
if ast.IsDeclaration(node) {
flags = ast.GetCombinedModifierFlags(node)
if isDeprecatedDeclaration(typeChecker, node) {
flags |= ast.ModifierFlagsDeprecated
}
flags &^= excludeFlags
}
if flags&ast.ModifierFlagsPrivate != 0 {
result |= ScriptElementKindModifierPrivate
}
if flags&ast.ModifierFlagsProtected != 0 {
result |= ScriptElementKindModifierProtected
}
if flags&ast.ModifierFlagsPublic != 0 {
result |= ScriptElementKindModifierPublic
}
if flags&ast.ModifierFlagsStatic != 0 {
result |= ScriptElementKindModifierStatic
}
if flags&ast.ModifierFlagsAbstract != 0 {
result |= ScriptElementKindModifierAbstract
}
if flags&ast.ModifierFlagsExport != 0 {
result |= ScriptElementKindModifierExported
}
if flags&ast.ModifierFlagsDeprecated != 0 {
result |= ScriptElementKindModifierDeprecated
}
if flags&ast.ModifierFlagsAmbient != 0 {
result |= ScriptElementKindModifierAmbient
}
if node.Flags&ast.NodeFlagsAmbient != 0 {
result |= ScriptElementKindModifierAmbient
}
if node.Kind == ast.KindExportAssignment {
result |= ScriptElementKindModifierExported
}
return result
}

View File

@@ -0,0 +1,901 @@
package lsutil
import (
"reflect"
"slices"
"strings"
"sync"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/modulespecifiers"
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)
func NewDefaultUserPreferences() UserPreferences {
return UserPreferences{
FormatCodeSettings: GetDefaultFormatCodeSettings(),
IncludeCompletionsForModuleExports: core.TSTrue,
IncludeCompletionsForImportStatements: core.TSTrue,
EnableAutoClosingTags: core.TSTrue,
EnableJSDocCompletions: core.TSTrue,
GenerateReturnInDocTemplate: core.TSTrue,
AllowRenameOfImportPath: core.TSTrue,
ProvideRefactorNotApplicableReason: core.TSTrue,
EnableFormatting: core.TSTrue,
EnableValidation: core.TSTrue,
DisplayPartsForJSDoc: core.TSTrue,
DisableLineTextInReferences: core.TSTrue,
ReportStyleChecksAsWarnings: core.TSTrue,
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
}
}
// UserPreferences represents TypeScript language service preferences.
//
// Fields are populated using two tags:
// - `raw:"name"` or `raw:"name,invert"` - TypeScript/raw name for unstable section lookup
// - `config:"path.to.setting"` or `config:"path.to.setting,invert"` - VS Code nested config path
//
// At least one tag must be present on each preference field.
// The `,invert` modifier inverts boolean values (e.g., VS Code's "suppress" -> our "include").
type UserPreferences struct {
FormatCodeSettings FormatCodeSettings
QuotePreference QuotePreference `raw:"quotePreference" config:"preferences.quoteStyle"`
LazyConfiguredProjectsFromExternalProject core.Tristate `raw:"lazyConfiguredProjectsFromExternalProject"` // !!!
// A positive integer indicating the maximum length of a hover text before it is truncated.
//
// Default: `500`
MaximumHoverLength int `raw:"maximumHoverLength"` // !!!
// ------- Completions -------
// If enabled, TypeScript will search through all external modules' exports and add them to the completions list.
// This affects lone identifier completions but not completions on the right hand side of `obj.`.
IncludeCompletionsForModuleExports core.Tristate `raw:"includeCompletionsForModuleExports" config:"suggest.autoImports"`
// Enables auto-import-style completions on partially-typed import statements. E.g., allows
// `import write|` to be completed to `import { writeFile } from "fs"`.
IncludeCompletionsForImportStatements core.Tristate `raw:"includeCompletionsForImportStatements" config:"suggest.includeCompletionsForImportStatements"`
// Unless this option is `false`, member completion lists triggered with `.` will include entries
// on potentially-null and potentially-undefined values, with insertion text to replace
// preceding `.` tokens with `?.`.
IncludeAutomaticOptionalChainCompletions core.Tristate `raw:"includeAutomaticOptionalChainCompletions" config:"suggest.includeAutomaticOptionalChainCompletions"`
// If enabled, completions for class members (e.g. methods and properties) will include
// a whole declaration for the member.
// E.g., `class A { f| }` could be completed to `class A { foo(): number {} }`, instead of
// `class A { foo }`.
IncludeCompletionsWithClassMemberSnippets core.Tristate `raw:"includeCompletionsWithClassMemberSnippets" config:"suggest.classMemberSnippets.enabled"` // !!!
// If enabled, object literal methods will have a method declaration completion entry in addition
// to the regular completion entry containing just the method name.
// E.g., `const objectLiteral: T = { f| }` could be completed to `const objectLiteral: T = { foo(): void {} }`,
// in addition to `const objectLiteral: T = { foo }`.
IncludeCompletionsWithObjectLiteralMethodSnippets core.Tristate `raw:"includeCompletionsWithObjectLiteralMethodSnippets" config:"suggest.objectLiteralMethodSnippets.enabled"` // !!!
JsxAttributeCompletionStyle JsxAttributeCompletionStyle `raw:"jsxAttributeCompletionStyle" config:"preferences.jsxAttributeCompletionStyle"`
EnableAutoClosingTags core.Tristate `raw:"autoClosingTags" config:"autoClosingTags.enabled" fallbackConfig:"autoClosingTags"`
EnableJSDocCompletions core.Tristate `raw:"completeJSDocs" config:"suggest.jsdoc.enabled" fallbackConfig:"suggest.completeJSDocs"`
GenerateReturnInDocTemplate core.Tristate `raw:"generateReturnInDocTemplate" config:"suggest.jsdoc.generateReturns"`
// ------- AutoImports --------
ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference `raw:"importModuleSpecifierPreference" config:"preferences.importModuleSpecifier"` // !!!
// Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js"
ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `raw:"importModuleSpecifierEnding" config:"preferences.importModuleSpecifierEnding"` // !!!
AutoImportSpecifierExcludeRegexes []string `raw:"autoImportSpecifierExcludeRegexes" config:"preferences.autoImportSpecifierExcludeRegexes"` // !!!
AutoImportFileExcludePatterns []string `raw:"autoImportFileExcludePatterns" config:"preferences.autoImportFileExcludePatterns"`
AutoImportEntrypointDirectorySearch core.Tristate `raw:"autoImportEntrypointDirectorySearch" config:"preferences.autoImportEntrypointDirectorySearch"`
PreferTypeOnlyAutoImports core.Tristate `raw:"preferTypeOnlyAutoImports" config:"preferences.preferTypeOnlyAutoImports"`
// ------- OrganizeImports -------
// Indicates which deterministic preset should be used to sort imports.
// "auto" detects the existing ordinal case sensitivity where possible.
OrganizeImportsSort OrganizeImportsSort `raw:"organizeImportsSort" config:"preferences.organizeImports.sort"` // !!!
// Indicates whether imports should be organized in a case-insensitive manner.
//
// Default: TSUnknown ("auto" in strada), will perform detection
OrganizeImportsIgnoreCase core.Tristate `raw:"organizeImportsIgnoreCase" config:"preferences.organizeImports.caseSensitivity"` // !!!
// Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value of their
// code points, or via "unicode" natural sorting. This implementation is locale-agnostic and approximates the practical
// import-sorting behavior rather than the full Unicode Collation Algorithm.
//
// Default: Ordinal
OrganizeImportsCollation OrganizeImportsCollation `raw:"organizeImportsCollation" config:"preferences.organizeImports.unicodeCollation"` // !!!
// Indicates the locale to use for "unicode" collation in legacy clients. This is accepted for compatibility, but
// currently ignored because organize-import sorting is deterministic and locale-agnostic.
//
// This preference is ignored if organizeImportsCollation is not `unicode`.
//
// Default: `"en"`
OrganizeImportsLocale string `raw:"organizeImportsLocale" config:"preferences.organizeImports.locale"` // !!!
// Indicates whether numeric collation should be used for digit sequences in strings. When `true`, will collate
// strings such that `a1z < a2z < a100z`. When `false`, will collate strings such that `a1z < a100z < a2z`.
//
// This preference is ignored if organizeImportsCollation is not `unicode`.
//
// Default: `false`
OrganizeImportsNumericCollation core.Tristate `raw:"organizeImportsNumericCollation" config:"preferences.organizeImports.numericCollation"` // !!!
// Indicates whether accents and other diacritic marks are considered unequal for the purpose of sorting.
//
// This preference is ignored if organizeImportsCollation is not `unicode`.
//
// Default: `true`
OrganizeImportsAccentCollation core.Tristate `raw:"organizeImportsAccentCollation" config:"preferences.organizeImports.accentCollation"` // !!!
// Indicates whether upper case or lower case should sort first.
//
// This permission is ignored if:
// - organizeImportsCollation is not `unicode`
// - organizeImportsIgnoreCase is `true`
// - organizeImportsIgnoreCase is `auto` and the auto-detected case sensitivity is case-insensitive.
//
// Default: `false`
OrganizeImportsCaseFirst OrganizeImportsCaseFirst `raw:"organizeImportsCaseFirst" config:"preferences.organizeImports.caseFirst"` // !!!
// Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is type-only.
//
// Default: `auto`, which defaults to `last`
OrganizeImportsTypeOrder OrganizeImportsTypeOrder `raw:"organizeImportsTypeOrder" config:"preferences.organizeImports.typeOrder"` // !!!
// ------- MoveToFile -------
AllowTextChangesInNewFiles core.Tristate `raw:"allowTextChangesInNewFiles"` // !!!
// ------- Rename -------
UseAliasesForRename core.Tristate `raw:"providePrefixAndSuffixTextForRename" config:"preferences.useAliasesForRenames"`
AllowRenameOfImportPath core.Tristate `raw:"allowRenameOfImportPath"`
// ------- CodeFixes/Refactors -------
ProvideRefactorNotApplicableReason core.Tristate `raw:"provideRefactorNotApplicableReason"` // !!!
// ------- InlayHints -------
InlayHints InlayHintsPreferences
// ------- CodeLens -------
CodeLens CodeLensUserPreferences
// ------- Definition -------
PreferGoToSourceDefinition bool `raw:"preferGoToSourceDefinition"`
// ------- Symbols -------
ExcludeLibrarySymbolsInNavTo core.Tristate `raw:"excludeLibrarySymbolsInNavTo" config:"workspaceSymbols.excludeLibrarySymbols"`
// ------- Misc -------
EnableFormatting core.Tristate `raw:"formatEnabled" config:"format.enabled" fallbackConfig:"format.enable"`
EnableValidation core.Tristate `raw:"validateEnabled" config:"validate.enabled" fallbackConfig:"validate.enable"`
DisableSuggestions core.Tristate `raw:"disableSuggestions"` // !!!
DisableLineTextInReferences core.Tristate `raw:"disableLineTextInReferences"` // !!!
DisplayPartsForJSDoc core.Tristate `raw:"displayPartsForJSDoc"` // !!!
ReportStyleChecksAsWarnings core.Tristate `raw:"reportStyleChecksAsWarnings" config:"reportStyleChecksAsWarnings"`
// ------- ATA -------
// DisableAutomaticTypeAcquisition is the deprecated setting from typescript.disableAutomaticTypeAcquisition.
DisableAutomaticTypeAcquisition core.Tristate `raw:"disableAutomaticTypeAcquisition" config:"disableAutomaticTypeAcquisition"`
// AutomaticTypeAcquisitionEnabled is the unified setting from tsserver.automaticTypeAcquisition.enabled under the js/ts section.
// When set, it takes precedence over DisableAutomaticTypeAcquisition.
AutomaticTypeAcquisitionEnabled core.Tristate `raw:"automaticTypeAcquisitionEnabled" config:"tsserver.automaticTypeAcquisition.enabled"`
// TODO: add tsserver.web.typeAcquisition.enabled under the js/ts section for the web variant when web support is implemented.
// ------- Project Configuration -------
// CustomConfigFileName specifies a custom config file name to use before defaulting to tsconfig.json/jsconfig.json.
CustomConfigFileName string `raw:"customConfigFileName" config:"customConfigFileName"`
}
// IsATADisabled returns whether Automatic Type Acquisition is disabled based on user preferences.
// It checks the unified setting (tsserver.automaticTypeAcquisition.enabled) first,
// then falls back to the deprecated setting (disableAutomaticTypeAcquisition).
func (p UserPreferences) IsATADisabled() bool {
if !p.AutomaticTypeAcquisitionEnabled.IsUnknown() {
return !p.AutomaticTypeAcquisitionEnabled.IsTrue()
}
return p.DisableAutomaticTypeAcquisition.IsTrue()
}
type InlayHintsPreferences struct {
IncludeInlayParameterNameHints IncludeInlayParameterNameHints `raw:"includeInlayParameterNameHints" config:"inlayHints.parameterNames.enabled"`
IncludeInlayParameterNameHintsWhenArgumentMatchesName core.Tristate `raw:"includeInlayParameterNameHintsWhenArgumentMatchesName" config:"inlayHints.parameterNames.suppressWhenArgumentMatchesName,invert"`
IncludeInlayFunctionParameterTypeHints core.Tristate `raw:"includeInlayFunctionParameterTypeHints" config:"inlayHints.parameterTypes.enabled"`
IncludeInlayVariableTypeHints core.Tristate `raw:"includeInlayVariableTypeHints" config:"inlayHints.variableTypes.enabled"`
IncludeInlayVariableTypeHintsWhenTypeMatchesName core.Tristate `raw:"includeInlayVariableTypeHintsWhenTypeMatchesName" config:"inlayHints.variableTypes.suppressWhenTypeMatchesName,invert"`
IncludeInlayPropertyDeclarationTypeHints core.Tristate `raw:"includeInlayPropertyDeclarationTypeHints" config:"inlayHints.propertyDeclarationTypes.enabled"`
IncludeInlayFunctionLikeReturnTypeHints core.Tristate `raw:"includeInlayFunctionLikeReturnTypeHints" config:"inlayHints.functionLikeReturnTypes.enabled"`
IncludeInlayEnumMemberValueHints core.Tristate `raw:"includeInlayEnumMemberValueHints" config:"inlayHints.enumMemberValues.enabled"`
}
type CodeLensUserPreferences struct {
ReferencesCodeLensEnabled core.Tristate `raw:"referencesCodeLensEnabled" config:"referencesCodeLens.enabled"`
ImplementationsCodeLensEnabled core.Tristate `raw:"implementationsCodeLensEnabled" config:"implementationsCodeLens.enabled"`
ReferencesCodeLensShowOnAllFunctions core.Tristate `raw:"referencesCodeLensShowOnAllFunctions" config:"referencesCodeLens.showOnAllFunctions"`
ImplementationsCodeLensShowOnInterfaceMethods core.Tristate `raw:"implementationsCodeLensShowOnInterfaceMethods" config:"implementationsCodeLens.showOnInterfaceMethods"`
ImplementationsCodeLensShowOnAllClassMethods core.Tristate `raw:"implementationsCodeLensShowOnAllClassMethods" config:"implementationsCodeLens.showOnAllClassMethods"`
}
// --- Enum Types ---
type QuotePreference string
const (
QuotePreferenceUnknown QuotePreference = ""
QuotePreferenceAuto QuotePreference = "auto"
QuotePreferenceDouble QuotePreference = "double"
QuotePreferenceSingle QuotePreference = "single"
)
type JsxAttributeCompletionStyle string
const (
JsxAttributeCompletionStyleUnknown JsxAttributeCompletionStyle = ""
JsxAttributeCompletionStyleAuto JsxAttributeCompletionStyle = "auto"
JsxAttributeCompletionStyleBraces JsxAttributeCompletionStyle = "braces"
JsxAttributeCompletionStyleNone JsxAttributeCompletionStyle = "none"
)
type IncludeInlayParameterNameHints string
const (
IncludeInlayParameterNameHintsNone IncludeInlayParameterNameHints = ""
IncludeInlayParameterNameHintsAll IncludeInlayParameterNameHints = "all"
IncludeInlayParameterNameHintsLiterals IncludeInlayParameterNameHints = "literals"
)
type OrganizeImportsSort int
const (
OrganizeImportsSortAuto OrganizeImportsSort = iota
OrganizeImportsSortOrdinal
OrganizeImportsSortOrdinalIgnoreCase
OrganizeImportsSortNatural
OrganizeImportsSortNaturalIgnoreCase
)
type OrganizeImportsCollation bool
const (
OrganizeImportsCollationOrdinal OrganizeImportsCollation = false
OrganizeImportsCollationUnicode OrganizeImportsCollation = true
)
type OrganizeImportsCaseFirst int
const (
OrganizeImportsCaseFirstFalse OrganizeImportsCaseFirst = 0
OrganizeImportsCaseFirstLower OrganizeImportsCaseFirst = 1
OrganizeImportsCaseFirstUpper OrganizeImportsCaseFirst = 2
)
type OrganizeImportsTypeOrder int
const (
OrganizeImportsTypeOrderAuto OrganizeImportsTypeOrder = 0
OrganizeImportsTypeOrderLast OrganizeImportsTypeOrder = 1
OrganizeImportsTypeOrderInline OrganizeImportsTypeOrder = 2
OrganizeImportsTypeOrderFirst OrganizeImportsTypeOrder = 3
)
// --- Reflection-based parsing infrastructure ---
// typeParsers maps reflect.Type to a function that parses a value into that type.
var typeParsers = map[reflect.Type]func(any) any{
reflect.TypeFor[core.Tristate](): func(val any) any {
if b, ok := val.(bool); ok {
if b {
return core.TSTrue
}
return core.TSFalse
}
return core.TSUnknown
},
reflect.TypeFor[IndentStyle](): func(val any) any {
return parseIndentStyle(val)
},
reflect.TypeFor[SemicolonPreference](): func(val any) any {
return parseSemicolonPreference(val)
},
reflect.TypeFor[QuotePreference](): func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "auto":
return QuotePreferenceAuto
case "double":
return QuotePreferenceDouble
case "single":
return QuotePreferenceSingle
}
}
return QuotePreferenceUnknown
},
reflect.TypeFor[JsxAttributeCompletionStyle](): func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "braces":
return JsxAttributeCompletionStyleBraces
case "none":
return JsxAttributeCompletionStyleNone
}
}
return JsxAttributeCompletionStyleAuto
},
reflect.TypeFor[IncludeInlayParameterNameHints](): func(val any) any {
if s, ok := val.(string); ok {
switch s {
case "all":
return IncludeInlayParameterNameHintsAll
case "literals":
return IncludeInlayParameterNameHintsLiterals
}
}
return IncludeInlayParameterNameHintsNone
},
reflect.TypeFor[OrganizeImportsSort](): func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "ordinal":
return OrganizeImportsSortOrdinal
case "ordinalignorecase":
return OrganizeImportsSortOrdinalIgnoreCase
case "natural":
return OrganizeImportsSortNatural
case "naturalignorecase":
return OrganizeImportsSortNaturalIgnoreCase
}
}
return OrganizeImportsSortAuto
},
reflect.TypeFor[OrganizeImportsCollation](): func(val any) any {
if s, ok := val.(string); ok && strings.ToLower(s) == "unicode" {
return OrganizeImportsCollationUnicode
}
return OrganizeImportsCollationOrdinal
},
reflect.TypeFor[OrganizeImportsCaseFirst](): func(val any) any {
if s, ok := val.(string); ok {
switch s {
case "lower":
return OrganizeImportsCaseFirstLower
case "upper":
return OrganizeImportsCaseFirstUpper
}
}
return OrganizeImportsCaseFirstFalse
},
reflect.TypeFor[OrganizeImportsTypeOrder](): func(val any) any {
if s, ok := val.(string); ok {
switch s {
case "last":
return OrganizeImportsTypeOrderLast
case "inline":
return OrganizeImportsTypeOrderInline
case "first":
return OrganizeImportsTypeOrderFirst
}
}
return OrganizeImportsTypeOrderAuto
},
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierPreference](): func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "project-relative":
return modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative
case "relative":
return modulespecifiers.ImportModuleSpecifierPreferenceRelative
case "non-relative":
return modulespecifiers.ImportModuleSpecifierPreferenceNonRelative
}
}
return modulespecifiers.ImportModuleSpecifierPreferenceShortest
},
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierEndingPreference](): func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "minimal":
return modulespecifiers.ImportModuleSpecifierEndingPreferenceMinimal
case "index":
return modulespecifiers.ImportModuleSpecifierEndingPreferenceIndex
case "js":
return modulespecifiers.ImportModuleSpecifierEndingPreferenceJs
}
}
return modulespecifiers.ImportModuleSpecifierEndingPreferenceAuto
},
}
// typeSerializers maps reflect.Type to a function that serializes a value of that type.
// For types which do not serialize as-is (tristate, enums, etc).
var typeSerializers = map[reflect.Type]func(any) any{
reflect.TypeFor[core.Tristate](): func(val any) any {
switch val.(core.Tristate) {
case core.TSTrue:
return true
case core.TSFalse:
return false
default:
return nil
}
},
reflect.TypeFor[OrganizeImportsSort](): func(val any) any {
switch val.(OrganizeImportsSort) {
case OrganizeImportsSortOrdinal:
return "ordinal"
case OrganizeImportsSortOrdinalIgnoreCase:
return "ordinalIgnoreCase"
case OrganizeImportsSortNatural:
return "natural"
case OrganizeImportsSortNaturalIgnoreCase:
return "naturalIgnoreCase"
default:
return "auto"
}
},
reflect.TypeFor[OrganizeImportsCollation](): func(val any) any {
if val.(OrganizeImportsCollation) == OrganizeImportsCollationUnicode {
return "unicode"
}
return "ordinal"
},
reflect.TypeFor[OrganizeImportsCaseFirst](): func(val any) any {
switch val.(OrganizeImportsCaseFirst) {
case OrganizeImportsCaseFirstLower:
return "lower"
case OrganizeImportsCaseFirstUpper:
return "upper"
default:
return "default"
}
},
reflect.TypeFor[OrganizeImportsTypeOrder](): func(val any) any {
switch val.(OrganizeImportsTypeOrder) {
case OrganizeImportsTypeOrderLast:
return "last"
case OrganizeImportsTypeOrderInline:
return "inline"
case OrganizeImportsTypeOrderFirst:
return "first"
default:
return "auto"
}
},
// These enums distinguish an unset zero value (e.g. "") from their effective
// default (e.g. "auto"): the parser promotes unset/unknown input to the
// non-zero default. Plain string serialization would therefore write "" for
// an unset field and the parser would read it back as the non-zero default,
// breaking round-tripping. Mirror the core.Tristate serializer above and omit
// the unset value (return nil) so it decodes back to the zero value. (Enums
// whose default already is their zero value, like the OrganizeImports* ones,
// round-trip without this.)
//
// TODO: These three are the only parsers whose fallback is a non-zero value;
// every other parser returns its zero value as the fallback. They should be
// made consistent: change the parser fallback to return the zero value and
// remove this serializer (relying on the default string serialization, which
// already omits ""). The consumer must then treat the zero value as the
// effective default. The two module-specifier enums are safe to convert (all
// read sites already treat the "" zero identically to the promoted default).
reflect.TypeFor[JsxAttributeCompletionStyle](): func(val any) any {
// TODO: make consistent with other enums (see note above). Unlike the
// module-specifier enums, the consumer in completions.go distinguishes
// JsxAttributeCompletionStyleUnknown from ...Auto, so converting this one
// requires updating that consumer to treat the zero value as "auto".
if v := val.(JsxAttributeCompletionStyle); v != JsxAttributeCompletionStyleUnknown {
return string(v)
}
return nil
},
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierPreference](): func(val any) any {
// TODO: make consistent with other enums (see note above): have the parser
// return the zero value (None) as its fallback and drop this serializer.
if v := val.(modulespecifiers.ImportModuleSpecifierPreference); v != "" {
return string(v)
}
return nil
},
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierEndingPreference](): func(val any) any {
// TODO: make consistent with other enums (see note above): have the parser
// return the zero value (None) as its fallback and drop this serializer.
if v := val.(modulespecifiers.ImportModuleSpecifierEndingPreference); v != "" {
return string(v)
}
return nil
},
}
// configPathParsers provides field-specific config value parsers that override the default
// type-based parser when the VS Code config value format differs from the Go field type.
var configPathParsers = map[string]func(any) any{
// VS Code sends caseSensitivity as a string ("auto"/"caseSensitive"/"caseInsensitive"),
// but OrganizeImportsIgnoreCase is a core.Tristate.
"preferences.organizeImports.caseSensitivity": func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "caseinsensitive":
return core.TSTrue
case "casesensitive":
return core.TSFalse
}
}
if b, ok := val.(bool); ok {
if b {
return core.TSTrue
}
return core.TSFalse
}
return core.TSUnknown
},
}
type fieldInfo struct {
rawName string // raw name for unstable section lookup (e.g., "quotePreference")
configPath string // dotted path for config (e.g., "preferences.quoteStyle")
fallbackConfigPaths []configPathInfo
fieldPath []int // index path to field in struct
rawInvert bool // whether to invert boolean values for raw name
configInvert bool // whether to invert boolean values for config path
}
type configPathInfo struct {
path string
invert bool
}
var fieldInfoCache = sync.OnceValue(func() []fieldInfo {
return collectFieldInfos(reflect.TypeFor[UserPreferences](), nil)
})
// unstableNameIndex maps raw names to fieldInfo index for unstable section lookup.
var unstableNameIndex = sync.OnceValue(func() map[string]int {
infos := fieldInfoCache()
index := make(map[string]int, len(infos))
for i, info := range infos {
if info.rawName != "" {
index[info.rawName] = i
}
}
return index
})
func collectFieldInfos(t reflect.Type, indexPath []int) []fieldInfo {
var infos []fieldInfo
for i := range t.NumField() {
field := t.Field(i)
currentPath := append(slices.Clone(indexPath), i)
rawTag := field.Tag.Get("raw")
configTag := field.Tag.Get("config")
fallbackConfigTag := field.Tag.Get("fallbackConfig")
if rawTag == "" && configTag == "" {
// Embedded struct without tags - recurse into it
if field.Type.Kind() == reflect.Struct {
infos = append(infos, collectFieldInfos(field.Type, currentPath)...)
continue
}
panic("raw or config tag required for field " + field.Name)
}
info := fieldInfo{
fieldPath: currentPath,
}
// Parse raw tag: "name" or "name,invert"
if rawTag != "" {
parts := strings.Split(rawTag, ",")
info.rawName = parts[0]
for _, part := range parts[1:] {
if part == "invert" {
info.rawInvert = true
}
}
}
// Parse config tag: "path.to.setting" or "path.to.setting,invert"
if configTag != "" {
configPath := parseConfigPathTag(configTag)
info.configPath = configPath.path
info.configInvert = configPath.invert
}
if fallbackConfigTag != "" {
for tag := range strings.SplitSeq(fallbackConfigTag, ";") {
info.fallbackConfigPaths = append(info.fallbackConfigPaths, parseConfigPathTag(tag))
}
}
infos = append(infos, info)
}
return infos
}
func parseConfigPathTag(tag string) configPathInfo {
parts := strings.Split(tag, ",")
info := configPathInfo{path: parts[0]}
for _, part := range parts[1:] {
if part == "invert" {
info.invert = true
}
}
return info
}
func getNestedValue(config map[string]any, path string) (any, bool) {
parts := strings.Split(path, ".")
current := any(config)
for _, part := range parts {
m, ok := current.(map[string]any)
if !ok {
return nil, false
}
current, ok = m[part]
if !ok {
return nil, false
}
}
return current, true
}
func setNestedValue(config map[string]any, path string, value any) {
parts := strings.Split(path, ".")
current := config
for _, part := range parts[:len(parts)-1] {
next, ok := current[part].(map[string]any)
if !ok {
next = make(map[string]any)
current[part] = next
}
current = next
}
current[parts[len(parts)-1]] = value
}
func setRawFieldsFromConfig(v reflect.Value, infos []fieldInfo, settings map[string]any) {
index := unstableNameIndex()
for name, value := range settings {
if idx, found := index[name]; found {
info := infos[idx]
field := getFieldByPath(v, info.fieldPath)
if info.rawInvert {
if b, ok := value.(bool); ok {
value = !b
}
}
setFieldFromValue(field, value)
}
}
}
func (p UserPreferences) withConfig(config map[string]any) UserPreferences {
v := reflect.ValueOf(&p).Elem()
infos := fieldInfoCache()
// Raw UserPreferences can be provided directly, notably via LSP initializationOptions.
setRawFieldsFromConfig(v, infos, config)
// Process "unstable" section first - allows any field to be set by raw name.
// This mirrors VS Code's behavior: { ...config.get('unstable'), ...stableOptions }
// where stable options are spread after and take precedence.
if unstable, ok := config["unstable"].(map[string]any); ok {
setRawFieldsFromConfig(v, infos, unstable)
}
// Process path-based config (VS Code style nested paths).
// These run after unstable, so stable config values take precedence.
for _, info := range infos {
if info.configPath == "" {
continue
}
configPath := configPathInfo{path: info.configPath, invert: info.configInvert}
val, ok := getNestedValue(config, configPath.path)
if !ok {
for _, fallbackConfigPath := range info.fallbackConfigPaths {
val, ok = getNestedValue(config, fallbackConfigPath.path)
if ok {
configPath = fallbackConfigPath
break
}
}
}
if !ok {
continue
}
field := getFieldByPath(v, info.fieldPath)
if configPath.invert {
if b, ok := val.(bool); ok {
val = !b
}
}
if parser, ok := configPathParsers[configPath.path]; ok {
field.Set(reflect.ValueOf(parser(val)))
continue
}
setFieldFromValue(field, val)
}
// Validate CustomConfigFileName for path traversal
if p.CustomConfigFileName != "" {
name := strings.TrimSpace(p.CustomConfigFileName)
if strings.ContainsAny(name, "/\\") || name == ".." || name == "." {
p.CustomConfigFileName = ""
} else {
p.CustomConfigFileName = name
}
}
return p
}
func getFieldByPath(v reflect.Value, path []int) reflect.Value {
for _, idx := range path {
v = v.Field(idx)
}
return v
}
func setFieldFromValue(field reflect.Value, val any) {
if val == nil {
return
}
// Check custom parsers first (for types like Tristate, enums, etc.)
if parser, ok := typeParsers[field.Type()]; ok {
field.Set(reflect.ValueOf(parser(val)))
return
}
switch field.Kind() {
case reflect.Bool:
if b, ok := val.(bool); ok {
field.SetBool(b)
}
case reflect.Int:
switch v := val.(type) {
case int:
field.SetInt(int64(v))
case float64:
field.SetInt(int64(v))
}
case reflect.String:
if s, ok := val.(string); ok {
field.SetString(s)
}
case reflect.Slice:
if arr, ok := val.([]any); ok {
result := reflect.MakeSlice(field.Type(), 0, len(arr))
for _, item := range arr {
if s, ok := item.(string); ok {
result = reflect.Append(result, reflect.ValueOf(s))
}
}
field.Set(result)
}
}
}
func (p *UserPreferences) MarshalJSONTo(enc *json.Encoder) error {
config := make(map[string]any)
v := reflect.ValueOf(p).Elem()
for _, info := range fieldInfoCache() {
field := getFieldByPath(v, info.fieldPath)
val := serializeField(field)
if val == nil {
continue
}
// Prefer config path if available, otherwise use unstable section
if info.configPath != "" {
if info.configInvert {
if b, ok := val.(bool); ok {
val = !b
}
}
setNestedValue(config, info.configPath, val)
} else if info.rawName != "" {
if info.rawInvert {
if b, ok := val.(bool); ok {
val = !b
}
}
setNestedValue(config, "unstable."+info.rawName, val)
}
}
return json.MarshalEncode(enc, config, json.Deterministic(true))
}
func serializeField(field reflect.Value) any {
// Check custom serializers first (for types like Tristate, enums, etc.)
if serializer, ok := typeSerializers[field.Type()]; ok {
return serializer(field.Interface())
}
switch field.Kind() {
case reflect.Bool:
return field.Bool()
case reflect.Int:
// Zero means "unset" for these preference fields. Omit it so a partial
// config does not clobber defaults with zeros when round-tripped through
// withConfig.
i := field.Int()
if i == 0 {
return nil
}
return int(i)
case reflect.String:
// Zero ("") means "unset"; omit it for the same reason as int above.
s := field.String()
if s == "" {
return nil
}
return s
case reflect.Slice:
if field.IsNil() {
return nil
}
result := make([]string, field.Len())
for i := range field.Len() {
result[i] = field.Index(i).String()
}
return result
default:
return field.Interface()
}
}
func (p *UserPreferences) UnmarshalJSONFrom(dec *json.Decoder) error {
var config map[string]any
if err := json.UnmarshalDecode(dec, &config); err != nil {
return err
}
// Start with defaults, then overlay parsed values
*p = NewDefaultUserPreferences().withConfig(config)
return nil
}
// --- Helper methods ---
func (p UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences {
return modulespecifiers.UserPreferences{
ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference,
ImportModuleSpecifierEnding: p.ImportModuleSpecifierEnding,
AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes,
}
}
func (p UserPreferences) ParsedAutoImportFileExcludePatterns(useCaseSensitiveFileNames bool) *vfsmatch.SpecMatcher {
return vfsmatch.NewSpecMatcher(p.AutoImportFileExcludePatterns, "", vfsmatch.UsageExclude, useCaseSensitiveFileNames)
}
func (p UserPreferences) IsModuleSpecifierExcluded(moduleSpecifier string) bool {
return modulespecifiers.IsExcludedByRegex(moduleSpecifier, p.AutoImportSpecifierExcludeRegexes)
}
func ParseUserPreferences(items map[string]any) UserPreferences {
prefs := NewDefaultUserPreferences()
// Apply editor settings first (tabSize, indentSize, etc.) as raw-name defaults,
// then overlay language-specific settings with increasing precedence:
// editor < javascript < typescript < js/ts
if editorItem, ok := items["editor"]; ok && editorItem != nil {
if editorSettings, ok := editorItem.(map[string]any); ok {
prefs = prefs.withConfig(map[string]any{"unstable": editorSettings})
}
}
// Apply javascript, then typescript, then js/ts (highest precedence).
for _, section := range []string{"javascript", "typescript", "js/ts"} {
if item, ok := items[section]; ok && item != nil {
if settings, ok := item.(map[string]any); ok {
prefs = prefs.withConfig(settings)
}
}
}
return prefs
}

View File

@@ -0,0 +1,644 @@
package lsutil
import (
"reflect"
"testing"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/modulespecifiers"
"gotest.tools/v3/assert"
)
func fillNonZeroValues(v reflect.Value) {
t := v.Type()
for i := range t.NumField() {
field := v.Field(i)
if !field.CanSet() {
continue
}
switch field.Kind() {
case reflect.Bool:
field.SetBool(true)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
field.SetInt(1)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
field.SetUint(1)
case reflect.String:
val := getValidStringValue(field.Type())
field.SetString(val)
case reflect.Slice:
if field.Type().Elem().Kind() == reflect.String {
field.Set(reflect.ValueOf([]string{"test"}))
}
case reflect.Struct:
fillNonZeroValues(field)
}
}
}
func getValidStringValue(t reflect.Type) string {
typeName := t.String()
switch typeName {
case "lsutil.QuotePreference":
return string(QuotePreferenceSingle)
case "lsutil.JsxAttributeCompletionStyle":
return string(JsxAttributeCompletionStyleBraces)
case "lsutil.IncludeInlayParameterNameHints":
return string(IncludeInlayParameterNameHintsAll)
case "lsutil.SemicolonPreference":
return string(SemicolonPreferenceInsert)
case "modulespecifiers.ImportModuleSpecifierPreference":
return string(modulespecifiers.ImportModuleSpecifierPreferenceRelative)
case "modulespecifiers.ImportModuleSpecifierEndingPreference":
return string(modulespecifiers.ImportModuleSpecifierEndingPreferenceJs)
default:
return "test"
}
}
func TestUserPreferencesRoundtrip(t *testing.T) {
t.Parallel()
var original UserPreferences
fillNonZeroValues(reflect.ValueOf(&original).Elem())
jsonBytes, err := json.Marshal(&original)
assert.NilError(t, err)
t.Run("UnmarshalJSONFrom", func(t *testing.T) {
t.Parallel()
var parsed UserPreferences
err2 := json.Unmarshal(jsonBytes, &parsed)
assert.NilError(t, err2)
assert.DeepEqual(t, original, parsed)
})
t.Run("withConfig", func(t *testing.T) {
t.Parallel()
var config map[string]any
err2 := json.Unmarshal(jsonBytes, &config)
assert.NilError(t, err2)
parsed := UserPreferences{}.withConfig(config)
assert.DeepEqual(t, original, parsed)
})
}
func TestUserPreferencesSerialize(t *testing.T) {
t.Parallel()
t.Run("config path field serializes to nested path", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
QuotePreference: QuotePreferenceSingle,
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
preferences := actual["preferences"].(map[string]any)
assert.Equal(t, "single", preferences["quoteStyle"])
})
t.Run("raw-only field serializes to unstable section", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
DisableSuggestions: core.TSTrue,
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
unstable := actual["unstable"].(map[string]any)
assert.Equal(t, true, unstable["disableSuggestions"])
})
t.Run("inlay hint inversion on serialize", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
InlayHints: InlayHintsPreferences{
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSTrue,
},
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
inlayHints := actual["inlayHints"].(map[string]any)
parameterNames := inlayHints["parameterNames"].(map[string]any)
assert.Equal(t, "all", parameterNames["enabled"])
assert.Equal(t, false, parameterNames["suppressWhenArgumentMatchesName"]) // inverted
})
t.Run("mixed config and unstable fields", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
QuotePreference: QuotePreferenceSingle,
DisableSuggestions: core.TSTrue,
DisplayPartsForJSDoc: core.TSTrue,
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
preferences := actual["preferences"].(map[string]any)
assert.Equal(t, "single", preferences["quoteStyle"])
unstable := actual["unstable"].(map[string]any)
assert.Equal(t, true, unstable["disableSuggestions"])
assert.Equal(t, true, unstable["displayPartsForJSDoc"])
})
}
func TestUserPreferencesParseUnstable(t *testing.T) {
t.Parallel()
tests := []struct {
name string
json string
expected UserPreferences
}{
{
name: "unstable fields with correct casing",
json: `{
"unstable": {
"disableSuggestions": true,
"maximumHoverLength": 100,
"allowRenameOfImportPath": true
}
}`,
expected: UserPreferences{
DisableSuggestions: core.TSTrue,
MaximumHoverLength: 100,
AllowRenameOfImportPath: core.TSTrue,
},
},
{
name: "nested preferences path",
json: `{
"preferences": {
"quoteStyle": "single",
"useAliasesForRenames": true
}
}`,
expected: UserPreferences{
QuotePreference: QuotePreferenceSingle,
UseAliasesForRename: core.TSTrue,
},
},
{
name: "suggest section",
json: `{
"suggest": {
"autoImports": false,
"includeCompletionsForImportStatements": true
}
}`,
expected: UserPreferences{
IncludeCompletionsForModuleExports: core.TSFalse,
IncludeCompletionsForImportStatements: core.TSTrue,
},
},
{
name: "inlayHints with invert",
json: `{
"inlayHints": {
"parameterNames": {
"enabled": "all",
"suppressWhenArgumentMatchesName": true
}
}
}`,
expected: UserPreferences{
InlayHints: InlayHintsPreferences{
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSFalse, // inverted
},
},
},
{
name: "mixed config",
json: `{
"unstable": {
"displayPartsForJSDoc": true
},
"preferences": {
"importModuleSpecifier": "relative"
},
"workspaceSymbols": {
"excludeLibrarySymbols": true
}
}`,
expected: UserPreferences{
DisplayPartsForJSDoc: core.TSTrue,
ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceRelative,
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
},
},
{
name: "stable config overrides unstable",
json: `{
"unstable": {
"quotePreference": "double"
},
"preferences": {
"quoteStyle": "single"
}
}`,
expected: UserPreferences{
QuotePreference: QuotePreferenceSingle, // stable wins
},
},
{
name: "unstable sets value when no stable config",
json: `{
"unstable": {
"includeAutomaticOptionalChainCompletions": false
}
}`,
expected: UserPreferences{
IncludeAutomaticOptionalChainCompletions: core.TSFalse,
},
},
{
name: "any field can be passed via unstable by its raw name",
json: `{
"unstable": {
"quotePreference": "double",
"includeCompletionsForModuleExports": true,
"excludeLibrarySymbolsInNavTo": true
}
}`,
expected: UserPreferences{
QuotePreference: QuotePreferenceDouble,
IncludeCompletionsForModuleExports: core.TSTrue,
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
},
},
{
name: "TypeScript raw names work in unstable section",
json: `{
"unstable": {
"includeCompletionsForModuleExports": true,
"quotePreference": "single",
"providePrefixAndSuffixTextForRename": true,
"includeInlayParameterNameHints": "all",
"organizeImportsLocale": "en"
}
}`,
expected: UserPreferences{
IncludeCompletionsForModuleExports: core.TSTrue,
QuotePreference: QuotePreferenceSingle,
UseAliasesForRename: core.TSTrue,
OrganizeImportsLocale: "en",
InlayHints: InlayHintsPreferences{
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
},
},
},
{
name: "old raw organize imports unicode preferences load as raw state",
json: `{
"unstable": {
"organizeImportsCollation": "unicode",
"organizeImportsCaseFirst": "upper",
"organizeImportsIgnoreCase": false,
"organizeImportsNumericCollation": true
}
}`,
expected: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper,
OrganizeImportsIgnoreCase: core.TSFalse,
OrganizeImportsNumericCollation: core.TSTrue,
},
},
{
name: "old top-level raw organize imports unicode preferences load as raw state",
json: `{
"organizeImportsCollation": "unicode",
"organizeImportsIgnoreCase": true
}`,
expected: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSTrue,
},
},
{
name: "new top-level raw organize imports sort is accepted",
json: `{
"organizeImportsSort": "natural"
}`,
expected: UserPreferences{
OrganizeImportsSort: OrganizeImportsSortNatural,
},
},
{
name: "old raw organize imports ignore case loads as raw state",
json: `{
"unstable": {
"organizeImportsIgnoreCase": true
}
}`,
expected: UserPreferences{
OrganizeImportsIgnoreCase: core.TSTrue,
},
},
{
name: "new raw organize imports sort loads alongside old raw preferences",
json: `{
"unstable": {
"organizeImportsSort": "ordinal",
"organizeImportsCollation": "unicode",
"organizeImportsIgnoreCase": true
}
}`,
expected: UserPreferences{
OrganizeImportsSort: OrganizeImportsSortOrdinal,
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSTrue,
},
},
{
name: "old nested organize imports unicode preferences load as raw state",
json: `{
"preferences": {
"organizeImports": {
"unicodeCollation": "unicode",
"caseSensitivity": "caseSensitive",
"numericCollation": true,
"caseFirst": "upper"
}
}
}`,
expected: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSFalse,
OrganizeImportsNumericCollation: core.TSTrue,
OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper,
},
},
{
name: "new nested organize imports sort loads alongside old nested preferences",
json: `{
"preferences": {
"organizeImports": {
"sort": "ordinalIgnoreCase",
"unicodeCollation": "unicode",
"caseSensitivity": "caseSensitive"
}
}
}`,
expected: UserPreferences{
OrganizeImportsSort: OrganizeImportsSortOrdinalIgnoreCase,
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSFalse,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var config map[string]any
err := json.Unmarshal([]byte(tt.json), &config)
assert.NilError(t, err)
parsed := UserPreferences{}.withConfig(config)
assert.DeepEqual(t, tt.expected, parsed)
})
}
}
func TestUserPreferencesReportStyleChecksAsWarnings(t *testing.T) {
t.Parallel()
t.Run("reportStyleChecksAsWarnings via config path", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"reportStyleChecksAsWarnings": false,
},
})
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
})
t.Run("reportStyleChecksAsWarnings defaults to true", func(t *testing.T) {
t.Parallel()
prefs := NewDefaultUserPreferences()
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSTrue)
})
t.Run("reportStyleChecksAsWarnings via unstable section", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"unstable": map[string]any{
"reportStyleChecksAsWarnings": false,
},
},
})
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
})
}
func TestUserPreferencesParseServerFeaturePreferences(t *testing.T) {
t.Parallel()
t.Run("preferred server feature settings", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"validate": map[string]any{"enabled": false},
"format": map[string]any{"enabled": false},
"autoClosingTags": map[string]any{
"enabled": false,
},
},
})
assert.Equal(t, prefs.EnableValidation, core.TSFalse)
assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
})
t.Run("legacy server feature fallbacks", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"validate": map[string]any{"enable": false},
"format": map[string]any{"enable": false},
"autoClosingTags": false,
},
})
assert.Equal(t, prefs.EnableValidation, core.TSFalse)
assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
})
t.Run("preferred settings take precedence over fallbacks", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"validate": map[string]any{"enable": false},
"format": map[string]any{"enable": false},
"autoClosingTags": false,
},
"js/ts": map[string]any{
"validate": map[string]any{"enabled": true},
"format": map[string]any{"enabled": true},
"autoClosingTags": map[string]any{
"enabled": true,
},
},
})
assert.Equal(t, prefs.EnableValidation, core.TSTrue)
assert.Equal(t, prefs.EnableFormatting, core.TSTrue)
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSTrue)
})
}
func TestUserPreferencesParseJSDocCompletionPreferences(t *testing.T) {
t.Parallel()
t.Run("unified jsdoc enabled setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"enabled": false,
},
},
},
})
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
})
t.Run("language fallback completeJSDocs setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"suggest": map[string]any{
"completeJSDocs": false,
},
},
})
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
})
t.Run("unified jsdoc enabled takes precedence over language fallback", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"suggest": map[string]any{
"completeJSDocs": false,
},
},
"js/ts": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"enabled": true,
},
},
},
})
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSTrue)
})
t.Run("unified jsdoc generateReturns setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"generateReturns": false,
},
},
},
})
assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
})
t.Run("language jsdoc generateReturns setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"generateReturns": false,
},
},
},
})
assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
})
}
func TestUserPreferencesParseATA(t *testing.T) {
t.Parallel()
t.Run("ParseUserPreferences with unified ATA setting in js/ts section", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"tsserver": map[string]any{
"automaticTypeAcquisition": map[string]any{
"enabled": false,
},
},
},
})
assert.Assert(t, prefs.IsATADisabled())
assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSFalse)
})
t.Run("ParseUserPreferences with deprecated disableAutomaticTypeAcquisition in typescript section", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"disableAutomaticTypeAcquisition": true,
},
})
assert.Assert(t, prefs.IsATADisabled())
assert.Equal(t, prefs.DisableAutomaticTypeAcquisition, core.TSTrue)
})
t.Run("unified setting takes precedence over deprecated setting", func(t *testing.T) {
t.Parallel()
// Both settings set: unified (js/ts) should take precedence
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"disableAutomaticTypeAcquisition": true,
},
"js/ts": map[string]any{
"tsserver": map[string]any{
"automaticTypeAcquisition": map[string]any{
"enabled": true,
},
},
},
})
assert.Assert(t, !prefs.IsATADisabled())
assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSTrue)
})
t.Run("IsATADisabled returns false when neither setting is configured", func(t *testing.T) {
t.Parallel()
prefs := NewDefaultUserPreferences()
assert.Assert(t, !prefs.IsATADisabled())
})
}

View File

@@ -0,0 +1,157 @@
package lsutil
import (
"strings"
"unicode"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/stringutil"
"github.com/microsoft/typescript-go/internal/tspath"
)
func ProbablyUsesSemicolons(file *ast.SourceFile) bool {
withSemicolon := 0
withoutSemicolon := 0
nStatementsToObserve := 5
var visit func(node *ast.Node) bool
visit = func(node *ast.Node) bool {
if node.Flags&ast.NodeFlagsReparsed != 0 {
return false
}
if SyntaxRequiresTrailingSemicolonOrASI(node.Kind) {
lastToken := GetLastToken(node, file)
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
withSemicolon++
} else {
withoutSemicolon++
}
} else if SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) {
lastToken := GetLastToken(node, file)
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
withSemicolon++
} else if lastToken != nil && lastToken.Kind != ast.KindCommaToken {
lastTokenLine := scanner.GetECMALineOfPosition(
file,
astnav.GetStartOfNode(lastToken, file, false /*includeJSDoc*/),
)
nextTokenLine := scanner.GetECMALineOfPosition(
file,
scanner.SkipTrivia(file.Text(), lastToken.End()),
)
// Avoid counting missing semicolon in single-line objects:
// `function f(p: { x: string /*no semicolon here is insignificant*/ }) {`
if lastTokenLine != nextTokenLine {
withoutSemicolon++
}
}
}
if withSemicolon+withoutSemicolon >= nStatementsToObserve {
return true
}
return node.ForEachChild(visit)
}
file.ForEachChild(visit)
// One statement missing a semicolon isn't sufficient evidence to say the user
// doesn't want semicolons, because they may not even be done writing that statement.
if withSemicolon == 0 && withoutSemicolon <= 1 {
return true
}
// When both kinds of observation exist, treat the file as using semicolons when the
// ratio withSemicolon/withoutSemicolon exceeds 1/nStatementsToObserve (real arithmetic),
// implemented as an integer inequality to avoid truncation.
if withoutSemicolon == 0 {
return true
}
return withSemicolon*nStatementsToObserve > withoutSemicolon
}
func ShouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) core.Tristate {
for _, node := range file.Imports() {
if core.NodeCoreModules()[node.Text()] && !core.ExclusivelyPrefixedNodeCoreModules[node.Text()] {
if strings.HasPrefix(node.Text(), "node:") {
return core.TSTrue
} else {
return core.TSFalse
}
}
}
return program.UsesUriStyleNodeCoreModules()
}
func QuotePreferenceFromString(str *ast.StringLiteral) QuotePreference {
if str.TokenFlags&ast.TokenFlagsSingleQuote != 0 {
return QuotePreferenceSingle
}
return QuotePreferenceDouble
}
func GetQuotePreference(sourceFile *ast.SourceFile, preferences UserPreferences) QuotePreference {
if preferences.QuotePreference != "" && preferences.QuotePreference != "auto" {
if preferences.QuotePreference == "single" {
return QuotePreferenceSingle
}
return QuotePreferenceDouble
}
// ignore synthetic import added when importHelpers: true
firstModuleSpecifier := core.Find(sourceFile.Imports(), func(n *ast.Node) bool {
return ast.IsStringLiteral(n) && !ast.NodeIsSynthesized(n.Parent)
})
if firstModuleSpecifier != nil {
return QuotePreferenceFromString(firstModuleSpecifier.AsStringLiteral())
}
return QuotePreferenceDouble
}
func ModuleSymbolToValidIdentifier(moduleSymbol *ast.Symbol, forceCapitalize bool) string {
return ModuleSpecifierToValidIdentifier(stringutil.StripQuotes(moduleSymbol.Name), forceCapitalize)
}
func ModuleSpecifierToValidIdentifier(moduleSpecifier string, forceCapitalize bool) string {
baseName := tspath.GetBaseFileName(strings.TrimSuffix(tspath.RemoveFileExtension(moduleSpecifier), "/index"))
res := []rune{}
lastCharWasValid := true
baseNameRunes := []rune(baseName)
if len(baseNameRunes) > 0 && scanner.IsIdentifierStart(baseNameRunes[0]) {
if forceCapitalize {
res = append(res, unicode.ToUpper(baseNameRunes[0]))
} else {
res = append(res, baseNameRunes[0])
}
} else {
lastCharWasValid = false
}
for i := 1; i < len(baseNameRunes); i++ {
isValid := scanner.IsIdentifierPart(baseNameRunes[i])
if isValid {
if !lastCharWasValid {
res = append(res, unicode.ToUpper(baseNameRunes[i]))
} else {
res = append(res, baseNameRunes[i])
}
}
lastCharWasValid = isValid
}
// Need `"_"` to ensure result isn't empty.
resString := string(res)
if resString != "" && !IsNonContextualKeyword(scanner.StringToToken(resString)) {
return resString
}
return "_" + resString
}
func IsNonContextualKeyword(token ast.Kind) bool {
return ast.IsKeywordKind(token) && !ast.IsContextualKeyword(token)
}

View File

@@ -0,0 +1,200 @@
package lsutil
import (
"testing"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/parser"
)
func parseTS(t *testing.T, text string) *ast.SourceFile {
t.Helper()
return parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/test.ts",
Path: "/test.ts",
}, text, core.ScriptKindTS)
}
func TestProbablyUsesSemicolons(t *testing.T) {
t.Parallel()
tests := []struct {
name string
src string
want bool
}{
{
name: "mixed semicolons and ASI favors semicolons when ratio exceeds one fifth",
// First five observations: 2 with semicolon, 3 without. Real ratio 2/3 > 1/5.
// Integer division bug compared against 1/5==0 and used with/without as ints,
// so the old check was effectively (with/without) > 0, which failed here.
src: `let a = 1;
let b = 2;
let c = 3
let d = 4
let e = 5
`,
want: true,
},
{
name: "consistent ASI with no semicolons",
src: `let a = 1
let b = 2
let c = 3
`,
want: false,
},
{
name: "consistent semicolons",
src: `let a = 1;
let b = 2;
let c = 3;
`,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
file := parseTS(t, tt.src)
if got := ProbablyUsesSemicolons(file); got != tt.want {
t.Errorf("ProbablyUsesSemicolons() = %v, want %v", got, tt.want)
}
})
}
}
func TestResolveOrganizeImportsSort(t *testing.T) {
t.Parallel()
tests := []struct {
name string
preferences UserPreferences
want OrganizeImportsSort
}{
{
name: "explicit sort wins",
preferences: UserPreferences{
OrganizeImportsSort: OrganizeImportsSortOrdinal,
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSTrue,
},
want: OrganizeImportsSortOrdinal,
},
{
name: "unicode case-sensitive maps to natural",
preferences: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSFalse,
},
want: OrganizeImportsSortNatural,
},
{
name: "unicode ignore case maps to natural ignore case",
preferences: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSTrue,
},
want: OrganizeImportsSortNaturalIgnoreCase,
},
{
name: "unicode unknown case sensitivity stays auto for detection",
preferences: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
},
want: OrganizeImportsSortAuto,
},
{
name: "ordinal ignore case maps to ordinal ignore case",
preferences: UserPreferences{
OrganizeImportsIgnoreCase: core.TSTrue,
},
want: OrganizeImportsSortOrdinalIgnoreCase,
},
{
name: "ordinal case sensitive maps to ordinal",
preferences: UserPreferences{
OrganizeImportsIgnoreCase: core.TSFalse,
},
want: OrganizeImportsSortOrdinal,
},
{
name: "unknown ordinal stays auto",
want: OrganizeImportsSortAuto,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := ResolveOrganizeImportsSort(tt.preferences); got != tt.want {
t.Fatalf("ResolveOrganizeImportsSort() = %v, want %v", got, tt.want)
}
})
}
}
func TestCompareOrganizeImportsNaturalStrings(t *testing.T) {
t.Parallel()
comparer := getOrganizeImportsPresetStringComparer(OrganizeImportsSortNaturalIgnoreCase)
tests := []struct {
name string
a string
b string
want int
}{
{
name: "numeric runs sort by numeric value",
a: "a2",
b: "a100",
want: -1,
},
{
name: "numeric runs with equal value use raw tie break",
a: "a02",
b: "a2",
want: -1,
},
{
name: "accents are folded for primary comparison",
a: "À",
b: "B",
want: -1,
},
{
name: "raw comparison breaks accent ties",
a: "A",
b: "À",
want: -1,
},
{
name: "hyphen sorts before slash like Intl.Collator fallback",
a: "app-init",
b: "app/app",
want: -1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := cmpSign(comparer(tt.a, tt.b)); got != tt.want {
t.Fatalf("comparer(%q, %q) = %v, want sign %v", tt.a, tt.b, got, tt.want)
}
})
}
}
func cmpSign(value int) int {
switch {
case value < 0:
return -1
case value > 0:
return 1
default:
return 0
}
}

View File

@@ -0,0 +1,954 @@
package ls
import (
"context"
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/change"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/printer"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/stringutil"
)
// OrganizeImports organizes imports by:
// 1. Removing unused imports
// 2. Coalescing imports from the same module
// 3. Sorting imports
func (l *LanguageService) OrganizeImports(
ctx context.Context,
sourceFile *ast.SourceFile,
program *compiler.Program,
kind lsproto.CodeActionKind,
) map[string][]*lsproto.TextEdit {
changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters)
shouldSort := kind == lsproto.CodeActionKindSourceSortImports || kind == lsproto.CodeActionKindSourceOrganizeImports
shouldCombine := shouldSort
shouldRemove := kind == lsproto.CodeActionKindSourceRemoveUnusedImports || kind == lsproto.CodeActionKindSourceOrganizeImports
topLevelImportDecls := lsutil.FilterImportDeclarations(sourceFile.Statements.Nodes)
topLevelImportGroupDecls := groupByNewlineContiguous(sourceFile, topLevelImportDecls)
preferences := l.UserPreferences()
comparersToTest, typeOrdersToTest := lsutil.GetDetectionLists(preferences)
defaultComparer := comparersToTest[0]
sort := lsutil.ResolveOrganizeImportsSort(preferences)
var moduleSpecifierComparer func(a, b string) int
var namedImportComparer func(a, b string) int
if sort != lsutil.OrganizeImportsSortAuto {
moduleSpecifierComparer = defaultComparer
namedImportComparer = defaultComparer
}
typeOrder := preferences.OrganizeImportsTypeOrder
if sort == lsutil.OrganizeImportsSortAuto {
result, _ := lsutil.DetectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest)
moduleSpecifierComparer = result
}
if typeOrder == lsutil.OrganizeImportsTypeOrderAuto || sort == lsutil.OrganizeImportsSortAuto {
namedImportComparer2, typeOrder2, found := lsutil.DetectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest)
if found {
if namedImportComparer == nil || sort == lsutil.OrganizeImportsSortAuto {
namedImportComparer = namedImportComparer2
}
if typeOrder == lsutil.OrganizeImportsTypeOrderAuto {
typeOrder = typeOrder2
}
}
}
comparer := organizeImportsComparerSettings{
moduleSpecifierComparer: moduleSpecifierComparer,
namedImportComparer: namedImportComparer,
typeOrder: typeOrder,
}
for _, importGroupDecl := range topLevelImportGroupDecls {
organizeImportsWorker(importGroupDecl, comparer, shouldSort, shouldCombine, shouldRemove, sourceFile, program, changeTracker, ctx)
}
if kind != lsproto.CodeActionKindSourceRemoveUnusedImports {
topLevelExportGroupDecls := getTopLevelExportGroups(sourceFile)
for _, exportGroupDecl := range topLevelExportGroupDecls {
organizeExportsWorker(exportGroupDecl, comparer, sourceFile, changeTracker)
}
}
for _, stmt := range sourceFile.Statements.Nodes {
if !ast.IsAmbientModule(stmt.AsNode()) {
continue
}
ambientModule := stmt.AsModuleDeclaration()
if ambientModule.Body == nil {
continue
}
moduleBody := ambientModule.Body.AsModuleBlock()
ambientModuleImportDecls := lsutil.FilterImportDeclarations(moduleBody.Statements.Nodes)
ambientModuleImportGroupDecls := groupByNewlineContiguous(sourceFile, ambientModuleImportDecls)
for _, importGroupDecl := range ambientModuleImportGroupDecls {
organizeImportsWorker(importGroupDecl, comparer, shouldSort, shouldCombine, shouldRemove, sourceFile, program, changeTracker, ctx)
}
if kind != lsproto.CodeActionKindSourceRemoveUnusedImports {
var ambientModuleExportDecls []*ast.Statement
for _, s := range moduleBody.Statements.Nodes {
if s.Kind == ast.KindExportDeclaration {
ambientModuleExportDecls = append(ambientModuleExportDecls, s)
}
}
organizeExportsWorker(ambientModuleExportDecls, comparer, sourceFile, changeTracker)
}
}
return changeTracker.GetChanges()
}
type organizeImportsComparerSettings struct {
moduleSpecifierComparer func(a, b string) int
namedImportComparer func(a, b string) int
typeOrder lsutil.OrganizeImportsTypeOrder
}
func organizeImportsWorker(
oldImportDecls []*ast.Statement,
comparer organizeImportsComparerSettings,
shouldSort bool,
shouldCombine bool,
shouldRemove bool,
sourceFile *ast.SourceFile,
program *compiler.Program,
changeTracker *change.Tracker,
ctx context.Context,
) {
if len(oldImportDecls) == 0 {
return
}
// Header comment preservation is handled via LeadingTriviaOptionExclude in the change tracker below
processedImports := slices.Clone(oldImportDecls)
if shouldRemove {
typeChecker, done := program.GetTypeCheckerForFile(ctx, sourceFile)
defer done()
processedImports = removeUnusedImports(processedImports, sourceFile, typeChecker, program, changeTracker)
}
var newImportDecls []*ast.Statement
if shouldCombine {
grouped := groupByModuleSpecifier(processedImports)
if shouldSort {
slices.SortFunc(grouped, func(a, b []*ast.Statement) int {
if len(a) == 0 || len(b) == 0 {
return 0
}
return lsutil.CompareModuleSpecifiers(
a[0].ModuleSpecifier(),
b[0].ModuleSpecifier(),
comparer.moduleSpecifierComparer,
)
})
}
specifierComparer := lsutil.GetNamedImportSpecifierComparer(
lsutil.UserPreferences{OrganizeImportsTypeOrder: comparer.typeOrder},
comparer.namedImportComparer,
)
for _, importGroup := range grouped {
coalesced := coalesceImportsWorker(importGroup, comparer.moduleSpecifierComparer, specifierComparer, sourceFile, changeTracker)
if shouldSort {
slices.SortFunc(coalesced, func(a, b *ast.Statement) int {
return lsutil.CompareImportsOrRequireStatements(a, b, comparer.moduleSpecifierComparer)
})
}
newImportDecls = append(newImportDecls, coalesced...)
}
} else {
newImportDecls = processedImports
}
if shouldSort && !shouldCombine {
slices.SortFunc(newImportDecls, func(a, b *ast.Statement) int {
return lsutil.CompareImportsOrRequireStatements(a, b, comparer.moduleSpecifierComparer)
})
}
if len(newImportDecls) == 0 {
changeTracker.DeleteNodeRange(
sourceFile,
oldImportDecls[0].AsNode(),
oldImportDecls[len(oldImportDecls)-1].AsNode(),
change.LeadingTriviaOptionExclude, // Preserve header comment
change.TrailingTriviaOptionInclude,
)
} else {
for _, imp := range newImportDecls {
changeTracker.SetEmitFlags(imp.AsNode(), printer.EFNoLeadingComments)
}
options := change.NodeOptions{
LeadingTriviaOption: change.LeadingTriviaOptionExclude, // Preserve header comment
TrailingTriviaOption: change.TrailingTriviaOptionInclude,
Suffix: "\n",
}
newNodes := core.Map(newImportDecls, func(s *ast.Statement) *ast.Node { return s.AsNode() })
changeTracker.ReplaceNodeWithNodes(sourceFile, oldImportDecls[0].AsNode(), newNodes, &options)
if len(oldImportDecls) > 1 {
for i := 1; i < len(oldImportDecls); i++ {
changeTracker.Delete(sourceFile, oldImportDecls[i].AsNode())
}
}
}
}
func groupByModuleSpecifier(imports []*ast.Statement) [][]*ast.Statement {
groups := make(map[string][]*ast.Statement)
var order []string
for _, imp := range imports {
specifier := lsutil.GetExternalModuleName(imp.ModuleSpecifier())
if _, exists := groups[specifier]; !exists {
order = append(order, specifier)
}
groups[specifier] = append(groups[specifier], imp)
}
result := make([][]*ast.Statement, 0, len(order))
for _, key := range order {
result = append(result, groups[key])
}
return result
}
func removeUnusedImports(oldImports []*ast.Statement, sourceFile *ast.SourceFile, typeChecker *checker.Checker, program *compiler.Program, changeTracker *change.Tracker) []*ast.Statement {
compilerOptions := program.Options()
jsxElementsPresent := (sourceFile.AsNode().SubtreeFacts() & ast.SubtreeContainsJsx) != 0
jsxModeNeedsExplicitImport := compilerOptions.Jsx == core.JsxEmitReact || compilerOptions.Jsx == core.JsxEmitReactNative
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
usedImports := make([]*ast.Statement, 0, len(oldImports))
for _, importDecl := range oldImports {
importClause := importDecl.AsImportDeclaration().ImportClause
if importClause == nil {
usedImports = append(usedImports, importDecl)
continue
}
clause := importClause.AsImportClause()
name := clause.Name()
namedBindings := clause.NamedBindings
if name != nil && !typeChecker.IsDeclarationUsed(sourceFile, name.AsIdentifier(), jsxElementsPresent, jsxModeNeedsExplicitImport) {
name = nil
}
if namedBindings != nil {
switch namedBindings.Kind {
case ast.KindNamespaceImport:
nsImport := namedBindings.AsNamespaceImport()
if !typeChecker.IsDeclarationUsed(sourceFile, nsImport.Name().AsIdentifier(), jsxElementsPresent, jsxModeNeedsExplicitImport) {
namedBindings = nil
}
case ast.KindNamedImports:
namedImports := namedBindings.AsNamedImports()
originalBindings := namedBindings
newElements := filterUsedImportSpecifiers(namedImports.Elements.Nodes, typeChecker, sourceFile, jsxElementsPresent, jsxModeNeedsExplicitImport)
if len(newElements) == 0 {
namedBindings = nil
} else if len(newElements) < len(namedImports.Elements.Nodes) {
newList := factory.NewNodeList(newElements)
updatedNamedImports := factory.UpdateNamedImports(namedImports, newList)
namedBindings = updatedNamedImports.AsNode()
}
if namedBindings != nil && !ast.NodeIsSynthesized(originalBindings.AsNode()) && !printer.RangeIsOnSingleLine(originalBindings.Loc, sourceFile) {
changeTracker.SetEmitFlags(namedBindings, printer.EFMultiLine)
}
}
}
if name != nil || namedBindings != nil {
importDeclNode := importDecl.AsImportDeclaration()
newClause := factory.UpdateImportClause(clause, clause.PhaseModifier, name, namedBindings)
newImportDecl := factory.UpdateImportDeclaration(
importDeclNode,
importDeclNode.Modifiers(),
newClause.AsNode(),
importDeclNode.ModuleSpecifier,
importDeclNode.Attributes,
)
usedImports = append(usedImports, newImportDecl)
} else {
moduleSpecifier := importDecl.ModuleSpecifier()
if hasModuleDeclarationMatchingSpecifier(sourceFile, moduleSpecifier) {
if sourceFile.IsDeclarationFile {
importDeclNode := importDecl.AsImportDeclaration()
newImportDecl := factory.UpdateImportDeclaration(
importDeclNode,
importDeclNode.Modifiers(),
nil, // no import clause
importDeclNode.ModuleSpecifier,
importDeclNode.Attributes,
)
usedImports = append(usedImports, newImportDecl)
} else {
usedImports = append(usedImports, importDecl)
}
}
}
}
return usedImports
}
func filterUsedImportSpecifiers(
elements []*ast.Statement,
typeChecker *checker.Checker,
sourceFile *ast.SourceFile,
jsxElementsPresent bool,
jsxModeNeedsExplicitImport bool,
) []*ast.Statement {
var result []*ast.Statement
for _, elem := range elements {
spec := elem.AsImportSpecifier()
if typeChecker.IsDeclarationUsed(sourceFile, spec.Name().AsIdentifier(), jsxElementsPresent, jsxModeNeedsExplicitImport) {
result = append(result, elem)
}
}
return result
}
func hasModuleDeclarationMatchingSpecifier(sourceFile *ast.SourceFile, moduleSpecifier *ast.Expression) bool {
if moduleSpecifier == nil || !ast.IsStringLiteral(moduleSpecifier.AsNode()) {
return false
}
moduleSpecifierText := moduleSpecifier.Text()
for _, moduleName := range sourceFile.ModuleAugmentations {
if ast.IsStringLiteral(moduleName) && moduleName.Text() == moduleSpecifierText {
return true
}
}
return false
}
// getImportAttributesKey returns a key for grouping imports by their attributes.
func getImportAttributesKey(attributes *ast.ImportAttributesNode) string {
if attributes == nil {
return ""
}
importAttrs := attributes.AsImportAttributes()
var key strings.Builder
key.WriteString(importAttrs.Token.String())
key.WriteString(" ")
attrNodes := make([]*ast.Node, len(importAttrs.Attributes.Nodes))
copy(attrNodes, importAttrs.Attributes.Nodes)
slices.SortFunc(attrNodes, func(a, b *ast.Node) int {
aName := a.AsImportAttribute().Name().Text()
bName := b.AsImportAttribute().Name().Text()
return stringutil.CompareStringsCaseSensitive(aName, bName)
})
for _, attrNode := range attrNodes {
attr := attrNode.AsImportAttribute()
key.WriteString(attr.Name().Text())
key.WriteString(":")
if ast.IsStringLiteralLike(attr.Value.AsNode()) {
key.WriteString(`"`)
key.WriteString(attr.Value.Text())
key.WriteString(`"`)
} else {
key.WriteString(attr.Value.AsNode().Text())
}
key.WriteString(" ")
}
return key.String()
}
// groupByNewlineContiguous groups declarations by blank lines between them.
func groupByNewlineContiguous(sourceFile *ast.SourceFile, decls []*ast.Statement) [][]*ast.Statement {
s := scanner.NewScanner()
s.SetSkipTrivia(false) // Must not skip trivia to detect newlines
var groups [][]*ast.Statement
var currentGroup []*ast.Statement
for _, decl := range decls {
if len(currentGroup) > 0 && isNewGroup(sourceFile, decl, s) {
groups = append(groups, currentGroup)
currentGroup = nil
}
currentGroup = append(currentGroup, decl)
}
if len(currentGroup) > 0 {
groups = append(groups, currentGroup)
}
return groups
}
func isNewGroup(sourceFile *ast.SourceFile, decl *ast.Statement, s *scanner.Scanner) bool {
fullStart := decl.Pos()
if fullStart < 0 {
return false
}
text := sourceFile.Text()
textLen := len(text)
if fullStart >= textLen {
return false
}
startPos := scanner.SkipTrivia(text, fullStart)
if startPos <= fullStart {
return false
}
triviaLen := startPos - fullStart
s.SetText(text[fullStart:startPos])
numberOfNewLines := 0
for s.TokenStart() < triviaLen {
tokenKind := s.Scan()
if tokenKind == ast.KindNewLineTrivia {
numberOfNewLines++
if numberOfNewLines >= 2 {
return true
}
}
}
return false
}
func coalesceImportsWorker(
importDecls []*ast.Statement,
comparer func(a, b string) int,
specifierComparer func(s1, s2 *ast.Node) int,
sourceFile *ast.SourceFile,
changeTracker *change.Tracker,
) []*ast.Statement {
if len(importDecls) == 0 {
return importDecls
}
importGroupsByAttributes := make(map[string][]*ast.Statement)
var attributeKeys []string
for _, importDecl := range importDecls {
key := getImportAttributesKey(importDecl.AsImportDeclaration().Attributes)
if _, exists := importGroupsByAttributes[key]; !exists {
attributeKeys = append(attributeKeys, key)
}
importGroupsByAttributes[key] = append(importGroupsByAttributes[key], importDecl)
}
coalescedImports := make([]*ast.Statement, 0)
for _, attributeKey := range attributeKeys {
importGroupSameAttrs := importGroupsByAttributes[attributeKey]
categorized := getCategorizedImports(importGroupSameAttrs)
if categorized.importWithoutClause != nil {
coalescedImports = append(coalescedImports, categorized.importWithoutClause)
}
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
for i, group := range []importGroup{categorized.regularImports, categorized.typeOnlyImports} {
if group.isEmpty() {
continue
}
isTypeOnly := i == 1
if !isTypeOnly && len(group.defaultImports) == 1 && len(group.namespaceImports) == 1 && len(group.namedImports) == 0 {
defaultImport := group.defaultImports[0]
namespaceImport := group.namespaceImports[0]
defaultClause := defaultImport.AsImportDeclaration().ImportClause.AsImportClause()
namespaceBindings := namespaceImport.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings
newClause := factory.UpdateImportClause(defaultClause, defaultClause.PhaseModifier, defaultClause.Name(), namespaceBindings)
defaultDeclNode := defaultImport.AsImportDeclaration()
newImportDecl := factory.UpdateImportDeclaration(
defaultDeclNode,
defaultDeclNode.Modifiers(),
newClause,
defaultDeclNode.ModuleSpecifier,
defaultDeclNode.Attributes,
)
coalescedImports = append(coalescedImports, newImportDecl)
continue
}
slices.SortFunc(group.namespaceImports, func(a, b *ast.Statement) int {
n1 := a.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings.AsNamespaceImport().Name()
n2 := b.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings.AsNamespaceImport().Name()
return comparer(n1.Text(), n2.Text())
})
for _, nsImport := range group.namespaceImports {
nsImportDecl := nsImport.AsImportDeclaration()
clause := nsImportDecl.ImportClause.AsImportClause()
newClause := factory.UpdateImportClause(clause, clause.PhaseModifier, nil, clause.NamedBindings)
newImportDecl := factory.UpdateImportDeclaration(
nsImportDecl,
nsImportDecl.Modifiers(),
newClause,
nsImportDecl.ModuleSpecifier,
nsImportDecl.Attributes,
)
coalescedImports = append(coalescedImports, newImportDecl)
}
var firstDefaultImport *ast.Statement
var firstNamedImport *ast.Statement
if len(group.defaultImports) > 0 {
firstDefaultImport = group.defaultImports[0]
}
if len(group.namedImports) > 0 {
firstNamedImport = group.namedImports[0]
}
importDecl := firstDefaultImport
if importDecl == nil {
importDecl = firstNamedImport
}
if importDecl == nil {
continue
}
var newDefaultImport *ast.IdentifierNode
var newImportSpecifiers []*ast.Node
if len(group.defaultImports) == 1 {
newDefaultImport = group.defaultImports[0].AsImportDeclaration().ImportClause.AsImportClause().Name()
} else {
for _, defaultImport := range group.defaultImports {
defaultClause := defaultImport.AsImportDeclaration().ImportClause.AsImportClause()
defaultName := defaultClause.Name()
propertyName := factory.NewIdentifier("default")
importSpec := factory.NewImportSpecifier(false, propertyName, defaultName)
newImportSpecifiers = append(newImportSpecifiers, importSpec)
}
}
newImportSpecifiers = append(newImportSpecifiers, getNewImportSpecifiers(group.namedImports, factory)...)
slices.SortStableFunc(newImportSpecifiers, specifierComparer)
var newNamedImports *ast.NamedImportBindings
if len(newImportSpecifiers) == 0 {
if newDefaultImport != nil {
newNamedImports = nil
} else {
newNamedImports = factory.NewNamedImports(factory.NewNodeList(nil))
}
} else {
sortedList := factory.NewNodeList(newImportSpecifiers)
if firstNamedImport != nil {
firstNamedBindings := firstNamedImport.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings.AsNamedImports()
originalElements := firstNamedBindings.Elements
if originalElements.HasTrailingComma() {
sortedList.Loc = originalElements.Loc
}
newNamedImports = factory.UpdateNamedImports(firstNamedBindings, sortedList).AsNode()
} else {
newNamedImports = factory.NewNamedImports(sortedList)
}
}
if sourceFile != nil && newNamedImports != nil && firstNamedImport != nil {
firstNamedBindings := firstNamedImport.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings
if !ast.NodeIsSynthesized(firstNamedBindings.AsNode()) && !printer.RangeIsOnSingleLine(firstNamedBindings.Loc, sourceFile) {
changeTracker.SetEmitFlags(newNamedImports.AsNode(), printer.EFMultiLine)
}
}
if isTypeOnly && newDefaultImport != nil && newNamedImports != nil {
importDeclNode := importDecl.AsImportDeclaration()
defaultClause := factory.NewImportClause(importDeclNode.ImportClause.AsImportClause().PhaseModifier, newDefaultImport, nil)
defaultImportDecl := factory.UpdateImportDeclaration(
importDeclNode,
importDeclNode.Modifiers(),
defaultClause,
importDeclNode.ModuleSpecifier,
importDeclNode.Attributes,
)
coalescedImports = append(coalescedImports, defaultImportDecl)
namedDeclNode := firstNamedImport
if namedDeclNode == nil {
namedDeclNode = importDecl
}
namedImportDeclNode := namedDeclNode.AsImportDeclaration()
namedClause := factory.NewImportClause(namedImportDeclNode.ImportClause.AsImportClause().PhaseModifier, nil, newNamedImports)
namedImportDecl := factory.UpdateImportDeclaration(
namedImportDeclNode,
namedImportDeclNode.Modifiers(),
namedClause,
namedImportDeclNode.ModuleSpecifier,
namedImportDeclNode.Attributes,
)
coalescedImports = append(coalescedImports, namedImportDecl)
} else {
importDeclNode := importDecl.AsImportDeclaration()
clauseNode := importDeclNode.ImportClause.AsImportClause()
newClause := factory.UpdateImportClause(clauseNode, clauseNode.PhaseModifier, newDefaultImport, newNamedImports)
newImportDecl := factory.UpdateImportDeclaration(
importDeclNode,
importDeclNode.Modifiers(),
newClause,
importDeclNode.ModuleSpecifier,
importDeclNode.Attributes,
)
coalescedImports = append(coalescedImports, newImportDecl)
}
}
}
return coalescedImports
}
type categorizedImports struct {
importWithoutClause *ast.Statement
typeOnlyImports importGroup
regularImports importGroup
}
type importGroup struct {
defaultImports []*ast.Statement
namespaceImports []*ast.Statement
namedImports []*ast.Statement
}
func (g importGroup) isEmpty() bool {
return len(g.defaultImports) == 0 && len(g.namespaceImports) == 0 && len(g.namedImports) == 0
}
func getCategorizedImports(importDecls []*ast.Statement) categorizedImports {
var importWithoutClause *ast.Statement
var typeOnlyImports, regularImports importGroup
for _, importDecl := range importDecls {
if importDecl.AsImportDeclaration().ImportClause == nil {
if importWithoutClause == nil {
importWithoutClause = importDecl
}
continue
}
clause := importDecl.AsImportDeclaration().ImportClause.AsImportClause()
group := &regularImports
if clause.IsTypeOnly() {
group = &typeOnlyImports
}
name := clause.Name()
namedBindings := clause.NamedBindings
if name != nil {
group.defaultImports = append(group.defaultImports, importDecl)
}
if namedBindings != nil {
switch namedBindings.Kind {
case ast.KindNamespaceImport:
group.namespaceImports = append(group.namespaceImports, importDecl)
case ast.KindNamedImports:
group.namedImports = append(group.namedImports, importDecl)
}
}
}
return categorizedImports{
importWithoutClause: importWithoutClause,
typeOnlyImports: typeOnlyImports,
regularImports: regularImports,
}
}
func getNewImportSpecifiers(namedImports []*ast.Statement, factory *ast.NodeFactory) []*ast.Node {
var result []*ast.Node
for _, namedImport := range namedImports {
elements := tryGetNamedBindingElements(namedImport)
if elements == nil {
continue
}
for _, elem := range elements {
spec := elem.AsImportSpecifier()
if spec.PropertyName != nil && spec.Name() != nil {
propertyText := spec.PropertyName.Text()
nameText := spec.Name().Text()
if propertyText == nameText {
normalized := factory.UpdateImportSpecifier(spec, spec.IsTypeOnly, nil, spec.Name())
result = append(result, normalized)
continue
}
}
result = append(result, elem)
}
}
return result
}
func tryGetNamedBindingElements(namedImport *ast.Statement) []*ast.Statement {
if namedImport.Kind != ast.KindImportDeclaration {
return nil
}
importDecl := namedImport.AsImportDeclaration()
if importDecl.ImportClause == nil {
return nil
}
clause := importDecl.ImportClause.AsImportClause()
namedBindings := clause.NamedBindings
if namedBindings != nil && namedBindings.Kind == ast.KindNamedImports {
namedImportsNode := namedBindings.AsNamedImports()
return namedImportsNode.Elements.Nodes
}
return nil
}
func getTopLevelExportGroups(sourceFile *ast.SourceFile) [][]*ast.Statement {
var topLevelExportGroups [][]*ast.Statement
statements := sourceFile.Statements.Nodes
statementsLen := len(statements)
i := 0
groupIndex := 0
for i < statementsLen {
if statements[i].Kind == ast.KindExportDeclaration {
if groupIndex >= len(topLevelExportGroups) {
topLevelExportGroups = append(topLevelExportGroups, []*ast.Statement{})
}
exportDecl := statements[i].AsExportDeclaration()
if exportDecl.ModuleSpecifier != nil {
topLevelExportGroups[groupIndex] = append(topLevelExportGroups[groupIndex], statements[i])
i++
} else {
for i < statementsLen && statements[i].Kind == ast.KindExportDeclaration {
topLevelExportGroups[groupIndex] = append(topLevelExportGroups[groupIndex], statements[i])
i++
}
groupIndex++
}
} else {
i++
if groupIndex < len(topLevelExportGroups) && len(topLevelExportGroups[groupIndex]) > 0 {
groupIndex++
}
}
}
var result [][]*ast.Statement
for _, exportGroup := range topLevelExportGroups {
subGroups := groupByNewlineContiguous(sourceFile, exportGroup)
result = append(result, subGroups...)
}
return result
}
func organizeExportsWorker(
oldExportDecls []*ast.Statement,
comparer organizeImportsComparerSettings,
sourceFile *ast.SourceFile,
changeTracker *change.Tracker,
) {
if len(oldExportDecls) == 0 {
return
}
specifierComparerFunc := lsutil.GetNamedImportSpecifierComparer(
lsutil.UserPreferences{OrganizeImportsTypeOrder: comparer.typeOrder},
comparer.namedImportComparer,
)
newExportDecls := coalesceExportsWorker(oldExportDecls, specifierComparerFunc, comparer.moduleSpecifierComparer, sourceFile, changeTracker)
if len(oldExportDecls) > 0 {
if len(newExportDecls) == 0 {
changeTracker.DeleteNodeRange(
sourceFile,
oldExportDecls[0].AsNode(),
oldExportDecls[len(oldExportDecls)-1].AsNode(),
change.LeadingTriviaOptionExclude,
change.TrailingTriviaOptionInclude,
)
} else {
for _, exp := range newExportDecls {
changeTracker.AddEmitFlags(exp.AsNode(), printer.EFNoLeadingComments)
}
options := change.NodeOptions{
LeadingTriviaOption: change.LeadingTriviaOptionExclude,
TrailingTriviaOption: change.TrailingTriviaOptionInclude,
Suffix: "\n",
}
newNodes := core.Map(newExportDecls, func(s *ast.Statement) *ast.Node { return s.AsNode() })
changeTracker.ReplaceNodeWithNodes(sourceFile, oldExportDecls[0].AsNode(), newNodes, &options)
if len(oldExportDecls) > 1 {
for i := 1; i < len(oldExportDecls); i++ {
changeTracker.Delete(sourceFile, oldExportDecls[i].AsNode())
}
}
}
}
}
func coalesceExportsWorker(
exportGroup []*ast.Statement,
specifierComparer func(s1, s2 *ast.Node) int,
moduleSpecifierComparer func(a, b string) int,
sourceFile *ast.SourceFile,
changeTracker *change.Tracker,
) []*ast.Statement {
if len(exportGroup) == 0 {
return exportGroup
}
exportsByModuleSpecifier := make(map[string][]*ast.Statement)
var moduleSpecifierOrder []string
for _, exportDecl := range exportGroup {
export := exportDecl.AsExportDeclaration()
var moduleSpecifier string
if export.ModuleSpecifier != nil {
moduleSpecifier = export.ModuleSpecifier.Text()
}
if _, exists := exportsByModuleSpecifier[moduleSpecifier]; !exists {
moduleSpecifierOrder = append(moduleSpecifierOrder, moduleSpecifier)
}
exportsByModuleSpecifier[moduleSpecifier] = append(exportsByModuleSpecifier[moduleSpecifier], exportDecl)
}
slices.SortStableFunc(moduleSpecifierOrder, func(a, b string) int {
if a == "" && b != "" {
return 1
}
if a != "" && b == "" {
return -1
}
return moduleSpecifierComparer(a, b)
})
var coalescedExports []*ast.Statement
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
for _, moduleSpecifier := range moduleSpecifierOrder {
group := exportsByModuleSpecifier[moduleSpecifier]
categorized := getCategorizedExports(group)
if categorized.exportWithoutClause != nil {
coalescedExports = append(coalescedExports, categorized.exportWithoutClause)
}
for _, subGroup := range [][]*ast.Statement{categorized.namedExports, categorized.typeOnlyExports} {
if len(subGroup) == 0 {
continue
}
var newExportSpecifiers []*ast.Node
for _, exportDecl := range subGroup {
exportClause := exportDecl.AsExportDeclaration().ExportClause
if exportClause != nil && exportClause.Kind == ast.KindNamedExports {
namedExports := exportClause.AsNamedExports()
newExportSpecifiers = append(newExportSpecifiers, namedExports.Elements.Nodes...)
}
}
slices.SortStableFunc(newExportSpecifiers, specifierComparer)
exportDecl := subGroup[0].AsExportDeclaration()
var updatedExportClause *ast.NamedExportBindings
if exportDecl.ExportClause != nil {
if exportDecl.ExportClause.Kind == ast.KindNamedExports {
namedExports := exportDecl.ExportClause.AsNamedExports()
sortedList := factory.NewNodeList(newExportSpecifiers)
updatedExportClause = factory.UpdateNamedExports(namedExports, sortedList)
if sourceFile != nil && !ast.NodeIsSynthesized(namedExports.AsNode()) && !printer.RangeIsOnSingleLine(namedExports.Loc, sourceFile) {
changeTracker.SetEmitFlags(updatedExportClause.AsNode(), printer.EFMultiLine)
}
} else {
updatedExportClause = exportDecl.ExportClause
}
}
newExportDecl := factory.UpdateExportDeclaration(
exportDecl,
exportDecl.Modifiers(),
exportDecl.IsTypeOnly,
updatedExportClause,
exportDecl.ModuleSpecifier,
exportDecl.Attributes,
)
coalescedExports = append(coalescedExports, newExportDecl)
}
}
return coalescedExports
}
type categorizedExports struct {
exportWithoutClause *ast.Statement
namedExports []*ast.Statement
typeOnlyExports []*ast.Statement
}
func getCategorizedExports(exportGroup []*ast.Statement) categorizedExports {
var exportWithoutClause *ast.Statement
var namedExports, typeOnlyExports []*ast.Statement
for _, exportDecl := range exportGroup {
export := exportDecl.AsExportDeclaration()
if export.ExportClause == nil {
if exportWithoutClause == nil {
exportWithoutClause = exportDecl
}
} else if export.IsTypeOnly {
typeOnlyExports = append(typeOnlyExports, exportDecl)
} else {
namedExports = append(namedExports, exportDecl)
}
}
return categorizedExports{
exportWithoutClause: exportWithoutClause,
namedExports: namedExports,
typeOnlyExports: typeOnlyExports,
}
}

View File

@@ -0,0 +1,379 @@
package ls
import (
"context"
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/tspath"
)
// RenameInfo represents the result of a rename validation check.
// It is used by the `textDocument/prepareRename` LSP handler.
type RenameInfo struct {
CanRename bool
LocalizedErrorMessage string
DisplayName string
TriggerSpan lsproto.Range
FileToRename string
NewFileName string
}
func (l *LanguageService) ProvideRename(ctx context.Context, params *lsproto.RenameParams, orchestrator CrossProjectOrchestrator) (lsproto.WorkspaceEditOrNull, error) {
return handleCrossProject(
l,
ctx,
params,
orchestrator,
(*LanguageService).symbolAndEntriesToRename,
combineRenameResponse,
true, /*isRename*/
false, /*implementations*/
symbolEntryTransformOptions{},
)
}
func (l *LanguageService) GetRenameInfo(ctx context.Context, newName string, documentURI lsproto.DocumentUri, position lsproto.Position) RenameInfo {
program, sourceFile := l.getProgramAndFile(documentURI)
pos := int(l.converters.LineAndCharacterToPosition(sourceFile, position))
node := astnav.GetTouchingPropertyName(sourceFile, pos)
node = getAdjustedLocation(node, true /*forRename*/, sourceFile)
if nodeIsEligibleForRename(node) {
if renameInfo, ok := l.getRenameInfoForNode(ctx, newName, node, sourceFile, program); ok {
return renameInfo
}
}
return getRenameInfoError(ctx, diagnostics.You_cannot_rename_this_element)
}
func (l *LanguageService) symbolAndEntriesToRename(ctx context.Context, params *lsproto.RenameParams, data SymbolAndEntriesData, options symbolEntryTransformOptions) (lsproto.WorkspaceEditOrNull, error) {
if !nodeIsEligibleForRename(data.OriginalNode) {
return lsproto.WorkspaceEditOrNull{}, nil
}
program := l.GetProgram()
// Defense-in-depth: validate rename eligibility even if the client skipped prepareRename.
// Use getRenameInfoForNode directly with the already-resolved node to avoid
// re-resolving the position and polluting state baselines.
sourceFile := ast.GetSourceFileOfNode(data.OriginalNode)
if info, ok := l.getRenameInfoForNode(ctx, params.NewName, data.OriginalNode, sourceFile, program); !ok || !info.CanRename {
return lsproto.WorkspaceEditOrNull{}, nil
}
entries := core.FlatMap(data.SymbolsAndEntries, func(s *SymbolAndEntries) []*ReferenceEntry { return s.references })
changes := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
ch, done := program.GetTypeChecker(ctx)
defer done()
quotePreference := lsutil.GetQuotePreference(sourceFile, l.UserPreferences())
useAliasesForRename := l.UserPreferences().UseAliasesForRename.IsTrueOrUnknown()
for _, entry := range entries {
uri := l.getFileNameOfEntry(entry)
if l.UserPreferences().AllowRenameOfImportPath != core.TSTrue && entry.node != nil && ast.IsStringLiteralLike(entry.node) && ast.TryGetImportFromModuleSpecifier(entry.node) != nil {
continue
}
textEdit := &lsproto.TextEdit{
Range: l.getRangeOfEntry(entry),
NewText: l.getTextForRename(data.OriginalNode, entry, params.NewName, ch, quotePreference, useAliasesForRename),
}
changes[uri] = append(changes[uri], textEdit)
}
return lsproto.WorkspaceEditOrNull{
WorkspaceEdit: &lsproto.WorkspaceEdit{
Changes: &changes,
},
}, nil
}
// getRenameInfoForNode performs detailed validation for a rename operation on a specific node.
func (l *LanguageService) getRenameInfoForNode(ctx context.Context, newName string, node *ast.Node, sourceFile *ast.SourceFile, program *compiler.Program) (RenameInfo, bool) {
ch, done := program.GetTypeChecker(ctx)
defer done()
symbol := ch.GetSymbolAtLocation(node)
if symbol == nil {
if ast.IsStringLiteralLike(node) {
// Allow renaming of string literal types with contextual string literal types
typ := getContextualTypeFromParentOrAncestorTypeNode(node, ch)
if typ != nil && (typ.IsStringLiteral() ||
(typ.IsUnion() && core.Every(typ.Types(), func(t *checker.Type) bool {
return t.IsStringLiteral()
}))) {
return getRenameInfoSuccess(node, sourceFile, node.Text(), l.converters), true
}
} else if ast.IsLabelName(node) {
name := node.Text()
return getRenameInfoSuccess(node, sourceFile, name, l.converters), true
}
return RenameInfo{}, false
}
// Only allow a symbol to be renamed if it actually has at least one declaration.
if len(symbol.Declarations) == 0 {
return RenameInfo{}, false
}
if msg := l.renameBlockedReason(sourceFile, node, symbol, ch, program); msg != nil {
return getRenameInfoError(ctx, msg), true
}
if ast.IsStringLiteralLike(node) && ast.TryGetImportFromModuleSpecifier(node) != nil {
if l.UserPreferences().AllowRenameOfImportPath.IsTrue() {
return l.getRenameInfoForModule(ctx, newName, node, sourceFile, symbol)
}
return RenameInfo{}, false
}
return getRenameInfoSuccess(node, sourceFile, ch.SymbolToString(symbol), l.converters), true
}
func nodeIsEligibleForRename(node *ast.Node) bool {
switch node.Kind {
case ast.KindIdentifier,
ast.KindPrivateIdentifier,
ast.KindStringLiteral,
ast.KindNoSubstitutionTemplateLiteral,
ast.KindThisKeyword:
return true
case ast.KindNumericLiteral:
return isLiteralNameOfPropertyDeclarationOrIndexAccess(node)
default:
return false
}
}
// renameBlockedReason returns a non-nil diagnostic message if the rename should be blocked
// because the symbol is a library definition, a default keyword, or would cross node_modules boundaries.
func (l *LanguageService) renameBlockedReason(sourceFile *ast.SourceFile, node *ast.Node, symbol *ast.Symbol, ch *checker.Checker, program *compiler.Program) *diagnostics.Message {
for _, declaration := range symbol.Declarations {
if isDefinedInLibraryFile(program, declaration) {
return diagnostics.You_cannot_rename_elements_that_are_defined_in_the_standard_TypeScript_library
}
}
// Cannot rename `default` as in `import { default as foo } from "./someModule"`
if ast.IsIdentifier(node) && node.Text() == "default" && symbol.Parent != nil && symbol.Parent.Flags&ast.SymbolFlagsModule != 0 {
return diagnostics.You_cannot_rename_this_element
}
if msg := wouldRenameInOtherNodeModules(sourceFile, symbol, ch, l.UserPreferences()); msg != nil {
return msg
}
return nil
}
// isDefinedInLibraryFile checks if a declaration is from a default library file (e.g., lib.d.ts).
func isDefinedInLibraryFile(program *compiler.Program, declaration *ast.Node) bool {
declSourceFile := ast.GetSourceFileOfNode(declaration)
return program.IsSourceFileDefaultLibrary(declSourceFile.Path()) && tspath.IsDeclarationFileName(declSourceFile.FileName())
}
// wouldRenameInOtherNodeModules checks if renaming the symbol would affect node_modules.
func wouldRenameInOtherNodeModules(originalFile *ast.SourceFile, symbol *ast.Symbol, ch *checker.Checker, preferences lsutil.UserPreferences) *diagnostics.Message {
sym := symbol
if !preferences.UseAliasesForRename.IsTrueOrUnknown() && sym.Flags&ast.SymbolFlagsAlias != 0 {
importSpecifier := core.Find(sym.Declarations, ast.IsImportSpecifier)
if importSpecifier != nil && importSpecifier.AsImportSpecifier().PropertyName == nil {
sym = ch.GetAliasedSymbol(sym)
}
}
declarations := sym.Declarations
if len(declarations) == 0 {
return nil
}
originalPackage := module.ParseNodeModuleFromPath(originalFile.FileName(), false /*isFolder*/)
if originalPackage == "" {
// Original source file is not in node_modules.
for _, declaration := range declarations {
if isInsideNodeModules(ast.GetSourceFileOfNode(declaration).FileName()) {
return diagnostics.You_cannot_rename_elements_that_are_defined_in_a_node_modules_folder
}
}
return nil
}
// Original source file is in node_modules.
for _, declaration := range declarations {
declPackage := module.ParseNodeModuleFromPath(ast.GetSourceFileOfNode(declaration).FileName(), false /*isFolder*/)
if declPackage != "" && declPackage != originalPackage {
return diagnostics.You_cannot_rename_elements_that_are_defined_in_another_node_modules_folder
}
}
return nil
}
func ClientSupportsWillRenameFiles(ctx context.Context) bool {
return lsproto.GetClientCapabilities(ctx).Workspace.FileOperations.WillRename
}
func ClientSupportsDocumentChanges(ctx context.Context) bool {
return lsproto.GetClientCapabilities(ctx).Workspace.WorkspaceEdit.DocumentChanges
}
func ClientSupportsRenameResourceOperations(ctx context.Context) bool {
return slices.Contains(lsproto.GetClientCapabilities(ctx).Workspace.WorkspaceEdit.ResourceOperations, lsproto.ResourceOperationKindRename)
}
// getRenameInfoForModule handles rename validation for module specifiers.
func (l *LanguageService) getRenameInfoForModule(ctx context.Context, newName string, specifier *ast.StringLiteralLike, sourceFile *ast.SourceFile, moduleSymbol *ast.Symbol) (RenameInfo, bool) {
if !tspath.IsExternalModuleNameRelative(specifier.Text()) {
return getRenameInfoError(ctx, diagnostics.You_cannot_rename_a_module_via_a_global_import), true
}
if !ClientSupportsDocumentChanges(ctx) || !ClientSupportsRenameResourceOperations(ctx) {
return getRenameInfoError(ctx, diagnostics.File_rename_is_not_supported_by_the_editor), true
}
moduleSourceFile := core.Find(moduleSymbol.Declarations, ast.IsSourceFile)
if moduleSourceFile == nil {
return RenameInfo{}, false
}
fileName := moduleSourceFile.AsSourceFile().FileName()
withoutIndex := ""
if !strings.HasSuffix(specifier.Text(), "/index") && !strings.HasSuffix(specifier.Text(), "/index.js") {
candidate := tspath.RemoveFileExtension(fileName)
if trimmed, ok := strings.CutSuffix(candidate, "/index"); ok {
withoutIndex = trimmed
}
}
displayName := fileName
if withoutIndex != "" {
displayName = withoutIndex
}
newFileName := l.getNewFileNameForModuleRename(displayName, specifier.Text(), newName)
// Span should only be the last component of the path. + 1 to account for the quote character.
indexAfterLastSlash := strings.LastIndex(specifier.Text(), "/") + 1
start := specifier.Pos() + 1 + indexAfterLastSlash
length := len(specifier.Text()) - indexAfterLastSlash
return RenameInfo{
CanRename: true,
DisplayName: specifier.Text()[indexAfterLastSlash:],
TriggerSpan: l.converters.ToLSPRange(sourceFile, core.NewTextRange(start, start+length)),
FileToRename: displayName,
NewFileName: newFileName,
}, true
}
// Adjust the new name based on the old path that an import specifier resolves to.
// For example, if specifier "a.js" resolves to file a.ts, renaming "a.js" -> "b.js" should mean file rename a.ts -> b.ts.
func (l *LanguageService) getNewFileNameForModuleRename(oldPath, specifierText, newName string) string {
newPath := tspath.CombinePaths(tspath.GetDirectoryPath(oldPath), newName)
ignoreCase := !l.host.UseCaseSensitiveFileNames()
var oldExt string
if tspath.IsDeclarationFileName(oldPath) {
oldExt = tspath.GetDeclarationFileExtension(oldPath)
} else {
oldExt = tspath.GetAnyExtensionFromPath(oldPath, nil /*extensions*/, ignoreCase)
}
if !tspath.HasExtension(newPath) {
newPath = newPath + oldExt
} else if tspath.GetAnyExtensionFromPath(newPath, nil /*extensions*/, ignoreCase) == tspath.GetAnyExtensionFromPath(specifierText, nil /*extensions*/, ignoreCase) {
newPath = tspath.ChangeAnyExtension(newPath, oldExt, nil /*extensions*/, ignoreCase)
}
return newPath
}
func (l *LanguageService) getTextForRename(originalNode *ast.Node, entry *ReferenceEntry, newText string, ch *checker.Checker, quotePreference lsutil.QuotePreference, useAliasesForRename bool) string {
if useAliasesForRename && entry.kind != entryKindRange && (ast.IsIdentifier(originalNode) || ast.IsStringLiteralLike(originalNode)) {
node := ast.GetReparsedNodeForNode(entry.node)
kind := entry.kind
parent := node.Parent
name := originalNode.Text()
isShorthandAssignment := ast.IsShorthandPropertyAssignment(parent)
switch {
case isShorthandAssignment || (isObjectBindingElementWithoutPropertyName(parent) && parent.Name() == node && parent.AsBindingElement().DotDotDotToken == nil):
if kind == entryKindSearchedLocalFoundProperty {
return name + ": " + newText
}
if kind == entryKindSearchedPropertyFoundLocal {
return newText + ": " + name
}
// In `const o = { x }; o.x`, symbolAtLocation at `x` in `{ x }` is the property symbol.
// For a binding element `const { x } = o;`, symbolAtLocation at `x` is the property symbol.
if isShorthandAssignment {
grandParent := parent.Parent
if ast.IsObjectLiteralExpression(grandParent) && ast.IsBinaryExpression(grandParent.Parent) && ast.IsModuleExportsAccessExpression(grandParent.Parent.AsBinaryExpression().Left) {
return name + ": " + newText
}
return newText + ": " + name
}
return name + ": " + newText
case ast.IsImportSpecifier(parent) && parent.PropertyName() == nil:
// If the original symbol was using this alias, just rename the alias.
var originalSymbol *ast.Symbol
if ast.IsExportSpecifier(originalNode.Parent) {
originalSymbol = ch.GetExportSpecifierLocalTargetSymbol(originalNode.Parent)
} else {
originalSymbol = ch.GetSymbolAtLocation(originalNode)
}
if originalSymbol != nil && slices.Contains(originalSymbol.Declarations, parent) {
return name + " as " + newText
}
return newText
case ast.IsExportSpecifier(parent) && parent.PropertyName() == nil:
// If the symbol for the node is same as declared node symbol use prefix text
if originalNode == entry.node || ch.GetSymbolAtLocation(originalNode) == ch.GetSymbolAtLocation(entry.node) {
return name + " as " + newText
}
return newText + " as " + name
}
}
// If the node is a numerical indexing literal, then add quotes around the property access.
if entry.kind != entryKindRange && ast.IsNumericLiteral(entry.node) && ast.IsAccessExpression(entry.node.Parent) {
quote := getQuoteFromPreference(quotePreference)
return quote + newText + quote
}
return newText
}
func getQuoteFromPreference(quotePreference lsutil.QuotePreference) string {
if quotePreference == lsutil.QuotePreferenceSingle {
return "'"
}
return `"`
}
func getRenameInfoError(ctx context.Context, message *diagnostics.Message) RenameInfo {
return RenameInfo{
CanRename: false,
LocalizedErrorMessage: message.Localize(locale.FromContext(ctx)),
}
}
func getRenameInfoSuccess(node *ast.Node, sourceFile *ast.SourceFile, displayName string, converters *lsconv.Converters) RenameInfo {
start := astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/)
end := node.End()
if ast.IsStringLiteralLike(node) {
// Exclude the quotes
start++
end--
}
return RenameInfo{
CanRename: true,
DisplayName: displayName,
TriggerSpan: converters.ToLSPRange(sourceFile, core.NewTextRange(start, end)),
}
}

View File

@@ -0,0 +1,211 @@
package ls
import (
"context"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
)
func (l *LanguageService) ProvideSelectionRanges(ctx context.Context, params *lsproto.SelectionRangeParams) (lsproto.SelectionRangeResponse, error) {
_, sourceFile := l.getProgramAndFile(params.TextDocument.Uri)
if sourceFile == nil {
return lsproto.SelectionRangesOrNull{}, nil
}
var results []*lsproto.SelectionRange
for _, position := range params.Positions {
pos := l.converters.LineAndCharacterToPosition(sourceFile, position)
selectionRange := getSmartSelectionRange(l, sourceFile, int(pos))
if selectionRange != nil {
results = append(results, selectionRange)
}
}
return lsproto.SelectionRangesOrNull{SelectionRanges: &results}, nil
}
func getSmartSelectionRange(l *LanguageService, sourceFile *ast.SourceFile, pos int) *lsproto.SelectionRange {
factory := &ast.NodeFactory{}
nodeContainsPosition := func(node *ast.Node) bool {
if node == nil {
return false
}
start := scanner.GetTokenPosOfNode(node, sourceFile, true /*includeJSDoc*/)
end := node.End()
return start <= pos && pos < end
}
pushSelectionRange := func(current *lsproto.SelectionRange, start, end int) *lsproto.SelectionRange {
if start == end {
return current
}
if !(start <= pos && pos <= end) {
return current
}
lspRange := l.converters.ToLSPRange(sourceFile, core.NewTextRange(start, end))
if current != nil && current.Range == lspRange {
return current
}
return &lsproto.SelectionRange{
Range: lspRange,
Parent: current,
}
}
pushSelectionCommentRange := func(current *lsproto.SelectionRange, start, end int) *lsproto.SelectionRange {
current = pushSelectionRange(current, start, end)
commentPos := start
text := sourceFile.Text()
for commentPos < end && commentPos < len(text) && text[commentPos] == '/' {
commentPos++
}
current = pushSelectionRange(current, commentPos, end)
return current
}
positionsAreOnSameLine := func(pos1, pos2 int) bool {
if pos1 == pos2 {
return true
}
lspPos1 := l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(pos1))
lspPos2 := l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(pos2))
return lspPos1.Line == lspPos2.Line
}
shouldSkipNode := func(node *ast.Node, parent *ast.Node) bool {
if ast.IsBlock(node) {
return true
}
if ast.IsTemplateSpan(node) || ast.IsTemplateHead(node) || ast.IsTemplateTail(node) {
return true
}
if parent != nil && ast.IsVariableDeclarationList(node) && ast.IsVariableStatement(parent) {
return true
}
// Skip lone variable declarations
if parent != nil && ast.IsVariableDeclaration(node) && ast.IsVariableDeclarationList(parent) {
decl := parent.AsVariableDeclarationList()
if decl != nil && len(decl.Declarations.Nodes) == 1 {
return true
}
}
if ast.IsJSDocTypeExpression(node) || ast.IsJSDocSignature(node) || ast.IsJSDocTypeLiteral(node) {
return true
}
return false
}
fullRange := l.converters.ToLSPRange(sourceFile, core.NewTextRange(sourceFile.Pos(), sourceFile.End()))
result := &lsproto.SelectionRange{
Range: fullRange,
}
var current *ast.Node
for current = sourceFile.AsNode(); current != nil; {
var next *ast.Node
parent := current
visit := func(node *ast.Node) *ast.Node {
if node != nil && next == nil {
var foundComment *ast.CommentRange
for comment := range scanner.GetTrailingCommentRanges(factory, sourceFile.Text(), node.End()) {
foundComment = &comment
break
}
if foundComment != nil && foundComment.Kind == ast.KindSingleLineCommentTrivia {
result = pushSelectionCommentRange(result, foundComment.Pos(), foundComment.End())
}
if nodeContainsPosition(node) {
// Add range for multi-line function bodies before skipping the block
if ast.IsBlock(node) && ast.IsFunctionLikeDeclaration(parent) {
if !positionsAreOnSameLine(astnav.GetStartOfNode(node, sourceFile, false), node.End()) {
start := astnav.GetStartOfNode(node, sourceFile, false)
end := node.End()
result = pushSelectionRange(result, start, end)
}
}
// Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings.
if ast.IsTemplateSpan(parent) {
templateSpan := parent.AsTemplateSpan()
if templateSpan.Literal != nil {
// Start from just before the '${' and end after the '}'
// The '${' is 2 characters before the expression start
spanStart := node.Pos() - 2
// The '}' is the first character of the template literal (middle or tail)
spanEnd := astnav.GetStartOfNode(templateSpan.Literal, sourceFile, false) + 1
// Validate the positions are reasonable
text := sourceFile.Text()
if spanStart >= 0 && spanEnd <= len(text) && spanStart < spanEnd {
result = pushSelectionRange(result, spanStart, spanEnd)
}
}
}
if !shouldSkipNode(node, parent) {
start := astnav.GetStartOfNode(node, sourceFile, false)
end := node.End()
result = pushSelectionRange(result, start, end)
// String literals should have a stop both inside and outside their quotes.
if ast.IsStringLiteral(node) || node.Kind == ast.KindTemplateExpression || node.Kind == ast.KindNoSubstitutionTemplateLiteral {
// Only add inner content range if there's actually content (handles unterminated literals)
if start+1 < end-1 {
result = pushSelectionRange(result, start+1, end-1)
}
}
}
next = node
}
}
return node
}
visitNodes := func(nodes *ast.NodeList, v *ast.NodeVisitor) *ast.NodeList {
if nodes != nil && len(nodes.Nodes) > 0 {
shouldSkipList := parent != nil && (ast.IsVariableDeclarationList(parent) || ast.IsTemplateExpression(parent))
if !shouldSkipList {
start := astnav.GetStartOfNode(nodes.Nodes[0], sourceFile, false)
end := nodes.Nodes[len(nodes.Nodes)-1].End()
if start <= pos && pos < end {
result = pushSelectionRange(result, start, end)
}
}
}
return v.VisitNodes(nodes)
}
// Visit JSDoc nodes first if they exist
for _, jsdoc := range current.JSDoc(sourceFile) {
visit(jsdoc)
}
tempVisitor := ast.NewNodeVisitor(visit, nil, ast.NodeVisitorHooks{
VisitNodes: visitNodes,
})
current.VisitEachChild(tempVisitor)
current = next
}
return result
}

View File

@@ -0,0 +1,576 @@
package ls
import (
"context"
"fmt"
"slices"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/tspath"
)
// tokenTypes defines the order of token types for encoding
var tokenTypes = []lsproto.SemanticTokenType{
lsproto.SemanticTokenTypeNamespace,
lsproto.SemanticTokenTypeClass,
lsproto.SemanticTokenTypeEnum,
lsproto.SemanticTokenTypeInterface,
lsproto.SemanticTokenTypeStruct,
lsproto.SemanticTokenTypeTypeParameter,
lsproto.SemanticTokenTypeType,
lsproto.SemanticTokenTypeParameter,
lsproto.SemanticTokenTypeVariable,
lsproto.SemanticTokenTypeProperty,
lsproto.SemanticTokenTypeEnumMember,
lsproto.SemanticTokenTypeDecorator,
lsproto.SemanticTokenTypeEvent,
lsproto.SemanticTokenTypeFunction,
lsproto.SemanticTokenTypeMethod,
lsproto.SemanticTokenTypeMacro,
lsproto.SemanticTokenTypeLabel,
lsproto.SemanticTokenTypeComment,
lsproto.SemanticTokenTypeString,
lsproto.SemanticTokenTypeKeyword,
lsproto.SemanticTokenTypeNumber,
lsproto.SemanticTokenTypeRegexp,
lsproto.SemanticTokenTypeOperator,
}
// tokenModifiers defines the order of token modifiers for encoding
var tokenModifiers = []lsproto.SemanticTokenModifier{
lsproto.SemanticTokenModifierDeclaration,
lsproto.SemanticTokenModifierDefinition,
lsproto.SemanticTokenModifierReadonly,
lsproto.SemanticTokenModifierStatic,
lsproto.SemanticTokenModifierDeprecated,
lsproto.SemanticTokenModifierAbstract,
lsproto.SemanticTokenModifierAsync,
lsproto.SemanticTokenModifierModification,
lsproto.SemanticTokenModifierDocumentation,
lsproto.SemanticTokenModifierDefaultLibrary,
"local",
}
type tokenType int
const (
tokenTypeNamespace tokenType = iota
tokenTypeClass
tokenTypeEnum
tokenTypeInterface
tokenTypeStruct
tokenTypeTypeParameter
tokenTypeType
tokenTypeParameter
tokenTypeVariable
tokenTypeProperty
tokenTypeEnumMember
tokenTypeDecorator
tokenTypeEvent
tokenTypeFunction
tokenTypeMethod // Previously called "member" in TypeScript
tokenTypeMacro
tokenTypeLabel
tokenTypeComment
tokenTypeString
tokenTypeKeyword
tokenTypeNumber
tokenTypeRegexp
tokenTypeOperator
)
type tokenModifier int
const (
tokenModifierDeclaration tokenModifier = 1 << iota
tokenModifierDefinition
tokenModifierReadonly
tokenModifierStatic
tokenModifierDeprecated
tokenModifierAbstract
tokenModifierAsync
tokenModifierModification
tokenModifierDocumentation
tokenModifierDefaultLibrary
tokenModifierLocal
)
// SemanticTokensLegend returns the legend describing the token types and modifiers.
// It filters the legend to only include types and modifiers that the client supports,
// as indicated by clientCapabilities.
func SemanticTokensLegend(clientCapabilities lsproto.ResolvedSemanticTokensClientCapabilities) *lsproto.SemanticTokensLegend {
types := make([]string, 0, len(tokenTypes))
for _, t := range tokenTypes {
if slices.Contains(clientCapabilities.TokenTypes, string(t)) {
types = append(types, string(t))
}
}
modifiers := make([]string, 0, len(tokenModifiers))
for _, m := range tokenModifiers {
if slices.Contains(clientCapabilities.TokenModifiers, string(m)) {
modifiers = append(modifiers, string(m))
}
}
return &lsproto.SemanticTokensLegend{
TokenTypes: types,
TokenModifiers: modifiers,
}
}
func (l *LanguageService) ProvideSemanticTokens(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.SemanticTokensResponse, error) {
program, file := l.getProgramAndFile(documentURI)
c, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
tokens := l.collectSemanticTokens(ctx, c, file, program)
if len(tokens) == 0 {
return lsproto.SemanticTokensOrNull{}, nil
}
// Convert to LSP format (relative encoding)
encoded := encodeSemanticTokens(ctx, tokens, file, l.converters)
return lsproto.SemanticTokensOrNull{
SemanticTokens: &lsproto.SemanticTokens{
Data: encoded,
},
}, nil
}
func (l *LanguageService) ProvideSemanticTokensRange(ctx context.Context, documentURI lsproto.DocumentUri, rng lsproto.Range) (lsproto.SemanticTokensRangeResponse, error) {
program, file := l.getProgramAndFile(documentURI)
c, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
start := int(l.converters.LineAndCharacterToPosition(file, rng.Start))
end := int(l.converters.LineAndCharacterToPosition(file, rng.End))
tokens := l.collectSemanticTokensInRange(ctx, c, file, program, start, end)
if len(tokens) == 0 {
return lsproto.SemanticTokensOrNull{}, nil
}
// Convert to LSP format (relative encoding)
encoded := encodeSemanticTokens(ctx, tokens, file, l.converters)
return lsproto.SemanticTokensOrNull{
SemanticTokens: &lsproto.SemanticTokens{
Data: encoded,
},
}, nil
}
type semanticToken struct {
node *ast.Node
tokenType tokenType
tokenModifier tokenModifier
}
func (l *LanguageService) collectSemanticTokens(ctx context.Context, c *checker.Checker, file *ast.SourceFile, program *compiler.Program) []semanticToken {
return l.collectSemanticTokensInRange(ctx, c, file, program, file.Pos(), file.End())
}
func (l *LanguageService) collectSemanticTokensInRange(ctx context.Context, c *checker.Checker, file *ast.SourceFile, program *compiler.Program, spanStart, spanEnd int) []semanticToken {
tokens := []semanticToken{}
inJSXElement := false
var visit func(*ast.Node) bool
visit = func(node *ast.Node) bool {
// Check for cancellation
if ctx.Err() != nil {
return false
}
if node == nil {
return false
}
if node.Flags&ast.NodeFlagsReparsed != 0 {
return false
}
nodeEnd := node.End()
if node.Pos() >= spanEnd || nodeEnd <= spanStart {
return false
}
prevInJSXElement := inJSXElement
if ast.IsJsxElement(node) || ast.IsJsxSelfClosingElement(node) {
inJSXElement = true
} else if ast.IsJsxExpression(node) {
inJSXElement = false
}
if ast.IsIdentifier(node) && node.Text() != "" && !inJSXElement && !isInImportClause(node) && !isInfinityOrNaNString(node.Text()) {
symbol := c.GetSymbolAtLocation(node)
if symbol != nil {
// Resolve aliases
if symbol.Flags&ast.SymbolFlagsAlias != 0 {
symbol = c.GetAliasedSymbol(symbol)
}
tokenType, ok := classifySymbol(symbol, getMeaningFromLocation(node))
if ok {
tokenModifier := tokenModifier(0)
// Check if this is a declaration
parent := node.Parent
if parent != nil {
parentIsDeclaration := ast.IsBindingElement(parent) || tokenFromDeclarationMapping(parent.Kind) == tokenType
if parentIsDeclaration && parent.Name() == node {
tokenModifier |= tokenModifierDeclaration
}
}
// Property declaration in constructor: reclassify parameters as properties in property access context
if tokenType == tokenTypeParameter && ast.IsRightSideOfQualifiedNameOrPropertyAccess(node) {
tokenType = tokenTypeProperty
}
// Type-based reclassification
tokenType = reclassifyByType(c, node, tokenType)
// Get the value declaration to check modifiers
if decl := symbol.ValueDeclaration; decl != nil {
modifiers := ast.GetCombinedModifierFlags(decl)
nodeFlags := ast.GetCombinedNodeFlags(decl)
if modifiers&ast.ModifierFlagsStatic != 0 {
tokenModifier |= tokenModifierStatic
}
if modifiers&ast.ModifierFlagsAsync != 0 {
tokenModifier |= tokenModifierAsync
}
if tokenType != tokenTypeClass && tokenType != tokenTypeInterface {
if (modifiers&ast.ModifierFlagsReadonly != 0) || (nodeFlags&ast.NodeFlagsConst != 0) || (symbol.Flags&ast.SymbolFlagsEnumMember != 0) {
tokenModifier |= tokenModifierReadonly
}
}
if (tokenType == tokenTypeVariable || tokenType == tokenTypeFunction) && isLocalDeclaration(decl, file) {
tokenModifier |= tokenModifierLocal
}
declSourceFile := ast.GetSourceFileOfNode(decl)
if declSourceFile != nil && program.IsSourceFileDefaultLibrary(tspath.Path(declSourceFile.FileName())) {
tokenModifier |= tokenModifierDefaultLibrary
}
} else if symbol.Declarations != nil {
for _, decl := range symbol.Declarations {
declSourceFile := ast.GetSourceFileOfNode(decl)
if declSourceFile != nil && program.IsSourceFileDefaultLibrary(tspath.Path(declSourceFile.FileName())) {
tokenModifier |= tokenModifierDefaultLibrary
break
}
}
}
tokens = append(tokens, semanticToken{
node: node,
tokenType: tokenType,
tokenModifier: tokenModifier,
})
}
}
}
node.ForEachChild(visit)
inJSXElement = prevInJSXElement
return false
}
visit(file.AsNode())
// Check for cancellation after collection
if ctx.Err() != nil {
return nil
}
return tokens
}
func classifySymbol(symbol *ast.Symbol, meaning ast.SemanticMeaning) (tokenType, bool) {
flags := symbol.Flags
if flags&ast.SymbolFlagsClass != 0 {
return tokenTypeClass, true
}
if flags&ast.SymbolFlagsEnum != 0 {
return tokenTypeEnum, true
}
if flags&ast.SymbolFlagsTypeAlias != 0 {
return tokenTypeType, true
}
if flags&ast.SymbolFlagsInterface != 0 {
if meaning&ast.SemanticMeaningType != 0 {
return tokenTypeInterface, true
}
}
if flags&ast.SymbolFlagsTypeParameter != 0 {
return tokenTypeTypeParameter, true
}
// Check the value declaration
decl := symbol.ValueDeclaration
if decl == nil && len(symbol.Declarations) > 0 {
decl = symbol.Declarations[0]
}
if decl != nil {
if ast.IsBindingElement(decl) {
decl = getDeclarationForBindingElement(decl)
}
if tokenType := tokenFromDeclarationMapping(decl.Kind); tokenType >= 0 {
return tokenType, true
}
}
return 0, false
}
func tokenFromDeclarationMapping(kind ast.Kind) tokenType {
switch kind {
case ast.KindVariableDeclaration:
return tokenTypeVariable
case ast.KindParameter:
return tokenTypeParameter
case ast.KindPropertyDeclaration:
return tokenTypeProperty
case ast.KindModuleDeclaration:
return tokenTypeNamespace
case ast.KindEnumDeclaration:
return tokenTypeEnum
case ast.KindEnumMember:
return tokenTypeEnumMember
case ast.KindClassDeclaration, ast.KindClassExpression:
return tokenTypeClass
case ast.KindMethodDeclaration:
return tokenTypeMethod
case ast.KindFunctionDeclaration, ast.KindFunctionExpression:
return tokenTypeFunction
case ast.KindMethodSignature:
return tokenTypeMethod
case ast.KindGetAccessor, ast.KindSetAccessor:
return tokenTypeProperty
case ast.KindPropertySignature:
return tokenTypeProperty
case ast.KindInterfaceDeclaration:
return tokenTypeInterface
case ast.KindTypeAliasDeclaration:
return tokenTypeType
case ast.KindTypeParameter:
return tokenTypeTypeParameter
case ast.KindPropertyAssignment, ast.KindShorthandPropertyAssignment:
return tokenTypeProperty
default:
return -1
}
}
func reclassifyByType(c *checker.Checker, node *ast.Node, tt tokenType) tokenType {
// Type-based reclassification for variables, properties, and parameters
if tt == tokenTypeVariable || tt == tokenTypeProperty || tt == tokenTypeParameter {
typ := c.GetTypeAtLocation(node)
if typ != nil {
test := func(condition func(*checker.Type) bool) bool {
if condition(typ) {
return true
}
if typ.Flags()&checker.TypeFlagsUnion != 0 {
if slices.ContainsFunc(typ.AsUnionType().Types(), condition) {
return true
}
}
return false
}
// Check for constructor signatures (class-like)
if tt != tokenTypeParameter && test(func(t *checker.Type) bool {
return len(c.GetSignaturesOfType(t, checker.SignatureKindConstruct)) > 0
}) {
return tokenTypeClass
}
// Check for call signatures (function-like)
// Must have call signatures AND (no properties OR be used in call context)
hasCallSignatures := test(func(t *checker.Type) bool {
return len(c.GetSignaturesOfType(t, checker.SignatureKindCall)) > 0
})
if hasCallSignatures {
hasNoProperties := !test(func(t *checker.Type) bool {
objType := t.AsObjectType()
return objType != nil && len(objType.Properties()) > 0
})
if hasNoProperties || isExpressionInCallExpression(node) {
if tt == tokenTypeProperty {
return tokenTypeMethod
}
return tokenTypeFunction
}
}
}
}
return tt
}
func isLocalDeclaration(decl *ast.Node, sourceFile *ast.SourceFile) bool {
if ast.IsBindingElement(decl) {
decl = getDeclarationForBindingElement(decl)
}
if ast.IsVariableDeclaration(decl) {
parent := decl.Parent
// Check if this is a catch clause parameter
if parent != nil && ast.IsCatchClause(parent) {
return ast.GetSourceFileOfNode(decl) == sourceFile
}
if parent != nil && ast.IsVariableDeclarationList(parent) {
grandparent := parent.Parent
if grandparent != nil {
greatGrandparent := grandparent.Parent
return (!ast.IsSourceFile(greatGrandparent) || ast.IsCatchClause(grandparent)) &&
ast.GetSourceFileOfNode(decl) == sourceFile
}
}
} else if ast.IsFunctionDeclaration(decl) {
parent := decl.Parent
return parent != nil && !ast.IsSourceFile(parent) && ast.GetSourceFileOfNode(decl) == sourceFile
}
return false
}
func getDeclarationForBindingElement(element *ast.Node) *ast.Node {
for {
parent := element.Parent
if parent != nil && ast.IsBindingPattern(parent) {
grandparent := parent.Parent
if grandparent != nil && ast.IsBindingElement(grandparent) {
element = grandparent
continue
}
return parent.Parent
}
return element
}
}
func isInImportClause(node *ast.Node) bool {
parent := node.Parent
return parent != nil && (ast.IsImportClause(parent) || ast.IsImportSpecifier(parent) || ast.IsNamespaceImport(parent))
}
func isExpressionInCallExpression(node *ast.Node) bool {
for ast.IsRightSideOfQualifiedNameOrPropertyAccess(node) {
node = node.Parent
}
parent := node.Parent
return parent != nil && ast.IsCallExpression(parent) && parent.Expression() == node
}
func isInfinityOrNaNString(text string) bool {
return text == "Infinity" || text == "NaN"
}
// encodeSemanticTokens encodes tokens into the LSP format using relative positioning.
// It filters tokens based on client capabilities, only including types and modifiers that the client supports.
func encodeSemanticTokens(ctx context.Context, tokens []semanticToken, file *ast.SourceFile, converters *lsconv.Converters) []uint32 {
// Build mapping from server token types/modifiers to client indices
typeMapping := make(map[tokenType]uint32)
modifierMapping := make(map[lsproto.SemanticTokenModifier]uint32)
clientCapabilities := lsproto.GetClientCapabilities(ctx).TextDocument.SemanticTokens
// Map server token types to client-supported indices
clientIdx := uint32(0)
for i, serverType := range tokenTypes {
if slices.Contains(clientCapabilities.TokenTypes, string(serverType)) {
typeMapping[tokenType(i)] = clientIdx
clientIdx++
}
}
// Map server token modifiers to client-supported bit positions
clientBit := uint32(0)
for _, serverModifier := range tokenModifiers {
if slices.Contains(clientCapabilities.TokenModifiers, string(serverModifier)) {
modifierMapping[serverModifier] = clientBit
clientBit++
}
}
// Each token encodes 5 uint32 values: deltaLine, deltaChar, length, tokenType, tokenModifiers
encoded := make([]uint32, 0, len(tokens)*5)
prevLine := uint32(0)
prevChar := uint32(0)
for _, token := range tokens {
// Skip tokens with types not supported by the client
clientTypeIdx, typeSupported := typeMapping[token.tokenType]
if !typeSupported {
continue
}
// Map modifiers to client-supported bit mask
clientModifierMask := uint32(0)
for i, serverModifier := range tokenModifiers {
if token.tokenModifier&(1<<i) != 0 {
if clientBit, ok := modifierMapping[serverModifier]; ok {
clientModifierMask |= 1 << clientBit
}
}
}
// Use GetTokenPosOfNode to skip trivia (comments, whitespace) before the identifier
tokenStart := scanner.GetTokenPosOfNode(token.node, file, false)
tokenEnd := token.node.End()
// Convert both start and end positions to LSP coordinates, then compute length
startPos := converters.PositionToLineAndCharacter(file, core.TextPos(tokenStart))
endPos := converters.PositionToLineAndCharacter(file, core.TextPos(tokenEnd))
// Length is the character difference when on the same line
var tokenLength uint32
if startPos.Line == endPos.Line {
tokenLength = endPos.Character - startPos.Character
} else {
panic(fmt.Sprintf("semantic tokens: token spans multiple lines: start=(%d,%d) end=(%d,%d) for token at offset %d",
startPos.Line, startPos.Character, endPos.Line, endPos.Character, tokenStart))
}
line := startPos.Line
char := startPos.Character
// Verify that positions are strictly increasing (visitor walks in order)
if len(encoded) > 0 && (line < prevLine || (line == prevLine && char <= prevChar)) {
panic(fmt.Sprintf("semantic tokens: positions must be strictly increasing: prev=(%d,%d) current=(%d,%d) for token at offset %d",
prevLine, prevChar, line, char, tokenStart))
}
// Encode as: [deltaLine, deltaChar, length, tokenType, tokenModifiers]
deltaLine := line - prevLine
var deltaChar uint32
if deltaLine == 0 {
deltaChar = char - prevChar
} else {
deltaChar = char
}
encoded = append(
encoded,
deltaLine,
deltaChar,
tokenLength,
clientTypeIdx,
clientModifierMask,
)
prevLine = line
prevChar = char
}
return encoded
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,134 @@
package ls
import (
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/outputpaths"
"github.com/microsoft/typescript-go/internal/sourcemap"
"github.com/microsoft/typescript-go/internal/tspath"
)
func (l *LanguageService) getMappedLocation(fileName string, fileRange core.TextRange) lsproto.Location {
startPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.Pos()))
if startPos == nil {
lspRange := l.createLspRangeFromRange(fileRange, l.getScript(fileName))
return lsproto.Location{
Uri: lsconv.FileNameToDocumentURI(fileName),
Range: lspRange,
}
}
endPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.End()))
if endPos == nil || endPos.FileName != startPos.FileName || endPos.Pos < startPos.Pos {
// When end doesn't map, maps to a different source file (e.g. in a .d.ts with a
// multi-source source map from --outFile compilation), or maps to a position before
// start (non-monotonic source map mappings), approximate the end position.
endPos = &sourcemap.DocumentPosition{
FileName: startPos.FileName,
Pos: startPos.Pos + fileRange.Len(),
}
}
newRange := core.NewTextRange(startPos.Pos, endPos.Pos)
lspRange := l.createLspRangeFromRange(newRange, l.getScript(startPos.FileName))
return lsproto.Location{
Uri: lsconv.FileNameToDocumentURI(startPos.FileName),
Range: lspRange,
}
}
type script struct {
fileName string
text string
}
func (s *script) FileName() string {
return s.fileName
}
func (s *script) Text() string {
return s.text
}
func (l *LanguageService) getScript(fileName string) *script {
text, ok := l.host.ReadFile(fileName)
if !ok {
return nil
}
return &script{fileName: fileName, text: text}
}
func (l *LanguageService) tryGetSourcePosition(
fileName string,
position core.TextPos,
) *sourcemap.DocumentPosition {
newPos := l.tryGetSourcePositionWorker(fileName, position)
if newPos != nil {
if _, ok := l.ReadFile(newPos.FileName); !ok { // File doesn't exist
return nil
}
}
return newPos
}
func (l *LanguageService) tryGetSourcePositionWorker(
fileName string,
position core.TextPos,
) *sourcemap.DocumentPosition {
if !tspath.IsDeclarationFileName(fileName) {
return nil
}
positionMapper := l.GetDocumentPositionMapper(fileName)
documentPos := positionMapper.GetSourcePosition(&sourcemap.DocumentPosition{FileName: fileName, Pos: int(position)})
if documentPos == nil {
return nil
}
if newPos := l.tryGetSourcePositionWorker(documentPos.FileName, core.TextPos(documentPos.Pos)); newPos != nil {
return newPos
}
return documentPos
}
func (l *LanguageService) tryGetGeneratedPosition(
fileName string,
position core.TextPos,
) *sourcemap.DocumentPosition {
newPos := l.tryGetGeneratedPositionWorker(fileName, position)
if newPos != nil {
if _, ok := l.ReadFile(newPos.FileName); !ok { // File doesn't exist
return nil
}
}
return newPos
}
func (l *LanguageService) tryGetGeneratedPositionWorker(
fileName string,
position core.TextPos,
) *sourcemap.DocumentPosition {
if tspath.IsDeclarationFileName(fileName) {
return nil
}
program := l.GetProgram()
if program == nil || program.GetSourceFile(fileName) == nil {
return nil
}
path := l.toPath(fileName)
// If this is source file of project reference source (instead of redirect) there is no generated position
if program.IsSourceFromProjectReference(path) {
return nil
}
declarationFileName := outputpaths.GetOutputDeclarationFileNameWorker(fileName, program.Options(), program)
positionMapper := l.GetDocumentPositionMapper(declarationFileName)
documentPos := positionMapper.GetGeneratedPosition(&sourcemap.DocumentPosition{FileName: fileName, Pos: int(position)})
if documentPos == nil {
return nil
}
if newPos := l.tryGetGeneratedPositionWorker(documentPos.FileName, core.TextPos(documentPos.Pos)); newPos != nil {
return newPos
}
return documentPos
}

View File

@@ -0,0 +1,707 @@
package ls
import (
"context"
"math"
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/binder"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/modulespecifiers"
"github.com/microsoft/typescript-go/internal/parser"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
)
func (l *LanguageService) ProvideSourceDefinition(
ctx context.Context,
documentURI lsproto.DocumentUri,
position lsproto.Position,
) (lsproto.DefinitionResponse, error) {
caps := lsproto.GetClientCapabilities(ctx)
clientSupportsLink := caps.TextDocument.Definition.LinkSupport
program, file := l.getProgramAndFile(documentURI)
pos := int(l.converters.LineAndCharacterToPosition(file, position))
resolver := l.newSourceDefResolver(program, file.FileName())
node := astnav.GetTouchingPropertyName(file, pos)
if node.Kind == ast.KindSourceFile {
// Triple-slash directives are comments, not AST nodes, so
// GetTouchingPropertyName returns the SourceFile node.
if declarations, ref := resolver.resolveTripleSlashReference(file, pos, program); len(declarations) != 0 {
originSelectionRange := l.createLspRangeFromBounds(ref.Pos(), ref.End(), file)
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, declarations, nil /*reference*/), nil
}
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil
}
originSelectionRange := l.createLspRangeFromNode(node, file)
// If the cursor is directly on a module specifier string, resolve to the
// implementation file's entry point.
containingModuleSpecifier := findContainingModuleSpecifier(node)
if node == containingModuleSpecifier {
specifierMode := program.GetModeForUsageLocation(file, containingModuleSpecifier)
if implementationFile := resolver.resolveImplementation(containingModuleSpecifier.Text(), specifierMode); implementationFile != "" {
if sourceFile := resolver.getOrParseSourceFile(implementationFile); sourceFile != nil {
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, getSourceDefinitionEntryDeclarations(sourceFile), nil), nil
}
}
return l.provideDefinitionWorker(ctx, documentURI, position)
}
// Phase 1: Syntactic fast path — when the cursor is inside an
// import/require/export, forward-resolve the module specifier to an
// implementation file and search it directly. This avoids acquiring
// the type checker entirely when the fast path succeeds.
var resolvedImplFile string
if containingModuleSpecifier != nil {
specifierMode := program.GetModeForUsageLocation(file, containingModuleSpecifier)
resolvedImplFile = resolver.resolveImplementation(containingModuleSpecifier.Text(), specifierMode)
}
if resolvedImplFile != "" {
names := getCandidateSourceDeclarationNames(node, nil)
moduleResults := resolver.searchImplementationFile(node, resolvedImplFile, names)
if len(moduleResults) != 0 {
if !ast.IsPartOfTypeNode(node) && !ast.IsPartOfTypeOnlyImportOrExportDeclaration(node) || hasConcreteSourceDeclarations(moduleResults) {
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, uniqueDeclarationNodes(moduleResults), nil), nil
}
}
}
// Phase 2: Type checker path — acquire the checker for the original file
// and use its declarations and module specifier to map to source
// implementations. This is the only point where the checker is used;
// after this, only the NoDts module resolver and file parsing are needed.
checkerDeclarations, moduleSpecifier := getSourceDefCheckerInfo(ctx, program, file, node)
// Phase 3: Map checker results to source definitions.
declarations := resolver.resolveFromCheckerInfo(node, resolvedImplFile, checkerDeclarations, moduleSpecifier)
if len(declarations) == 0 {
// If we resolved an implementation file from an import/export but
// couldn't find specific declarations, fall back to the file entry
// point rather than the standard definition provider — unless the
// checker found declarations that are all type-only (e.g. interfaces),
// in which case the .d.ts definition is more appropriate.
if containingModuleSpecifier != nil && resolvedImplFile != "" && !hasConcreteSourceDeclarations(checkerDeclarations) {
if sourceFile := resolver.getOrParseSourceFile(resolvedImplFile); sourceFile != nil {
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, getSourceDefinitionEntryDeclarations(sourceFile), nil), nil
}
}
return l.provideDefinitionWorker(ctx, documentURI, position)
}
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, declarations, nil /*reference*/), nil
}
// sourceDefResolver resolves source definitions by mapping .d.ts declarations
// to their implementation files (.js/.ts). It uses the NoDts module resolver
// and file parsing for resolution, but never acquires the type checker or
// the original program; all checker-dependent work is done before results
// are passed in.
type sourceDefResolver struct {
ls *LanguageService
fs vfs.FS
options *core.CompilerOptions
getSourceFile func(string) *ast.SourceFile
resolveFrom string
resolver *module.Resolver
parsedFiles map[string]*ast.SourceFile
}
func (l *LanguageService) newSourceDefResolver(
program *compiler.Program,
resolveFrom string,
) *sourceDefResolver {
options := program.Options()
noDtsOptions := options.Clone()
noDtsOptions.NoDtsResolution = core.TSTrue
return &sourceDefResolver{
ls: l,
fs: program.Host().FS(),
options: options,
getSourceFile: program.GetSourceFile,
resolveFrom: resolveFrom,
resolver: module.NewResolver(program.Host(), noDtsOptions, program.GetGlobalTypingsCacheLocation(), ""),
}
}
// resolveFromCheckerInfo maps type-checker declarations to source
// implementations. It uses only the NoDts module resolver and file parsing;
// the type checker and original request file are not needed.
func (r *sourceDefResolver) resolveFromCheckerInfo(
node *ast.Node,
resolvedImplFile string,
checkerDeclarations []*ast.Node,
moduleSpecifier string,
) []*ast.Node {
// If we don't yet have a forward-resolved implementation file, try to
// recover a module specifier from the checker (e.g. from the import that
// brought the symbol into scope, or from the root of an access expression).
if resolvedImplFile == "" && moduleSpecifier != "" {
resolvedImplFile = r.resolveImplementation(moduleSpecifier, r.inferImpliedNodeFormat(r.resolveFrom))
}
// For property access where the checker found no declarations (e.g.
// mapped types), search the implementation file for the property name.
if len(checkerDeclarations) == 0 && resolvedImplFile != "" {
names := getCandidateSourceDeclarationNames(node, nil)
if results := r.searchImplementationFile(node, resolvedImplFile, names); results != nil {
return uniqueDeclarationNodes(results)
}
}
var declarations []*ast.Node
for _, declaration := range checkerDeclarations {
declarations = append(declarations, r.mapDeclarationToSource(node, declaration, resolvedImplFile)...)
}
declarations = uniqueDeclarationNodes(declarations)
if hasConcreteSourceDeclarations(declarations) {
return declarations
}
return nil
}
// getSourceDefCheckerInfo acquires the type checker for the given file and
// returns the definition declarations for node along with the module specifier
// of the import that brought the symbol into scope (empty if not applicable).
func getSourceDefCheckerInfo(
ctx context.Context,
program *compiler.Program,
file *ast.SourceFile,
node *ast.Node,
) ([]*ast.Node, string) {
c, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
declarations := getDeclarationsFromLocation(c, node)
isPropertyName := node.Parent != nil && ast.IsAccessExpression(node.Parent) && node.Parent.Name() == node
if len(declarations) == 0 && isPropertyName {
if left := node.Parent.Expression(); left != nil {
if prop := c.GetPropertyOfType(c.GetTypeAtLocation(left), node.Text()); prop != nil {
declarations = prop.Declarations
}
}
}
if calledDeclaration := tryGetSignatureDeclaration(c, node); calledDeclaration != nil {
nonFunctionDeclarations := core.Filter(declarations, func(node *ast.Node) bool { return !ast.IsFunctionLike(node) })
declarations = append(nonFunctionDeclarations, calledDeclaration)
}
// Extract module specifier from the import that brought this symbol into
// scope. For property access (obj.prop), walk up the access chain to the
// root expression's symbol.
var moduleSpecifier string
resolveNode := node
if isPropertyName {
expr := node.Parent.Expression()
for expr != nil && ast.IsAccessExpression(expr) {
expr = expr.Expression()
}
if expr != nil {
resolveNode = expr
}
}
if sym := c.GetSymbolAtLocation(resolveNode); sym != nil {
for _, d := range sym.Declarations {
if !ast.IsImportSpecifier(d) && !ast.IsImportClause(d) && !ast.IsNamespaceImport(d) && !ast.IsImportEqualsDeclaration(d) {
continue
}
if spec := checker.TryGetModuleSpecifierFromDeclaration(d); spec != nil {
moduleSpecifier = spec.Text()
break
}
}
}
return declarations, moduleSpecifier
}
// resolveTripleSlashReference handles /// <reference path/types="..."/> directives.
// For path references to .js files, it returns the entry declarations directly.
// For path references to .d.ts files or type references, it uses the NoDts
// resolver to find the corresponding implementation file.
func (r *sourceDefResolver) resolveTripleSlashReference(file *ast.SourceFile, pos int, program *compiler.Program) ([]*ast.Node, *ast.FileReference) {
ref := getReferenceAtPosition(file, pos, program)
if ref == nil || ref.file == nil {
return nil, nil
}
// If the referenced file is already an implementation file, return it directly.
if !ref.file.IsDeclarationFile {
return getSourceDefinitionEntryDeclarations(ref.file), ref.reference
}
// The referenced file is a .d.ts. Try to find the implementation file
// using the NoDts module resolver via findImplementationFileFromDtsFileName.
dtsFileName := ref.file.FileName()
preferredMode := r.inferImpliedNodeFormat(dtsFileName)
implementationFile := r.findImplementationFileFromDtsFileName(dtsFileName, preferredMode)
if implementationFile == "" {
return nil, nil
}
sourceFile := r.getOrParseSourceFile(implementationFile)
if sourceFile == nil {
return nil, nil
}
return getSourceDefinitionEntryDeclarations(sourceFile), ref.reference
}
// searchImplementationFile searches an implementation file for declarations
// matching the given names. Returns nil when no declarations matched; callers
// fall through to the checker path or to the standard definition provider.
func (r *sourceDefResolver) searchImplementationFile(
originalNode *ast.Node,
implementationFile string,
names []string,
) []*ast.Node {
if implementationFile == "" {
return nil
}
sourceFile := r.getOrParseSourceFile(implementationFile)
if sourceFile == nil {
return nil
}
if isDefaultImportName(originalNode) {
// For default imports, only search for "default" declarations to avoid
// matching unrelated declarations with the same identifier name.
defaultDeclarations := r.findDeclarationsInFile(implementationFile, []string{"default"}, &collections.Set[string]{})
if len(defaultDeclarations) != 0 {
return filterPreferredSourceDeclarations(originalNode, defaultDeclarations)
}
return getSourceDefinitionEntryDeclarations(sourceFile)
}
declarations := r.findDeclarationsInFile(implementationFile, names, &collections.Set[string]{})
if len(declarations) != 0 {
return filterPreferredSourceDeclarations(originalNode, declarations)
}
return nil
}
func isDefaultImportName(node *ast.Node) bool {
if node == nil || node.Parent == nil || !ast.IsImportClause(node.Parent) || node.Parent.Name() != node || node.Parent.Parent == nil {
return false
}
return ast.IsDefaultImport(node.Parent.Parent)
}
func getSourceDefinitionEntryNode(sourceFile *ast.SourceFile) *ast.Node {
if len(sourceFile.Statements.Nodes) != 0 {
return sourceFile.Statements.Nodes[0].AsNode()
}
return sourceFile.AsNode()
}
func getSourceDefinitionEntryDeclarations(sourceFile *ast.SourceFile) []*ast.Node {
return []*ast.Node{getSourceDefinitionEntryNode(sourceFile)}
}
func (r *sourceDefResolver) mapDeclarationToSource(
originalNode *ast.Node,
declaration *ast.Node,
resolvedImplFile string,
) []*ast.Node {
file, startPos := getFileAndStartPosFromDeclaration(declaration)
fileName := file.FileName()
if mapped := r.ls.tryGetSourcePosition(fileName, startPos); mapped != nil {
if sourceFile := r.getOrParseSourceFile(mapped.FileName); sourceFile != nil {
return []*ast.Node{findClosestDeclarationNode(sourceFile, mapped.Pos)}
}
}
if !tspath.IsDeclarationFileName(fileName) {
return []*ast.Node{declaration}
}
implementationFile := resolvedImplFile
if implementationFile == "" {
// Reverse-resolve .d.ts path to implementation file. This path is only
// reached for declarations with no associated module specifier (e.g.
// globals, ambient declarations, or when forward resolution failed).
dtsFileName := ast.GetSourceFileOfNode(declaration).FileName()
preferredMode := r.inferImpliedNodeFormat(dtsFileName)
implementationFile = r.findImplementationFileFromDtsFileName(dtsFileName, preferredMode)
}
return r.searchImplementationFile(originalNode, implementationFile, getCandidateSourceDeclarationNames(originalNode, declaration))
}
func (r *sourceDefResolver) findImplementationFileFromDtsFileName(
dtsFileName string,
preferredMode core.ResolutionMode,
) string {
if jsExt := module.TryGetJSExtensionForFile(dtsFileName, r.options); jsExt != "" {
candidate := tspath.ChangeExtension(dtsFileName, jsExt)
if r.fs.FileExists(candidate) {
return candidate
}
}
parts := modulespecifiers.GetNodeModulePathParts(dtsFileName)
if parts == nil {
return ""
}
// Ensure the file only contains one /node_modules/ segment. If there's more
// than one, the package name extraction may be incorrect, so bail out.
if strings.LastIndex(dtsFileName, "/node_modules/") != parts.TopLevelNodeModulesIndex {
return ""
}
packageNamePathPart := dtsFileName[parts.TopLevelPackageNameIndex+1 : parts.PackageRootIndex]
packageName := module.GetPackageNameFromTypesPackageName(module.UnmangleScopedPackageName(packageNamePathPart))
if packageName == "" {
return ""
}
pathToFileInPackage := dtsFileName[parts.PackageRootIndex+1:]
// Try resolving as a package subpath first (e.g. "pkg/dist/utils"), then
// fall back to the bare package name (e.g. "pkg"). This covers both main
// entrypoints and deep imports without needing to inspect package.json
// entrypoints.
if pathToFileInPackage != "" {
specifier := packageName + "/" + tspath.RemoveFileExtension(pathToFileInPackage)
if implementationFile := r.resolveImplementation(specifier, preferredMode); implementationFile != "" {
return implementationFile
}
}
return r.resolveImplementation(packageName, preferredMode)
}
func (r *sourceDefResolver) resolveImplementation(
moduleName string,
preferredMode core.ResolutionMode,
) string {
return r.resolveImplementationFrom(moduleName, r.resolveFrom, preferredMode)
}
func (r *sourceDefResolver) resolveImplementationFrom(
moduleName string,
resolveFromFile string,
preferredMode core.ResolutionMode,
) string {
modes := []core.ResolutionMode{preferredMode}
if preferredMode != core.ModuleKindESNext {
modes = append(modes, core.ModuleKindESNext)
}
if preferredMode != core.ModuleKindCommonJS {
modes = append(modes, core.ModuleKindCommonJS)
}
for _, mode := range modes {
resolved, _ := r.resolver.ResolveModuleName(moduleName, resolveFromFile, mode, nil)
if resolved != nil && resolved.IsResolved() && !tspath.IsDeclarationFileName(resolved.ResolvedFileName) {
return resolved.ResolvedFileName
}
}
return ""
}
func (r *sourceDefResolver) getOrParseSourceFile(fileName string) *ast.SourceFile {
if sourceFile := r.getSourceFile(fileName); sourceFile != nil {
return sourceFile
}
if sourceFile, ok := r.parsedFiles[fileName]; ok {
return sourceFile
}
var sourceFile *ast.SourceFile
if text, ok := r.ls.ReadFile(fileName); ok {
sourceFile = parser.ParseSourceFile(
ast.SourceFileParseOptions{FileName: fileName, Path: r.ls.toPath(fileName)},
text,
core.GetScriptKindFromFileName(fileName),
)
binder.BindSourceFile(sourceFile)
}
if r.parsedFiles == nil {
r.parsedFiles = map[string]*ast.SourceFile{}
}
r.parsedFiles[fileName] = sourceFile
return sourceFile
}
// inferImpliedNodeFormat determines the module format for a source file that may not be
// in the program, using the file extension and nearest package.json "type" field.
func (r *sourceDefResolver) inferImpliedNodeFormat(fileName string) core.ResolutionMode {
var packageJsonType string
if scope := r.resolver.GetPackageScopeForPath(tspath.GetDirectoryPath(fileName)); scope.Exists() {
if value, ok := scope.Contents.Type.GetValue(); ok {
packageJsonType = value
}
}
return ast.GetImpliedNodeFormatForFile(fileName, packageJsonType)
}
func findContainingModuleSpecifier(node *ast.Node) *ast.Node {
for current := node; current != nil; current = current.Parent {
if ast.IsAnyImportOrReExport(current) || ast.IsRequireCall(current, true /*requireStringLiteralLikeArgument*/) || ast.IsImportCall(current) {
if moduleSpecifier := ast.GetExternalModuleName(current); moduleSpecifier != nil && ast.IsStringLiteralLike(moduleSpecifier) {
return moduleSpecifier
}
}
}
return nil
}
func (r *sourceDefResolver) findDeclarationsInFile(
fileName string,
names []string,
seen *collections.Set[string],
) []*ast.Node {
if fileName == "" || len(names) == 0 {
return nil
}
if !seen.AddIfAbsent(fileName) {
return nil
}
sourceFile := r.getOrParseSourceFile(fileName)
if sourceFile == nil {
return nil
}
declarations := findDeclarationNodesByName(sourceFile, names)
if len(declarations) != 0 && hasConcreteSourceDeclarations(declarations) {
return declarations
}
var forwarded []*ast.Node
for _, forwardedFile := range r.getForwardedImplementationFiles(sourceFile) {
forwarded = append(forwarded, r.findDeclarationsInFile(forwardedFile, names, seen)...)
}
if len(forwarded) != 0 {
if hasConcreteSourceDeclarations(forwarded) {
return uniqueDeclarationNodes(forwarded)
}
return uniqueDeclarationNodes(append(slices.Clip(declarations), forwarded...))
}
return declarations
}
func (r *sourceDefResolver) getForwardedImplementationFiles(sourceFile *ast.SourceFile) []string {
preferredMode := r.inferImpliedNodeFormat(sourceFile.FileName())
var files []string
for _, imp := range sourceFile.Imports() {
moduleName := imp.Text()
if implementationFile := r.resolveImplementationFrom(moduleName, sourceFile.FileName(), preferredMode); implementationFile != "" {
files = append(files, implementationFile)
}
}
return core.Deduplicate(files)
}
func getCandidateSourceDeclarationNames(originalNode *ast.Node, declaration *ast.Node) []string {
var names []string
if declaration != nil {
if name := ast.GetNameOfDeclaration(declaration); name != nil {
if text := ast.GetTextOfPropertyName(name); text != "" {
names = append(names, text)
}
}
if declaration.Kind == ast.KindExportAssignment {
names = append(names, "default")
}
if (ast.IsFunctionDeclaration(declaration) || ast.IsClassDeclaration(declaration)) && declaration.ModifierFlags()&ast.ModifierFlagsExportDefault == ast.ModifierFlagsExportDefault {
names = append(names, "default")
}
if ast.IsImportSpecifier(declaration) || ast.IsExportSpecifier(declaration) {
if propName := declaration.PropertyName(); propName != nil {
names = append(names, propName.Text())
}
}
}
if originalNode != nil {
if ast.IsIdentifier(originalNode) || ast.IsPrivateIdentifier(originalNode) {
names = append(names, originalNode.Text())
}
if isDefaultImportName(originalNode) {
names = append(names, "default")
}
if originalNode.Parent != nil {
if ast.IsImportSpecifier(originalNode.Parent) || ast.IsExportSpecifier(originalNode.Parent) {
if propName := originalNode.Parent.PropertyName(); propName != nil {
names = append(names, propName.Text())
}
}
}
}
return names
}
func findDeclarationNodesByName(sourceFile *ast.SourceFile, names []string) []*ast.Node {
names = core.Deduplicate(core.Filter(names, func(name string) bool { return name != "" }))
if len(names) == 0 {
return nil
}
var wanted collections.Set[string]
wantDefault := false
for _, name := range names {
if name == "default" {
wantDefault = true
continue
}
wanted.Add(name)
}
type candidate struct {
node *ast.Node
depth int
}
var candidates []candidate
minDepth := math.MaxInt
var visit ast.Visitor
visit = func(node *ast.Node) bool {
matched := false
if name := ast.GetNameOfDeclaration(node); name != nil {
if text := ast.GetTextOfPropertyName(name); text != "" {
if wanted.Has(text) {
matched = true
}
}
}
if wantDefault && node.Kind == ast.KindExportAssignment {
matched = true
}
if wantDefault && (ast.IsFunctionDeclaration(node) || ast.IsClassDeclaration(node)) && node.ModifierFlags()&ast.ModifierFlagsExportDefault == ast.ModifierFlagsExportDefault {
matched = true
}
if matched {
depth := getContainerDepth(node)
candidates = append(candidates, candidate{node: node, depth: depth})
if depth < minDepth {
minDepth = depth
}
}
return node.ForEachChild(visit)
}
sourceFile.AsNode().ForEachChild(visit)
// Only keep declarations at the shallowest depth, like getTopMostDeclarationNamesInFile.
var declarations []*ast.Node
for _, c := range candidates {
if c.depth == minDepth {
declarations = append(declarations, c.node)
}
}
return uniqueDeclarationNodes(declarations)
}
// getContainerDepth counts the number of container nodes above a declaration,
// matching the behavior of getDepth in getTopMostDeclarationNamesInFile.
func getContainerDepth(node *ast.Node) int {
depth := 0
current := node
for current != nil {
current = getContainerNode(current)
depth++
}
return depth
}
func filterPreferredSourceDeclarations(originalNode *ast.Node, declarations []*ast.Node) []*ast.Node {
if len(declarations) <= 1 || originalNode == nil {
return declarations
}
if preferred := getPropertyLikeSourceDeclarations(originalNode, declarations); len(preferred) != 0 {
return preferred
}
if preferred := core.Filter(declarations, isConcreteSourceDeclaration); len(preferred) != 0 {
return preferred
}
return declarations
}
func getPropertyLikeSourceDeclarations(originalNode *ast.Node, declarations []*ast.Node) []*ast.Node {
if originalNode.Parent == nil || !ast.IsAccessExpression(originalNode.Parent) || originalNode.Parent.Name() != originalNode {
return nil
}
return core.Filter(declarations, func(node *ast.Node) bool {
switch node.Kind {
case ast.KindPropertyAssignment,
ast.KindShorthandPropertyAssignment,
ast.KindPropertyDeclaration,
ast.KindPropertySignature,
ast.KindMethodDeclaration,
ast.KindMethodSignature,
ast.KindGetAccessor,
ast.KindSetAccessor,
ast.KindEnumMember:
return true
default:
return false
}
})
}
func hasConcreteSourceDeclarations(declarations []*ast.Node) bool {
return slices.ContainsFunc(declarations, isConcreteSourceDeclaration)
}
func isConcreteSourceDeclaration(node *ast.Node) bool {
if !ast.IsDeclaration(node) || node.Kind == ast.KindExportAssignment {
return false
}
if (ast.IsBinaryExpression(node) || ast.IsCallExpression(node)) && ast.GetAssignmentDeclarationKind(node) != ast.JSDeclarationKindNone {
return false
}
switch node.Kind {
case ast.KindParameter,
ast.KindTypeParameter,
ast.KindBindingElement,
ast.KindImportClause,
ast.KindImportSpecifier,
ast.KindNamespaceImport,
ast.KindExportSpecifier,
ast.KindPropertyAccessExpression,
ast.KindElementAccessExpression:
return false
default:
return true
}
}
func uniqueDeclarationNodes(nodes []*ast.Node) []*ast.Node {
type declarationKey struct {
fileName string
loc core.TextRange
}
var seen collections.Set[declarationKey]
result := make([]*ast.Node, 0, len(nodes))
for _, node := range nodes {
if node == nil {
continue
}
fileName := ast.GetSourceFileOfNode(node).FileName()
key := declarationKey{fileName: fileName, loc: node.Loc}
if !seen.AddIfAbsent(key) {
continue
}
result = append(result, node)
}
return result
}
func findClosestDeclarationNode(sourceFile *ast.SourceFile, pos int) *ast.Node {
node := astnav.GetTouchingPropertyName(sourceFile, pos)
for current := node; current != nil; current = current.Parent {
if ast.IsDeclaration(current) || current.Kind == ast.KindExportAssignment {
return current
}
}
return getSourceDefinitionEntryNode(sourceFile)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,694 @@
package ls
import (
"context"
"slices"
"strings"
"unicode"
"unicode/utf8"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/printer"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/stringutil"
"github.com/microsoft/typescript-go/internal/tspath"
)
func (l *LanguageService) ProvideDocumentSymbols(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.DocumentSymbolResponse, error) {
_, file := l.getProgramAndFile(documentURI)
if lsproto.GetClientCapabilities(ctx).TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport {
symbols := l.getDocumentSymbolsForChildren(ctx, file.AsNode(), file)
return lsproto.SymbolInformationsOrDocumentSymbolsOrNull{DocumentSymbols: &symbols}, nil
}
// Client doesn't support hierarchical document symbols, return flat SymbolInformation array
symbolInfos := l.getDocumentSymbolInformations(ctx, file, documentURI)
symbolInfoPtrs := make([]*lsproto.SymbolInformation, len(symbolInfos))
for i := range symbolInfos {
symbolInfoPtrs[i] = &symbolInfos[i]
}
return lsproto.SymbolInformationsOrDocumentSymbolsOrNull{SymbolInformations: &symbolInfoPtrs}, nil
}
// getDocumentSymbolInformations converts hierarchical DocumentSymbols to a flat SymbolInformation array
func (l *LanguageService) getDocumentSymbolInformations(ctx context.Context, file *ast.SourceFile, documentURI lsproto.DocumentUri) []lsproto.SymbolInformation {
// First get hierarchical symbols
docSymbols := l.getDocumentSymbolsForChildren(ctx, file.AsNode(), file)
// Flatten the hierarchy
var result []lsproto.SymbolInformation
var flatten func(symbols []*lsproto.DocumentSymbol, containerName *string)
flatten = func(symbols []*lsproto.DocumentSymbol, containerName *string) {
for _, symbol := range symbols {
info := lsproto.SymbolInformation{
Name: symbol.Name,
Kind: symbol.Kind,
Location: lsproto.Location{
Uri: documentURI,
Range: symbol.Range,
},
ContainerName: containerName,
Tags: symbol.Tags,
Deprecated: symbol.Deprecated,
}
result = append(result, info)
// Recursively flatten children with this symbol as container
if symbol.Children != nil && len(*symbol.Children) > 0 {
flatten(*symbol.Children, &symbol.Name)
}
}
}
flatten(docSymbols, nil)
return result
}
func (l *LanguageService) getDocumentSymbolsForChildren(ctx context.Context, node *ast.Node, file *ast.SourceFile) []*lsproto.DocumentSymbol {
var symbols []*lsproto.DocumentSymbol
expandoTargets := collections.Set[string]{}
addSymbolForNode := func(node *ast.Node, name *ast.Node, children []*lsproto.DocumentSymbol) {
if node.Flags&ast.NodeFlagsReparsed == 0 {
symbol := l.newDocumentSymbol(node, name, children)
if symbol != nil {
symbols = append(symbols, symbol)
}
}
}
var visit func(*ast.Node) bool
getSymbolsForChildren := func(node *ast.Node) []*lsproto.DocumentSymbol {
var result []*lsproto.DocumentSymbol
if node != nil {
saveExpandoTargets := expandoTargets
expandoTargets = collections.Set[string]{}
saveSymbols := symbols
symbols = nil
node.ForEachChild(visit)
result = symbols
symbols = saveSymbols
expandoTargets = saveExpandoTargets
}
return result
}
startNode := func(node *ast.Node, name *ast.Node) func() {
if node == nil {
return func() {}
}
saveExpandoTargets := expandoTargets
expandoTargets = collections.Set[string]{}
saveSymbols := symbols
symbols = nil
return func() {
result := symbols
symbols = saveSymbols
expandoTargets = saveExpandoTargets
addSymbolForNode(node, name, result)
}
}
getSymbolsForNode := func(node *ast.Node) []*lsproto.DocumentSymbol {
var result []*lsproto.DocumentSymbol
if node != nil {
saveSymbols := symbols
symbols = nil
visit(node)
result = symbols
symbols = saveSymbols
}
return result
}
visit = func(node *ast.Node) bool {
if ctx.Err() != nil {
return true
}
if node.Flags&ast.NodeFlagsReparsed == 0 {
if jsdocs := node.JSDoc(file); len(jsdocs) > 0 {
for _, jsdoc := range jsdocs {
if tagList := jsdoc.AsJSDoc().Tags; tagList != nil {
for _, tag := range tagList.Nodes {
if ast.IsJSDocTypedefTag(tag) || ast.IsJSDocCallbackTag(tag) {
addSymbolForNode(tag, nil /*name*/, nil /*children*/)
}
}
}
}
}
}
switch node.Kind {
case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration:
if ast.IsClassLike(node) && ast.GetDeclarationName(node) != "" {
expandoTargets.Add(ast.GetDeclarationName(node))
}
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node))
case ast.KindModuleDeclaration:
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(getInteriorModule(node)))
case ast.KindConstructor:
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node.Body()))
for _, param := range node.Parameters() {
if ast.IsParameterPropertyDeclaration(param, node) {
addSymbolForNode(param, nil /*name*/, nil /*children*/)
}
}
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindMethodDeclaration, ast.KindGetAccessor,
ast.KindSetAccessor:
declName := ast.GetDeclarationName(node)
if declName != "" {
expandoTargets.Add(declName)
}
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node.Body()))
case ast.KindVariableDeclaration, ast.KindBindingElement, ast.KindPropertyAssignment, ast.KindPropertyDeclaration:
nodeName := node.Name()
if nodeName != nil {
if ast.IsBindingPattern(nodeName) {
visit(nodeName)
} else {
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node.Initializer()))
}
}
case ast.KindSpreadAssignment:
addSymbolForNode(node, node.Expression(), nil /*children*/)
case ast.KindMethodSignature, ast.KindPropertySignature, ast.KindCallSignature, ast.KindConstructSignature, ast.KindIndexSignature,
ast.KindEnumMember, ast.KindShorthandPropertyAssignment, ast.KindTypeAliasDeclaration, ast.KindImportEqualsDeclaration, ast.KindExportSpecifier:
addSymbolForNode(node, nil /*name*/, nil /*children*/)
case ast.KindImportClause:
// Handle default import case e.g.:
// import d from "mod";
if node.Name() != nil {
addSymbolForNode(node.Name(), node.Name(), nil /*children*/)
}
// Handle named bindings in imports e.g.:
// import * as NS from "mod";
// import {a, b as B} from "mod";
if namedBindings := node.AsImportClause().NamedBindings; namedBindings != nil {
if namedBindings.Kind == ast.KindNamespaceImport {
addSymbolForNode(namedBindings, nil /*name*/, nil /*children*/)
} else {
for _, element := range namedBindings.Elements() {
addSymbolForNode(element, nil /*name*/, nil /*children*/)
}
}
}
case ast.KindBinaryExpression, ast.KindCallExpression:
assignmentKind := ast.GetAssignmentDeclarationKind(node)
switch assignmentKind {
// `module.exports = ...`` should be reparsed into a JSExportAssignment,
// and `exports.a = ...`` into a CommonJSExport.
case ast.JSDeclarationKindNone, ast.JSDeclarationKindThisProperty,
ast.JSDeclarationKindModuleExports, ast.JSDeclarationKindExportsProperty,
ast.JSDeclarationKindObjectDefinePropertyExports:
node.ForEachChild(visit)
case ast.JSDeclarationKindProperty, ast.JSDeclarationKindObjectDefinePropertyValue:
var target *ast.Expression
var targetFunction *ast.Expression
var definition *ast.Node
var propertyName *ast.Node
// `A.b = ... ` or `A.prototype.b = ...`
if ast.IsBinaryExpression(node) {
binaryExpr := node.AsBinaryExpression()
target = binaryExpr.Left
targetFunction = target.Expression()
definition = binaryExpr.Right
// `A.b` or `A.prototype.b`
if ast.IsPropertyAccessExpression(target) {
propertyName = target.AsPropertyAccessExpression().Name()
} else { // `A["b"]` or `A.prototype["b"]`
propertyName = target.AsElementAccessExpression().ArgumentExpression
}
} else { // `Object.defineProperty(A, "b", {...})`
args := node.Arguments()
targetFunction = args[0]
target = args[1]
propertyName = target
definition = args[2]
}
if isPrototypeExpando(targetFunction) {
targetFunction = targetFunction.Expression()
// If we see a prototype assignment, start tracking the target as an expando target.
if ast.IsIdentifier(targetFunction) {
expandoTargets.Add(targetFunction.Text())
}
}
if ast.IsIdentifier(targetFunction) &&
expandoTargets.Has(targetFunction.Text()) {
endNode := startNode(node, targetFunction)
addSymbolForNode(target, propertyName, getSymbolsForNode(definition))
endNode()
} else {
node.ForEachChild(visit)
}
}
case ast.KindExportAssignment:
if node.AsExportAssignment().IsExportEquals {
addSymbolForNode(node, nil /*name*/, getSymbolsForNode(node.Expression()))
} else {
node.ForEachChild(visit)
}
default:
node.ForEachChild(visit)
}
return false
}
node.ForEachChild(visit)
return mergeExpandos(symbols)
}
// Target is `f.prototype`.
func isPrototypeExpando(target *ast.Node) bool {
if ast.IsAccessExpression(target) {
accessName := ast.GetElementOrPropertyAccessName(target)
return accessName != nil && accessName.Text() == "prototype"
}
return false
}
const maxLength = 150
func (l *LanguageService) newDocumentSymbol(node *ast.Node, name *ast.Node, children []*lsproto.DocumentSymbol) *lsproto.DocumentSymbol {
result := new(lsproto.DocumentSymbol)
file := ast.GetSourceFileOfNode(node)
nodeStartPos := scanner.SkipTrivia(file.Text(), node.Pos())
if name == nil {
name = ast.GetNameOfDeclaration(node)
}
var text string
var nameStartPos, nameEndPos int
if ast.IsModuleDeclaration(node) && !ast.IsAmbientModule(node) {
text = getModuleName(node)
nameStartPos = scanner.SkipTrivia(file.Text(), name.Pos())
nameEndPos = getInteriorModule(node).Name().End()
} else if ast.IsAnyExportAssignment(node) && node.AsExportAssignment().IsExportEquals {
text = "export="
if !ast.NodeIsMissing(name) {
nameStartPos = scanner.SkipTrivia(file.Text(), name.Pos())
nameEndPos = name.End()
} else {
nameStartPos = nodeStartPos
nameEndPos = node.End()
}
} else if name != nil {
text = getTextOfName(name)
nameStartPos = max(scanner.SkipTrivia(file.Text(), name.Pos()), nodeStartPos)
nameEndPos = max(name.End(), nodeStartPos)
} else {
text = getUnnamedNodeLabel(node)
nameStartPos = nodeStartPos
nameEndPos = nodeStartPos
}
if text == "" {
return nil
}
truncatedText := stringutil.TruncateByRunes(text, maxLength)
if len(truncatedText) < len(text) {
text = truncatedText + "..."
}
result.Name = text
result.Kind = getSymbolKindFromNode(node)
result.Range = lsproto.Range{
Start: l.converters.PositionToLineAndCharacter(file, core.TextPos(nodeStartPos)),
End: l.converters.PositionToLineAndCharacter(file, core.TextPos(node.End())),
}
result.SelectionRange = lsproto.Range{
Start: l.converters.PositionToLineAndCharacter(file, core.TextPos(nameStartPos)),
End: l.converters.PositionToLineAndCharacter(file, core.TextPos(nameEndPos)),
}
if children == nil {
children = []*lsproto.DocumentSymbol{}
}
result.Children = &children
return result
}
// Merges expando symbols into their target symbols, and namespaces of same name.
// Modifies the input slice.
func mergeExpandos(symbols []*lsproto.DocumentSymbol) []*lsproto.DocumentSymbol {
mergedSymbols := make([]*lsproto.DocumentSymbol, 0, len(symbols))
// Collect symbols that can be an expando target.
nameToExpandoTargetIndex := collections.MultiMap[string, int]{}
// Collect namespaces.
nameToNamespaceIndex := map[string]int{}
for i, symbol := range symbols {
if isAnonymousName(symbol.Name) {
continue
}
if symbol.Kind == lsproto.SymbolKindClass || symbol.Kind == lsproto.SymbolKindFunction || symbol.Kind == lsproto.SymbolKindVariable {
nameToExpandoTargetIndex.Add(symbol.Name, i)
}
if symbol.Kind == lsproto.SymbolKindNamespace {
if _, ok := nameToNamespaceIndex[symbol.Name]; !ok {
nameToNamespaceIndex[symbol.Name] = i
}
}
}
for i, symbol := range symbols {
if symbol.Children != nil {
children := mergeExpandos(*symbol.Children)
symbol.Children = &children
}
// Anonymous symbols never merge.
if isAnonymousName(symbol.Name) {
continue
}
// Merge expandos.
if symbol.Kind == lsproto.SymbolKindProperty {
symbolsWithSameName := nameToExpandoTargetIndex.Get(symbol.Name)
for j := len(symbolsWithSameName) - 1; j >= 0; j-- {
targetIndex := symbolsWithSameName[j]
targetSymbol := symbols[targetIndex]
mergeChildren(targetSymbol, symbol)
// Mark this symbol as merged.
symbols[i] = nil
}
}
// Merge namespaces.
if symbol.Kind == lsproto.SymbolKindNamespace {
if targetIndex, ok := nameToNamespaceIndex[symbol.Name]; ok && targetIndex != i {
targetSymbol := symbols[targetIndex]
mergeChildren(targetSymbol, symbol)
// Mark this symbol as merged.
symbols[i] = nil
}
}
}
for _, symbol := range symbols {
if symbol != nil {
mergedSymbols = append(mergedSymbols, symbol)
}
}
return mergedSymbols
}
func mergeChildren(target *lsproto.DocumentSymbol, source *lsproto.DocumentSymbol) {
if source.Children != nil {
if target.Children == nil {
target.Children = source.Children
} else {
*target.Children = mergeExpandos(append(*target.Children, *source.Children...))
slices.SortFunc(*target.Children, func(a, b *lsproto.DocumentSymbol) int {
return lsproto.CompareRanges(a.Range, b.Range)
})
}
}
}
// See `getUnnamedNodeLabel`.
func isAnonymousName(name string) bool {
return name == "<function>" || name == "<class>" || name == "export=" || name == "default" ||
name == "constructor" || name == "()" || name == "new()" || name == "[]" || strings.HasSuffix(name, ") callback")
}
func getTextOfName(node *ast.Node) string {
switch node.Kind {
case ast.KindIdentifier, ast.KindPrivateIdentifier, ast.KindNumericLiteral:
return node.Text()
case ast.KindStringLiteral:
return "\"" + printer.EscapeString(node.Text(), '"') + "\""
case ast.KindNoSubstitutionTemplateLiteral:
return "`" + printer.EscapeString(node.Text(), '`') + "`"
case ast.KindComputedPropertyName:
if ast.IsStringOrNumericLiteralLike(node.Expression()) {
return getTextOfName(node.Expression())
}
}
return scanner.GetTextOfNode(node)
}
func getUnnamedNodeLabel(node *ast.Node) string {
if parent := ast.WalkUpParenthesizedExpressions(node.Parent); parent != nil && ast.IsExportAssignment(parent) {
if parent.AsExportAssignment().IsExportEquals {
return "export="
}
return "default"
}
switch node.Kind {
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction:
if node.ModifierFlags()&ast.ModifierFlagsDefault != 0 {
return "default"
}
if ast.IsCallExpression(node.Parent) {
name := getCallExpressionName(node.Parent.Expression())
if name != "" {
name = cleanCallbackText(name)
if len(name) > maxLength {
return name + " callback"
}
args := cleanCallbackText(getCallExpressionLiteralArgs(node.Parent))
return name + "(" + args + ") callback"
}
}
return "<function>"
case ast.KindClassDeclaration, ast.KindClassExpression:
if node.ModifierFlags()&ast.ModifierFlagsDefault != 0 {
return "default"
}
return "<class>"
case ast.KindConstructor:
return "constructor"
case ast.KindCallSignature:
return "()"
case ast.KindConstructSignature:
return "new()"
case ast.KindIndexSignature:
return "[]"
}
return ""
}
func getCallExpressionName(node *ast.Node) string {
switch node.Kind {
case ast.KindIdentifier, ast.KindPrivateIdentifier:
return node.Text()
case ast.KindPropertyAccessExpression:
left := getCallExpressionName(node.Expression())
right := getCallExpressionName(node.Name())
if left != "" {
return left + "." + right
}
return right
}
return ""
}
func getCallExpressionLiteralArgs(callExpr *ast.Node) string {
var parts []string
for _, arg := range callExpr.Arguments() {
if ast.IsStringLiteralLike(arg) || ast.IsTemplateExpression(arg) {
parts = append(parts, scanner.GetTextOfNode(arg))
}
}
return strings.Join(parts, ", ")
}
func cleanCallbackText(text string) string {
truncated := stringutil.TruncateByRunes(text, maxLength)
if len(truncated) < len(text) {
text = truncated + "..."
}
return strings.Map(func(r rune) rune {
if stringutil.IsLineBreak(r) {
return -1
}
return r
}, text)
}
func getInteriorModule(node *ast.Node) *ast.Node {
for node.Body() != nil && ast.IsModuleDeclaration(node.Body()) {
node = node.Body()
}
return node
}
func getModuleName(node *ast.Node) string {
result := node.Name().Text()
for node.Body() != nil && ast.IsModuleDeclaration(node.Body()) {
node = node.Body()
result = result + "." + node.Name().Text()
}
return result
}
type DeclarationInfo struct {
name string
declaration *ast.Node
matchScore int
}
func ProvideWorkspaceSymbols(
ctx context.Context,
programs []*compiler.Program,
converters *lsconv.Converters,
preferences lsutil.UserPreferences,
query string,
) (lsproto.WorkspaceSymbolResponse, error) {
excludeLibrarySymbols := preferences.ExcludeLibrarySymbolsInNavTo.IsTrue()
// Obtain set of non-declaration source files from all active programs.
sourceFiles := map[tspath.Path]*ast.SourceFile{}
for _, program := range programs {
for _, sourceFile := range program.SourceFiles() {
if (program.HasTSFile() || !sourceFile.IsDeclarationFile) &&
!shouldExcludeFile(sourceFile, program, excludeLibrarySymbols) {
sourceFiles[sourceFile.Path()] = sourceFile
}
}
}
// Create DeclarationInfos for all declarations in the source files.
var infos []DeclarationInfo
for _, sourceFile := range sourceFiles {
if ctx.Err() != nil {
return lsproto.SymbolInformationsOrWorkspaceSymbolsOrNull{}, nil
}
declarationMap := sourceFile.GetDeclarationMap()
for name, declarations := range declarationMap {
score := getMatchScore(name, query)
if score >= 0 {
for _, declaration := range declarations {
infos = append(infos, DeclarationInfo{name, declaration, score})
}
}
}
}
// Sort the DeclarationInfos and return the top 256 matches.
slices.SortFunc(infos, compareDeclarationInfos)
count := min(len(infos), 256)
symbols := make([]*lsproto.SymbolInformation, count)
for i, info := range infos[0:count] {
node := info.declaration
sourceFile := ast.GetSourceFileOfNode(node)
container := getContainerNode(info.declaration)
var containerName *string
if container != nil {
containerName = strPtrTo(ast.GetDeclarationName(container))
}
// Use the name node's span so that VS selects just the symbol name (matching
// the TS5 navto behaviour). GetNameOfDeclaration is always non-nil here because
// computeDeclarationMap only adds declarations whose GetDeclarationName (string
// form) is non-empty, which implies a name node exists.
nameNode := ast.GetNameOfDeclaration(node)
nameStart := astnav.GetStartOfNode(nameNode, sourceFile, false /*includeJsDoc*/)
nameRange := core.NewTextRange(nameStart, nameNode.End())
var symbol lsproto.SymbolInformation
symbol.Name = info.name
symbol.Kind = getSymbolKindFromNode(info.declaration)
symbol.Location = converters.ToLSPLocation(sourceFile, nameRange)
symbol.ContainerName = containerName
symbols[i] = &symbol
}
return lsproto.SymbolInformationsOrWorkspaceSymbolsOrNull{SymbolInformations: &symbols}, nil
}
func shouldExcludeFile(file *ast.SourceFile, program *compiler.Program, excludeLibrarySymbols bool) bool {
return excludeLibrarySymbols && (isInsideNodeModules(file.FileName()) || program.IsLibFile(file))
}
func isInsideNodeModules(fileName string) bool {
return strings.Contains(fileName, "/node_modules/")
}
// Return a score for matching `s` against `pattern`. In order to match, `s` must contain each of the characters in
// `pattern` in the same order. Upper case characters in `pattern` must match exactly, whereas lower case characters
// in `pattern` match either case in `s`. If `s` doesn't match, -1 is returned. Otherwise, the returned score is the
// number of characters in `s` that weren't matched. Thus, zero represents an exact match, and higher values represent
// increasingly less specific partial matches.
func getMatchScore(s string, pattern string) int {
score := 0
for _, p := range pattern {
exact := unicode.IsUpper(p)
for {
c, size := utf8.DecodeRuneInString(s)
if size == 0 {
return -1
}
s = s[size:]
if exact && c == p || !exact && unicode.ToLower(c) == unicode.ToLower(p) {
break
}
score++
}
}
return score
}
// Sort DeclarationInfos by ascending match score, then ascending case insensitive name, then
// ascending case sensitive name, and finally by source file name and position.
func compareDeclarationInfos(d1, d2 DeclarationInfo) int {
if d1.matchScore != d2.matchScore {
return d1.matchScore - d2.matchScore
}
if c := stringutil.CompareStringsCaseInsensitive(d1.name, d2.name); c != 0 {
return c
}
if c := strings.Compare(d1.name, d2.name); c != 0 {
return c
}
s1 := ast.GetSourceFileOfNode(d1.declaration)
s2 := ast.GetSourceFileOfNode(d2.declaration)
if s1 != s2 {
return strings.Compare(string(s1.Path()), string(s2.Path()))
}
return d1.declaration.Pos() - d2.declaration.Pos()
}
// getSymbolKindFromNode converts an AST node to an LSP SymbolKind.
// Combines getNodeKind with VS Code's fromProtocolScriptElementKind.
func getSymbolKindFromNode(node *ast.Node) lsproto.SymbolKind {
switch node.Kind {
case ast.KindSourceFile:
if ast.IsExternalModule(node.AsSourceFile()) {
return lsproto.SymbolKindModule
}
return lsproto.SymbolKindFile
case ast.KindModuleDeclaration:
return lsproto.SymbolKindNamespace
case ast.KindClassDeclaration, ast.KindClassExpression:
return lsproto.SymbolKindClass
case ast.KindInterfaceDeclaration:
return lsproto.SymbolKindInterface
case ast.KindTypeAliasDeclaration, ast.KindJSDocTypedefTag, ast.KindJSDocCallbackTag:
return lsproto.SymbolKindClass
case ast.KindEnumDeclaration:
return lsproto.SymbolKindEnum
case ast.KindVariableDeclaration:
return lsproto.SymbolKindVariable
case ast.KindArrowFunction, ast.KindFunctionDeclaration, ast.KindFunctionExpression:
return lsproto.SymbolKindFunction
case ast.KindGetAccessor, ast.KindSetAccessor:
return lsproto.SymbolKindProperty
case ast.KindMethodDeclaration, ast.KindMethodSignature:
return lsproto.SymbolKindMethod
case ast.KindPropertyDeclaration, ast.KindPropertySignature, ast.KindPropertyAssignment,
ast.KindShorthandPropertyAssignment, ast.KindSpreadAssignment, ast.KindIndexSignature:
return lsproto.SymbolKindProperty
case ast.KindCallSignature:
return lsproto.SymbolKindMethod
case ast.KindConstructSignature:
return lsproto.SymbolKindConstructor
case ast.KindConstructor, ast.KindClassStaticBlockDeclaration:
return lsproto.SymbolKindConstructor
case ast.KindTypeParameter:
return lsproto.SymbolKindTypeParameter
case ast.KindEnumMember:
return lsproto.SymbolKindEnumMember
case ast.KindParameter:
if ast.HasSyntacticModifier(node, ast.ModifierFlagsParameterPropertyModifier) {
return lsproto.SymbolKindProperty
}
return lsproto.SymbolKindVariable
case ast.KindBinaryExpression, ast.KindCallExpression:
kind := ast.GetAssignmentDeclarationKind(node)
switch kind {
case ast.JSDeclarationKindThisProperty, ast.JSDeclarationKindProperty, ast.JSDeclarationKindObjectDefinePropertyValue:
return lsproto.SymbolKindProperty
}
case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral, ast.KindNumericLiteral:
// String literals used as property names (e.g., in Object.defineProperty)
return lsproto.SymbolKindProperty
}
return lsproto.SymbolKindVariable
}

File diff suppressed because it is too large Load Diff