From 8dbdd93c62b4dfa64ed67ab29eb10d09593e8e49 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 25 Mar 2025 09:54:23 -0700 Subject: [PATCH 01/25] start completions and getSymbolsInScope --- internal/checker/checker.go | 91 ++++++++++++++++++++++++++++++++--- internal/checker/utilities.go | 26 ++++++++++ internal/ls/completions.go | 60 +++++++++++++++++++++++ internal/lsp/server.go | 13 +++++ 4 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 internal/ls/completions.go diff --git a/internal/checker/checker.go b/internal/checker/checker.go index c484837ca5..b902a326ba 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -20251,13 +20251,6 @@ func (c *Checker) isNamedMember(symbol *ast.Symbol, id string) bool { return !isReservedMemberName(id) && c.symbolIsValue(symbol) } -// A reserved member name consists of the byte 0xFE (which is an invalid UTF-8 encoding) followed by one or more -// characters where the first character is not '@' or '#'. The '@' character indicates that the name is denoted by -// a well known ES Symbol instance and the '#' character indicates that the name is a PrivateIdentifier. -func isReservedMemberName(name string) bool { - return len(name) >= 2 && name[0] == '\xFE' && name[1] != '@' && name[1] != '#' -} - func (c *Checker) symbolIsValue(symbol *ast.Symbol) bool { return c.symbolIsValueEx(symbol, false /*includeTypeOnlyMembers*/) } @@ -29523,3 +29516,87 @@ func (c *Checker) GetEmitResolver(file *ast.SourceFile, skipDiagnostics bool) pr } return c.emitResolver } + +func (c *Checker) GetSymbolsInScope(location *ast.Node, meaning ast.SymbolFlags) []*ast.Symbol { + return c.getSymbolsInScope(location, meaning) +} + +func (c *Checker) getSymbolsInScope(location *ast.Node, meaning ast.SymbolFlags) []*ast.Symbol { + if location.Flags&ast.NodeFlagsInWithStatement != 0 { + // We cannot answer semantic questions within a with block, do not proceed any further + return nil + } + + symbols := createSymbolTable(nil) + isStaticSymbol := false + + // Copy the given symbol into symbol tables if the symbol has the given meaning + // and it doesn't already exists in the symbol table. + copySymbol := func(symbol *ast.Symbol, meaning ast.SymbolFlags) { + + // !!! + } + + copySymbols := func(source ast.SymbolTable, meaning ast.SymbolFlags) { + // !!! + } + + copyLocallyVisibleExportSymbols := func(source ast.SymbolTable, meaning ast.SymbolFlags) { + // !!! + } + + populateSymbols := func() { + for location != nil { + if canHaveLocals(location) && location.Locals() != nil && !ast.IsGlobalSourceFile(location) { + copySymbols(location.Locals(), meaning) + } + + switch location.Kind { + case ast.KindSourceFile: + if !ast.IsExternalModule(location.AsSourceFile()) { + break + } + fallthrough + case ast.KindModuleDeclaration: + copyLocallyVisibleExportSymbols(c.getSymbolOfDeclaration(location).Exports, meaning&ast.SymbolFlagsModuleMember) + case ast.KindEnumDeclaration: + copySymbols(c.getSymbolOfDeclaration(location).Exports, meaning&ast.SymbolFlagsEnumMember) + case ast.KindClassExpression: + className := location.AsClassExpression().Name() + if className != nil { + copySymbol(location.Symbol(), meaning) + } + // this fall-through is necessary because we would like to handle + // type parameter inside class expression similar to how we handle it in classDeclaration and interface Declaration. + fallthrough + case ast.KindClassDeclaration, ast.KindInterfaceDeclaration: + // If we didn't come from static member of class or interface, + // add the type parameters into the symbol table + // (type parameters of classDeclaration/classExpression and interface are in member property of the symbol. + // Note: that the memberFlags come from previous iteration. + if !isStaticSymbol { + copySymbols(c.getMembersOfSymbol(c.getSymbolOfDeclaration(location)), meaning&ast.SymbolFlagsType) + } + case ast.KindFunctionExpression: + funcName := location.Name() + if funcName != nil { + copySymbol(location.Symbol(), meaning) + } + } + + if introducesArgumentsExoticObject(location) { + copySymbol(c.argumentsSymbol, meaning) + } + + isStaticSymbol = ast.IsStatic(location) + location = location.Parent + } + + copySymbols(c.globals, meaning) + } + + populateSymbols() + + delete(symbols, ast.InternalSymbolNameThis) // Not a symbol, a keyword + return symbolsToArray(symbols) +} diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go index 90fe7eb686..493353d95d 100644 --- a/internal/checker/utilities.go +++ b/internal/checker/utilities.go @@ -2111,3 +2111,29 @@ func allDeclarationsInSameSourceFile(symbol *ast.Symbol) bool { } return true } + +// A reserved member name consists of the byte 0xFE (which is an invalid UTF-8 encoding) followed by one or more +// characters where the first character is not '@' or '#'. The '@' character indicates that the name is denoted by +// a well known ES Symbol instance and the '#' character indicates that the name is a PrivateIdentifier. +func isReservedMemberName(name string) bool { + return len(name) >= 2 && name[0] == '\xFE' && name[1] != '@' && name[1] != '#' +} + +func introducesArgumentsExoticObject(node *ast.Node) bool { + switch node.Kind { + case ast.KindMethodDeclaration, ast.KindMethodSignature, ast.KindConstructor, ast.KindGetAccessor, + ast.KindSetAccessor, ast.KindFunctionDeclaration, ast.KindFunctionExpression: + return true + } + return false +} + +func symbolsToArray(symbols ast.SymbolTable) []*ast.Symbol { + var result []*ast.Symbol + for id, symbol := range symbols { + if !isReservedMemberName(id) { + result = append(result, symbol) + } + } + return result +} diff --git a/internal/ls/completions.go b/internal/ls/completions.go new file mode 100644 index 0000000000..bcd233668a --- /dev/null +++ b/internal/ls/completions.go @@ -0,0 +1,60 @@ +package ls + +import ( + "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/lsp/lsproto" +) + +func (l *LanguageService) ProvideCompletion(fileName string, position int) *lsproto.CompletionList { + program, file := l.getProgramAndFile(fileName) + node := astnav.GetTouchingPropertyName(file, position) + if node.Kind == ast.KindSourceFile { + return nil + } + return l.getCompletionsAtPosition(program, file, position) +} + +func (l *LanguageService) getCompletionsAtPosition(program *compiler.Program, file *ast.SourceFile, position int) *lsproto.CompletionList { + // !!! trigger kind + + // !!! see if incomplete completion list + + // !!! string literal completions + + // !!! get completion data + + // !!! transform data into completion list + + return nil // !!! +} + +type completionData struct { + // !!! +} + +func getCompletionData(program *compiler.Program, file *ast.SourceFile, position int) *completionData { + typeChecker := program.GetTypeChecker() + + // !!! why do we use this? just for detecting comments/jsdoc scenario? + currentToken := astnav.GetTokenAtPosition(file, position) + + // !!! comment, jsdoc stuff + + // !!! get relevant tokens + + // !!! adjust context token + + // !!! see if jsx tag or dot completion + + // !!! global completions + + return nil +} + +// !!! Document this +// `contextToken` and `previousToken` can both be nil if we are at the beginning of the file. +func getRelevantTokens(position int, file *ast.SourceFile) (contextToken *ast.Node, previousToken *ast.Node) { + +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index b2ef52fd5e..6b0e4900a1 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -373,6 +373,19 @@ func (s *Server) handleDefinition(req *lsproto.RequestMessage) error { return s.sendResult(req.ID, &lsproto.Definition{Locations: &lspLocations}) } +func (s *Server) handleCompletion(req *lsproto.RequestMessage) error { + params := req.Params.(*lsproto.CompletionParams) + file, project := s.getFileAndProject(params.TextDocument.Uri) + pos, err := s.converters.lineAndCharacterToPositionForFile(params.Position, file.FileName()) + if err != nil { + return s.sendError(req.ID, err) + } + + list := project.LanguageService().ProvideCompletion(file.FileName(), pos) + // !!! convert completion to `lsproto.CompletionList` + return s.sendResult(req.ID, nil) // !!! +} + func (s *Server) getFileAndProject(uri lsproto.DocumentUri) (*project.ScriptInfo, *project.Project) { fileName := documentUriToFileName(uri) return s.projectService.EnsureDefaultProjectForFile(fileName) From 4e2514bade8e4a9a4eec54080808981ab096456d Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 1 Apr 2025 12:21:03 -0700 Subject: [PATCH 02/25] some of getcompletiondata --- internal/ast/ast.go | 3 + internal/ast/utilities.go | 54 +++- internal/astnav/tokens.go | 128 ++++++++ internal/checker/checker.go | 140 ++------- internal/checker/flow.go | 6 +- internal/checker/relater.go | 4 +- internal/checker/services.go | 240 +++++++++++++++ internal/checker/utilities.go | 67 ++-- internal/core/core.go | 17 ++ internal/ls/completions.go | 554 ++++++++++++++++++++++++++++++++-- internal/ls/types.go | 11 + internal/ls/utilities.go | 127 ++++++++ internal/lsp/server.go | 2 +- 13 files changed, 1167 insertions(+), 186 deletions(-) create mode 100644 internal/checker/services.go create mode 100644 internal/ls/utilities.go diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 2581134d54..8d079b7f23 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -1575,6 +1575,9 @@ type ( JSDocComment = Node // JSDocText | JSDocLink | JSDocLinkCode | JSDocLinkPlain; JSDocTag = Node // Node with JSDocTagBase SignatureDeclaration = Node // CallSignatureDeclaration | ConstructSignatureDeclaration | MethodSignature | IndexSignatureDeclaration | FunctionTypeNode | ConstructorTypeNode | FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | AccessorDeclaration | FunctionExpression | ArrowFunction; + StringLiteralLike = Node // StringLiteral | NoSubstitutionTemplateLiteral + AnyValidImportOrReExport = Node // (ImportDeclaration | ExportDeclaration | JSDocImportTag) & { moduleSpecifier: StringLiteral } | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteral }} | RequireOrImportCall | ValidImportTypeNode + ValidImportTypeNode = Node // ImportTypeNode & { argument: LiteralTypeNode & { literal: StringLiteral } } ) // Aliases for node singletons diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index fa111d54cd..207758621f 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -510,7 +510,7 @@ func IsFunctionLikeDeclaration(node *Node) bool { return node != nil && isFunctionLikeDeclarationKind(node.Kind) } -func isFunctionLikeKind(kind Kind) bool { +func IsFunctionLikeKind(kind Kind) bool { switch kind { case KindMethodSignature, KindCallSignature, @@ -527,7 +527,7 @@ func isFunctionLikeKind(kind Kind) bool { // Determines if a node is function- or signature-like. func IsFunctionLike(node *Node) bool { // TODO(rbuckton): Move `node != nil` test to call sites - return node != nil && isFunctionLikeKind(node.Kind) + return node != nil && IsFunctionLikeKind(node.Kind) } func IsFunctionLikeOrClassStaticBlockDeclaration(node *Node) bool { @@ -2569,3 +2569,53 @@ func IsRequireCall(node *Node, requireStringLiteralLikeArgument bool) bool { } return !requireStringLiteralLikeArgument || IsStringLiteralLike(call.Arguments.Nodes[0]) } + +func IsCheckJsEnabledForFile(sourceFile *SourceFile, compilerOptions *core.CompilerOptions) bool { + // !!! + // if sourceFile.CheckJsDirective != nil { + // return sourceFile.CheckJsDirective.Enabled + // } + return compilerOptions.CheckJs == core.TSTrue +} + +func GetLeftmostAccessExpression(expr *Node) *Node { + for IsAccessExpression(expr) { + expr = expr.Expression() + } + return expr +} + +func IsSourceFileJs(file *SourceFile) bool { + return IsInJSFile(file.AsNode()) +} + +func IsTypeOnlyImportDeclaration(node *Node) bool { + switch node.Kind { + case KindImportSpecifier: + return node.AsImportSpecifier().IsTypeOnly || node.Parent.Parent.AsImportClause().IsTypeOnly + case KindNamespaceImport: + return node.Parent.AsImportClause().IsTypeOnly + case KindImportClause: + return node.AsImportClause().IsTypeOnly + case KindImportEqualsDeclaration: + return node.AsImportEqualsDeclaration().IsTypeOnly + } + return false +} + +func isTypeOnlyExportDeclaration(node *Node) bool { + switch node.Kind { + case KindExportSpecifier: + return node.AsExportSpecifier().IsTypeOnly || node.Parent.Parent.AsExportDeclaration().IsTypeOnly + case KindExportDeclaration: + d := node.AsExportDeclaration() + return d.IsTypeOnly && d.ModuleSpecifier != nil && d.ExportClause == nil + case KindNamespaceExport: + return node.Parent.AsExportDeclaration().IsTypeOnly + } + return false +} + +func IsTypeOnlyImportOrExportDeclaration(node *Node) bool { + return IsTypeOnlyImportDeclaration(node) || isTypeOnlyExportDeclaration(node) +} diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go index 6a1e7cd57f..f17268d891 100644 --- a/internal/astnav/tokens.go +++ b/internal/astnav/tokens.go @@ -246,3 +246,131 @@ func visitEachChildAndJSDoc(node *ast.Node, sourceFile *ast.SourceFile, visitor } node.VisitEachChild(visitor) } + +// Finds the rightmost token satisfying `token.end <= position`, +func FindPrecedingToken(sourceFile *ast.SourceFile, position int) *ast.Node { + var next *ast.Node + current := sourceFile.AsNode() + left := 0 + + testNode := func(node *ast.Node) int { + if node.Pos() == position { + return 1 + } + if node.End() < position { + return -1 + } + if getPosition(node, sourceFile, false) > position || node.Pos() == position+1 { + return 1 + } + if node.End()+1 == position { + //prevSubtree = node + return 0 + } + return 0 + } + + visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { + // We can't abort visiting children, so once a match is found, we set `next` + // and do nothing on subsequent visits. + if node != nil && next == nil { + switch testNode(node) { + case -1: + if !ast.IsJSDocKind(node.Kind) { + // We can't move the left boundary into or beyond JSDoc, + // because we may end up returning the token after this JSDoc, + // constructing it with the scanner, and we need to include + // all its leading trivia in its position. + left = node.End() + } + case 0: + next = node + } + } + return node + } + + visitNodes := func(nodes []*ast.Node) { + index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, node *ast.Node) int { + cmp := testNode(node) + if cmp < 0 { + left = node.End() + } + return cmp + }) + + if match { + next = nodes[index] + } + } + + visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { + if nodeList != nil && len(nodeList.Nodes) > 0 && next == nil { + if nodeList.End() == position { + left = nodeList.End() + //prevSubtree = nodeList.Nodes[len(nodeList.Nodes)-1] + } else if nodeList.End() <= position { + left = nodeList.End() + } else if nodeList.Pos() <= position { + visitNodes(nodeList.Nodes) + } + } + return nodeList + } + + nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ + VisitNode: visitNode, + VisitToken: visitNode, + VisitNodes: visitNodeList, + VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { + if modifiers != nil { + visitNodeList(&modifiers.NodeList, visitor) + } + return modifiers + }, + }) + + for { + visitEachChildAndJSDoc(current, sourceFile, nodeVisitor) + + // No node was found that contains the target position, so we've gone as deep as + // we can in the AST. We've either found a token, or we need to run the scanner + // to construct one that isn't stored in the AST. + if next == nil { + if ast.IsTokenKind(current.Kind) || ast.IsJSDocCommentContainingNode(current) { + return current + } + scanner := scanner.GetScannerForSourceFile(sourceFile, left) + for left < current.End() { + token := scanner.Token() + tokenFullStart := scanner.TokenFullStart() + tokenStart := scanner.TokenStart() + tokenEnd := scanner.TokenEnd() + if tokenStart <= position && (position < tokenEnd) { + if token == ast.KindIdentifier || !ast.IsTokenKind(token) { + if ast.IsJSDocKind(current.Kind) { + return current + } + panic(fmt.Sprintf("did not expect %s to have %s in its trivia", current.Kind.String(), token.String())) + } + return sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, current) + } + if tokenStart <= position && (tokenEnd == position || tokenEnd == position-1) { + return sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, current) + } + // if includePrecedingTokenAtEndPosition != nil && tokenEnd == position { + // prevToken := sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, current) + // if includePrecedingTokenAtEndPosition(prevToken) { + // return prevToken + // } + // } + left = tokenEnd + scanner.Scan() + } + return current + } + current = next + left = current.Pos() + next = nil + } +} diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 65c7c52f1b..4a75710a61 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -1711,7 +1711,7 @@ func (c *Checker) onSuccessfullyResolvedSymbol(errorLocation *ast.Node, result * importDecl := core.Find(nonValueSymbol.Declarations, func(d *ast.Node) bool { return ast.NodeKindIs(d, ast.KindImportSpecifier, ast.KindImportClause, ast.KindNamespaceImport, ast.KindImportEqualsDeclaration) }) - if importDecl != nil && !isTypeOnlyImportDeclaration(importDecl) { + if importDecl != nil && !ast.IsTypeOnlyImportDeclaration(importDecl) { c.error(importDecl, diagnostics.Import_0_conflicts_with_global_value_used_in_this_file_so_must_be_declared_with_a_type_only_import_when_isolatedModules_is_enabled, name) } } @@ -4994,7 +4994,7 @@ func (c *Checker) checkImportAttributes(declaration *ast.Node) { if importAttributesType != c.emptyObjectType { c.checkTypeAssignableTo(c.getTypeFromImportAttributes(node), c.getNullableType(importAttributesType, TypeFlagsUndefined), node, nil) } - isTypeOnly := isTypeOnlyImportOrExportDeclaration(declaration) + isTypeOnly := ast.IsTypeOnlyImportOrExportDeclaration(declaration) override := c.getResolutionModeOverride(node.AsImportAttributes(), isTypeOnly) isImportAttributes := node.AsImportAttributes().Token == ast.KindWithKeyword if isTypeOnly && override != core.ResolutionModeNone { @@ -6243,12 +6243,12 @@ func (c *Checker) checkAliasSymbol(node *ast.Node) { } else if !ast.IsExportSpecifier(node) { // Look at 'compilerOptions.isolatedModules' and not 'getIsolatedModules(...)' (which considers 'verbatimModuleSyntax') // here because 'verbatimModuleSyntax' will already have an error for importing a type without 'import type'. - appearsValueyToTranspiler := c.compilerOptions.IsolatedModules.IsTrue() && ast.FindAncestor(node, isTypeOnlyImportOrExportDeclaration) == nil + appearsValueyToTranspiler := c.compilerOptions.IsolatedModules.IsTrue() && ast.FindAncestor(node, ast.IsTypeOnlyImportOrExportDeclaration) == nil if appearsValueyToTranspiler && symbol.Flags&(ast.SymbolFlagsValue|ast.SymbolFlagsExportValue) != 0 { c.error(node, diagnostics.Import_0_conflicts_with_local_value_so_must_be_declared_with_a_type_only_import_when_isolatedModules_is_enabled, c.symbolToString(symbol), c.getIsolatedModulesLikeFlagName()) } } - if c.compilerOptions.GetIsolatedModules() && !isTypeOnlyImportOrExportDeclaration(node) && node.Flags&ast.NodeFlagsAmbient == 0 { + if c.compilerOptions.GetIsolatedModules() && !ast.IsTypeOnlyImportOrExportDeclaration(node) && node.Flags&ast.NodeFlagsAmbient == 0 { typeOnlyAlias := c.getTypeOnlyAliasDeclaration(symbol) isType := targetFlags&ast.SymbolFlagsValue == 0 if isType || typeOnlyAlias != nil { @@ -6297,7 +6297,7 @@ func (c *Checker) checkAliasSymbol(node *ast.Node) { c.error(node, diagnostics.ESM_syntax_is_not_allowed_in_a_CommonJS_module_when_module_is_set_to_preserve) } // !!! - // if c.compilerOptions.VerbatimModuleSyntax.IsTrue() && !isTypeOnlyImportOrExportDeclaration(node) && node.Flags&ast.NodeFlagsAmbient == 0 && targetFlags&ast.SymbolFlagsConstEnum != 0 { + // if c.compilerOptions.VerbatimModuleSyntax.IsTrue() && !ast.IsTypeOnlyImportOrExportDeclaration(node) && node.Flags&ast.NodeFlagsAmbient == 0 && targetFlags&ast.SymbolFlagsConstEnum != 0 { // constEnumDeclaration := target.ValueDeclaration // redirect := host.getRedirectReferenceForResolutionFromSourceOfProject(ast.GetSourceFileOfNode(constEnumDeclaration).ResolvedPath) // if constEnumDeclaration.Flags&ast.NodeFlagsAmbient != 0 && (redirect == nil || !shouldPreserveConstEnums(redirect.commandLine.options)) { @@ -6847,7 +6847,7 @@ func (c *Checker) checkNonNullTypeWithReporter(t *Type, node *ast.Node, reportEr facts := c.getTypeFacts(t, TypeFactsIsUndefinedOrNull) if facts&TypeFactsIsUndefinedOrNull != 0 { reportError(c, node, facts) - nonNullable := c.getNonNullableType(t) + nonNullable := c.GetNonNullableType(t) if nonNullable.flags&(TypeFlagsNullable|TypeFlagsNever) != 0 { return c.errorType } @@ -7030,7 +7030,7 @@ func (c *Checker) instantiateTypeWithSingleGenericCallSignature(node *ast.Node, if contextualType == nil { return t } - contextualSignature := c.getSingleSignature(c.getNonNullableType(contextualType), core.IfElse(callSignature != nil, SignatureKindCall, SignatureKindConstruct), false /*allowMembers*/) + contextualSignature := c.getSingleSignature(c.GetNonNullableType(contextualType), core.IfElse(callSignature != nil, SignatureKindCall, SignatureKindConstruct), false /*allowMembers*/) if contextualSignature == nil || len(contextualSignature.typeParameters) != 0 { return t } @@ -8891,7 +8891,7 @@ func (c *Checker) getThisArgumentType(node *ast.Node) *Type { thisArgumentType := c.checkExpression(node) switch { case ast.IsOptionalChainRoot(node.Parent): - return c.getNonNullableType(thisArgumentType) + return c.GetNonNullableType(thisArgumentType) case ast.IsOptionalChain(node.Parent): return c.removeOptionalTypeMarker(thisArgumentType) } @@ -9925,7 +9925,7 @@ func (c *Checker) needCollisionCheckForIdentifier(node *ast.Node, identifier *as } if ast.IsImportClause(node) || ast.IsImportEqualsDeclaration(node) || ast.IsImportSpecifier(node) { // type-only imports do not require collision checks against runtime values. - if isTypeOnlyImportOrExportDeclaration(node) { + if ast.IsTypeOnlyImportOrExportDeclaration(node) { return false } } @@ -9946,13 +9946,13 @@ func (c *Checker) checkNonNullAssertion(node *ast.Node) *Type { if node.Flags&ast.NodeFlagsOptionalChain != 0 { return c.checkNonNullChain(node) } - return c.getNonNullableType(c.checkExpression(node.Expression())) + return c.GetNonNullableType(c.checkExpression(node.Expression())) } func (c *Checker) checkNonNullChain(node *ast.Node) *Type { leftType := c.checkExpression(node.Expression()) nonOptionalType := c.getOptionalExpressionType(leftType, node.Expression()) - return c.propagateOptionalTypeMarker(c.getNonNullableType(nonOptionalType), node, nonOptionalType != leftType) + return c.propagateOptionalTypeMarker(c.GetNonNullableType(nonOptionalType), node, nonOptionalType != leftType) } func (c *Checker) checkExpressionWithTypeArguments(node *ast.Node) *Type { @@ -10522,7 +10522,7 @@ func (c *Checker) checkIdentifier(node *ast.Node, checkMode CheckMode) *Type { } var flowType *Type if isAutomaticTypeInNonNull { - flowType = c.getNonNullableType(c.getFlowTypeOfReferenceEx(node, t, initialType, flowContainer, nil)) + flowType = c.GetNonNullableType(c.getFlowTypeOfReferenceEx(node, t, initialType, flowContainer, nil)) } else { flowType = c.getFlowTypeOfReferenceEx(node, t, initialType, flowContainer, nil) } @@ -10871,7 +10871,7 @@ func (c *Checker) reportNonexistentProperty(propNode *ast.Node, containingType * typeName := c.TypeToString(containingType) diagnostic = NewDiagnosticChainForNode(diagnostic, propNode, diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_to_access_the_static_member_2_instead, propName, typeName, typeName+"."+propName) } else { - promisedType := c.getPromisedTypeOfPromise(containingType) + promisedType := c.GetPromisedTypeOfPromise(containingType) if promisedType != nil && c.getPropertyOfType(promisedType, propNode.Text()) != nil { diagnostic = NewDiagnosticChainForNode(diagnostic, propNode, diagnostics.Property_0_does_not_exist_on_type_1, scanner.DeclarationNameToString(propNode), c.TypeToString(containingType)) diagnostic.AddRelatedInfo(NewDiagnosticForNode(propNode, diagnostics.Did_you_forget_to_use_await)) @@ -11377,7 +11377,7 @@ func (c *Checker) getContextualThisParameterType(fn *ast.Node) *Type { // for 'this' is the non-null form of the contextual type for the containing object literal or // the type of the object literal itself. if contextualType != nil { - thisType = c.getNonNullableType(contextualType) + thisType = c.GetNonNullableType(contextualType) } else { thisType = c.checkExpressionCached(containingLiteral) } @@ -11788,7 +11788,7 @@ func (c *Checker) checkBinaryLikeExpression(left *ast.Node, operatorToken *ast.N case ast.KindBarBarToken, ast.KindBarBarEqualsToken: resultType := leftType if c.hasTypeFacts(leftType, TypeFactsFalsy) { - resultType = c.getUnionTypeEx([]*Type{c.getNonNullableType(c.removeDefinitelyFalsyTypes(leftType)), rightType}, UnionReductionSubtype, nil, nil) + resultType = c.getUnionTypeEx([]*Type{c.GetNonNullableType(c.removeDefinitelyFalsyTypes(leftType)), rightType}, UnionReductionSubtype, nil, nil) } if operator == ast.KindBarBarEqualsToken { c.checkAssignmentOperator(left, operator, right, leftType, rightType) @@ -11800,7 +11800,7 @@ func (c *Checker) checkBinaryLikeExpression(left *ast.Node, operatorToken *ast.N } resultType := leftType if c.hasTypeFacts(leftType, TypeFactsEQUndefinedOrNull) { - resultType = c.getUnionTypeEx([]*Type{c.getNonNullableType(leftType), rightType}, UnionReductionSubtype, nil, nil) + resultType = c.getUnionTypeEx([]*Type{c.GetNonNullableType(leftType), rightType}, UnionReductionSubtype, nil, nil) } if operator == ast.KindQuestionQuestionEqualsToken { c.checkAssignmentOperator(left, operator, right, leftType, rightType) @@ -13588,7 +13588,7 @@ func (c *Checker) getTargetOfImportEqualsDeclaration(node *ast.Node, dontResolve commonJSPropertyAccess := c.getCommonJSPropertyAccess(node) if commonJSPropertyAccess != nil { access := commonJSPropertyAccess.AsPropertyAccessExpression() - name := getLeftmostAccessExpression(access.Expression).AsCallExpression().Arguments.Nodes[0] + name := ast.GetLeftmostAccessExpression(access.Expression).AsCallExpression().Arguments.Nodes[0] if ast.IsIdentifier(access.Name()) { return c.resolveSymbol(c.getPropertyOfType(c.resolveExternalModuleTypeByLiteral(name), access.Name().Text())) } @@ -14197,7 +14197,7 @@ func (c *Checker) markSymbolOfAliasDeclarationIfTypeOnly(aliasDeclaration *ast.N } // If the declaration itself is type-only, mark it and return. No need to check what it resolves to. sourceSymbol := c.getSymbolOfDeclaration(aliasDeclaration) - if isTypeOnlyImportOrExportDeclaration(aliasDeclaration) { + if ast.IsTypeOnlyImportOrExportDeclaration(aliasDeclaration) { links := c.aliasSymbolLinks.Get(sourceSymbol) links.typeOnlyDeclaration = aliasDeclaration return true @@ -14218,7 +14218,7 @@ func (c *Checker) markSymbolOfAliasDeclarationIfTypeOnlyWorker(aliasDeclarationL if target != nil && (!aliasDeclarationLinks.typeOnlyDeclarationResolved || overwriteEmpty && aliasDeclarationLinks.typeOnlyDeclaration == nil) { exportSymbol := core.OrElse(target.Exports[ast.InternalSymbolNameExportEquals], target) aliasDeclarationLinks.typeOnlyDeclarationResolved = true - if typeOnly := core.Find(exportSymbol.Declarations, isTypeOnlyImportOrExportDeclaration); typeOnly != nil { + if typeOnly := core.Find(exportSymbol.Declarations, ast.IsTypeOnlyImportOrExportDeclaration); typeOnly != nil { aliasDeclarationLinks.typeOnlyDeclaration = typeOnly } else { aliasDeclarationLinks.typeOnlyDeclaration = c.aliasSymbolLinks.Get(exportSymbol).typeOnlyDeclaration @@ -16319,7 +16319,7 @@ func (c *Checker) getBindingElementTypeFromParentType(declaration *ast.Node, par pattern := declaration.Parent // Relax null check on ambient destructuring parameters, since the parameters have no implementation and are just documentation if c.strictNullChecks && declaration.Flags&ast.NodeFlagsAmbient != 0 && ast.IsPartOfParameterDeclaration(declaration) { - parentType = c.getNonNullableType(parentType) + parentType = c.GetNonNullableType(parentType) } else if c.strictNullChecks && pattern.Parent.Initializer() != nil && !(c.hasTypeFacts(c.getTypeOfInitializer(pattern.Parent.Initializer()), TypeFactsEQUndefined)) { parentType = c.getTypeWithFacts(parentType, TypeFactsNEUndefined) } @@ -17071,20 +17071,20 @@ func (c *Checker) getNullableType(t *Type, flags TypeFlags) *Type { return c.getUnionType([]*Type{t, c.undefinedType, c.nullType}) } -func (c *Checker) getNonNullableType(t *Type) *Type { +func (c *Checker) GetNonNullableType(t *Type) *Type { if c.strictNullChecks { return c.getAdjustedTypeWithFacts(t, TypeFactsNEUndefinedOrNull) } return t } -func (c *Checker) isNullableType(t *Type) bool { +func (c *Checker) IsNullableType(t *Type) bool { return c.hasTypeFacts(t, TypeFactsIsUndefinedOrNull) } func (c *Checker) getNonNullableTypeIfNeeded(t *Type) *Type { - if c.isNullableType(t) { - return c.getNonNullableType(t) + if c.IsNullableType(t) { + return c.GetNonNullableType(t) } return t } @@ -18755,7 +18755,7 @@ func (c *Checker) getWidenedLiteralLikeTypeForContextualReturnTypeIfNeeded(t *Ty case contextualSignatureReturnType == nil: // No contextual type case isAsync: - contextualType = c.getPromisedTypeOfPromise(contextualSignatureReturnType) + contextualType = c.GetPromisedTypeOfPromise(contextualSignatureReturnType) default: contextualType = contextualSignatureReturnType } @@ -26531,7 +26531,7 @@ func (c *Checker) markEntityNameOrEntityExpressionAsReference(typeName *ast.Node c.compilerOptions.GetIsolatedModules() && c.compilerOptions.GetEmitModuleKind() >= core.ModuleKindES2015 && !c.symbolIsValue(rootSymbol) && - !core.Some(rootSymbol.Declarations, isTypeOnlyImportOrExportDeclaration) { + !core.Some(rootSymbol.Declarations, ast.IsTypeOnlyImportOrExportDeclaration) { diag := c.error(typeName, diagnostics.A_type_referenced_in_a_decorated_signature_must_be_imported_with_import_type_or_a_namespace_import_when_isolatedModules_and_emitDecoratorMetadata_are_enabled) var aliasDeclaration *ast.Node for _, decl := range rootSymbol.Declarations { @@ -26574,7 +26574,7 @@ func (c *Checker) markTypeNodeAsReferenced(node *ast.TypeNode) { } } -func (c *Checker) getPromisedTypeOfPromise(t *Type) *Type { +func (c *Checker) GetPromisedTypeOfPromise(t *Type) *Type { return c.getPromisedTypeOfPromiseEx(t, nil, nil) } @@ -26722,7 +26722,7 @@ func isPartialMappedType(t *Type) bool { func (c *Checker) getOptionalExpressionType(exprType *Type, expression *ast.Node) *Type { switch { case ast.IsExpressionOfOptionalChainRoot(expression): - return c.getNonNullableType(exprType) + return c.GetNonNullableType(exprType) case ast.IsOptionalChain(expression): return c.removeOptionalTypeMarker(exprType) default: @@ -29706,87 +29706,3 @@ func (c *Checker) GetEmitResolver(file *ast.SourceFile, skipDiagnostics bool) pr } return c.emitResolver } - -func (c *Checker) GetSymbolsInScope(location *ast.Node, meaning ast.SymbolFlags) []*ast.Symbol { - return c.getSymbolsInScope(location, meaning) -} - -func (c *Checker) getSymbolsInScope(location *ast.Node, meaning ast.SymbolFlags) []*ast.Symbol { - if location.Flags&ast.NodeFlagsInWithStatement != 0 { - // We cannot answer semantic questions within a with block, do not proceed any further - return nil - } - - symbols := createSymbolTable(nil) - isStaticSymbol := false - - // Copy the given symbol into symbol tables if the symbol has the given meaning - // and it doesn't already exists in the symbol table. - copySymbol := func(symbol *ast.Symbol, meaning ast.SymbolFlags) { - - // !!! - } - - copySymbols := func(source ast.SymbolTable, meaning ast.SymbolFlags) { - // !!! - } - - copyLocallyVisibleExportSymbols := func(source ast.SymbolTable, meaning ast.SymbolFlags) { - // !!! - } - - populateSymbols := func() { - for location != nil { - if canHaveLocals(location) && location.Locals() != nil && !ast.IsGlobalSourceFile(location) { - copySymbols(location.Locals(), meaning) - } - - switch location.Kind { - case ast.KindSourceFile: - if !ast.IsExternalModule(location.AsSourceFile()) { - break - } - fallthrough - case ast.KindModuleDeclaration: - copyLocallyVisibleExportSymbols(c.getSymbolOfDeclaration(location).Exports, meaning&ast.SymbolFlagsModuleMember) - case ast.KindEnumDeclaration: - copySymbols(c.getSymbolOfDeclaration(location).Exports, meaning&ast.SymbolFlagsEnumMember) - case ast.KindClassExpression: - className := location.AsClassExpression().Name() - if className != nil { - copySymbol(location.Symbol(), meaning) - } - // this fall-through is necessary because we would like to handle - // type parameter inside class expression similar to how we handle it in classDeclaration and interface Declaration. - fallthrough - case ast.KindClassDeclaration, ast.KindInterfaceDeclaration: - // If we didn't come from static member of class or interface, - // add the type parameters into the symbol table - // (type parameters of classDeclaration/classExpression and interface are in member property of the symbol. - // Note: that the memberFlags come from previous iteration. - if !isStaticSymbol { - copySymbols(c.getMembersOfSymbol(c.getSymbolOfDeclaration(location)), meaning&ast.SymbolFlagsType) - } - case ast.KindFunctionExpression: - funcName := location.Name() - if funcName != nil { - copySymbol(location.Symbol(), meaning) - } - } - - if introducesArgumentsExoticObject(location) { - copySymbol(c.argumentsSymbol, meaning) - } - - isStaticSymbol = ast.IsStatic(location) - location = location.Parent - } - - copySymbols(c.globals, meaning) - } - - populateSymbols() - - delete(symbols, ast.InternalSymbolNameThis) // Not a symbol, a keyword - return symbolsToArray(symbols) -} diff --git a/internal/checker/flow.go b/internal/checker/flow.go index 0093e5a80e..55a491e5cc 100644 --- a/internal/checker/flow.go +++ b/internal/checker/flow.go @@ -309,7 +309,7 @@ func (c *Checker) narrowTypeByTypePredicate(f *FlowState, t *Type, predicate *Ty if c.isMatchingReference(f.reference, predicateArgument) { return c.getNarrowedType(t, predicate.t, assumeTrue, false /*checkDerived*/) } - if c.strictNullChecks && c.optionalChainContainsReference(predicateArgument, f.reference) && (assumeTrue && !(c.hasTypeFacts(predicate.t, TypeFactsEQUndefined)) || !assumeTrue && everyType(predicate.t, c.isNullableType)) { + if c.strictNullChecks && c.optionalChainContainsReference(predicateArgument, f.reference) && (assumeTrue && !(c.hasTypeFacts(predicate.t, TypeFactsEQUndefined)) || !assumeTrue && everyType(predicate.t, c.IsNullableType)) { t = c.getAdjustedTypeWithFacts(t, TypeFactsNEUndefinedOrNull) } access := c.getDiscriminantPropertyAccess(f, predicateArgument, t) @@ -2423,7 +2423,7 @@ func (c *Checker) getFlowTypeInConstructor(symbol *ast.Symbol, constructor *ast. c.error(symbol.ValueDeclaration, diagnostics.Member_0_implicitly_has_an_1_type, c.symbolToString(symbol), c.TypeToString(flowType)) } // We don't infer a type if assignments are only null or undefined. - if everyType(flowType, c.isNullableType) { + if everyType(flowType, c.IsNullableType) { return nil } return c.convertAutoToAny(flowType) @@ -2446,7 +2446,7 @@ func (c *Checker) getFlowTypeInStaticBlocks(symbol *ast.Symbol, staticBlocks []* c.error(symbol.ValueDeclaration, diagnostics.Member_0_implicitly_has_an_1_type, c.symbolToString(symbol), c.TypeToString(flowType)) } // We don't infer a type if assignments are only null or undefined. - if everyType(flowType, c.isNullableType) { + if everyType(flowType, c.IsNullableType) { continue } return c.convertAutoToAny(flowType) diff --git a/internal/checker/relater.go b/internal/checker/relater.go index c3281879da..eb88ce5a61 100644 --- a/internal/checker/relater.go +++ b/internal/checker/relater.go @@ -1533,10 +1533,10 @@ func (c *Checker) compareSignaturesRelated(source *Signature, target *Signature, var sourceSig *Signature var targetSig *Signature if checkMode&SignatureCheckModeCallback == 0 && !c.isInstantiatedGenericParameter(source, i) { - sourceSig = c.getSingleCallSignature(c.getNonNullableType(sourceType)) + sourceSig = c.getSingleCallSignature(c.GetNonNullableType(sourceType)) } if checkMode&SignatureCheckModeCallback == 0 && !c.isInstantiatedGenericParameter(target, i) { - targetSig = c.getSingleCallSignature(c.getNonNullableType(targetType)) + targetSig = c.getSingleCallSignature(c.GetNonNullableType(targetType)) } callbacks := sourceSig != nil && targetSig != nil && c.getTypePredicateOfSignature(sourceSig) == nil && c.getTypePredicateOfSignature(targetSig) == nil && c.getTypeFacts(sourceType, TypeFactsIsUndefinedOrNull) == c.getTypeFacts(targetType, TypeFactsIsUndefinedOrNull) diff --git a/internal/checker/services.go b/internal/checker/services.go new file mode 100644 index 0000000000..5c28bfb6bd --- /dev/null +++ b/internal/checker/services.go @@ -0,0 +1,240 @@ +package checker + +import ( + "maps" + "slices" + + "github.com/microsoft/typescript-go/internal/ast" +) + +func (c *Checker) GetSymbolsInScope(location *ast.Node, meaning ast.SymbolFlags) []*ast.Symbol { + return c.getSymbolsInScope(location, meaning) +} + +func (c *Checker) getSymbolsInScope(location *ast.Node, meaning ast.SymbolFlags) []*ast.Symbol { + if location.Flags&ast.NodeFlagsInWithStatement != 0 { + // We cannot answer semantic questions within a with block, do not proceed any further + return nil + } + + symbols := createSymbolTable(nil) + isStaticSymbol := false + + // Copy the given symbol into symbol tables if the symbol has the given meaning + // and it doesn't already exists in the symbol table. + copySymbol := func(symbol *ast.Symbol, meaning ast.SymbolFlags) { + if getCombinedLocalAndExportSymbolFlags(symbol)&meaning != 0 { + id := symbol.Name + // We will copy all symbol regardless of its reserved name because + // symbolsToArray will check whether the key is a reserved name and + // it will not copy symbol with reserved name to the array + if _, ok := symbols[id]; !ok { + symbols[id] = symbol + } + } + } + + copySymbols := func(source ast.SymbolTable, meaning ast.SymbolFlags) { + if meaning != 0 { + for _, symbol := range source { + copySymbol(symbol, meaning) + } + } + } + + copyLocallyVisibleExportSymbols := func(source ast.SymbolTable, meaning ast.SymbolFlags) { + if meaning != 0 { + for _, symbol := range source { + // Similar condition as in `resolveNameHelper` + if ast.GetDeclarationOfKind(symbol, ast.KindExportSpecifier) == nil && + ast.GetDeclarationOfKind(symbol, ast.KindNamespaceExport) == nil && + symbol.Name != ast.InternalSymbolNameDefault { + copySymbol(symbol, meaning) + } + } + } + } + + populateSymbols := func() { + for location != nil { + if canHaveLocals(location) && location.Locals() != nil && !ast.IsGlobalSourceFile(location) { + copySymbols(location.Locals(), meaning) + } + + switch location.Kind { + case ast.KindSourceFile: + if !ast.IsExternalModule(location.AsSourceFile()) { + break + } + fallthrough + case ast.KindModuleDeclaration: + copyLocallyVisibleExportSymbols(c.getSymbolOfDeclaration(location).Exports, meaning&ast.SymbolFlagsModuleMember) + case ast.KindEnumDeclaration: + copySymbols(c.getSymbolOfDeclaration(location).Exports, meaning&ast.SymbolFlagsEnumMember) + case ast.KindClassExpression: + className := location.AsClassExpression().Name() + if className != nil { + copySymbol(location.Symbol(), meaning) + } + // this fall-through is necessary because we would like to handle + // type parameter inside class expression similar to how we handle it in classDeclaration and interface Declaration. + fallthrough + case ast.KindClassDeclaration, ast.KindInterfaceDeclaration: + // If we didn't come from static member of class or interface, + // add the type parameters into the symbol table + // (type parameters of classDeclaration/classExpression and interface are in member property of the symbol. + // Note: that the memberFlags come from previous iteration. + if !isStaticSymbol { + copySymbols(c.getMembersOfSymbol(c.getSymbolOfDeclaration(location)), meaning&ast.SymbolFlagsType) + } + case ast.KindFunctionExpression: + funcName := location.Name() + if funcName != nil { + copySymbol(location.Symbol(), meaning) + } + } + + if introducesArgumentsExoticObject(location) { + copySymbol(c.argumentsSymbol, meaning) + } + + isStaticSymbol = ast.IsStatic(location) + location = location.Parent + } + + copySymbols(c.globals, meaning) + } + + populateSymbols() + + delete(symbols, ast.InternalSymbolNameThis) // Not a symbol, a keyword + return symbolsToArray(symbols) +} + +func (c *Checker) GetAliasedSymbol(symbol *ast.Symbol) *ast.Symbol { + return c.resolveAlias(symbol) +} + +func (c *Checker) GetExportsOfModule(symbol *ast.Symbol) []*ast.Symbol { + return symbolsToArray(c.getExportsOfModule(symbol)) +} + +func (c *Checker) IsValidPropertyAccess(node *ast.Node, propertyName string) bool { + return c.isValidPropertyAccess(node, propertyName) +} + +func (c *Checker) isValidPropertyAccess(node *ast.Node, propertyName string) bool { + switch node.Kind { + case ast.KindPropertyAccessExpression: + return c.isValidPropertyAccessWithType(node, node.Expression().Kind == ast.KindSuperKeyword, propertyName, c.getWidenedType(c.checkExpression(node.Expression()))) + case ast.KindQualifiedName: + return c.isValidPropertyAccessWithType(node, false /*isSuper*/, propertyName, c.getWidenedType(c.checkExpression(node.AsQualifiedName().Left))) + case ast.KindImportType: + return c.isValidPropertyAccessWithType(node, false /*isSuper*/, propertyName, c.getTypeFromTypeNode(node)) + } + panic("Unexpected node kind in isValidPropertyAccess: " + node.Kind.String()) +} + +func (c *Checker) isValidPropertyAccessWithType(node *ast.Node, isSuper bool, propertyName string, t *Type) bool { + // Short-circuiting for improved performance. + if IsTypeAny(t) { + return true + } + + prop := c.getPropertyOfType(t, propertyName) + return prop != nil && c.isPropertyAccessible(node, isSuper, false /*isWrite*/, t, prop) +} + +// Checks if an existing property access is valid for completions purposes. +// node: a property access-like node where we want to check if we can access a property. +// This node does not need to be an access of the property we are checking. +// e.g. in completions, this node will often be an incomplete property access node, as in `foo.`. +// Besides providing a location (i.e. scope) used to check property accessibility, we use this node for +// computing whether this is a `super` property access. +// type: the type whose property we are checking. +// property: the accessed property's symbol. +func (c *Checker) IsValidPropertyAccessForCompletions(node *ast.Node, t *Type, property *ast.Symbol) bool { + return c.isPropertyAccessible( + node, + node.Kind == ast.KindPropertyAccessExpression && node.Expression().Kind == ast.KindSuperKeyword, + false, /*isWrite*/ + t, + property, + ) + // Previously we validated the 'this' type of methods but this adversely affected performance. See #31377 for more context. +} + +func (c *Checker) GetAllPossiblePropertiesOfTypes(types []*Type) []*ast.Symbol { + unionType := c.getUnionType(types) + if unionType.flags&TypeFlagsUnion == 0 { + return c.getAugmentedPropertiesOfType(unionType) + } + + props := createSymbolTable(nil) + for _, memberType := range types { + augmentedProps := c.getAugmentedPropertiesOfType(memberType) + for _, p := range augmentedProps { + if _, ok := props[p.Name]; !ok { + prop := c.createUnionOrIntersectionProperty(unionType, p.Name, false /*skipObjectFunctionPropertyAugment*/) + // May be undefined if the property is private + if prop != nil { + props[p.Name] = prop + } + } + } + } + return slices.Collect(maps.Values(props)) +} + +func (c *Checker) IsUnknownSymbol(symbol *ast.Symbol) bool { + return symbol == c.unknownSymbol +} + +// Originally from services.ts +func (c *Checker) GetNonOptionalType(t *Type) *Type { + return c.removeOptionalTypeMarker(t) +} + +func (c *Checker) GetStringIndexType(t *Type) *Type { + return c.getIndexTypeOfType(t, c.stringType) +} + +func (c *Checker) GetNumberIndexType(t *Type) *Type { + return c.getIndexTypeOfType(t, c.numberType) +} + +func (c *Checker) GetCallSignatures(t *Type) []*Signature { + return c.getSignaturesOfType(t, SignatureKindCall) +} + +func (c *Checker) GetConstructSignatures(t *Type) []*Signature { + return c.getSignaturesOfType(t, SignatureKindConstruct) +} + +func (c *Checker) GetApparentProperties(t *Type) []*ast.Symbol { + return c.getAugmentedPropertiesOfType(t) +} + +func (c *Checker) getAugmentedPropertiesOfType(t *Type) []*ast.Symbol { + t = c.getApparentType(t) + propsByName := createSymbolTable(c.getPropertiesOfType(t)) + var functionType *Type + if len(c.getSignaturesOfType(t, SignatureKindCall)) > 0 { + functionType = c.globalCallableFunctionType + } else if len(c.getSignaturesOfType(t, SignatureKindConstruct)) > 0 { + functionType = c.globalNewableFunctionType + } + + if functionType != nil { + for _, p := range c.getPropertiesOfType(functionType) { + if _, ok := propsByName[p.Name]; !ok { + propsByName[p.Name] = p + } + } + } + return c.getNamedMembers(propsByName) +} + +func (c *Checker) IsUnion(t *Type) bool { + return t.flags&TypeFlagsUnion != 0 +} diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go index 493353d95d..f9d912f15f 100644 --- a/internal/checker/utilities.go +++ b/internal/checker/utilities.go @@ -120,20 +120,13 @@ func isVariableDeclarationInitializedWithRequireHelper(node *ast.Node, allowAcce if node.Kind == ast.KindVariableDeclaration && node.AsVariableDeclaration().Initializer != nil { initializer := node.AsVariableDeclaration().Initializer if allowAccessedRequire { - initializer = getLeftmostAccessExpression(initializer) + initializer = ast.GetLeftmostAccessExpression(initializer) } return isRequireCall(initializer, true /*requireStringLiteralLikeArgument*/) } return false } -func getLeftmostAccessExpression(expr *ast.Node) *ast.Node { - for ast.IsAccessExpression(expr) { - expr = expr.Expression() - } - return expr -} - func isRequireCall(node *ast.Node, requireStringLiteralLikeArgument bool) bool { if ast.IsCallExpression(node) { callExpression := node.AsCallExpression() @@ -255,37 +248,6 @@ func isInTypeQuery(node *ast.Node) bool { }) != nil } -func isTypeOnlyImportDeclaration(node *ast.Node) bool { - switch node.Kind { - case ast.KindImportSpecifier: - return node.AsImportSpecifier().IsTypeOnly || node.Parent.Parent.AsImportClause().IsTypeOnly - case ast.KindNamespaceImport: - return node.Parent.AsImportClause().IsTypeOnly - case ast.KindImportClause: - return node.AsImportClause().IsTypeOnly - case ast.KindImportEqualsDeclaration: - return node.AsImportEqualsDeclaration().IsTypeOnly - } - return false -} - -func isTypeOnlyExportDeclaration(node *ast.Node) bool { - switch node.Kind { - case ast.KindExportSpecifier: - return node.AsExportSpecifier().IsTypeOnly || node.Parent.Parent.AsExportDeclaration().IsTypeOnly - case ast.KindExportDeclaration: - d := node.AsExportDeclaration() - return d.IsTypeOnly && d.ModuleSpecifier != nil && d.ExportClause == nil - case ast.KindNamespaceExport: - return node.Parent.AsExportDeclaration().IsTypeOnly - } - return false -} - -func isTypeOnlyImportOrExportDeclaration(node *ast.Node) bool { - return isTypeOnlyImportDeclaration(node) || isTypeOnlyExportDeclaration(node) -} - func getNameFromImportDeclaration(node *ast.Node) *ast.Node { switch node.Kind { case ast.KindImportSpecifier: @@ -468,7 +430,7 @@ func isSideEffectImport(node *ast.Node) bool { func getExternalModuleRequireArgument(node *ast.Node) *ast.Node { if isVariableDeclarationInitializedToBareOrAccessedRequire(node) { - return getLeftmostAccessExpression(node.AsVariableDeclaration().Initializer).AsCallExpression().Arguments.Nodes[0] + return ast.GetLeftmostAccessExpression(node.AsVariableDeclaration().Initializer).AsCallExpression().Arguments.Nodes[0] } return nil } @@ -1795,7 +1757,7 @@ func canIncludeBindAndCheckDiagnostics(sourceFile *ast.SourceFile, options *core } isJs := sourceFile.ScriptKind == core.ScriptKindJS || sourceFile.ScriptKind == core.ScriptKindJSX - isCheckJs := isJs && isCheckJsEnabledForFile(sourceFile, options) + isCheckJs := isJs && ast.IsCheckJsEnabledForFile(sourceFile, options) isPlainJs := isPlainJsFile(sourceFile, options.CheckJs) // By default, only type-check .ts, .tsx, Deferred, plain JS, checked JS and External @@ -1805,14 +1767,6 @@ func canIncludeBindAndCheckDiagnostics(sourceFile *ast.SourceFile, options *core return isPlainJs || isCheckJs || sourceFile.ScriptKind == core.ScriptKindDeferred } -func isCheckJsEnabledForFile(sourceFile *ast.SourceFile, compilerOptions *core.CompilerOptions) bool { - // !!! - // if sourceFile.CheckJsDirective != nil { - // return sourceFile.CheckJsDirective.Enabled - // } - return compilerOptions.CheckJs == core.TSTrue -} - func isPlainJsFile(file *ast.SourceFile, checkJs core.Tristate) bool { // !!! // return file != nil && (file.ScriptKind == core.ScriptKindJS || file.ScriptKind == core.ScriptKindJSX) && file.CheckJsDirective == nil && checkJs == core.TSUnknown @@ -2137,3 +2091,18 @@ func symbolsToArray(symbols ast.SymbolTable) []*ast.Symbol { } return result } + +// See comment on `declareModuleMember` in `binder.go`. +func getCombinedLocalAndExportSymbolFlags(symbol *ast.Symbol) ast.SymbolFlags { + if symbol.ExportSymbol != nil { + return symbol.Flags | symbol.ExportSymbol.Flags + } + return symbol.Flags +} + +func SkipAlias(symbol *ast.Symbol, checker *Checker) *ast.Symbol { + if symbol.Flags&ast.SymbolFlagsAlias != 0 { + return checker.GetAliasedSymbol(symbol) + } + return symbol +} diff --git a/internal/core/core.go b/internal/core/core.go index 0b4154d6b5..ea29e42575 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -524,3 +524,20 @@ func levenshteinWithMax(s1 []rune, s2 []rune, maxValue float64) float64 { func Identity[T any](t T) T { return t } + +func AddToSeen[K comparable, V any](seen map[K]V, key K, value V) bool { + if _, ok := seen[key]; ok { + return false + } + seen[key] = value + return true +} + +func CheckEachDefined[S any](s []*S, msg string) []*S { + for _, value := range s { + if value == nil { + panic(msg) + } + } + return s +} diff --git a/internal/ls/completions.go b/internal/ls/completions.go index bcd233668a..c76c831ff6 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -1,28 +1,107 @@ package ls import ( + "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/compiler" + "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" ) -func (l *LanguageService) ProvideCompletion(fileName string, position int) *lsproto.CompletionList { +func (l *LanguageService) ProvideCompletion(fileName string, position int, context *lsproto.CompletionContext) *lsproto.CompletionList { program, file := l.getProgramAndFile(fileName) node := astnav.GetTouchingPropertyName(file, position) if node.Kind == ast.KindSourceFile { return nil } - return l.getCompletionsAtPosition(program, file, position) + return l.getCompletionsAtPosition(program, file, position, context, nil /*preferences*/) // !!! get preferences } -func (l *LanguageService) getCompletionsAtPosition(program *compiler.Program, file *ast.SourceFile, position int) *lsproto.CompletionList { - // !!! trigger kind +type completionData struct { + // !!! +} - // !!! see if incomplete completion list +type importStatementCompletionInfo struct { + // !!! +} + +// If we're after the `=` sign but no identifier has been typed yet, +// value will be `true` but initializer will be `nil`. +type jsxInitializer struct { + isInitializer bool + initializer *ast.Identifier +} + +type KeywordCompletionFilters int + +const ( + KeywordCompletionFiltersNone KeywordCompletionFilters = iota // No keywords + KeywordCompletionFiltersAll // Every possible kewyord + KeywordCompletionFiltersClassElementKeywords // Keywords inside class body + KeywordCompletionFiltersInterfaceElementKeywords // Keywords inside interface body + KeywordCompletionFiltersConstructorParameterKeywords // Keywords at constructor parameter + KeywordCompletionFiltersFunctionLikeBodyKeywords // Keywords at function like body + KeywordCompletionFiltersTypeAssertionKeywords + KeywordCompletionFiltersTypeKeywords + KeywordCompletionFiltersTypeKeyword // Literally just `type` + KeywordCompletionFiltersLast = KeywordCompletionFiltersTypeKeyword +) + +type CompletionKind int + +const ( + CompletionKindNone CompletionKind = iota + CompletionKindObjectPropertyDeclaration + CompletionKindGlobal + CompletionKindPropertyAccess + CompletionKindMemberLike + CompletionKindString +) + +// All commit characters, valid when `isNewIdentifierLocation` is false. +var allCommitCharacters = []string{".", ",", ";"} + +// Commit characters valid at expression positions where we could be inside a parameter list. +var noCommaCommitCharacters = []string{".", ";"} + +func (l *LanguageService) getCompletionsAtPosition( + program *compiler.Program, + file *ast.SourceFile, + position int, + context *lsproto.CompletionContext, + preferences *UserPreferences) *lsproto.CompletionList { + previousToken, _ := getRelevantTokens(position, file) + if context.TriggerCharacter != nil && !isInString(file, position, previousToken) && !isValidTrigger(file, *context.TriggerCharacter, previousToken, position) { + return nil + } + + if *context.TriggerCharacter == " " { + // `isValidTrigger` ensures we are at `import |` + if preferences.includeCompletionsForImportStatements { + // !!! isMemberCompletion + return &lsproto.CompletionList{ + IsIncomplete: true, + ItemDefaults: &lsproto.CompletionItemDefaults{ // !!! do we need this if no entries? also, check if client supports item defaults + CommitCharacters: getDefaultCommitCharacters(true /*isNewIdentifierLocation*/), + }, + } + } + return nil + } + + compilerOptions := program.GetCompilerOptions() + checker := program.GetTypeChecker() + + // !!! see if incomplete completion list and continue or clean // !!! string literal completions + // !!! label completions + + completionData := getCompletionData(program, file, position, preferences) // !!! get completion data // !!! transform data into completion list @@ -30,31 +109,472 @@ func (l *LanguageService) getCompletionsAtPosition(program *compiler.Program, fi return nil // !!! } -type completionData struct { - // !!! -} - -func getCompletionData(program *compiler.Program, file *ast.SourceFile, position int) *completionData { +func getCompletionData(program *compiler.Program, file *ast.SourceFile, position int, preferences *UserPreferences) *completionData { typeChecker := program.GetTypeChecker() + inCheckedFile := isCheckedFile(file, program.GetCompilerOptions()) - // !!! why do we use this? just for detecting comments/jsdoc scenario? currentToken := astnav.GetTokenAtPosition(file, position) - // !!! comment, jsdoc stuff + insideComment := isInComment(file, position, currentToken) + + insideJsDocTagTypeExpression := false + insideJsDocImportTag := false + isInSnippetScope := false + if insideComment != nil { + // !!! jsdoc + } + + // The decision to provide completion depends on the contextToken, which is determined through the previousToken. + // Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file + isJSOnlyLocation := !insideJsDocTagTypeExpression && !insideJsDocImportTag && ast.IsSourceFileJs(file) + previousToken, contextToken := getRelevantTokens(position, file) + + // Find the node where completion is requested on. + // Also determine whether we are trying to complete with members of that node + // or attributes of a JSX tag. + node := currentToken + var propertyAccessToConvert *ast.PropertyAccessExpression + isRightOfDot := false + isRightOfQuestionDot := false + isRightOfOpenTag := false + isStartingCloseTag := false + var jsxInitializer jsxInitializer + isJsxIdentifierExpected := false + var importStatementCompletion *importStatementCompletionInfo + location := astnav.GetTouchingPropertyName(file, position) + keywordFilters := KeywordCompletionFiltersNone + isNewIdentifierLocation := false + // !!! + // flags := CompletionInfoFlagsNone + var defaultCommitCharacters []string + + if contextToken != nil { + // !!! import completions + + parent := contextToken.Parent + if contextToken.Kind == ast.KindDotToken || contextToken.Kind == ast.KindQuestionDotToken { + isRightOfDot = contextToken.Kind == ast.KindDotToken + isRightOfQuestionDot = contextToken.Kind == ast.KindQuestionDotToken + switch parent.Kind { + case ast.KindPropertyAccessExpression: + propertyAccessToConvert = parent.AsPropertyAccessExpression() + node = propertyAccessToConvert.Expression + leftMostAccessExpression := ast.GetLeftmostAccessExpression(parent) + if ast.NodeIsMissing(leftMostAccessExpression) || + ((ast.IsCallExpression(node) || ast.IsFunctionLike(node)) && + node.End() == contextToken.Pos() && + getLastChild(node, file).Kind != ast.KindCloseParenToken) { + // This is likely dot from incorrectly parsed expression and user is starting to write spread + // eg: Math.min(./**/) + // const x = function (./**/) {} + // ({./**/}) + return nil + } + case ast.KindQualifiedName: + node = parent.AsQualifiedName().Left + case ast.KindModuleDeclaration: + node = parent.Name() + case ast.KindImportType: + node = parent + case ast.KindMetaProperty: + node = getFirstToken(parent, file) + if node.Kind != ast.KindImportKeyword || node.Kind != ast.KindNewKeyword { + panic("Unexpected token kind: " + node.Kind.String()) + } + default: + // There is nothing that precedes the dot, so this likely just a stray character + // or leading into a '...' token. Just bail out instead. + return nil + } + } else { // !!! else if (!importStatementCompletion) + // + // If the tagname is a property access expression, we will then walk up to the top most of property access expression. + // Then, try to get a JSX container and its associated attributes type. + if parent != nil && parent.Kind == ast.KindPropertyAccessExpression { + contextToken = parent + parent = parent.Parent + } - // !!! get relevant tokens + // Fix location + if parent == location { + switch currentToken.Kind { + case ast.KindGreaterThanToken: + if parent.Kind == ast.KindJsxElement || parent.Kind == ast.KindJsxOpeningElement { + location = currentToken + } + case ast.KindSlashToken: + if parent.Kind == ast.KindJsxSelfClosingElement { + location = currentToken + } + } + } - // !!! adjust context token + switch parent.Kind { + case ast.KindJsxClosingElement: + if contextToken.Kind == ast.KindSlashToken { + isStartingCloseTag = true + location = contextToken + } + case ast.KindBinaryExpression: + if !binaryExpressionMayBeOpenTag(parent.AsBinaryExpression()) { + break + } + fallthrough + case ast.KindJsxSelfClosingElement, ast.KindJsxElement, ast.KindJsxOpeningElement: + isJsxIdentifierExpected = true + if contextToken.Kind == ast.KindLessThanToken { + isRightOfOpenTag = true + location = contextToken + } + case ast.KindJsxExpression, ast.KindJsxSpreadAttribute: + // First case is for `
` or `
`, + // `parent` will be `{true}` and `previousToken` will be `}`. + // Second case is for `
`. + // Second case must not match for `
`. + if previousToken.Kind == ast.KindCloseBraceToken || + previousToken.Kind == ast.KindIdentifier && previousToken.Parent.Kind == ast.KindJsxAttribute { + isJsxIdentifierExpected = true + } + case ast.KindJsxAttribute: + // For `
`, `parent` will be JsxAttribute and `previousToken` will be its initializer. + if parent.Initializer() == previousToken && previousToken.End() < position { + isJsxIdentifierExpected = true + } else { + switch previousToken.Kind { + case ast.KindEqualsToken: + jsxInitializer.isInitializer = true + case ast.KindIdentifier: + isJsxIdentifierExpected = true + // For `
` we don't want to treat this as a jsx inializer, instead it's the attribute name. + if parent != previousToken.Parent && + parent.Initializer() == nil && + findChildOfKind(parent, ast.KindEqualsToken, file) != nil { + jsxInitializer.initializer = previousToken.AsIdentifier() + } + } + } + } + } + } + + completionKind := CompletionKindNone + hasUnresolvedAutoImports := false + // This also gets mutated in nested-functions after the return + var symbols []*ast.Symbol // !!! symbol: this will not work + var symbolToOriginInfoMap []any // !!! symbol: refactor + var symbolToSortTextMap []any // !!! symbol: refactor + var importSpecifierResolver any // !!! + seenPropertySymbols := make(map[ast.SymbolId]struct{}) + isTypeOnlyLocation := insideJsDocTagTypeExpression || insideJsDocImportTag || + importStatementCompletion != nil && ast.IsTypeOnlyImportOrExportDeclaration(location.Parent) || + !isContextTokenValueLocation(contextToken) && + (isPossiblyTypeArgumentPosition(contextToken, file, typeChecker) || + ast.IsPartOfTypeNode(location) || + isContextTokenTypeLocation(contextToken)) + var getModuleSpecifierResolutionHost any // !!! + + addPropertySymbol := func(symbol *ast.Symbol, insertAwait bool, insertQuestionDot bool) { + // For a computed property with an accessible name like `Symbol.iterator`, + // we'll add a completion for the *name* `Symbol` instead of for the property. + // If this is e.g. [Symbol.iterator], add a completion for `Symbol`. + computedPropertyName = core.FirstNonNil(symbol.Declarations, func(decl *ast.Node) *ast.Node { + name := ast.GetNameOfDeclaration(decl) + if name != nil && name.Kind == ast.KindComputedPropertyName { + return name + } + return nil + }) + + // !!! HERE + } - // !!! see if jsx tag or dot completion + addTypeProperties := func(t *checker.Type, insertAwait bool, insertQuestionDot bool) { + if typeChecker.GetStringIndexType(t) != nil { + isNewIdentifierLocation = true + defaultCommitCharacters = make([]string, 0) + } + if isRightOfQuestionDot && len(typeChecker.GetCallSignatures(t)) != 0 { + isNewIdentifierLocation = true + if len(defaultCommitCharacters) == 0 { + defaultCommitCharacters = slices.Clone(allCommitCharacters) // Only invalid commit character here would be `(`. + } + } - // !!! global completions + var propertyAccess *ast.Node + if node.Kind == ast.KindImportType { + propertyAccess = node + } else { + propertyAccess = node.Parent + } + + if inCheckedFile { + for _, symbol := range typeChecker.GetApparentProperties(t) { + if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, t, symbol) { + addPropertySymbol(symbol, false /*insertAwait*/, insertQuestionDot) + } + } + } else { + // In javascript files, for union types, we don't just get the members that + // the individual types have in common, we also include all the members that + // each individual type has. This is because we're going to add all identifiers + // anyways. So we might as well elevate the members that were at least part + // of the individual types to a higher status since we know what they are. + for _, symbol := range getPropertiesForCompletion(t, typeChecker) { + if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, t, symbol) { + symbols = append(symbols, symbol) + } + } + } + + if insertAwait { + promiseType := typeChecker.GetPromisedTypeOfPromise(t) + if promiseType != nil { + for _, symbol := range typeChecker.GetApparentProperties(promiseType) { + if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, promiseType, symbol) { + addPropertySymbol(symbol, true /*insertAwait*/, insertQuestionDot) + } + } + } + } + } + + getTypeScriptMemberSymbols := func() { + // Right of dot member completion list + completionKind = CompletionKindPropertyAccess + + // Since this is qualified name check it's a type node location + isImportType := ast.IsLiteralImportTypeNode(node) + isTypeLocation := (isImportType && !node.AsImportTypeNode().IsTypeOf) || + ast.IsPartOfTypeNode(node.Parent) || + isPossiblyTypeArgumentPosition(contextToken, file, typeChecker) + isRhsOfImportDeclaration := isInRightSideOfInternalImportEqualsDeclaration(node) + if ast.IsEntityName(node) || isImportType || ast.IsPropertyAccessExpression(node) { + isNamespaceName := ast.IsModuleDeclaration(node.Parent) + if isNamespaceName { + isNewIdentifierLocation = true + defaultCommitCharacters = make([]string, 0) + } + symbol := typeChecker.GetSymbolAtLocation(node) + if symbol != nil { + symbol := checker.SkipAlias(symbol, typeChecker) + if symbol.Flags&(ast.SymbolFlagsModule|ast.SymbolFlagsEnum) != 0 { + var valueAccessNode *ast.Node + if isImportType { + valueAccessNode = node + } else { + valueAccessNode = node.Parent + } + // Extract module or enum members + exportedSymbols := typeChecker.GetExportsOfModule(symbol) + for _, exportedSymbol := range exportedSymbols { + if exportedSymbol == nil { + panic("getExporsOfModule() should all be defined") + } + isValidValueAccess := func(s *ast.Symbol) bool { + return typeChecker.IsValidPropertyAccess(valueAccessNode, s.Name) + } + isValidTypeAccess := func(s *ast.Symbol) bool { + return symbolCanBeReferencedAtTypeLocation(s, typeChecker, nil /*seenModules*/) + } + var isValidAccess bool + if isNamespaceName { + // At `namespace N.M/**/`, if this is the only declaration of `M`, don't include `M` as a completion. + isValidAccess = exportedSymbol.Flags&ast.SymbolFlagsNamespace != 0 && + !core.Every(exportedSymbol.Declarations, func(declaration *ast.Declaration) bool { + return declaration.Parent == node.Parent + }) + } else if isRhsOfImportDeclaration { + // Any kind is allowed when dotting off namespace in internal import equals declaration + isValidAccess = isValidTypeAccess(exportedSymbol) || isValidValueAccess(exportedSymbol) + } else if isTypeLocation || insideJsDocTagTypeExpression { + isValidAccess = isValidTypeAccess(exportedSymbol) + } else { + isValidAccess = isValidValueAccess(exportedSymbol) + } + if isValidAccess { + symbols = append(symbols, exportedSymbol) + } + } + + // If the module is merged with a value, we must get the type of the class and add its properties (for inherited static methods). + if !isTypeLocation && !insideJsDocTagTypeExpression && + core.Some( + symbol.Declarations, + func(decl *ast.Declaration) bool { + return decl.Kind != ast.KindSourceFile && decl.Kind != ast.KindModuleDeclaration && decl.Kind != ast.KindEnumDeclaration + }) { + t := typeChecker.GetNonOptionalType(typeChecker.GetTypeOfSymbolAtLocation(symbol, node)) + insertQuestionDot := false + if typeChecker.IsNullableType(t) { + canCorrectToQuestionDot := isRightOfDot && !isRightOfQuestionDot && + preferences.includeAutomaticOptionalChainCompletions + if canCorrectToQuestionDot || isRightOfQuestionDot { + t = typeChecker.GetNonNullableType(t) + if canCorrectToQuestionDot { + insertQuestionDot = true + } + } + } + addTypeProperties(t, node.Flags&ast.NodeFlagsAwaitContext != 0, insertQuestionDot) + } + } + } + } + } + + if isRightOfDot || isRightOfQuestionDot { + getTypeScriptMemberSymbols() + } else if isRightOfOpenTag { + // !!! + } else if isStartingCloseTag { + // !!! + } else { + // !!! + } + + // !!! adjustments return nil } -// !!! Document this +// In a scenarion such as `const x = 1 * |`, the context and previous tokens are both `*`. +// In `const x = 1 * o|`, the context token is *, and the previous token is `o`. // `contextToken` and `previousToken` can both be nil if we are at the beginning of the file. func getRelevantTokens(position int, file *ast.SourceFile) (contextToken *ast.Node, previousToken *ast.Node) { + previousToken = astnav.FindPrecedingToken(file, position) + if previousToken != nil && position <= previousToken.End() && (ast.IsMemberName(previousToken) || ast.IsKeywordKind(previousToken.Kind)) { + contextToken := astnav.FindPrecedingToken(file, previousToken.Pos()) + return contextToken, previousToken + } + return previousToken, previousToken +} + +// "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " " +type CompletionsTriggerCharacter = string + +func isValidTrigger(file *ast.SourceFile, triggerCharacter CompletionsTriggerCharacter, contextToken *ast.Node, position int) bool { + switch triggerCharacter { + case ".", "@": + return true + case "\"", "'", "`": + // Only automatically bring up completions if this is an opening quote. + return contextToken != nil && + isStringLiteralOrTemplate(contextToken) && + position == getStartOfNode(contextToken, file)+1 + case "#": + return contextToken != nil && + ast.IsPrivateIdentifier(contextToken) && + ast.GetContainingClass(contextToken) != nil + case "<": + // Opening JSX tag + return contextToken != nil && + contextToken.Kind == ast.KindLessThanToken && + (!ast.IsBinaryExpression(contextToken.Parent) || binaryExpressionMayBeOpenTag(contextToken.Parent.AsBinaryExpression())) + case "/": + if contextToken == nil { + return false + } + if ast.IsStringLiteralLike(contextToken) { + return tryGetImportFromModuleSpecifier(contextToken) != nil + } + return contextToken.Kind == ast.KindSlashToken && ast.IsJsxClosingElement(contextToken.Parent) + case " ": + return contextToken != nil && contextToken.Kind == ast.KindImportKeyword && contextToken.Parent.Kind == ast.KindSourceFile + default: + panic("Unknown trigger character: " + triggerCharacter) + } +} + +func isStringLiteralOrTemplate(node *ast.Node) bool { + switch node.Kind { + case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral, ast.KindTemplateExpression, + ast.KindTaggedTemplateExpression: + return true + } + return false +} + +func binaryExpressionMayBeOpenTag(binaryExpression *ast.BinaryExpression) bool { + return ast.NodeIsMissing(binaryExpression.Left) +} + +func getDefaultCommitCharacters(isNewIdentifierLocation bool) *[]string { + // !!! + return nil +} + +func isCheckedFile(file *ast.SourceFile, compilerOptions *core.CompilerOptions) bool { + return !ast.IsSourceFileJs(file) || ast.IsCheckJsEnabledForFile(file, compilerOptions) +} + +func isContextTokenValueLocation(contextToken *ast.Node) bool { + return contextToken != nil && ((contextToken.Kind == ast.KindTypeOfKeyword && + (contextToken.Parent.Kind == ast.KindTypeQuery || ast.IsTypeOfExpression(contextToken.Parent))) || + (contextToken.Kind == ast.KindAssertsKeyword && contextToken.Parent.Kind == ast.KindTypePredicate)) +} + +func isPossiblyTypeArgumentPosition(token *ast.Node, sourceFile *ast.SourceFile, typeChecker *checker.Checker) bool { + info := getPossibleTypeArgumentsInfo(token, sourceFile) + return info != nil && (ast.IsPartOfTypeNode(info.called) || + len(getPossibleGenericSignatures(info.called, info.nTypeArguments, typeChecker)) != 0 || + isPossiblyTypeArgumentPosition(info.called, sourceFile, typeChecker)) +} + +func isContextTokenTypeLocation(contextToken *ast.Node) bool { + if contextToken != nil { + parentKind := contextToken.Parent.Kind + switch contextToken.Kind { + case ast.KindColonToken: + return parentKind == ast.KindPropertyDeclaration || + parentKind == ast.KindPropertySignature || + parentKind == ast.KindParameter || + parentKind == ast.KindVariableDeclaration || + ast.IsFunctionLikeKind(parentKind) + case ast.KindEqualsToken: + return parentKind == ast.KindTypeAliasDeclaration || parentKind == ast.KindTypeParameter + case ast.KindAsKeyword: + return parentKind == ast.KindAsExpression + case ast.KindLessThanToken: + return parentKind == ast.KindTypeReference || parentKind == ast.KindTypeAssertionExpression + case ast.KindExtendsKeyword: + return parentKind == ast.KindTypeParameter + case ast.KindSatisfiesKeyword: + return parentKind == ast.KindSatisfiesExpression + } + } + return false +} + +// True if symbol is a type or a module containing at least one type. +func symbolCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules map[ast.SymbolId]struct{}) bool { + if seenModules == nil { + seenModules = make(map[ast.SymbolId]struct{}) + } + // Since an alias can be merged with a local declaration, we need to test both the alias and its target. + // This code used to just test the result of `skipAlias`, but that would ignore any locally introduced meanings. + return nonAliasCanBeReferencedAtTypeLocation(symbol, typeChecker, seenModules) || + nonAliasCanBeReferencedAtTypeLocation( + checker.SkipAlias(core.IfElse(symbol.ExportSymbol != nil, symbol.ExportSymbol, symbol), typeChecker), + typeChecker, + seenModules, + ) +} +func nonAliasCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules map[ast.SymbolId]struct{}) bool { + return symbol.Flags&ast.SymbolFlagsType != 0 || typeChecker.IsUnknownSymbol(symbol) || + symbol.Flags&ast.SymbolFlagsModule != 0 && core.AddToSeen(seenModules, ast.GetSymbolId(symbol), struct{}{}) && + core.Some( + typeChecker.GetExportsOfModule(symbol), + func(e *ast.Symbol) bool { return symbolCanBeReferencedAtTypeLocation(e, typeChecker, seenModules) }) +} + +// Gets all properties on a type, but if that type is a union of several types, +// excludes array-like types or callable/constructable types. +func getPropertiesForCompletion(t *checker.Type, typeChecker *checker.Checker) []*ast.Symbol { + if typeChecker.IsUnion(t) { + return core.CheckEachDefined(typeChecker.GetAllPossiblePropertiesOfTypes(t.Types()), "getAllPossiblePropertiesOfTypes() should all be defined.") + } else { + return core.CheckEachDefined(typeChecker.GetApparentProperties(t), "getApparentProperties() should all be defined.") + } } diff --git a/internal/ls/types.go b/internal/ls/types.go index 830a6bfc3d..291c10eda8 100644 --- a/internal/ls/types.go +++ b/internal/ls/types.go @@ -15,3 +15,14 @@ type Location struct { FileName string Range core.TextRange } + +type UserPreferences struct { + // Enables auto-import-style completions on partially-typed import statements. E.g., allows + // `import write|` to be completed to `import { writeFile } from "fs"`. + includeCompletionsForImportStatements bool + + // 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 bool +} diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go new file mode 100644 index 0000000000..2855b05ba6 --- /dev/null +++ b/internal/ls/utilities.go @@ -0,0 +1,127 @@ +package ls + +import ( + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/checker" +) + +// !!! Shared (placeholder) +func isInString(file *ast.SourceFile, position int, previousToken *ast.Node) bool { + if previousToken == nil { + previousToken = astnav.FindPrecedingToken(file, position) + } + + if previousToken != nil && isStringTextContainingNode(previousToken) { + // start := previousToken. + // !!! HERE + } + + return false +} + +// !!! Shared (placeholder) +func isStringTextContainingNode(node *ast.Node) bool { + return true +} + +// !!! Shared (placeholder) +func getStartOfNode(node *ast.Node, file *ast.SourceFile) int { + return node.Pos() +} + +func tryGetImportFromModuleSpecifier(node *ast.StringLiteralLike) *ast.Node { + switch node.Parent.Kind { + case ast.KindImportDeclaration, ast.KindExportDeclaration, ast.KindJSDocImportTag: + return node.Parent + case ast.KindExternalModuleReference: + return node.Parent.Parent + case ast.KindCallExpression: + if ast.IsImportCall(node.Parent) || ast.IsRequireCall(node.Parent, false /*requireStringLiteralLikeArgument*/) { + return node.Parent + } + return nil + case ast.KindLiteralType: + if !ast.IsStringLiteral(node) { + return nil + } + if ast.IsImportTypeNode(node.Parent.Parent) { + return node.Parent.Parent + } + return nil + } + return nil +} + +// !!! Shared (placeholder) +func isInComment(file *ast.SourceFile, position int, tokenAtPosition *ast.Node) *ast.CommentRange { + return nil +} + +// // Returns a non-nil comment range if the cursor at position in sourceFile is within a comment. +// // tokenAtPosition: must equal `getTokenAtPosition(sourceFile, position)` +// // predicate: additional predicate to test on the comment range. +// func isInComment(file *ast.SourceFile, position int, tokenAtPosition *ast.Node) *ast.CommentRange { +// return getRangeOfEnclosingComment(file, position, nil /*precedingToken*/, tokenAtPosition) +// } + +// !!! +// Replaces last(node.getChildren(sourceFile)) +func getLastChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { + return nil +} + +func getLastToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { + assertHasRealPosition(node) + + lastChild := getLastChild(node, sourceFile) + if lastChild == nil { + return nil + } + + if lastChild.Kind < ast.KindFirstNode { + return lastChild + } else { + return getLastToken(lastChild, sourceFile) + } +} + +// !!! +func getFirstToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { + return nil +} + +func assertHasRealPosition(node *ast.Node) { + if ast.PositionIsSynthesized(node.Pos()) || ast.PositionIsSynthesized(node.End()) { + panic("Node must have a real position for this operation.") + } +} + +// !!! +func findChildOfKind(node *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) *ast.Node { + return nil +} + +// !!! Shared: placeholder +type PossibleTypeArgumentInfo struct { + called *ast.IdentifierNode + nTypeArguments int +} + +// !!! Shared: placeholder +func getPossibleTypeArgumentsInfo(tokenIn *ast.Node, sourceFile *ast.SourceFile) *PossibleTypeArgumentInfo { + return nil +} + +// !!! Shared: placeholder +func getPossibleGenericSignatures(called *ast.Expression, typeArgumentCount int, checker *checker.Checker) []*checker.Signature { + return nil +} + +func isInRightSideOfInternalImportEqualsDeclaration(node *ast.Node) bool { + for node.Parent.Kind == ast.KindQualifiedName { + node = node.Parent + } + + return ast.IsInternalModuleImportEqualsDeclaration(node.Parent) && node.Parent.AsImportEqualsDeclaration().ModuleReference == node +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 6b0e4900a1..1f7aa73bb2 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -381,7 +381,7 @@ func (s *Server) handleCompletion(req *lsproto.RequestMessage) error { return s.sendError(req.ID, err) } - list := project.LanguageService().ProvideCompletion(file.FileName(), pos) + list := project.LanguageService().ProvideCompletion(file.FileName(), pos, params.Context) // !!! convert completion to `lsproto.CompletionList` return s.sendResult(req.ID, nil) // !!! } From 8db06be052aaef616dd71ef2ed1e720fa68ba0d1 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 2 Apr 2025 13:51:17 -0700 Subject: [PATCH 03/25] finish initial port of getTypeScriptMemberSymbols --- internal/ast/utilities.go | 18 +++ internal/checker/checker.go | 21 +++- internal/checker/emitresolver.go | 2 +- internal/checker/relater.go | 6 +- internal/checker/services.go | 31 +++++ internal/checker/utilities.go | 33 ++---- internal/core/core.go | 9 ++ internal/ls/completions.go | 190 +++++++++++++++++++++++++++++-- 8 files changed, 270 insertions(+), 40 deletions(-) diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 207758621f..92d9d19b01 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -2619,3 +2619,21 @@ func isTypeOnlyExportDeclaration(node *Node) bool { func IsTypeOnlyImportOrExportDeclaration(node *Node) bool { return IsTypeOnlyImportDeclaration(node) || isTypeOnlyExportDeclaration(node) } + +func GetSourceFileOfModule(module *Symbol) *SourceFile { + declaration := module.ValueDeclaration + if declaration == nil { + declaration = getNonAugmentationDeclaration(module) + } + return GetSourceFileOfNode(declaration) +} + +func getNonAugmentationDeclaration(symbol *Symbol) *Node { + return core.Find(symbol.Declarations, func(d *Node) bool { + return !IsExternalModuleAugmentation(d) && !IsGlobalScopeAugmentation(d) + }) +} + +func IsExternalModuleAugmentation(node *Node) bool { + return IsAmbientModule(node) && IsModuleAugmentationExternal(node) +} diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 4a75710a61..1f318b164b 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2659,8 +2659,8 @@ func (c *Checker) checkAccessorDeclaration(node *ast.Node) { setter := ast.GetDeclarationOfKind(symbol, ast.KindSetAccessor) if getter != nil && setter != nil && c.nodeLinks.Get(getter).flags&NodeCheckFlagsTypeChecked == 0 { c.nodeLinks.Get(getter).flags |= NodeCheckFlagsTypeChecked - getterFlags := getEffectiveModifierFlags(getter) - setterFlags := getEffectiveModifierFlags(setter) + getterFlags := GetEffectiveModifierFlags(getter) + setterFlags := GetEffectiveModifierFlags(setter) if (getterFlags & ast.ModifierFlagsAbstract) != (setterFlags & ast.ModifierFlagsAbstract) { c.error(getter.Name(), diagnostics.Accessors_must_both_be_abstract_or_non_abstract) c.error(setter.Name(), diagnostics.Accessors_must_both_be_abstract_or_non_abstract) @@ -4550,7 +4550,7 @@ func (c *Checker) checkPropertyInitialization(node *ast.Node) { } constructor := ast.FindConstructorDeclaration(node) for _, member := range node.Members() { - if getEffectiveModifierFlags(member)&ast.ModifierFlagsAmbient != 0 { + if GetEffectiveModifierFlags(member)&ast.ModifierFlagsAmbient != 0 { continue } if !ast.IsStatic(member) && c.isPropertyWithoutInitializer(member) { @@ -4791,7 +4791,7 @@ func (c *Checker) checkModuleDeclaration(node *ast.Node) { } } if isAmbientExternalModule { - if isExternalModuleAugmentation(node) { + if ast.IsExternalModuleAugmentation(node) { // body of the augmentation should be checked for consistency only if augmentation was applied to its target (either global scope or module) // otherwise we'll be swamped in cascading errors. // We can detect if augmentation was applied using following rules: @@ -10774,7 +10774,7 @@ func (c *Checker) getControlFlowContainer(node *ast.Node) *ast.Node { func (c *Checker) getFlowTypeOfProperty(reference *ast.Node, prop *ast.Symbol) *Type { initialType := c.undefinedType - if prop != nil && prop.ValueDeclaration != nil && (!c.isAutoTypedProperty(prop) || getEffectiveModifierFlags(prop.ValueDeclaration)&ast.ModifierFlagsAmbient != 0) { + if prop != nil && prop.ValueDeclaration != nil && (!c.isAutoTypedProperty(prop) || GetEffectiveModifierFlags(prop.ValueDeclaration)&ast.ModifierFlagsAmbient != 0) { initialType = c.getTypeOfPropertyInBaseClass(prop) } return c.getFlowTypeOfReferenceEx(reference, c.autoType, initialType, nil, nil) @@ -13935,7 +13935,7 @@ func (c *Checker) isOnlyImportableAsDefault(usage *ast.Node, resolvedModule *ast } var targetFile *ast.SourceFile if resolvedModule != nil { - targetFile = getSourceFileOfModule(resolvedModule) + targetFile = ast.GetSourceFileOfModule(resolvedModule) } return targetFile != nil && (ast.IsJsonSourceFile(targetFile) || tspath.GetDeclarationFileExtension(targetFile.FileName()) == ".d.json.ts") } @@ -29706,3 +29706,12 @@ func (c *Checker) GetEmitResolver(file *ast.SourceFile, skipDiagnostics bool) pr } return c.emitResolver } + +// !!! +func (c *Checker) GetAccessibleSymbolChain( + symbol *ast.Symbol, + enclosingDeclaration *ast.Node, + meaning ast.SymbolFlags, + useOnlyExternalAliasing bool) []*ast.Symbol { + return nil +} diff --git a/internal/checker/emitresolver.go b/internal/checker/emitresolver.go index a40e52f8b3..7923379607 100644 --- a/internal/checker/emitresolver.go +++ b/internal/checker/emitresolver.go @@ -38,7 +38,7 @@ func (r *emitResolver) IsReferencedAliasDeclaration(node *ast.Node) bool { return true } target := aliasLinks.aliasTarget - if target != nil && getEffectiveModifierFlags(node)&ast.ModifierFlagsExport != 0 && + if target != nil && GetEffectiveModifierFlags(node)&ast.ModifierFlagsExport != 0 && c.getSymbolFlags(target)&ast.SymbolFlagsValue != 0 && (c.compilerOptions.ShouldPreserveConstEnums() || !isConstEnumOrConstEnumOnlyModule(target)) { return true diff --git a/internal/checker/relater.go b/internal/checker/relater.go index eb88ce5a61..bf180bff9f 100644 --- a/internal/checker/relater.go +++ b/internal/checker/relater.go @@ -1411,7 +1411,7 @@ func (c *Checker) getTypeParameterModifiers(tp *Type) ast.ModifierFlags { var flags ast.ModifierFlags if tp.symbol != nil { for _, d := range tp.symbol.Declarations { - flags |= getEffectiveModifierFlags(d) + flags |= GetEffectiveModifierFlags(d) } } return flags & (ast.ModifierFlagsIn | ast.ModifierFlagsOut | ast.ModifierFlagsConst) @@ -4435,8 +4435,8 @@ func (r *Relater) constructorVisibilitiesAreCompatible(sourceSignature *Signatur if sourceSignature.declaration == nil || targetSignature.declaration == nil { return true } - sourceAccessibility := getEffectiveModifierFlags(sourceSignature.declaration) & ast.ModifierFlagsNonPublicAccessibilityModifier - targetAccessibility := getEffectiveModifierFlags(targetSignature.declaration) & ast.ModifierFlagsNonPublicAccessibilityModifier + sourceAccessibility := GetEffectiveModifierFlags(sourceSignature.declaration) & ast.ModifierFlagsNonPublicAccessibilityModifier + targetAccessibility := GetEffectiveModifierFlags(targetSignature.declaration) & ast.ModifierFlagsNonPublicAccessibilityModifier // A public, protected and private signature is assignable to a private signature. if targetAccessibility == ast.ModifierFlagsPrivate { return true diff --git a/internal/checker/services.go b/internal/checker/services.go index 5c28bfb6bd..1d8a1d54b7 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -238,3 +238,34 @@ func (c *Checker) getAugmentedPropertiesOfType(t *Type) []*ast.Symbol { func (c *Checker) IsUnion(t *Type) bool { return t.flags&TypeFlagsUnion != 0 } + +func (c *Checker) TryGetMemberInModuleExportsAndProperties(memberName string, moduleSymbol *ast.Symbol) *ast.Symbol { + symbol := c.TryGetMemberInModuleExports(memberName, moduleSymbol) + if symbol != nil { + return symbol + } + + exportEquals := c.resolveExternalModuleSymbol(moduleSymbol, false /*dontResolveAlias*/) + if exportEquals == moduleSymbol { + return nil + } + + t := c.getTypeOfSymbol(exportEquals) + if c.shouldTreatPropertiesOfExternalModuleAsExports(t) { + return c.getPropertyOfType(t, memberName) + } + return nil +} + +func (c *Checker) TryGetMemberInModuleExports(memberName string, moduleSymbol *ast.Symbol) *ast.Symbol { + symbolTable := c.getExportsOfModule(moduleSymbol) + return symbolTable[memberName] +} + +func (c *Checker) shouldTreatPropertiesOfExternalModuleAsExports(resolvedExternalModuleType *Type) bool { + return resolvedExternalModuleType.flags&TypeFlagsPrimitive == 0 || + resolvedExternalModuleType.objectFlags&ObjectFlagsClass != 0 || + // `isArrayOrTupleLikeType` is too expensive to use in this auto-imports hot path. + c.isArrayType(resolvedExternalModuleType) || + isTupleType(resolvedExternalModuleType) +} diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go index f9d912f15f..dd49bef469 100644 --- a/internal/checker/utilities.go +++ b/internal/checker/utilities.go @@ -5,6 +5,7 @@ import ( "slices" "strings" "sync" + "unicode/utf8" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/binder" @@ -89,16 +90,16 @@ func hasDecorators(node *ast.Node) bool { return ast.HasSyntacticModifier(node, ast.ModifierFlagsDecorator) } -func getEffectiveModifierFlags(node *ast.Node) ast.ModifierFlags { +func GetEffectiveModifierFlags(node *ast.Node) ast.ModifierFlags { return node.ModifierFlags() // !!! Handle JSDoc } func getSelectedEffectiveModifierFlags(node *ast.Node, flags ast.ModifierFlags) ast.ModifierFlags { - return getEffectiveModifierFlags(node) & flags + return GetEffectiveModifierFlags(node) & flags } func hasEffectiveModifier(node *ast.Node, flags ast.ModifierFlags) bool { - return getEffectiveModifierFlags(node)&flags != 0 + return GetEffectiveModifierFlags(node)&flags != 0 } func hasEffectiveReadonlyModifier(node *ast.Node) bool { @@ -453,26 +454,8 @@ func isRightSideOfQualifiedNameOrPropertyAccess(node *ast.Node) bool { return false } -func getSourceFileOfModule(module *ast.Symbol) *ast.SourceFile { - declaration := module.ValueDeclaration - if declaration == nil { - declaration = getNonAugmentationDeclaration(module) - } - return ast.GetSourceFileOfNode(declaration) -} - -func getNonAugmentationDeclaration(symbol *ast.Symbol) *ast.Node { - return core.Find(symbol.Declarations, func(d *ast.Node) bool { - return !isExternalModuleAugmentation(d) && !ast.IsGlobalScopeAugmentation(d) - }) -} - -func isExternalModuleAugmentation(node *ast.Node) bool { - return ast.IsAmbientModule(node) && ast.IsModuleAugmentationExternal(node) -} - func isTopLevelInExternalModuleAugmentation(node *ast.Node) bool { - return node != nil && node.Parent != nil && ast.IsModuleBlock(node.Parent) && isExternalModuleAugmentation(node.Parent.Parent) + return node != nil && node.Parent != nil && ast.IsModuleBlock(node.Parent) && ast.IsExternalModuleAugmentation(node.Parent.Parent) } func isSyntacticDefault(node *ast.Node) bool { @@ -2106,3 +2089,9 @@ func SkipAlias(symbol *ast.Symbol, checker *Checker) *ast.Symbol { } return symbol } + +// True if the symbol is for an external module, as opposed to a namespace. +func IsExternalModuleSymbol(moduleSymbol *ast.Symbol) bool { + firstRune, _ := utf8.DecodeRuneInString(moduleSymbol.Name) + return moduleSymbol.Flags&ast.SymbolFlagsModule != 0 && firstRune == '"' +} diff --git a/internal/core/core.go b/internal/core/core.go index ea29e42575..646cb1548f 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -541,3 +541,12 @@ func CheckEachDefined[S any](s []*S, msg string) []*S { } return s } + +func StripQuotes(name string) string { + firstChar, _ := utf8.DecodeRuneInString(name) + lastChar, _ := utf8.DecodeLastRuneInString(name) + if firstChar == lastChar && (firstChar == '\'' || firstChar == '"' || firstChar == '`') { + return name[1 : len(name)-1] + } + return name +} diff --git a/internal/ls/completions.go b/internal/ls/completions.go index c76c831ff6..c22033a700 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -9,6 +9,7 @@ import ( "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" ) func (l *LanguageService) ProvideCompletion(fileName string, position int, context *lsproto.CompletionContext) *lsproto.CompletionList { @@ -67,6 +68,47 @@ var allCommitCharacters = []string{".", ",", ";"} // Commit characters valid at expression positions where we could be inside a parameter list. var noCommaCommitCharacters = []string{".", ";"} +type sortText string + +const ( + SortTextLocalDeclarationPriority sortText = "10" + sortTextLocationPriority sortText = "11" + sortTextOptionalMember sortText = "12" + sortTextMemberDeclaredBySpreadAssignment sortText = "13" + sortTextSuggestedClassMembers sortText = "14" + sortTextGlobalsOrKeywords sortText = "15" + sortTextAutoImportSuggestions sortText = "16" + sortTextClassMemberSnippets sortText = "17" + sortTextJavascriptIdentifiers sortText = "18" +) + +// !!! sort text transformations + +type symbolOriginInfoKind int + +const ( + symbolOriginInfoKindThisType symbolOriginInfoKind = 1 << iota + symbolOriginInfoKindSymbolMember + symbolOriginInfoKindExport + symbolOriginInfoKindPromise + symbolOriginInfoKindNullable + symbolOriginInfoKindResolvedExport + symbolOriginInfoKindTypeOnlyAlias + symbolOriginInfoKindObjectLiteralMethod + symbolOriginInfoKindIgnore + symbolOriginInfoKindComputedPropertyName + + symbolOriginInfoKindSymbolMemberNoExport symbolOriginInfoKind = symbolOriginInfoKindSymbolMember + symbolOriginInfoKindSymbolMemberExport = symbolOriginInfoKindSymbolMember | symbolOriginInfoKindExport +) + +type symbolOriginInfo struct { + kind symbolOriginInfoKind + isDefaultExport bool + isFromPackageJson bool + fileName string +} + func (l *LanguageService) getCompletionsAtPosition( program *compiler.Program, file *ast.SourceFile, @@ -261,9 +303,9 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position completionKind := CompletionKindNone hasUnresolvedAutoImports := false // This also gets mutated in nested-functions after the return - var symbols []*ast.Symbol // !!! symbol: this will not work - var symbolToOriginInfoMap []any // !!! symbol: refactor - var symbolToSortTextMap []any // !!! symbol: refactor + var symbols []*ast.Symbol + var symbolToOriginInfoMap map[ast.SymbolId]symbolOriginInfo + var symbolToSortTextMap map[ast.SymbolId]sortText var importSpecifierResolver any // !!! seenPropertySymbols := make(map[ast.SymbolId]struct{}) isTypeOnlyLocation := insideJsDocTagTypeExpression || insideJsDocImportTag || @@ -274,11 +316,27 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position isContextTokenTypeLocation(contextToken)) var getModuleSpecifierResolutionHost any // !!! + addSymbolOriginInfo := func(symbol *ast.Symbol, insertQuestionDot bool, insertAwait bool) { + symbolId := ast.GetSymbolId(symbol) + if insertAwait && core.AddToSeen(seenPropertySymbols, symbolId, struct{}{}) { + symbolToOriginInfoMap[symbolId] = symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindPromise, insertQuestionDot)} + } else if insertQuestionDot { + symbolToOriginInfoMap[symbolId] = symbolOriginInfo{kind: symbolOriginInfoKindNullable} + } + } + + addSymbolSortInfo := func(symbol *ast.Symbol) { + symbolId := ast.GetSymbolId(symbol) + if isStaticProperty(symbol) { + symbolToSortTextMap[symbolId] = SortTextLocalDeclarationPriority + } + } + addPropertySymbol := func(symbol *ast.Symbol, insertAwait bool, insertQuestionDot bool) { // For a computed property with an accessible name like `Symbol.iterator`, // we'll add a completion for the *name* `Symbol` instead of for the property. // If this is e.g. [Symbol.iterator], add a completion for `Symbol`. - computedPropertyName = core.FirstNonNil(symbol.Declarations, func(decl *ast.Node) *ast.Node { + computedPropertyName := core.FirstNonNil(symbol.Declarations, func(decl *ast.Node) *ast.Node { name := ast.GetNameOfDeclaration(decl) if name != nil && name.Kind == ast.KindComputedPropertyName { return name @@ -286,7 +344,74 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position return nil }) - // !!! HERE + if computedPropertyName != nil { + leftMostName := getLeftMostName(computedPropertyName.Expression()) // The completion is for `Symbol`, not `iterator`. + var nameSymbol *ast.Symbol + if leftMostName != nil { + nameSymbol = typeChecker.GetSymbolAtLocation(leftMostName) + } + // If this is nested like for `namespace N { export const sym = Symbol(); }`, we'll add the completion for `N`. + var firstAccessibleSymbol *ast.Symbol + if nameSymbol != nil { + firstAccessibleSymbol = getFirstSymbolInChain(nameSymbol, contextToken, typeChecker) + } + var firstAccessibleSymbolId ast.SymbolId + if firstAccessibleSymbol != nil { + firstAccessibleSymbolId = ast.GetSymbolId(firstAccessibleSymbol) + } + if firstAccessibleSymbolId != 0 && core.AddToSeen(seenPropertySymbols, firstAccessibleSymbolId, struct{}{}) { + index := len(symbols) + symbols = append(symbols, firstAccessibleSymbol) + moduleSymbol := firstAccessibleSymbol.Parent + if moduleSymbol == nil || + !checker.IsExternalModuleSymbol(moduleSymbol) || + typeChecker.TryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.Name, moduleSymbol) != firstAccessibleSymbol { + symbolToOriginInfoMap[ast.GetSymbolId(symbol)] = symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberNoExport, insertQuestionDot)} + } else { + var fileName string + if tspath.IsExternalModuleNameRelative(core.StripQuotes(moduleSymbol.Name)) { + fileName = ast.GetSourceFileOfModule(moduleSymbol).FileName() + } + if importSpecifierResolver == nil { // !!! verify if this is right, depending on the type of importSpecifierResolver + // !!! + // importSpecifierResolver ||= codefix.createImportSpecifierResolver(sourceFile, program, host, preferences)) + } + // !!! + // const { moduleSpecifier } = importSpecifier.getModuleSpecifierForBestExportInfo( + // [{ + // exportKind: ExportKind.Named, + // moduleFileName: fileName, + // isFromPackageJson: false, + // moduleSymbol, + // symbol: firstAccessibleSymbol, + // targetFlags: skipAlias(firstAccessibleSymbol, typeChecker).flags, + // }], + // position, + // isValidTypeOnlyAliasUseSite(location), + // ) || {}; + // if (moduleSpecifier) { + // const origin: SymbolOriginInfoResolvedExport = { + // kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), + // moduleSymbol, + // isDefaultExport: false, + // symbolName: firstAccessibleSymbol.name, + // exportName: firstAccessibleSymbol.name, + // fileName, + // moduleSpecifier, + // }; + // symbolToOriginInfoMap[index] = origin; + // } + } + } else if _, ok := seenPropertySymbols[firstAccessibleSymbolId]; firstAccessibleSymbolId == 0 || !ok { + symbols = append(symbols, symbol) + addSymbolOriginInfo(symbol, insertQuestionDot, insertAwait) + addSymbolSortInfo(symbol) + } + } else { + symbols = append(symbols, symbol) + addSymbolOriginInfo(symbol, insertQuestionDot, insertAwait) + addSymbolSortInfo(symbol) + } } addTypeProperties := func(t *checker.Type, insertAwait bool, insertQuestionDot bool) { @@ -426,11 +551,14 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position if isRightOfDot || isRightOfQuestionDot { getTypeScriptMemberSymbols() } else if isRightOfOpenTag { - // !!! + // !!! jsx completions } else if isStartingCloseTag { - // !!! + // !!! jsx completions } else { - // !!! + // For JavaScript or TypeScript, if we're not after a dot, then just try to get the + // global symbols in scope. These results should be valid for either language as + // the set of symbols that can be referenced from this location. + // !!! global completions } // !!! adjustments @@ -578,3 +706,49 @@ func getPropertiesForCompletion(t *checker.Type, typeChecker *checker.Checker) [ return core.CheckEachDefined(typeChecker.GetApparentProperties(t), "getApparentProperties() should all be defined.") } } + +// Given 'a.b.c', returns 'a'. +func getLeftMostName(e *ast.Expression) *ast.IdentifierNode { + if ast.IsIdentifier(e) { + return e + } else if ast.IsPropertyAccessExpression(e) { + return getLeftMostName(e.Expression()) + } else { + return nil + } +} + +func getFirstSymbolInChain(symbol *ast.Symbol, enclosingDeclaration *ast.Node, typeChecker *checker.Checker) *ast.Symbol { + chain := typeChecker.GetAccessibleSymbolChain( + symbol, + enclosingDeclaration, + ast.SymbolFlagsAll, /*meaning*/ + false /*useOnlyExternalAliasing*/) + if len(chain) > 0 { + return chain[0] + } + if symbol.Parent != nil { + if isModuleSymbol(symbol.Parent) { + return symbol + } + return getFirstSymbolInChain(symbol.Parent, enclosingDeclaration, typeChecker) + } + return nil +} + +func isModuleSymbol(symbol *ast.Symbol) bool { + return core.Some(symbol.Declarations, func(decl *ast.Declaration) bool { return decl.Kind == ast.KindSourceFile }) +} + +func getNullableSymbolOriginInfoKind(kind symbolOriginInfoKind, insertQuestionDot bool) symbolOriginInfoKind { + if insertQuestionDot { + kind |= symbolOriginInfoKindNullable + } + return kind +} + +func isStaticProperty(symbol *ast.Symbol) bool { + return symbol.ValueDeclaration != nil && + checker.GetEffectiveModifierFlags(symbol.ValueDeclaration)&ast.ModifierFlagsStatic != 0 && + ast.IsClassLike(symbol.ValueDeclaration.Parent) +} From 0c29714daa457f97aae2708ebb5742c0a9c2e0ec Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 2 Apr 2025 14:44:29 -0700 Subject: [PATCH 04/25] update submodule --- _submodules/TypeScript | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_submodules/TypeScript b/_submodules/TypeScript index 52c59dbcbe..0693cc72e6 160000 --- a/_submodules/TypeScript +++ b/_submodules/TypeScript @@ -1 +1 @@ -Subproject commit 52c59dbcbee274e523ef39e6c8be1bd5e110c2f1 +Subproject commit 0693cc72e6ddb6c7468a14e05888efa2f43ac7b0 From 601c4ea2d3513387c796d34ed3ee75491ed1b74e Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 9 Apr 2025 15:16:50 -0700 Subject: [PATCH 05/25] more completions --- internal/ast/ast.go | 13 + internal/ast/nodeflags.go | 2 + internal/ast/utilities.go | 61 ++ internal/astnav/tokens.go | 11 + internal/checker/checker.go | 61 +- internal/checker/flow.go | 12 +- internal/checker/inference.go | 10 +- internal/checker/relater.go | 10 +- internal/checker/services.go | 156 ++++- internal/checker/types.go | 16 + internal/checker/utilities.go | 27 +- internal/core/core.go | 6 +- internal/ls/completions.go | 1157 ++++++++++++++++++++++++++++++++- internal/ls/symbol_display.go | 260 ++++++++ internal/ls/types.go | 23 + internal/ls/utilities.go | 144 +++- internal/parser/parser.go | 2 +- internal/parser/utilities.go | 9 - internal/scanner/scanner.go | 38 +- internal/scanner/utilities.go | 4 +- 20 files changed, 1892 insertions(+), 130 deletions(-) create mode 100644 internal/ls/symbol_display.go diff --git a/internal/ast/ast.go b/internal/ast/ast.go index fc849f2b1e..b2d956b94b 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -1621,6 +1621,7 @@ type ( CatchClauseNode = Node CaseBlockNode = Node CaseOrDefaultClauseNode = Node + CaseClauseNode = Node VariableDeclarationNode = Node VariableDeclarationListNode = Node BindingElementNode = Node @@ -7897,6 +7898,10 @@ func (node *JsxElement) computeSubtreeFacts() SubtreeFacts { SubtreeContainsJsx } +func IsJsxElement(node *Node) bool { + return node.Kind == KindJsxElement +} + // JsxAttributes type JsxAttributes struct { ExpressionBase @@ -8126,6 +8131,10 @@ func (node *JsxFragment) computeSubtreeFacts() SubtreeFacts { SubtreeContainsJsx } +func IsJsxFragment(node *Node) bool { + return node.Kind == KindJsxFragment +} + /// The opening element of a <>... JsxFragment type JsxOpeningFragment struct { @@ -8330,6 +8339,10 @@ func (node *JsxExpression) computeSubtreeFacts() SubtreeFacts { return propagateSubtreeFacts(node.Expression) | SubtreeContainsJsx } +func IsJsxExpression(node *Node) bool { + return node.Kind == KindJsxExpression +} + // JsxText type JsxText struct { diff --git a/internal/ast/nodeflags.go b/internal/ast/nodeflags.go index 2feb4f2a52..5b6441ed58 100644 --- a/internal/ast/nodeflags.go +++ b/internal/ast/nodeflags.go @@ -43,6 +43,8 @@ const ( NodeFlagsJsonFile NodeFlags = 1 << 25 // If node was parsed in a Json NodeFlagsDeprecated NodeFlags = 1 << 26 // If has '@deprecated' JSDoc tag + NodeFlagsSkipDirectInference NodeFlags = 1 << 27 // If the node should skip direct type inference. + NodeFlagsBlockScoped = NodeFlagsLet | NodeFlagsConst | NodeFlagsUsing NodeFlagsConstant = NodeFlagsConst | NodeFlagsUsing NodeFlagsAwaitUsing = NodeFlagsConst | NodeFlagsUsing // Variable declaration (NOTE: on a single node these flags would otherwise be mutually exclusive) diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 4cb484d8d6..b04936decf 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -911,6 +911,13 @@ const ( FindAncestorQuit ) +func ToFindAncestorResult(b bool) FindAncestorResult { + if b { + return FindAncestorTrue + } + return FindAncestorFalse +} + // Walks up the parents of a node to find the ancestor that matches the callback func FindAncestorOrQuit(node *Node, callback func(*Node) FindAncestorResult) *Node { for node != nil { @@ -2643,3 +2650,57 @@ func getNonAugmentationDeclaration(symbol *Symbol) *Node { func IsExternalModuleAugmentation(node *Node) bool { return IsAmbientModule(node) && IsModuleAugmentationExternal(node) } + +func GetClassLikeDeclarationOfSymbol(symbol *Symbol) *Node { + return core.Find(symbol.Declarations, IsClassLike) +} + +func GetLanguageVariant(scriptKind core.ScriptKind) core.LanguageVariant { + switch scriptKind { + case core.ScriptKindTSX, core.ScriptKindJSX, core.ScriptKindJS, core.ScriptKindJSON: + // .tsx and .jsx files are treated as jsx language variant. + return core.LanguageVariantJSX + } + return core.LanguageVariantStandard +} + +// !!! Shared? +func IsCallLikeExpression(node *Node) bool { + switch node.Kind { + case KindJsxOpeningElement, KindJsxSelfClosingElement, KindCallExpression, KindNewExpression, + KindTaggedTemplateExpression, KindDecorator: + return true + } + return false +} + +// !!! Shared? +func IsCallLikeOrFunctionLikeExpression(node *Node) bool { + return IsCallLikeExpression(node) || IsFunctionExpressionOrArrowFunction(node) +} + +func NodeHasKind(node *Node, kind Kind) bool { + if node == nil { + return false + } + return node.Kind == kind +} + +func IsContextualKeyword(token Kind) bool { + return KindFirstContextualKeyword <= token && token <= KindLastContextualKeyword +} + +func IsThisInTypeQuery(node *Node) bool { + if !IsThisIdentifier(node) { + return false + } + for IsQualifiedName(node.Parent) && node.Parent.AsQualifiedName().Left == node { + node = node.Parent + } + return node.Parent.Kind == KindTypeQuery +} + +// Gets whether a bound `VariableDeclaration` or `VariableDeclarationList` is part of a `let` declaration. +func IsLet(node *Node) bool { + return GetCombinedNodeFlags(node)&NodeFlagsBlockScoped == NodeFlagsLet +} diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go index f17268d891..e7f7a76c7d 100644 --- a/internal/astnav/tokens.go +++ b/internal/astnav/tokens.go @@ -247,6 +247,7 @@ func visitEachChildAndJSDoc(node *ast.Node, sourceFile *ast.SourceFile, visitor node.VisitEachChild(visitor) } +// !!! Shared (placeholder) // Finds the rightmost token satisfying `token.end <= position`, func FindPrecedingToken(sourceFile *ast.SourceFile, position int) *ast.Node { var next *ast.Node @@ -374,3 +375,13 @@ func FindPrecedingToken(sourceFile *ast.SourceFile, position int) *ast.Node { next = nil } } + +// !!! +func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *ast.Node) *ast.Node { + return FindPrecedingToken(sourceFile, position) +} + +// !!! +func FindNextToken(previousToken *ast.Node, parent *ast.Node, file *ast.SourceFile) *ast.Node { + return nil +} diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 0e86f7f7be..2eef959817 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -817,6 +817,7 @@ type Checker struct { markNodeAssignments func(*ast.Node) bool emitResolver *emitResolver emitResolverOnce sync.Once + skipDirectInferenceNodes core.Set[*ast.Node] } func NewChecker(program Program) *Checker { @@ -1147,7 +1148,7 @@ func (c *Checker) getGlobalSymbol(name string, meaning ast.SymbolFlags, diagnost func (c *Checker) initializeClosures() { c.isPrimitiveOrObjectOrEmptyType = func(t *Type) bool { - return t.flags&(TypeFlagsPrimitive|TypeFlagsNonPrimitive) != 0 || c.isEmptyAnonymousObjectType(t) + return t.flags&(TypeFlagsPrimitive|TypeFlagsNonPrimitive) != 0 || c.IsEmptyAnonymousObjectType(t) } c.containsMissingType = func(t *Type) bool { return t == c.missingType || t.flags&TypeFlagsUnion != 0 && t.Types()[0] == c.missingType @@ -4100,7 +4101,7 @@ func (c *Checker) checkBaseTypeAccessibility(t *Type, node *ast.Node) { if len(signatures) != 0 { declaration := signatures[0].declaration if declaration != nil && hasEffectiveModifier(declaration, ast.ModifierFlagsPrivate) { - typeClassDeclaration := getClassLikeDeclarationOfSymbol(t.symbol) + typeClassDeclaration := ast.GetClassLikeDeclarationOfSymbol(t.symbol) if !c.isNodeWithinClass(node, typeClassDeclaration) { c.error(node, diagnostics.Cannot_extend_a_class_0_Class_constructor_is_marked_as_private, c.getFullyQualifiedName(t.symbol, nil)) } @@ -4193,7 +4194,7 @@ basePropertyCheck: // It is an error to inherit an abstract member without implementing it or being declared abstract. // If there is no declaration for the derived class (as in the case of class expressions), // then the class cannot be declared abstract. - derivedClassDecl := getClassLikeDeclarationOfSymbol(t.symbol) + derivedClassDecl := ast.GetClassLikeDeclarationOfSymbol(t.symbol) if derivedClassDecl == nil || !ast.HasSyntacticModifier(derivedClassDecl, ast.ModifierFlagsAbstract) { // Searches other base types for a declaration that would satisfy the inherited abstract member. // (The class may have more than one base type via declaration merging with an interface with the @@ -4254,7 +4255,7 @@ basePropertyCheck: if uninitialized != nil && derived.Flags&ast.SymbolFlagsTransient == 0 && baseDeclarationFlags&ast.ModifierFlagsAbstract == 0 && derivedDeclarationFlags&ast.ModifierFlagsAbstract == 0 && !core.Some(derived.Declarations, func(d *ast.Node) bool { return d.Flags&ast.NodeFlagsAmbient != 0 }) { - constructor := ast.FindConstructorDeclaration(getClassLikeDeclarationOfSymbol(t.symbol)) + constructor := ast.FindConstructorDeclaration(ast.GetClassLikeDeclarationOfSymbol(t.symbol)) propName := uninitialized.Name() if isExclamationToken(uninitialized.AsPropertyDeclaration().PostfixToken) || constructor == nil || !ast.IsIdentifier(propName) || !c.strictNullChecks || !c.isPropertyInitializedInConstructor(propName, t, constructor) { errorMessage := diagnostics.Property_0_will_overwrite_the_base_property_in_1_If_this_is_intentional_add_an_initializer_Otherwise_add_a_declare_modifier_or_remove_the_redundant_declaration @@ -8102,7 +8103,7 @@ func (c *Checker) resolveNewExpression(node *ast.Node, candidatesOutArray *[]*Si return c.resolveErrorCall(node) } if expressionType.symbol != nil { - valueDecl := getClassLikeDeclarationOfSymbol(expressionType.symbol) + valueDecl := ast.GetClassLikeDeclarationOfSymbol(expressionType.symbol) if valueDecl != nil && hasEffectiveModifier(valueDecl, ast.ModifierFlagsAbstract) { c.error(node, diagnostics.Cannot_create_an_instance_of_an_abstract_class) return c.resolveErrorCall(node) @@ -8141,7 +8142,7 @@ func (c *Checker) isConstructorAccessible(node *ast.Node, signature *Signature) if modifiers == 0 || !ast.IsConstructorDeclaration(declaration) { return true } - declaringClassDeclaration := getClassLikeDeclarationOfSymbol(declaration.Parent.Symbol()) + declaringClassDeclaration := ast.GetClassLikeDeclarationOfSymbol(declaration.Parent.Symbol()) declaringClass := c.getDeclaredTypeOfSymbol(declaration.Parent.Symbol()) // A private or protected constructor can only be instantiated within its own class (or a subclass, for protected) if !c.isNodeWithinClass(node, declaringClassDeclaration) { @@ -10389,7 +10390,7 @@ func (c *Checker) checkJsxAttributes(node *ast.Node, checkMode CheckMode) *Type } func (c *Checker) checkIdentifier(node *ast.Node, checkMode CheckMode) *Type { - if isThisInTypeQuery(node) { + if ast.IsThisInTypeQuery(node) { return c.checkThisExpression(node) } symbol := c.getResolvedSymbol(node) @@ -10694,7 +10695,7 @@ func (c *Checker) checkPropertyAccessExpressionOrQualifiedName(node *ast.Node, l if c.compilerOptions.NoPropertyAccessFromIndexSignature == core.TSTrue && ast.IsPropertyAccessExpression(node) { c.error(right, diagnostics.Property_0_comes_from_an_index_signature_so_it_must_be_accessed_with_0, right.Text()) } - if indexInfo.declaration != nil && c.isDeprecatedDeclaration(indexInfo.declaration) { + if indexInfo.declaration != nil && c.IsDeprecatedDeclaration(indexInfo.declaration) { c.addDeprecatedSuggestion(right, []*ast.Node{indexInfo.declaration}, right.Text()) } } else { @@ -11010,11 +11011,11 @@ func (c *Checker) isUncalledFunctionReference(node *ast.Node, symbol *ast.Symbol if parent == nil { parent = node.Parent } - if isCallLikeExpression(parent) { + if ast.IsCallLikeExpression(parent) { return isCallOrNewExpression(parent) && ast.IsIdentifier(node) && c.hasMatchingArgument(parent, node) } return core.Every(symbol.Declarations, func(d *ast.Node) bool { - return !ast.IsFunctionLike(d) || c.isDeprecatedDeclaration(d) + return !ast.IsFunctionLike(d) || c.IsDeprecatedDeclaration(d) }) } return true @@ -11146,7 +11147,7 @@ func (c *Checker) checkPropertyAccessibilityAtLocation(location *ast.Node, isSup if flags&ast.ModifierFlagsAbstract != 0 && c.symbolHasNonMethodDeclaration(prop) && (isThisProperty(location) || isThisInitializedObjectBindingExpression(location) || ast.IsObjectBindingPattern(location.Parent) && isThisInitializedDeclaration(location.Parent.Parent)) { - declaringClassDeclaration := getClassLikeDeclarationOfSymbol(c.getParentOfSymbol(prop)) + declaringClassDeclaration := ast.GetClassLikeDeclarationOfSymbol(c.getParentOfSymbol(prop)) if declaringClassDeclaration != nil && c.isNodeUsedDuringClassInitialization(location) { if errorNode != nil { c.error(errorNode, diagnostics.Abstract_property_0_in_class_1_cannot_be_accessed_in_the_constructor, c.symbolToString(prop), declaringClassDeclaration.Name().Text()) @@ -11161,7 +11162,7 @@ func (c *Checker) checkPropertyAccessibilityAtLocation(location *ast.Node, isSup // Property is known to be private or protected at this point // Private property is accessible if the property is within the declaring class if flags&ast.ModifierFlagsPrivate != 0 { - declaringClassDeclaration := getClassLikeDeclarationOfSymbol(c.getParentOfSymbol(prop)) + declaringClassDeclaration := ast.GetClassLikeDeclarationOfSymbol(c.getParentOfSymbol(prop)) if !c.isNodeWithinClass(location, declaringClassDeclaration) { if errorNode != nil { c.error(errorNode, diagnostics.Property_0_is_private_and_only_accessible_within_class_1, c.symbolToString(prop), c.TypeToString(c.getDeclaringClass(prop))) @@ -12358,7 +12359,7 @@ func (c *Checker) checkInExpression(left *ast.Expression, right *ast.Expression, func (c *Checker) hasEmptyObjectIntersection(t *Type) bool { return someType(t, func(t *Type) bool { - return t == c.unknownEmptyObjectType || t.flags&TypeFlagsIntersection != 0 && c.isEmptyAnonymousObjectType(c.getBaseConstraintOrType(t)) + return t == c.unknownEmptyObjectType || t.flags&TypeFlagsIntersection != 0 && c.IsEmptyAnonymousObjectType(c.getBaseConstraintOrType(t)) }) } @@ -13169,7 +13170,7 @@ func (c *Checker) addErrorOrSuggestion(isError bool, diagnostic *ast.Diagnostic) } } -func (c *Checker) isDeprecatedDeclaration(declaration *ast.Node) bool { +func (c *Checker) IsDeprecatedDeclaration(declaration *ast.Node) bool { return c.getCombinedNodeFlagsCached(declaration)&ast.NodeFlagsDeprecated != 0 } @@ -13194,12 +13195,12 @@ func (c *Checker) isDeprecatedSymbol(symbol *ast.Symbol) bool { parentSymbol := c.getParentOfSymbol(symbol) if parentSymbol != nil && len(symbol.Declarations) > 1 { if parentSymbol.Flags&ast.SymbolFlagsInterface != 0 { - return core.Some(symbol.Declarations, c.isDeprecatedDeclaration) + return core.Some(symbol.Declarations, c.IsDeprecatedDeclaration) } else { - return core.Every(symbol.Declarations, c.isDeprecatedDeclaration) + return core.Every(symbol.Declarations, c.IsDeprecatedDeclaration) } } - return symbol.ValueDeclaration != nil && c.isDeprecatedDeclaration(symbol.ValueDeclaration) || len(symbol.Declarations) != 0 && core.Every(symbol.Declarations, c.isDeprecatedDeclaration) + return symbol.ValueDeclaration != nil && c.IsDeprecatedDeclaration(symbol.ValueDeclaration) || len(symbol.Declarations) != 0 && core.Every(symbol.Declarations, c.IsDeprecatedDeclaration) } func (c *Checker) hasParseDiagnostics(sourceFile *ast.SourceFile) bool { @@ -17676,7 +17677,7 @@ func (c *Checker) resolveBaseTypesOfClass(t *Type) { } func getBaseTypeNodeOfClass(t *Type) *ast.Node { - decl := getClassLikeDeclarationOfSymbol(t.symbol) + decl := ast.GetClassLikeDeclarationOfSymbol(t.symbol) if decl != nil { return ast.GetExtendsHeritageClauseElement(decl) } @@ -18147,7 +18148,7 @@ func (c *Checker) getObjectLiteralIndexInfo(isReadonly bool, properties []*ast.S } func (c *Checker) isSymbolWithSymbolName(symbol *ast.Symbol) bool { - if isKnownSymbol(symbol) { + if IsKnownSymbol(symbol) { return true } if len(symbol.Declarations) != 0 { @@ -19217,7 +19218,7 @@ func isThislessTypeParameter(node *ast.Node) bool { func (c *Checker) getDefaultConstructSignatures(classType *Type) []*Signature { baseConstructorType := c.getBaseConstructorTypeOfClass(classType) baseSignatures := c.getSignaturesOfType(baseConstructorType, SignatureKindConstruct) - declaration := getClassLikeDeclarationOfSymbol(classType.symbol) + declaration := ast.GetClassLikeDeclarationOfSymbol(classType.symbol) isAbstract := declaration != nil && ast.HasSyntacticModifier(declaration, ast.ModifierFlagsAbstract) if len(baseSignatures) == 0 { flags := core.IfElse(isAbstract, SignatureFlagsConstruct|SignatureFlagsAbstract, SignatureFlagsConstruct) @@ -24423,7 +24424,7 @@ func (c *Checker) addTypeToIntersection(typeSet *orderedSet[*Type], includes Typ if flags&TypeFlagsIntersection != 0 { return c.addTypesToIntersection(typeSet, includes, t.Types()) } - if c.isEmptyAnonymousObjectType(t) { + if c.IsEmptyAnonymousObjectType(t) { if includes&TypeFlagsIncludesEmptyObject == 0 { includes |= TypeFlagsIncludesEmptyObject typeSet.add(t) @@ -24465,7 +24466,7 @@ func (c *Checker) removeRedundantSupertypes(types []*Type, includes TypeFlags) [ t.flags&TypeFlagsBigInt != 0 && includes&TypeFlagsBigIntLiteral != 0 || t.flags&TypeFlagsESSymbol != 0 && includes&TypeFlagsUniqueESSymbol != 0 || t.flags&TypeFlagsVoid != 0 && includes&TypeFlagsUndefined != 0 || - c.isEmptyAnonymousObjectType(t) && includes&TypeFlagsDefinitelyNonNullable != 0 + c.IsEmptyAnonymousObjectType(t) && includes&TypeFlagsDefinitelyNonNullable != 0 if remove { types = slices.Delete(types, i, i+1) } @@ -24631,7 +24632,7 @@ func (c *Checker) filterTypes(types []*Type, predicate func(*Type) bool) { } } -func (c *Checker) isEmptyAnonymousObjectType(t *Type) bool { +func (c *Checker) IsEmptyAnonymousObjectType(t *Type) bool { return t.objectFlags&ObjectFlagsAnonymous != 0 && (t.objectFlags&ObjectFlagsMembersResolved != 0 && c.isEmptyResolvedType(t.AsStructuredType()) || t.symbol != nil && t.symbol.Flags&ast.SymbolFlagsTypeLiteral != 0 && len(c.getMembersOfSymbol(t.symbol)) == 0) } @@ -24876,7 +24877,7 @@ func (c *Checker) getLiteralTypeFromProperty(prop *ast.Symbol, include TypeFlags if name != nil { t = c.getLiteralTypeFromPropertyName(name) } - if t == nil && !isKnownSymbol(prop) { + if t == nil && !IsKnownSymbol(prop) { t = c.getStringLiteralType(ast.SymbolName(prop)) } } @@ -24953,7 +24954,7 @@ func (c *Checker) shouldDeferIndexType(t *Type, indexFlags IndexFlags) bool { c.isGenericTupleType(t) || c.isGenericMappedType(t) && (!c.hasDistributiveNameType(t) || c.getMappedTypeNameTypeKind(t) == MappedTypeNameTypeKindRemapping) || t.flags&TypeFlagsUnion != 0 && indexFlags&IndexFlagsNoReducibleCheck == 0 && c.isGenericReducibleType(t) || - t.flags&TypeFlagsIntersection != 0 && c.maybeTypeOfKind(t, TypeFlagsInstantiable) && core.Some(t.Types(), c.isEmptyAnonymousObjectType) + t.flags&TypeFlagsIntersection != 0 && c.maybeTypeOfKind(t, TypeFlagsInstantiable) && core.Some(t.Types(), c.IsEmptyAnonymousObjectType) } // Ordinarily we reduce a keyof M, where M is a mapped type { [P in K as N

]: X }, to simply N. This however presumes @@ -25528,7 +25529,7 @@ func (c *Checker) isNoInferTargetType(t *Type) bool { // examine all object type members. return t.flags&TypeFlagsUnionOrIntersection != 0 && core.Some(t.AsUnionOrIntersectionType().types, c.isNoInferTargetType) || t.flags&TypeFlagsSubstitution != 0 && !c.isNoInferType(t) && c.isNoInferTargetType(t.AsSubstitutionType().baseType) || - t.flags&TypeFlagsObject != 0 && !c.isEmptyAnonymousObjectType(t) || + t.flags&TypeFlagsObject != 0 && !c.IsEmptyAnonymousObjectType(t) || t.flags&(TypeFlagsInstantiable & ^TypeFlagsSubstitution) != 0 && !c.isPatternLiteralType(t) } @@ -25932,7 +25933,7 @@ func (c *Checker) isUnknownLikeUnionType(t *Type) bool { if t.objectFlags&ObjectFlagsIsUnknownLikeUnionComputed == 0 { t.objectFlags |= ObjectFlagsIsUnknownLikeUnionComputed types := t.Types() - if len(types) >= 3 && types[0].flags&TypeFlagsUndefined != 0 && types[1].flags&TypeFlagsNull != 0 && core.Some(types, c.isEmptyAnonymousObjectType) { + if len(types) >= 3 && types[0].flags&TypeFlagsUndefined != 0 && types[1].flags&TypeFlagsNull != 0 && core.Some(types, c.IsEmptyAnonymousObjectType) { t.objectFlags |= ObjectFlagsIsUnknownLikeUnion } } @@ -26147,7 +26148,7 @@ func (c *Checker) shouldNormalizeIntersection(t *Type) bool { hasNullableOrEmpty := false for _, t := range t.Types() { hasInstantiable = hasInstantiable || t.flags&TypeFlagsInstantiable != 0 - hasNullableOrEmpty = hasNullableOrEmpty || t.flags&TypeFlagsNullable != 0 || c.isEmptyAnonymousObjectType(t) + hasNullableOrEmpty = hasNullableOrEmpty || t.flags&TypeFlagsNullable != 0 || c.IsEmptyAnonymousObjectType(t) if hasInstantiable && hasNullableOrEmpty { return true } @@ -26406,7 +26407,7 @@ func isInternalModuleImportEqualsDeclaration(node *ast.Node) bool { func (c *Checker) markIdentifierAliasReferenced(location *ast.IdentifierNode) { symbol := c.getResolvedSymbol(location) - if symbol != nil && symbol != c.argumentsSymbol && symbol != c.unknownSymbol && !isThisInTypeQuery(location) { + if symbol != nil && symbol != c.argumentsSymbol && symbol != c.unknownSymbol && !ast.IsThisInTypeQuery(location) { c.markAliasReferenced(symbol, location) } } @@ -29321,7 +29322,7 @@ func (c *Checker) getSymbolAtLocation(node *ast.Node, ignoreErrors bool) *ast.Sy switch node.Kind { case ast.KindIdentifier, ast.KindPrivateIdentifier, ast.KindPropertyAccessExpression, ast.KindQualifiedName: - if !isThisInTypeQuery(node) { + if !ast.IsThisInTypeQuery(node) { return c.getSymbolOfNameOrPropertyAccessExpression(node) } fallthrough diff --git a/internal/checker/flow.go b/internal/checker/flow.go index 9820f82662..57ecd1838a 100644 --- a/internal/checker/flow.go +++ b/internal/checker/flow.go @@ -566,8 +566,8 @@ func (c *Checker) narrowTypeByEquality(t *Type, operator ast.Kind, value *ast.No return c.getAdjustedTypeWithFacts(t, facts) } if assumeTrue { - if !doubleEquals && (t.flags&TypeFlagsUnknown != 0 || someType(t, c.isEmptyAnonymousObjectType)) { - if valueType.flags&(TypeFlagsPrimitive|TypeFlagsNonPrimitive) != 0 || c.isEmptyAnonymousObjectType(valueType) { + if !doubleEquals && (t.flags&TypeFlagsUnknown != 0 || someType(t, c.IsEmptyAnonymousObjectType)) { + if valueType.flags&(TypeFlagsPrimitive|TypeFlagsNonPrimitive) != 0 || c.IsEmptyAnonymousObjectType(valueType) { return valueType } if valueType.flags&TypeFlagsObject != 0 { @@ -812,7 +812,7 @@ func (c *Checker) narrowTypeByInstanceof(f *FlowState, t *Type, expr *ast.Binary instanceType := c.mapType(rightType, c.getInstanceType) // Don't narrow from `any` if the target type is exactly `Object` or `Function`, and narrow // in the false branch only if the target is a non-empty object type. - if IsTypeAny(t) && (instanceType == c.globalObjectType || instanceType == c.globalFunctionType) || !assumeTrue && !(instanceType.flags&TypeFlagsObject != 0 && !c.isEmptyAnonymousObjectType(instanceType)) { + if IsTypeAny(t) && (instanceType == c.globalObjectType || instanceType == c.globalFunctionType) || !assumeTrue && !(instanceType.flags&TypeFlagsObject != 0 && !c.IsEmptyAnonymousObjectType(instanceType)) { return t } return c.getNarrowedType(t, instanceType, assumeTrue, true /*checkDerived*/) @@ -1563,7 +1563,7 @@ func (c *Checker) isMatchingReference(source *ast.Node, target *ast.Node) bool { case ast.KindMetaProperty: return ast.IsMetaProperty(target) && source.AsMetaProperty().KeywordToken == target.AsMetaProperty().KeywordToken && source.Name().Text() == target.Name().Text() case ast.KindIdentifier, ast.KindPrivateIdentifier: - if isThisInTypeQuery(source) { + if ast.IsThisInTypeQuery(source) { return target.Kind == ast.KindThisKeyword } return ast.IsIdentifier(target) && c.getResolvedSymbol(source) == c.getResolvedSymbol(target) || @@ -1619,7 +1619,7 @@ func (c *Checker) getFlowReferenceKey(f *FlowState) string { func (c *Checker) writeFlowCacheKey(b *KeyBuilder, node *ast.Node, declaredType *Type, initialType *Type, flowContainer *ast.Node) bool { switch node.Kind { case ast.KindIdentifier: - if !isThisInTypeQuery(node) { + if !ast.IsThisInTypeQuery(node) { symbol := c.getResolvedSymbol(node) if symbol == c.unknownSymbol { return false @@ -1778,7 +1778,7 @@ func (c *Checker) isConstantReference(node *ast.Node) bool { case ast.KindThisKeyword: return true case ast.KindIdentifier: - if !isThisInTypeQuery(node) { + if !ast.IsThisInTypeQuery(node) { symbol := c.getResolvedSymbol(node) return c.isConstantVariable(symbol) || c.isParameterOrMutableLocalVariable(symbol) && !c.isSymbolAssigned(symbol) || symbol.ValueDeclaration != nil && ast.IsFunctionExpression(symbol.ValueDeclaration) } diff --git a/internal/checker/inference.go b/internal/checker/inference.go index 7fb543263a..80e465aada 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -759,7 +759,7 @@ func (c *Checker) inferFromProperties(n *InferenceState, source *Type, target *T properties := c.getPropertiesOfObjectType(target) for _, targetProp := range properties { sourceProp := c.getPropertyOfType(source, targetProp.Name) - if sourceProp != nil && !core.Some(sourceProp.Declarations, c.hasSkipDirectInferenceFlag) { + if sourceProp != nil && !core.Some(sourceProp.Declarations, c.isSkipDirectInferenceNode) { c.inferFromTypes(n, c.removeMissingType(c.getTypeOfSymbol(sourceProp), sourceProp.Flags&ast.SymbolFlagsOptional != 0), c.removeMissingType(c.getTypeOfSymbol(targetProp), targetProp.Flags&ast.SymbolFlagsOptional != 0)) } } @@ -1514,13 +1514,11 @@ func (c *Checker) literalTypesWithSameBaseType(types []*Type) bool { } func (c *Checker) isFromInferenceBlockedSource(t *Type) bool { - return t.symbol != nil && core.Some(t.symbol.Declarations, c.hasSkipDirectInferenceFlag) + return t.symbol != nil && core.Some(t.symbol.Declarations, c.isSkipDirectInferenceNode) } -func (c *Checker) hasSkipDirectInferenceFlag(node *ast.Node) bool { - // !!! Make this a new NodeFlag - // return c.getNodeLinks(node).skipDirectInference - return false +func (c *Checker) isSkipDirectInferenceNode(node *ast.Node) bool { + return c.skipDirectInferenceNodes.Has(node) } // Returns `true` if `type` has the shape `[T[0]]` where `T` is `typeParameter` diff --git a/internal/checker/relater.go b/internal/checker/relater.go index a2ede4a5d6..72042caae5 100644 --- a/internal/checker/relater.go +++ b/internal/checker/relater.go @@ -256,7 +256,7 @@ func (c *Checker) isSimpleTypeRelatedTo(source *Type, target *Type, relation *Re if s&TypeFlagsNull != 0 && (!c.strictNullChecks && t&TypeFlagsUnionOrIntersection == 0 || t&TypeFlagsNull != 0) { return true } - if s&TypeFlagsObject != 0 && t&TypeFlagsNonPrimitive != 0 && !(relation == c.strictSubtypeRelation && c.isEmptyAnonymousObjectType(source) && source.objectFlags&ObjectFlagsFreshLiteral == 0) { + if s&TypeFlagsObject != 0 && t&TypeFlagsNonPrimitive != 0 && !(relation == c.strictSubtypeRelation && c.IsEmptyAnonymousObjectType(source) && source.objectFlags&ObjectFlagsFreshLiteral == 0) { return true } if relation == c.assignableRelation || relation == c.comparableRelation { @@ -2266,7 +2266,7 @@ func (c *Checker) getEffectiveConstraintOfIntersection(types []*Type, targetIsUn constraints = append(constraints, t) } } - } else if t.flags&TypeFlagsDisjointDomains != 0 || c.isEmptyAnonymousObjectType(t) { + } else if t.flags&TypeFlagsDisjointDomains != 0 || c.IsEmptyAnonymousObjectType(t) { hasDisjointDomainType = true } } @@ -2277,7 +2277,7 @@ func (c *Checker) getEffectiveConstraintOfIntersection(types []*Type, targetIsUn // We add any types belong to one of the disjoint domains because they might cause the final // intersection operation to reduce the union constraints. for _, t := range types { - if t.flags&TypeFlagsDisjointDomains != 0 || c.isEmptyAnonymousObjectType(t) { + if t.flags&TypeFlagsDisjointDomains != 0 || c.IsEmptyAnonymousObjectType(t) { constraints = append(constraints, t) } } @@ -4894,10 +4894,10 @@ func (c *Checker) isTypeDerivedFrom(source *Type, target *Type) bool { constraint = c.unknownType } return c.isTypeDerivedFrom(constraint, target) - case c.isEmptyAnonymousObjectType(target): + case c.IsEmptyAnonymousObjectType(target): return source.flags&(TypeFlagsObject|TypeFlagsNonPrimitive) != 0 case target == c.globalObjectType: - return source.flags&(TypeFlagsObject|TypeFlagsNonPrimitive) != 0 && !c.isEmptyAnonymousObjectType(source) + return source.flags&(TypeFlagsObject|TypeFlagsNonPrimitive) != 0 && !c.IsEmptyAnonymousObjectType(source) case target == c.globalFunctionType: return source.flags&TypeFlagsObject != 0 && c.isFunctionObjectType(source) default: diff --git a/internal/checker/services.go b/internal/checker/services.go index 1d8a1d54b7..34bd4c25e2 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -5,6 +5,7 @@ import ( "slices" "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" ) func (c *Checker) GetSymbolsInScope(location *ast.Node, meaning ast.SymbolFlags) []*ast.Symbol { @@ -23,7 +24,7 @@ func (c *Checker) getSymbolsInScope(location *ast.Node, meaning ast.SymbolFlags) // Copy the given symbol into symbol tables if the symbol has the given meaning // and it doesn't already exists in the symbol table. copySymbol := func(symbol *ast.Symbol, meaning ast.SymbolFlags) { - if getCombinedLocalAndExportSymbolFlags(symbol)&meaning != 0 { + if GetCombinedLocalAndExportSymbolFlags(symbol)&meaning != 0 { id := symbol.Name // We will copy all symbol regardless of its reserved name because // symbolsToArray will check whether the key is a reserved name and @@ -190,6 +191,14 @@ func (c *Checker) IsUnknownSymbol(symbol *ast.Symbol) bool { return symbol == c.unknownSymbol } +func (c *Checker) IsUndefinedSymbol(symbol *ast.Symbol) bool { + return symbol == c.undefinedSymbol +} + +func (c *Checker) IsArgumentsSymbol(symbol *ast.Symbol) bool { + return symbol == c.argumentsSymbol +} + // Originally from services.ts func (c *Checker) GetNonOptionalType(t *Type) *Type { return c.removeOptionalTypeMarker(t) @@ -235,10 +244,34 @@ func (c *Checker) getAugmentedPropertiesOfType(t *Type) []*ast.Symbol { return c.getNamedMembers(propsByName) } -func (c *Checker) IsUnion(t *Type) bool { +func IsUnion(t *Type) bool { return t.flags&TypeFlagsUnion != 0 } +func IsStringLiteral(t *Type) bool { + return t.flags&TypeFlagsStringLiteral != 0 +} + +func IsNumberLiteral(t *Type) bool { + return t.flags&TypeFlagsNumberLiteral != 0 +} + +func IsBigIntLiteral(t *Type) bool { + return t.flags&TypeFlagsBigIntLiteral != 0 +} + +func IsEnumLiteral(t *Type) bool { + return t.flags&TypeFlagsEnumLiteral != 0 +} + +func IsBooleanLike(t *Type) bool { + return t.flags&TypeFlagsBooleanLike != 0 +} + +func IsStringLike(t *Type) bool { + return t.flags&TypeFlagsStringLike != 0 +} + func (c *Checker) TryGetMemberInModuleExportsAndProperties(memberName string, moduleSymbol *ast.Symbol) *ast.Symbol { symbol := c.TryGetMemberInModuleExports(memberName, moduleSymbol) if symbol != nil { @@ -269,3 +302,122 @@ func (c *Checker) shouldTreatPropertiesOfExternalModuleAsExports(resolvedExterna c.isArrayType(resolvedExternalModuleType) || isTupleType(resolvedExternalModuleType) } + +func (c *Checker) GetContextualType(node *ast.Expression, contextFlags ContextFlags) *Type { + if contextFlags&ContextFlagsCompletions != 0 { + return runWithInferenceBlockedFromSourceNode(c, node, func() *Type { return c.getContextualType(node, contextFlags) }) + } + return c.getContextualType(node, contextFlags) +} + +func runWithInferenceBlockedFromSourceNode[T any](c *Checker, node *ast.Node, fn func() T) T { + containingCall := ast.FindAncestor(node, ast.IsCallLikeExpression) + if containingCall != nil { + toMarkSkip := node + for { + c.skipDirectInferenceNodes.Add(toMarkSkip) + toMarkSkip = toMarkSkip.Parent + if toMarkSkip == nil || toMarkSkip == containingCall { + break + } + } + } + + c.isInferencePartiallyBlocked = true + result := runWithoutResolvedSignatureCaching(c, node, fn) + c.isInferencePartiallyBlocked = false + + c.skipDirectInferenceNodes.Clear() + return result +} + +// !!! Shared, made generic +func runWithoutResolvedSignatureCaching[T any](c *Checker, node *ast.Node, fn func() T) T { + ancestorNode := ast.FindAncestor(node, func(n *ast.Node) bool { + return ast.IsCallLikeOrFunctionLikeExpression(n) + }) + if ancestorNode != nil { + cachedResolvedSignatures := make(map[*SignatureLinks]*Signature) + cachedTypes := make(map[*ValueSymbolLinks]*Type) + for ancestorNode != nil { + signatureLinks := c.signatureLinks.Get(ancestorNode) + cachedResolvedSignatures[signatureLinks] = signatureLinks.resolvedSignature + signatureLinks.resolvedSignature = nil + if ast.IsFunctionExpressionOrArrowFunction(ancestorNode) { + symbolLinks := c.valueSymbolLinks.Get(c.getSymbolOfDeclaration(ancestorNode)) + resolvedType := symbolLinks.resolvedType + cachedTypes[symbolLinks] = resolvedType + symbolLinks.resolvedType = nil + } + ancestorNode = ast.FindAncestor(ancestorNode.Parent, ast.IsCallLikeOrFunctionLikeExpression) + } + result := fn() + for signatureLinks, resolvedSignature := range cachedResolvedSignatures { + signatureLinks.resolvedSignature = resolvedSignature + } + for symbolLinks, resolvedType := range cachedTypes { + symbolLinks.resolvedType = resolvedType + } + return result + } + return fn() +} + +func (c *Checker) GetRootSymbols(symbol *ast.Symbol) []*ast.Symbol { + roots := c.getImmediateRootSymbols(symbol) + if roots != nil { + var result []*ast.Symbol + for _, root := range roots { + result = append(result, c.GetRootSymbols(root)...) + } + } + return []*ast.Symbol{symbol} +} + +func (c *Checker) getImmediateRootSymbols(symbol *ast.Symbol) []*ast.Symbol { + if symbol.CheckFlags&ast.CheckFlagsSynthetic != 0 { + return core.MapNonNil( + c.valueSymbolLinks.Get(symbol).containingType.Types(), + func(t *Type) *ast.Symbol { + return c.getPropertyOfType(t, symbol.Name) + }) + } else if symbol.Flags&ast.SymbolFlagsTransient != 0 { + if c.spreadLinks.Has(symbol) { + leftSpread := c.spreadLinks.Get(symbol).leftSpread + rightSpread := c.spreadLinks.Get(symbol).rightSpread + if leftSpread != nil { + return []*ast.Symbol{leftSpread, rightSpread} + } + } + if c.mappedSymbolLinks.Has(symbol) { + syntheticOrigin := c.mappedSymbolLinks.Get(symbol).syntheticOrigin + if syntheticOrigin != nil { + return []*ast.Symbol{syntheticOrigin} + } + } + target := c.tryGetTarget(symbol) + if target != nil { + return []*ast.Symbol{target} + } + return nil + } + + return nil +} + +func (c *Checker) tryGetTarget(symbol *ast.Symbol) *ast.Symbol { + var target *ast.Symbol + next := symbol + for { + if c.valueSymbolLinks.Has(next) { + next = c.valueSymbolLinks.Get(next).target + } else if c.exportTypeLinks.Has(next) { + next = c.exportTypeLinks.Get(next).target + } + if next == nil { + break + } + target = next + } + return target +} diff --git a/internal/checker/types.go b/internal/checker/types.go index 2c56c14bee..1f08c02a36 100644 --- a/internal/checker/types.go +++ b/internal/checker/types.go @@ -534,6 +534,14 @@ type Type struct { data TypeData // Type specific data } +func (t *Type) Id() TypeId { + return t.id +} + +func (t *Type) Flags() TypeFlags { + return t.flags +} + // Casts for concrete struct types func (t *Type) AsIntrinsicType() *IntrinsicType { return t.data.(*IntrinsicType) } @@ -624,6 +632,10 @@ func (t *Type) TargetTupleType() *TupleType { return t.AsTypeReference().target.AsTupleType() } +func (t *Type) Symbol() *ast.Symbol { + return t.symbol +} + // TypeData type TypeData interface { @@ -668,6 +680,10 @@ type LiteralType struct { regularType *Type // Regular version of type } +func (t *LiteralType) Value() any { + return t.value +} + // UniqueESSymbolTypeData type UniqueESSymbolType struct { diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go index 0d6f6db62b..896ab949ae 100644 --- a/internal/checker/utilities.go +++ b/internal/checker/utilities.go @@ -908,20 +908,6 @@ func compareTypeMappers(m1, m2 *TypeMapper) int { return 0 } -func getClassLikeDeclarationOfSymbol(symbol *ast.Symbol) *ast.Node { - return core.Find(symbol.Declarations, ast.IsClassLike) -} - -func isThisInTypeQuery(node *ast.Node) bool { - if !ast.IsThisIdentifier(node) { - return false - } - for ast.IsQualifiedName(node.Parent) && node.Parent.AsQualifiedName().Left == node { - node = node.Parent - } - return node.Parent.Kind == ast.KindTypeQuery -} - func getDeclarationModifierFlagsFromSymbol(s *ast.Symbol) ast.ModifierFlags { return getDeclarationModifierFlagsFromSymbolEx(s, false /*isWrite*/) } @@ -1199,7 +1185,7 @@ func isVariableDeclarationInVariableStatement(node *ast.Node) bool { return ast.IsVariableDeclarationList(node.Parent) && ast.IsVariableStatement(node.Parent.Parent) } -func isKnownSymbol(symbol *ast.Symbol) bool { +func IsKnownSymbol(symbol *ast.Symbol) bool { return isLateBoundName(symbol.Name) } @@ -1245,15 +1231,6 @@ func isThisTypeParameter(t *Type) bool { return t.flags&TypeFlagsTypeParameter != 0 && t.AsTypeParameter().isThisType } -func isCallLikeExpression(node *ast.Node) bool { - switch node.Kind { - case ast.KindJsxOpeningElement, ast.KindJsxSelfClosingElement, ast.KindCallExpression, ast.KindNewExpression, - ast.KindTaggedTemplateExpression, ast.KindDecorator: - return true - } - return false -} - func isCallOrNewExpression(node *ast.Node) bool { return ast.IsCallExpression(node) || ast.IsNewExpression(node) } @@ -2130,7 +2107,7 @@ func symbolsToArray(symbols ast.SymbolTable) []*ast.Symbol { } // See comment on `declareModuleMember` in `binder.go`. -func getCombinedLocalAndExportSymbolFlags(symbol *ast.Symbol) ast.SymbolFlags { +func GetCombinedLocalAndExportSymbolFlags(symbol *ast.Symbol) ast.SymbolFlags { if symbol.ExportSymbol != nil { return symbol.Flags | symbol.ExportSymbol.Flags } diff --git a/internal/core/core.go b/internal/core/core.go index edc4e8a369..ec3ef4955d 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -525,11 +525,11 @@ func Identity[T any](t T) T { return t } -func AddToSeen[K comparable, V any](seen map[K]V, key K, value V) bool { - if _, ok := seen[key]; ok { +func AddIfAbsent[K comparable](seen Set[K], key K) bool { + if seen.Has(key) { return false } - seen[key] = value + seen.Add(key) return true } diff --git a/internal/ls/completions.go b/internal/ls/completions.go index c22033a700..5f508afe01 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -1,7 +1,11 @@ package ls import ( + "fmt" "slices" + "strings" + "unicode" + "unicode/utf8" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" @@ -9,10 +13,15 @@ import ( "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/scanner" "github.com/microsoft/typescript-go/internal/tspath" ) -func (l *LanguageService) ProvideCompletion(fileName string, position int, context *lsproto.CompletionContext) *lsproto.CompletionList { +func (l *LanguageService) ProvideCompletion( + fileName string, + position int, + context *lsproto.CompletionContext, + clientOptions *lsproto.ClientCompletionItemOptions) *lsproto.CompletionList { program, file := l.getProgramAndFile(fileName) node := astnav.GetTouchingPropertyName(file, position) if node.Kind == ast.KindSourceFile { @@ -21,8 +30,34 @@ func (l *LanguageService) ProvideCompletion(fileName string, position int, conte return l.getCompletionsAtPosition(program, file, position, context, nil /*preferences*/) // !!! get preferences } +// !!! figure out other kinds of completion data return type completionData struct { // !!! + symbols []*ast.Symbol + completionKind CompletionKind + isInSnippetScope bool + // Note that the presence of this alone doesn't mean that we need a conversion. Only do that if the completion is not an ordinary identifier. + propertyAccessToConvert *ast.PropertyAccessExpression + isNewIdentifierLocation bool + location *ast.Node + keywordFilters KeywordCompletionFilters + literals []any + symbolToOriginInfoMap map[ast.SymbolId]*symbolOriginInfo + symbolToSortTextMap map[ast.SymbolId]sortText + recommendedCompletion *ast.Symbol + previousToken *ast.Node + contextToken *ast.Node + jsxInitializer jsxInitializer + insideJsDocTagTypeExpression bool + isTypeOnlyLocation bool + // In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. + isJsxIdentifierExpected bool + isRightOfOpenTag bool + isRightOfDotOrQuestionDot bool + importStatementCompletion any // !!! + hasUnresolvedAutoImports bool // !!! + // flags CompletionInfoFlags // !!! + defaultCommitCharacters []string } type importStatementCompletionInfo struct { @@ -33,7 +68,7 @@ type importStatementCompletionInfo struct { // value will be `true` but initializer will be `nil`. type jsxInitializer struct { isInitializer bool - initializer *ast.Identifier + initializer *ast.IdentifierNode } type KeywordCompletionFilters int @@ -82,6 +117,14 @@ const ( sortTextJavascriptIdentifiers sortText = "18" ) +func deprecateSortText(original sortText) sortText { + return "z" + original +} + +func sortBelow(original sortText) sortText { + return original + "1" +} + // !!! sort text transformations type symbolOriginInfoKind int @@ -107,8 +150,89 @@ type symbolOriginInfo struct { isDefaultExport bool isFromPackageJson bool fileName string + data any +} + +// type originData interface { +// symbolName() string +// } + +// !!! origin info +func (s *symbolOriginInfo) symbolName() string { + switch s.data.(type) { + case *symbolOriginInfoExport: + return s.data.(*symbolOriginInfoExport).symbolName + case *symbolOriginInfoResolvedExport: + return s.data.(*symbolOriginInfoResolvedExport).symbolName + default: + panic(fmt.Sprintf("symbolOriginInfo: unknown data type for symbolName(): %T", s.data)) + } +} + +type symbolOriginInfoExport struct { + symbolName string + moduleSymbol *ast.Symbol + isDefaultExport bool + exporName string + // exportMapKey ExportMapInfoKey // !!! +} + +func (s *symbolOriginInfo) asExport() *symbolOriginInfoExport { + return s.data.(*symbolOriginInfoExport) +} + +type symbolOriginInfoResolvedExport struct { + symbolName string + moduleSymbol *ast.Symbol + exportName string + // exportMapKey ExportMapInfoKey // !!! + moduleSpecifier string +} + +func (s *symbolOriginInfo) asResolvedExport() *symbolOriginInfoResolvedExport { + return s.data.(*symbolOriginInfoResolvedExport) +} + +type symbolOriginInfoObjectLiteralMethod struct { + insertText string + labelDetails *lsproto.CompletionItemLabelDetails + isSnippet bool +} + +func (s *symbolOriginInfo) asObjectLiteralMethod() *symbolOriginInfoObjectLiteralMethod { + return s.data.(*symbolOriginInfoObjectLiteralMethod) } +// Special values for `CompletionInfo['source']` used to disambiguate +// completion items with the same `name`. (Each completion item must +// have a unique name/source combination, because those two fields +// comprise `CompletionEntryIdentifier` in `getCompletionEntryDetails`. +// +// When the completion item is an auto-import suggestion, the source +// is the module specifier of the suggestion. To avoid collisions, +// the values here should not be a module specifier we would ever +// generate for an auto-import. +type completionSource string + +const ( + // Completions that require `this.` insertion text. + completionSourceThisProperty completionSource = "ThisProperty/" + // Auto-import that comes attached to a class member snippet. + completionSourceClassMemberSnippet completionSource = "ClassMemberSnippet/" + // A type-only import that needs to be promoted in order to be used at the completion location. + completionSourceTypeOnlyAlias completionSource = "TypeOnlyAlias/" + // Auto-import that comes attached to an object literal method snippet. + completionSourceObjectLiteralMethodSnippet completionSource = "ObjectLiteralMethodSnippet/" + // Case completions for switch statements. + completionSourceSwitchCases completionSource = "SwitchCases/" + // Completions for an object literal expression. + completionSourceObjectLiteralMemberWithComma completionSource = "ObjectLiteralMemberWithComma/" +) + +// Value is set to false for global variables or completions from external module exports, +// true otherwise. +type uniqueNameSet = map[string]bool + func (l *LanguageService) getCompletionsAtPosition( program *compiler.Program, file *ast.SourceFile, @@ -144,11 +268,16 @@ func (l *LanguageService) getCompletionsAtPosition( // !!! label completions completionData := getCompletionData(program, file, position, preferences) - // !!! get completion data + if completionData == nil { + return nil + } + // switch completionData.Kind // !!! other data cases // !!! transform data into completion list - return nil // !!! + response := completionInfoFromData(file, program, compilerOptions, completionData, preferences, position) + // !!! check if response is incomplete + return response } func getCompletionData(program *compiler.Program, file *ast.SourceFile, position int, preferences *UserPreferences) *completionData { @@ -292,7 +421,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position if parent != previousToken.Parent && parent.Initializer() == nil && findChildOfKind(parent, ast.KindEqualsToken, file) != nil { - jsxInitializer.initializer = previousToken.AsIdentifier() + jsxInitializer.initializer = previousToken } } } @@ -304,24 +433,24 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position hasUnresolvedAutoImports := false // This also gets mutated in nested-functions after the return var symbols []*ast.Symbol - var symbolToOriginInfoMap map[ast.SymbolId]symbolOriginInfo + var symbolToOriginInfoMap map[ast.SymbolId]*symbolOriginInfo var symbolToSortTextMap map[ast.SymbolId]sortText - var importSpecifierResolver any // !!! - seenPropertySymbols := make(map[ast.SymbolId]struct{}) + var importSpecifierResolver any // !!! auto import + var seenPropertySymbols core.Set[ast.SymbolId] isTypeOnlyLocation := insideJsDocTagTypeExpression || insideJsDocImportTag || importStatementCompletion != nil && ast.IsTypeOnlyImportOrExportDeclaration(location.Parent) || !isContextTokenValueLocation(contextToken) && (isPossiblyTypeArgumentPosition(contextToken, file, typeChecker) || ast.IsPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken)) - var getModuleSpecifierResolutionHost any // !!! + // var getModuleSpecifierResolutionHost any // !!! auto import addSymbolOriginInfo := func(symbol *ast.Symbol, insertQuestionDot bool, insertAwait bool) { symbolId := ast.GetSymbolId(symbol) - if insertAwait && core.AddToSeen(seenPropertySymbols, symbolId, struct{}{}) { - symbolToOriginInfoMap[symbolId] = symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindPromise, insertQuestionDot)} + if insertAwait && core.AddIfAbsent(seenPropertySymbols, symbolId) { + symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindPromise, insertQuestionDot)} } else if insertQuestionDot { - symbolToOriginInfoMap[symbolId] = symbolOriginInfo{kind: symbolOriginInfoKindNullable} + symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{kind: symbolOriginInfoKindNullable} } } @@ -359,14 +488,14 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position if firstAccessibleSymbol != nil { firstAccessibleSymbolId = ast.GetSymbolId(firstAccessibleSymbol) } - if firstAccessibleSymbolId != 0 && core.AddToSeen(seenPropertySymbols, firstAccessibleSymbolId, struct{}{}) { + if firstAccessibleSymbolId != 0 && core.AddIfAbsent(seenPropertySymbols, firstAccessibleSymbolId) { index := len(symbols) symbols = append(symbols, firstAccessibleSymbol) moduleSymbol := firstAccessibleSymbol.Parent if moduleSymbol == nil || !checker.IsExternalModuleSymbol(moduleSymbol) || typeChecker.TryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.Name, moduleSymbol) != firstAccessibleSymbol { - symbolToOriginInfoMap[ast.GetSymbolId(symbol)] = symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberNoExport, insertQuestionDot)} + symbolToOriginInfoMap[ast.GetSymbolId(symbol)] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberNoExport, insertQuestionDot)} } else { var fileName string if tspath.IsExternalModuleNameRelative(core.StripQuotes(moduleSymbol.Name)) { @@ -402,7 +531,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position // symbolToOriginInfoMap[index] = origin; // } } - } else if _, ok := seenPropertySymbols[firstAccessibleSymbolId]; firstAccessibleSymbolId == 0 || !ok { + } else if firstAccessibleSymbolId == 0 || !seenPropertySymbols.Has(firstAccessibleSymbolId) { symbols = append(symbols, symbol) addSymbolOriginInfo(symbol, insertQuestionDot, insertAwait) addSymbolSortInfo(symbol) @@ -500,7 +629,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position return typeChecker.IsValidPropertyAccess(valueAccessNode, s.Name) } isValidTypeAccess := func(s *ast.Symbol) bool { - return symbolCanBeReferencedAtTypeLocation(s, typeChecker, nil /*seenModules*/) + return symbolCanBeReferencedAtTypeLocation(s, typeChecker, core.Set[ast.SymbolId]{}) } var isValidAccess bool if isNamespaceName { @@ -561,11 +690,653 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position // !!! global completions } - // !!! adjustments + var contextualType *checker.Type + if previousToken != nil { + contextualType = getContextualType(previousToken, position, file, typeChecker) + } + + // exclude literal suggestions after (#51667) and after closing quote (#52675) + // for strings getStringLiteralCompletions handles completions + isLiteralExpected := !ast.IsStringLiteralLike(previousToken) && !isJsxIdentifierExpected + var literals []any + if isLiteralExpected { + var types []*checker.Type + if contextualType != nil && checker.IsUnion(contextualType) { + types = contextualType.Types() + } else if contextualType != nil { + types = []*checker.Type{contextualType} + } + literals = core.MapNonNil(types, func(t *checker.Type) any { + if isLiteral(t) && !checker.IsEnumLiteral(t) { + return t.AsLiteralType().Value() + } + return nil + }) + } + + var recommendedCompletion *ast.Symbol + if previousToken != nil && contextualType != nil { + recommendedCompletion = getRecommendedCompletion(previousToken, contextualType, typeChecker) + } + + return &completionData{ + symbols: symbols, + completionKind: completionKind, + isInSnippetScope: isInSnippetScope, + propertyAccessToConvert: propertyAccessToConvert, + isNewIdentifierLocation: isNewIdentifierLocation, + location: location, + keywordFilters: keywordFilters, + literals: literals, + symbolToOriginInfoMap: symbolToOriginInfoMap, + symbolToSortTextMap: symbolToSortTextMap, + recommendedCompletion: recommendedCompletion, + previousToken: previousToken, + contextToken: contextToken, + jsxInitializer: jsxInitializer, + insideJsDocTagTypeExpression: insideJsDocTagTypeExpression, + isTypeOnlyLocation: isTypeOnlyLocation, + isJsxIdentifierExpected: isJsxIdentifierExpected, + isRightOfOpenTag: isRightOfOpenTag, + isRightOfDotOrQuestionDot: isRightOfDot || isRightOfQuestionDot, + importStatementCompletion: importStatementCompletion, + hasUnresolvedAutoImports: hasUnresolvedAutoImports, + defaultCommitCharacters: defaultCommitCharacters, + } +} + +func completionInfoFromData( + file *ast.SourceFile, + program *compiler.Program, + compilerOptions *core.CompilerOptions, + data *completionData, + preferences *UserPreferences, + position int, +) *lsproto.CompletionList { + keywordFilters := data.keywordFilters + symbols := data.symbols + isNewIdentifierLocation := data.isNewIdentifierLocation + contextToken := data.contextToken + literals := data.literals + typeChecker := program.GetTypeChecker() + + // Verify if the file is JSX language variant + if ast.GetLanguageVariant(file.ScriptKind) == core.LanguageVariantJSX { + // !!! jsx + return nil + } + + // When the completion is for the expression of a case clause (e.g. `case |`), + // filter literals & enum symbols whose values are already present in existing case clauses. + caseClause := ast.FindAncestor(contextToken, ast.IsCaseClause) + if caseClause != nil && + (contextToken.Kind == ast.KindCaseKeyword || + ast.IsNodeDescendantOf(contextToken, caseClause.Expression())) { + // !!! switch completions + } + + isChecked := isCheckedFile(file, compilerOptions) + if isChecked && !isNewIdentifierLocation && len(symbols) == 0 && keywordFilters == KeywordCompletionFiltersNone { + return nil + } + uniqueNames, sortedEntries := getCompletionEntriesFromSymbols( + data, + nil, /*replacementToken*/ + file, + program, + compilerOptions.GetEmitScriptTarget(), + preferences, + compilerOptions, + ) + + // !!! here return nil } +func getCompletionEntriesFromSymbols( + data *completionData, + replacementToken *ast.Node, + file *ast.SourceFile, + program *compiler.Program, + target core.ScriptTarget, + preferences *UserPreferences, + compilerOptions *core.CompilerOptions, +) (uniqueNames uniqueNameSet, sortedEntries []*lsproto.CompletionItem) { + closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location) + typeChecker := program.GetTypeChecker() + // Tracks unique names. + // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; + // true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports. + // So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name. + uniques := make(uniqueNameSet) + for _, symbol := range data.symbols { + symbolId := ast.GetSymbolId(symbol) + origin := data.symbolToOriginInfoMap[symbolId] + name, needsConvertPropertyAccess := getCompletionEntryDisplayNameForSymbol( + symbol, + target, + origin, + data.completionKind, + data.isJsxIdentifierExpected, + ) + if name == "" || + uniques[name] && (origin == nil || !originIsObjectLiteralMethod(origin)) || + data.completionKind == CompletionKindGlobal && + !shouldIncludeSymbol(symbol, data, closestSymbolDeclaration, file, typeChecker, compilerOptions) { + continue + } + + // When in a value location in a JS file, ignore symbols that definitely seem to be type-only. + if !data.isTypeOnlyLocation && ast.IsSourceFileJs(file) && symbolAppearsToBeTypeOnly(symbol, typeChecker) { + continue + } + + originalSortText := data.symbolToSortTextMap[ast.GetSymbolId(symbol)] + if originalSortText == "" { + originalSortText = sortTextLocationPriority + } + sortText := core.IfElse(isDeprecated(symbol, typeChecker), deprecateSortText(originalSortText), originalSortText) + + } + + // !!! + return nil, nil +} + +func createCompletionItem( + symbol *ast.Symbol, + sortText sortText, + replacementToken *ast.Node, + contextToken *ast.Node, + location *ast.Node, + position int, + file *ast.SourceFile, + program *compiler.Program, + name string, + needsConvertPropertyAccess bool, + origin *symbolOriginInfo, + recommendedCompletion *ast.Symbol, + propertyAccessToConvert *ast.Node, + jsxInitializer jsxInitializer, + importStatementCompletion any, + useSemicolons bool, + options *core.CompilerOptions, + preferences *UserPreferences, + clientOptions *lsproto.ClientCompletionItemOptions, + completionKind CompletionKind, + isJsxIdentifierExpected bool, + isRightOfOpenTag bool, +) *lsproto.CompletionItem { + var insertText string + var filterText *string + replacementSpan := getReplacementRangeForContextToken(file, replacementToken, position) + var isSnippet, hasAction bool + source := getSourceFromOrigin(origin) + var labelDetails *lsproto.CompletionItemLabelDetails + + typeChecker := program.GetTypeChecker() + insertQuestionDot := originIsNullableMember(origin) + useBraces := originIsSymbolMember(origin) || needsConvertPropertyAccess + if originIsThisType(origin) { + if needsConvertPropertyAccess { + insertText = fmt.Sprintf( + "this%s[%s]", + core.IfElse(insertQuestionDot, "?.", ""), + quotePropertyName(file, preferences, name)) + } else { + insertText = fmt.Sprintf( + "this%s%s", + core.IfElse(insertQuestionDot, "?.", ""), + name) + } + } else if propertyAccessToConvert != nil && (useBraces || insertQuestionDot) { + // We should only have needsConvertPropertyAccess if there's a property access to convert. But see #21790. + // Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro. + if useBraces { + if needsConvertPropertyAccess { + insertText = fmt.Sprintf("[%s]", quotePropertyName(file, preferences, name)) + } else { + insertText = fmt.Sprintf("[%s]", name) + } + } else { + insertText = name + } + + if insertQuestionDot || propertyAccessToConvert.AsPropertyAccessExpression().QuestionDotToken != nil { + insertText = fmt.Sprintf("?.%s", insertText) + } + + dot := findChildOfKind(propertyAccessToConvert, ast.KindDotToken, file) + if dot == nil { + dot = findChildOfKind(propertyAccessToConvert, ast.KindQuestionDotToken, file) + } + + if dot == nil { + return nil + } + + // If the text after the '.' starts with this name, write over it. Else, add new text. + var end int + if strings.HasPrefix(name, propertyAccessToConvert.Name().Text()) { + end = propertyAccessToConvert.End() + } else { + end = dot.End() + } + replacementSpan = createLspRangeFromBounds(getStartOfNode(dot, file), end, file) + } + + if jsxInitializer.isInitializer { + if insertText == "" { + insertText = name + } + insertText = fmt.Sprintf("{%s}", insertText) + if jsxInitializer.initializer != nil { + replacementSpan = createLspRangeFromNode(jsxInitializer.initializer, file) + } + } + + if originIsPromise(origin) && propertyAccessToConvert != nil { + if insertText == "" { + insertText = name + } + precedingToken := astnav.FindPrecedingToken(file, propertyAccessToConvert.Pos()) + var awaitText string + if precedingToken != nil && positionIsASICandidate(precedingToken.End(), precedingToken.Parent, file) { + awaitText = ";" + } + + awaitText += "(await " + scanner.GetTextOfNode(propertyAccessToConvert.Expression()) + ")" + if needsConvertPropertyAccess { + insertText = awaitText + insertText + } else { + dotStr := core.IfElse(insertQuestionDot, "?.", ".") + insertText = awaitText + dotStr + insertText + } + isInAwaitExpression := ast.IsAwaitExpression(propertyAccessToConvert.Parent) + wrapNode := core.IfElse(isInAwaitExpression, propertyAccessToConvert.Parent, propertyAccessToConvert.Expression()) + replacementSpan = createLspRangeFromBounds(getStartOfNode(wrapNode, file), propertyAccessToConvert.End(), file) + } + + if originIsResolvedExport(origin) { + labelDetails = &lsproto.CompletionItemLabelDetails{ + Description: &origin.asResolvedExport().moduleSpecifier, // !!! vscode @link support + } + if importStatementCompletion != nil { + // !!! auto-imports + } + } + + if originIsTypeOnlyAlias(origin) { + hasAction = true + } + + // Provide object member completions when missing commas, and insert missing commas. + // For example: + // + // interface I { + // a: string; + // b: number + // } + // + // const cc: I = { a: "red" | } + // + // Completion should add a comma after "red" and provide completions for b + if completionKind == CompletionKindObjectPropertyDeclaration && + contextToken != nil && + !ast.NodeHasKind(astnav.FindPrecedingTokenEx(file, contextToken.Pos(), contextToken), ast.KindCommaToken) { + if ast.IsMethodDeclaration(contextToken.Parent.Parent) || + ast.IsGetAccessorDeclaration(contextToken.Parent.Parent) || + ast.IsSetAccessorDeclaration(contextToken.Parent.Parent) || + ast.IsSpreadAssignment(contextToken.Parent) || + getLastToken(ast.FindAncestor(contextToken.Parent, ast.IsPropertyAssignment), file) == contextToken || + ast.IsShorthandPropertyAssignment(contextToken.Parent) && + getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position) { + source = string(completionSourceObjectLiteralMemberWithComma) + hasAction = true + } + } + + if preferences.includeCompletionsWithClassMemberSnippets && + completionKind == CompletionKindMemberLike && + isClassLikeMemberCompletion(symbol, location, file) { + // !!! class member completions + } + + if originIsObjectLiteralMethod(origin) { + insertText = origin.asObjectLiteralMethod().insertText + isSnippet = origin.asObjectLiteralMethod().isSnippet + labelDetails = origin.asObjectLiteralMethod().labelDetails // !!! check if this can conflict with case above where we set label details + if !ptrIsTrue(clientOptions.LabelDetailsSupport) { + name = name + *origin.asObjectLiteralMethod().labelDetails.Detail + labelDetails = nil + } + source = string(completionSourceObjectLiteralMethodSnippet) + sortText = sortBelow(sortText) + } + + if isJsxIdentifierExpected && + !isRightOfOpenTag && + ptrIsTrue(clientOptions.SnippetSupport) && + preferences.jsxAttributeCompletionStyle != JsxAttributeCompletionStyleNone && + !(ast.IsJsxAttribute(location.Parent) && location.Parent.Initializer() != nil) { + useBraces := preferences.jsxAttributeCompletionStyle == JsxAttributeCompletionStyleBraces + t := typeChecker.GetTypeOfSymbolAtLocation(symbol, location) + + // If is boolean like or undefined, don't return a snippet, we want to return just the completion. + if preferences.jsxAttributeCompletionStyle == JsxAttributeCompletionStyleAuto && + t.Flags()&checker.TypeFlagsBooleanLike == 0 && + !(t.Flags()&checker.TypeFlagsUnion != 0 && core.Some(t.Types(), func(t *checker.Type) bool { return t.Flags()&checker.TypeFlagsBooleanLike != 0 })) { + if t.Flags()&checker.TypeFlagsStringLike != 0 || + t.Flags()&checker.TypeFlagsUnion != 0 && + core.Every( + t.Types(), + func(t *checker.Type) bool { + return t.Flags()&(checker.TypeFlagsStringLike|checker.TypeFlagsUndefined) != 0 || + isStringAndEmptyAnonymousObjectIntersection(typeChecker, t) + }) { + // If type is string-like or undefined, use quotes. + insertText = fmt.Sprintf("%s=%s", escapeSnippetText(name), quote(file, preferences, "$1")) + isSnippet = true + } else { + // Use braces for everything else. + useBraces = true + } + } + + if useBraces { + insertText = fmt.Sprintf("%s={$1}", escapeSnippetText(name)) + isSnippet = true + } + } + + if originIsExport(origin) || originIsResolvedExport(origin) { + // !!! auto-imports + // data = originToCompletionEntryData(origin) + // hasAction = importStatementCompletion == nil + } + + parentNamedImportOrExport := ast.FindAncestor(location, isNamedImportsOrExports) + if parentNamedImportOrExport != nil { + languageVersion := options.GetEmitScriptTarget() + if !scanner.IsIdentifierText(name, languageVersion) { + insertText = quotePropertyName(file, preferences, name) + + if parentNamedImportOrExport.Kind == ast.KindNamedImports { + // Check if it is `import { ^here as name } from '...'``. + // We have to access the scanner here to check if it is `{ ^here as name }`` or `{ ^here, as, name }`. + scanner := scanner.NewScanner() + scanner.SetText(file.Text) + scanner.ResetPos(position) + if !(scanner.Scan() == ast.KindAsKeyword && scanner.Scan() == ast.KindIdentifier) { + insertText += " as " + generateIdentifierForArbitraryString(name, languageVersion) + } + } + } else if parentNamedImportOrExport.Kind == ast.KindNamedImports { + possibleToken := scanner.StringToToken(name) + if possibleToken != ast.KindUnknown && + (possibleToken == ast.KindAwaitKeyword || isNonContextualKeyword(possibleToken)) { + insertText = fmt.Sprintf("%s as %s_") + } + } + } + + elementKind := getSymbolKind(typeChecker, symbol, location) + kind := getCompletionsSymbolKind(elementKind) + var commitCharacters []string + if elementKind == ScriptElementKindWarning || elementKind == ScriptElementKindString { + commitCharacters = []string{} + } else { + commitCharacters = nil + } + + return &lsproto.CompletionItem{ + SortText: ptrTo(string(sortText)), + FilterText: filterText, + InsertText: ptrTo(insertText), + } + // !!! HERE +} + +func ptrTo[T any](v T) *T { + return &v +} + +func ptrIsTrue(ptr *bool) bool { + if ptr == nil { + return false + } + return *ptr +} + +func getLineOfPosition(file *ast.SourceFile, pos int) int { + line, _ := scanner.GetLineAndCharacterOfPosition(file, pos) + return line +} + +func isClassLikeMemberCompletion(symbol *ast.Symbol, location *ast.Node, file *ast.SourceFile) bool { + // !!! class member completions + return false +} + +func symbolAppearsToBeTypeOnly(symbol *ast.Symbol, typeChecker *checker.Checker) bool { + flags := checker.GetCombinedLocalAndExportSymbolFlags(checker.SkipAlias(symbol, typeChecker)) + return flags&ast.SymbolFlagsValue == 0 && + (len(symbol.Declarations) == 0 || !ast.IsInJSFile(symbol.Declarations[0]) || flags&ast.SymbolFlagsType != 0) +} + +func shouldIncludeSymbol( + symbol *ast.Symbol, + data *completionData, + closestSymbolDeclaration *ast.Declaration, + file *ast.SourceFile, + typeChecker *checker.Checker, + compilerOptions *core.CompilerOptions) bool { + allFlags := symbol.Flags + location := data.location + if !ast.IsSourceFile(location) { + // export = /**/ here we want to get all meanings, so any symbol is ok + if ast.IsExportAssignment(location.Parent) { + return true + } + + // Filter out variables from their own initializers + // `const a = /* no 'a' here */` + if closestSymbolDeclaration != nil && + ast.IsVariableDeclaration(closestSymbolDeclaration) && + symbol.ValueDeclaration == closestSymbolDeclaration { + return false + } + + // Filter out current and latter parameters from defaults + // `function f(a = /* no 'a' and 'b' here */, b) { }` or + // `function f(a: T, b: T2) { }` + var symbolDeclaration *ast.Declaration + if symbol.ValueDeclaration != nil { + symbolDeclaration = symbol.ValueDeclaration + } else if len(symbol.Declarations) > 0 { + symbolDeclaration = symbol.Declarations[0] + } + + if closestSymbolDeclaration != nil && symbolDeclaration != nil { + if ast.IsParameter(closestSymbolDeclaration) && ast.IsParameter(symbolDeclaration) { + parameters := closestSymbolDeclaration.Parent.ParameterList() + if symbolDeclaration.Pos() >= closestSymbolDeclaration.Pos() && + symbolDeclaration.Pos() < parameters.End() { + return false + } + } else if ast.IsTypeParameterDeclaration(closestSymbolDeclaration) && + ast.IsTypeParameterDeclaration(symbolDeclaration) { + if closestSymbolDeclaration == symbolDeclaration && data.contextToken != nil && data.contextToken.Kind == ast.KindExtendsKeyword { + // filter out the directly self-recursive type parameters + // `type A = K` + return false + } + if isInTypeParameterDefault(data.contextToken) && !ast.IsInferTypeNode(closestSymbolDeclaration.Parent) { + typeParameters := closestSymbolDeclaration.Parent.TypeParameterList() + if typeParameters != nil && symbolDeclaration.Pos() >= closestSymbolDeclaration.Pos() && + symbolDeclaration.Pos() < typeParameters.End() { + return false + } + } + } + } + + // External modules can have global export declarations that will be + // available as global keywords in all scopes. But if the external module + // already has an explicit export and user only wants to use explicit + // module imports then the global keywords will be filtered out so auto + // import suggestions will win in the completion. + symbolOrigin := checker.SkipAlias(symbol, typeChecker) + // We only want to filter out the global keywords. + // Auto Imports are not available for scripts so this conditional is always false. + if file.AsSourceFile().ExternalModuleIndicator != nil && + compilerOptions.AllowUmdGlobalAccess != core.TSTrue && + data.symbolToSortTextMap[ast.GetSymbolId(symbol)] == sortTextGlobalsOrKeywords && + (data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == sortTextAutoImportSuggestions || + data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == sortTextLocationPriority) { + return false + } + + allFlags = allFlags | checker.GetCombinedLocalAndExportSymbolFlags(symbolOrigin) + + // import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace) + if isInRightSideOfInternalImportEqualsDeclaration(data.location) { + return allFlags&ast.SymbolFlagsNamespace != 0 + } + + if data.isTypeOnlyLocation { + // It's a type, but you can reach it by namespace.type as well. + return symbolCanBeReferencedAtTypeLocation(symbol, typeChecker, core.Set[ast.SymbolId]{}) + } + } + + // expressions are value space (which includes the value namespaces) + return allFlags&ast.SymbolFlagsValue != 0 +} + +func getCompletionEntryDisplayNameForSymbol( + symbol *ast.Symbol, + target core.ScriptTarget, + origin *symbolOriginInfo, + completionKind CompletionKind, + isJsxIdentifierExpected bool, +) (displayName string, needsConvertPropertyAccess bool) { + if originIsIgnore(origin) { + return "", false + } + + name := core.IfElse(originIncludesSymbolName(origin), origin.symbolName(), symbol.Name) + if name == "" || + // If the symbol is external module, don't show it in the completion list + // (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there) + symbol.Flags&ast.SymbolFlagsModule != 0 && startsWithQuote(name) || + // If the symbol is the internal name of an ES symbol, it is not a valid entry. Internal names for ES symbols start with "__@" + checker.IsKnownSymbol(symbol) { + return "", false + } + + // !!! isIdentifierText should take in identifierVariant language variant + // name is a valid identifier or private identifier text + if scanner.IsIdentifierText(name, target) || + symbol.ValueDeclaration != nil && ast.IsPrivateIdentifierClassElementDeclaration(symbol.ValueDeclaration) { + return name, false + } + if symbol.Flags&ast.SymbolFlagsAlias != 0 { + // Allow non-identifier import/export aliases since we can insert them as string literals + return name, true + } + + switch completionKind { + case CompletionKindMemberLike: + if originIsComputedPropertyName(origin) { + return origin.symbolName(), false + } + return "", false + case CompletionKindObjectPropertyDeclaration: + // TODO: GH#18169 + escapedName, _ := core.StringifyJson(name, "", "") + return escapedName, false + case CompletionKindPropertyAccess, CompletionKindGlobal: + // For a 'this.' completion it will be in a global context, but may have a non-identifier name. + // Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547 + ch, _ := utf8.DecodeRuneInString(name) + if ch == ' ' { + return "", false + } + return name, true + case CompletionKindNone, CompletionKindString: + return name, false + default: + panic(fmt.Sprintf("Unexpected completion kind: %v", completionKind)) + } +} + +// !!! refactor symbolOriginInfo so that we can tell the difference between flags and the kind of data it has +func originIsIgnore(origin *symbolOriginInfo) bool { + return origin != nil && origin.kind&symbolOriginInfoKindIgnore != 0 +} + +func originIncludesSymbolName(origin *symbolOriginInfo) bool { + return originIsExport(origin) || originIsResolvedExport(origin) || originIsComputedPropertyName(origin) +} + +func originIsExport(origin *symbolOriginInfo) bool { + return origin != nil && origin.kind&symbolOriginInfoKindExport != 0 +} + +func originIsResolvedExport(origin *symbolOriginInfo) bool { + return origin != nil && origin.kind&symbolOriginInfoKindResolvedExport != 0 +} + +func originIsComputedPropertyName(origin *symbolOriginInfo) bool { + return origin != nil && origin.kind&symbolOriginInfoKindComputedPropertyName != 0 +} + +func originIsObjectLiteralMethod(origin *symbolOriginInfo) bool { + return origin != nil && origin.kind&symbolOriginInfoKindObjectLiteralMethod != 0 +} + +func originIsThisType(origin *symbolOriginInfo) bool { + return origin != nil && origin.kind&symbolOriginInfoKindThisType != 0 +} + +func originIsTypeOnlyAlias(origin *symbolOriginInfo) bool { + return origin != nil && origin.kind&symbolOriginInfoKindTypeOnlyAlias != 0 +} + +func originIsSymbolMember(origin *symbolOriginInfo) bool { + return origin != nil && origin.kind&symbolOriginInfoKindSymbolMember != 0 +} + +func originIsNullableMember(origin *symbolOriginInfo) bool { + return origin != nil && origin.kind&symbolOriginInfoKindNullable != 0 +} + +func originIsPromise(origin *symbolOriginInfo) bool { + return origin != nil && origin.kind&symbolOriginInfoKindPromise != 0 +} + +func getSourceFromOrigin(origin *symbolOriginInfo) string { + if originIsExport(origin) { + return core.StripQuotes(origin.asExport().moduleSymbol.Name) + } + + if originIsResolvedExport(origin) { + return origin.asResolvedExport().moduleSpecifier + } + + if originIsThisType(origin) { + return string(completionSourceThisProperty) + } + + if originIsTypeOnlyAlias(origin) { + return string(completionSourceTypeOnlyAlias) + } + + return "" +} + // In a scenarion such as `const x = 1 * |`, the context and previous tokens are both `*`. // In `const x = 1 * o|`, the context token is *, and the previous token is `o`. // `contextToken` and `previousToken` can both be nil if we are at the beginning of the file. @@ -675,10 +1446,7 @@ func isContextTokenTypeLocation(contextToken *ast.Node) bool { } // True if symbol is a type or a module containing at least one type. -func symbolCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules map[ast.SymbolId]struct{}) bool { - if seenModules == nil { - seenModules = make(map[ast.SymbolId]struct{}) - } +func symbolCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules core.Set[ast.SymbolId]) bool { // Since an alias can be merged with a local declaration, we need to test both the alias and its target. // This code used to just test the result of `skipAlias`, but that would ignore any locally introduced meanings. return nonAliasCanBeReferencedAtTypeLocation(symbol, typeChecker, seenModules) || @@ -689,9 +1457,9 @@ func symbolCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checke ) } -func nonAliasCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules map[ast.SymbolId]struct{}) bool { +func nonAliasCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules core.Set[ast.SymbolId]) bool { return symbol.Flags&ast.SymbolFlagsType != 0 || typeChecker.IsUnknownSymbol(symbol) || - symbol.Flags&ast.SymbolFlagsModule != 0 && core.AddToSeen(seenModules, ast.GetSymbolId(symbol), struct{}{}) && + symbol.Flags&ast.SymbolFlagsModule != 0 && core.AddIfAbsent(seenModules, ast.GetSymbolId(symbol)) && core.Some( typeChecker.GetExportsOfModule(symbol), func(e *ast.Symbol) bool { return symbolCanBeReferencedAtTypeLocation(e, typeChecker, seenModules) }) @@ -700,7 +1468,7 @@ func nonAliasCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *chec // Gets all properties on a type, but if that type is a union of several types, // excludes array-like types or callable/constructable types. func getPropertiesForCompletion(t *checker.Type, typeChecker *checker.Checker) []*ast.Symbol { - if typeChecker.IsUnion(t) { + if checker.IsUnion(t) { return core.CheckEachDefined(typeChecker.GetAllPossiblePropertiesOfTypes(t.Types()), "getAllPossiblePropertiesOfTypes() should all be defined.") } else { return core.CheckEachDefined(typeChecker.GetApparentProperties(t), "getApparentProperties() should all be defined.") @@ -752,3 +1520,344 @@ func isStaticProperty(symbol *ast.Symbol) bool { checker.GetEffectiveModifierFlags(symbol.ValueDeclaration)&ast.ModifierFlagsStatic != 0 && ast.IsClassLike(symbol.ValueDeclaration.Parent) } + +func getContextualType(previousToken *ast.Node, position int, file *ast.SourceFile, typeChecker *checker.Checker) *checker.Type { + parent := previousToken.Parent + switch previousToken.Kind { + case ast.KindIdentifier: + return getContextualTypeFromParent(previousToken, typeChecker, checker.ContextFlagsNone) + case ast.KindEqualsToken: + switch parent.Kind { + case ast.KindVariableDeclaration: + return typeChecker.GetContextualType(parent.Initializer(), checker.ContextFlagsNone) + case ast.KindBinaryExpression: + return typeChecker.GetTypeAtLocation(parent.AsBinaryExpression().Left) + case ast.KindJsxAttribute: + // return typeChecker.GetContextualTypeForJsxAttribute(parent) // !!! jsx + return nil + default: + return nil + } + case ast.KindNewKeyword: + return typeChecker.GetContextualType(parent, checker.ContextFlagsNone) + case ast.KindCaseKeyword: + caseClause := core.IfElse(ast.IsCaseClause(parent), parent, nil) + if caseClause != nil { + return getSwitchedType(caseClause, typeChecker) + } + return nil + case ast.KindOpenBraceToken: + if ast.IsJsxExpression(parent) && !ast.IsJsxElement(parent.Parent) && !ast.IsJsxFragment(parent.Parent) { + // return typeChecker.GetContextualTypeForJsxAttribute(parent.Parent) // !!! jsx + return nil + } + return nil + default: + // argInfo := getArgumentInfoForCompletions(previousToken, position, file, typeChecker) // !!! signature help + var argInfo *struct{} // !!! signature help + if argInfo != nil { + // return typeChecker.GetContextualTypeForArgumentAtIndex() // !!! signature help + return nil + } else if isEqualityOperatorKind(previousToken.Kind) && ast.IsBinaryExpression(parent) && isEqualityOperatorKind(parent.AsBinaryExpression().OperatorToken.Kind) { + // completion at `x ===/**/` + return typeChecker.GetTypeAtLocation(parent.AsBinaryExpression().Left) + } else { + contextualType := typeChecker.GetContextualType(previousToken, checker.ContextFlagsCompletions) + if contextualType != nil { + return contextualType + } + return typeChecker.GetContextualType(previousToken, checker.ContextFlagsNone) + } + } +} + +// TODO: this is also used by string completions originally, but passing a flag `ContextFlagsCompletions`. +// What difference does it make? +func getContextualTypeFromParent(node *ast.Expression, typeChecker *checker.Checker, contextFlags checker.ContextFlags) *checker.Type { + parent := ast.WalkUpParenthesizedExpressions(node.Parent) + switch parent.Kind { + case ast.KindNewExpression: + return typeChecker.GetContextualType(parent, contextFlags) + case ast.KindBinaryExpression: + if isEqualityOperatorKind(parent.AsBinaryExpression().OperatorToken.Kind) { + return typeChecker.GetTypeAtLocation(core.IfElse(node == parent.AsBinaryExpression().Right, parent.AsBinaryExpression().Left, parent.AsBinaryExpression().Right)) + } + return typeChecker.GetContextualType(node, contextFlags) + case ast.KindCaseClause: + return getSwitchedType(parent, typeChecker) + default: + return typeChecker.GetContextualType(node, contextFlags) + } +} + +func getSwitchedType(caseClause *ast.CaseClauseNode, typeChecker *checker.Checker) *checker.Type { + return typeChecker.GetTypeAtLocation(caseClause.Parent.Parent.Expression()) +} + +func isEqualityOperatorKind(kind ast.Kind) bool { + switch kind { + case ast.KindEqualsEqualsEqualsToken, ast.KindEqualsEqualsToken, + ast.KindExclamationEqualsEqualsToken, ast.KindExclamationEqualsToken: + return true + default: + return false + } +} + +func isLiteral(t *checker.Type) bool { + return checker.IsStringLiteral(t) || checker.IsNumberLiteral(t) || checker.IsBigIntLiteral(t) +} + +func getRecommendedCompletion(previousToken *ast.Node, contextualType *checker.Type, typeChecker *checker.Checker) *ast.Symbol { + // For a union, return the first one with a recommended completion. + return core.FirstNonNil( + core.IfElse(checker.IsUnion(contextualType), contextualType.Types(), []*checker.Type{contextualType}), + func(t *checker.Type) *ast.Symbol { + symbol := t.Symbol() + // Don't make a recommended completion for an abstract class. + if symbol != nil && + symbol.Flags&(ast.SymbolFlagsEnumMember|ast.SymbolFlagsEnum|ast.SymbolFlagsClass) != 0 && + !isAbstractConstructorSymbol(symbol) { + return getFirstSymbolInChain(symbol, previousToken, typeChecker) + } + return nil + }, + ) +} + +func isAbstractConstructorSymbol(symbol *ast.Symbol) bool { + if symbol.Flags&ast.SymbolFlagsClass != 0 { + declaration := ast.GetClassLikeDeclarationOfSymbol(symbol) + return declaration != nil && ast.HasSyntacticModifier(declaration, ast.ModifierFlagsAbstract) + } + return false +} + +func startsWithQuote(s string) bool { + r, _ := utf8.DecodeRuneInString(s) + return r == '"' || r == '\'' +} + +func getClosestSymbolDeclaration(contextToken *ast.Node, location *ast.Node) *ast.Declaration { + if contextToken == nil { + return nil + } + + closestDeclaration := ast.FindAncestorOrQuit(contextToken, func(node *ast.Node) ast.FindAncestorResult { + if ast.IsFunctionBlock(node) || isArrowFunctionBody(node) || ast.IsBindingPattern(node) { + return ast.FindAncestorQuit + } + + if (ast.IsParameter(node) || ast.IsTypeParameterDeclaration(node)) && + !ast.IsIndexSignatureDeclaration(node.Parent) { + return ast.FindAncestorTrue + } + return ast.FindAncestorFalse + }) + + if closestDeclaration == nil { + closestDeclaration = ast.FindAncestorOrQuit(contextToken, func(node *ast.Node) ast.FindAncestorResult { + if ast.IsFunctionBlock(node) || isArrowFunctionBody(node) || ast.IsBindingPattern(node) { + return ast.FindAncestorQuit + } + + if ast.IsVariableDeclaration(node) { + return ast.FindAncestorTrue + } + return ast.FindAncestorFalse + }) + } + return closestDeclaration +} + +func isArrowFunctionBody(node *ast.Node) bool { + return node.Parent != nil && ast.IsArrowFunction(node.Parent) && + (node.Parent.Body() == node || + // const a = () => /**/; + node.Kind == ast.KindEqualsGreaterThanToken) +} + +func isInTypeParameterDefault(contextToken *ast.Node) bool { + if contextToken == nil { + return false + } + + node := contextToken + parent := contextToken.Parent + for parent != nil { + if ast.IsTypeParameterDeclaration(parent) { + return parent.AsTypeParameter().DefaultType == node || node.Kind == ast.KindEqualsToken + } + node = parent + parent = parent.Parent + } + + return false +} + +func isDeprecated(symbol *ast.Symbol, typeChecker *checker.Checker) bool { + declarations := checker.SkipAlias(symbol, typeChecker).Declarations + return len(declarations) > 0 && core.Every(declarations, func(decl *ast.Declaration) bool { return typeChecker.IsDeprecatedDeclaration(decl) }) +} + +func getReplacementRangeForContextToken(file *ast.SourceFile, contextToken *ast.Node, position int) *lsproto.Range { + if contextToken == nil { + return nil + } + + // !!! ensure range is single line + switch contextToken.Kind { + case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral: + return createRangeFromStringLiteralLikeContent(file, contextToken, position) + default: + return createLspRangeFromNode(contextToken, file) + } +} + +func createRangeFromStringLiteralLikeContent(file *ast.SourceFile, node *ast.StringLiteralLike, position int) *lsproto.Range { + replacementEnd := node.End() - 1 + nodeStart := getStartOfNode(node, file) + if ast.IsUnterminatedLiteral(node) { + // we return no replacement range only if unterminated string is empty + if nodeStart == replacementEnd { + return nil + } + replacementEnd = min(position, node.End()) + } + return createLspRangeFromBounds(nodeStart+1, replacementEnd, file) +} + +func quotePropertyName(file *ast.SourceFile, preferences *UserPreferences, name string) string { + r, _ := utf8.DecodeRuneInString(name) + if unicode.IsDigit(r) { + return name + } + return quote(file, preferences, name) +} + +// Checks whether type is `string & {}`, which is semantically equivalent to string but +// is not reduced by the checker as a special case used for supporting string literal completions +// for string type. +func isStringAndEmptyAnonymousObjectIntersection(typeChecker *checker.Checker, t *checker.Type) bool { + if t.Flags()&checker.TypeFlagsIntersection == 0 { + return false + } + + return len(t.Types()) == 2 && + (areIntersectedTypesAvoidingStringReduction(typeChecker, t.Types()[0], t.Types()[1]) || + areIntersectedTypesAvoidingStringReduction(typeChecker, t.Types()[1], t.Types()[0])) +} + +func areIntersectedTypesAvoidingStringReduction(typeChecker *checker.Checker, t1 *checker.Type, t2 *checker.Type) bool { + return t1.Flags()&checker.TypeFlagsString != 0 && typeChecker.IsEmptyAnonymousObjectType(t2) +} + +func escapeSnippetText(text string) string { + return strings.ReplaceAll(text, `$`, `\$`) +} + +func isNamedImportsOrExports(node *ast.Node) bool { + return ast.IsNamedImports(node) || ast.IsNamedExports(node) +} + +func generateIdentifierForArbitraryString(text string, languageVersion core.ScriptTarget) string { + needsUnderscore := false + identifier := "" + var ch rune + var size int + + // Convert "(example, text)" into "_example_text_" + for pos := 0; pos < len(text); pos += size { + ch, size = utf8.DecodeRuneInString(text[pos:]) + var validChar bool + if pos == 0 { + validChar = scanner.IsIdentifierStart(ch, languageVersion) + } else { + validChar = scanner.IsIdentifierPart(ch, languageVersion) + } + if size > 0 && validChar { + if needsUnderscore { + identifier += "_" + identifier += string(ch) + needsUnderscore = false + } + } else { + needsUnderscore = true + } + } + + if needsUnderscore { + identifier += "_" + } + + // Default to "_" if the provided text was empty + if identifier == "" { + return "_" + } + + return identifier +} + +// Copied from vscode TS extension. +func getCompletionsSymbolKind(kind ScriptElementKind) lsproto.CompletionItemKind { + switch kind { + case ScriptElementKindPrimitiveType: + case ScriptElementKindKeyword: + return lsproto.CompletionItemKindKeyword + case ScriptElementKindConstElement: + case ScriptElementKindLetElement: + case ScriptElementKindVariableElement: + case ScriptElementKindLocalVariableElement: + case ScriptElementKindAlias: + case ScriptElementKindParameterElement: + return lsproto.CompletionItemKindVariable + + case ScriptElementKindMemberVariableElement: + case ScriptElementKindMemberGetAccessorElement: + case ScriptElementKindMemberSetAccessorElement: + return lsproto.CompletionItemKindField + + case ScriptElementKindFunctionElement: + case ScriptElementKindLocalFunctionElement: + return lsproto.CompletionItemKindFunction + + case ScriptElementKindMemberFunctionElement: + case ScriptElementKindConstructSignatureElement: + case ScriptElementKindCallSignatureElement: + case ScriptElementKindIndexSignatureElement: + return lsproto.CompletionItemKindMethod + + case ScriptElementKindEnumElement: + return lsproto.CompletionItemKindEnum + + case ScriptElementKindEnumMemberElement: + return lsproto.CompletionItemKindEnumMember + + case ScriptElementKindModuleElement: + case ScriptElementKindExternalModuleName: + return lsproto.CompletionItemKindModule + + case ScriptElementKindClassElement: + case ScriptElementKindTypeElement: + return lsproto.CompletionItemKindClass + + case ScriptElementKindInterfaceElement: + return lsproto.CompletionItemKindInterface + + case ScriptElementKindWarning: + return lsproto.CompletionItemKindText + + case ScriptElementKindScriptElement: + return lsproto.CompletionItemKindFile + + case ScriptElementKindDirectory: + return lsproto.CompletionItemKindFolder + + case ScriptElementKindString: + return lsproto.CompletionItemKindConstant + + default: + return lsproto.CompletionItemKindProperty + } + panic("Unhandled script element kind: " + kind) +} diff --git a/internal/ls/symbol_display.go b/internal/ls/symbol_display.go new file mode 100644 index 0000000000..52240f771b --- /dev/null +++ b/internal/ls/symbol_display.go @@ -0,0 +1,260 @@ +package ls + +import ( + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/core" +) + +type ScriptElementKind string + +const ( + ScriptElementKindUnknown ScriptElementKind = "" + ScriptElementKindWarning ScriptElementKind = "warning" + // predefined type (void) or keyword (class) + ScriptElementKindKeyword ScriptElementKind = "keyword" + // top level script node + ScriptElementKindScriptElement ScriptElementKind = "script" + // module foo {} + ScriptElementKindModuleElement ScriptElementKind = "module" + // class X {} + ScriptElementKindClassElement ScriptElementKind = "class" + // var x = class X {} + ScriptElementKindLocalClassElement ScriptElementKind = "local class" + // interface Y {} + ScriptElementKindInterfaceElement ScriptElementKind = "interface" + // type T = ... + ScriptElementKindTypeElement ScriptElementKind = "type" + // enum E {} + ScriptElementKindEnumElement ScriptElementKind = "enum" + ScriptElementKindEnumMemberElement ScriptElementKind = "enum member" + // Inside module and script only. + // const v = ... + ScriptElementKindVariableElement ScriptElementKind = "var" + // Inside function. + ScriptElementKindLocalVariableElement ScriptElementKind = "local var" + // using foo = ... + ScriptElementKindVariableUsingElement ScriptElementKind = "using" + // await using foo = ... + ScriptElementKindVariableAwaitUsingElement ScriptElementKind = "await using" + // Inside module and script only. + // function f() {} + ScriptElementKindFunctionElement ScriptElementKind = "function" + // Inside function. + ScriptElementKindLocalFunctionElement ScriptElementKind = "local function" + // class X { [public|private]* foo() {} } + ScriptElementKindMemberFunctionElement ScriptElementKind = "method" + // class X { [public|private]* [get|set] foo:number; } + ScriptElementKindMemberGetAccessorElement ScriptElementKind = "getter" + ScriptElementKindMemberSetAccessorElement ScriptElementKind = "setter" + // class X { [public|private]* foo:number; } + // interface Y { foo:number; } + ScriptElementKindMemberVariableElement ScriptElementKind = "property" + // class X { [public|private]* accessor foo: number; } + ScriptElementKindMemberAccessorVariableElement ScriptElementKind = "accessor" + // class X { constructor() { } } + // class X { static { } } + ScriptElementKindConstructorImplementationElement ScriptElementKind = "constructor" + // interface Y { ():number; } + ScriptElementKindCallSignatureElement ScriptElementKind = "call" + // interface Y { []:number; } + ScriptElementKindIndexSignatureElement ScriptElementKind = "index" + // interface Y { new():Y; } + ScriptElementKindConstructSignatureElement ScriptElementKind = "construct" + // function foo(*Y*: string) + ScriptElementKindParameterElement ScriptElementKind = "parameter" + ScriptElementKindTypeParameterElement ScriptElementKind = "type parameter" + ScriptElementKindPrimitiveType ScriptElementKind = "primitive type" + ScriptElementKindLabel ScriptElementKind = "label" + ScriptElementKindAlias ScriptElementKind = "alias" + ScriptElementKindConstElement ScriptElementKind = "const" + ScriptElementKindLetElement ScriptElementKind = "let" + ScriptElementKindDirectory ScriptElementKind = "directory" + ScriptElementKindExternalModuleName ScriptElementKind = "external module name" + // String literal + ScriptElementKindString ScriptElementKind = "string" + // Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}" + ScriptElementKindLink ScriptElementKind = "link" + // Jsdoc @link: in `{@link C link text}`, the entity name "C" + ScriptElementKindLinkName ScriptElementKind = "link name" + // Jsdoc @link: in `{@link C link text}`, the link text "link text" + ScriptElementKindLinkText ScriptElementKind = "link text" +) + +func getSymbolKind(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind { + result := getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location) + if result != ScriptElementKindUnknown { + return result + } + + flags := checker.GetCombinedLocalAndExportSymbolFlags(symbol) + 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 { + roots := typeChecker.GetRootSymbols(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 && + len(typeChecker.GetCallSignatures(typeChecker.GetNonNullableType(typeChecker.GetTypeOfSymbolAtLocation(symbol, location)))) > 0 { + return ScriptElementKindMemberFunctionElement + } + + 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 := checker.GetCombinedLocalAndExportSymbolFlags(symbol) + 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 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.IsParameter(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. + for parent := decl.Parent; !ast.IsFunctionBlock(parent); parent = parent.Parent { + // Reached source file or module block + if parent.Kind == ast.KindSourceFile || parent.Kind == ast.KindModuleBlock { + continue + } + } + + // Parent is in function block. + return true + } + return false +} diff --git a/internal/ls/types.go b/internal/ls/types.go index 291c10eda8..9d1f00ee47 100644 --- a/internal/ls/types.go +++ b/internal/ls/types.go @@ -16,7 +16,16 @@ type Location struct { Range core.TextRange } +type JsxAttributeCompletionStyle string + +const ( + JsxAttributeCompletionStyleAuto JsxAttributeCompletionStyle = "auto" + JsxAttributeCompletionStyleBraces JsxAttributeCompletionStyle = "braces" + JsxAttributeCompletionStyleNone JsxAttributeCompletionStyle = "none" +) + type UserPreferences struct { + // !!! use *bool?? // Enables auto-import-style completions on partially-typed import statements. E.g., allows // `import write|` to be completed to `import { writeFile } from "fs"`. includeCompletionsForImportStatements bool @@ -25,4 +34,18 @@ type UserPreferences struct { // on potentially-null and potentially-undefined values, with insertion text to replace // preceding `.` tokens with `?.`. includeAutomaticOptionalChainCompletions bool + + // 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 bool + + // 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 bool + + jsxAttributeCompletionStyle JsxAttributeCompletionStyle } diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 2855b05ba6..b8c27ae063 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -1,9 +1,14 @@ package ls import ( + "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/core" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/scanner" ) // !!! Shared (placeholder) @@ -13,8 +18,7 @@ func isInString(file *ast.SourceFile, position int, previousToken *ast.Node) boo } if previousToken != nil && isStringTextContainingNode(previousToken) { - // start := previousToken. - // !!! HERE + } return false @@ -72,6 +76,10 @@ func getLastChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { } func getLastToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { + if node == nil { + return nil + } + assertHasRealPosition(node) lastChild := getLastChild(node, sourceFile) @@ -125,3 +133,135 @@ func isInRightSideOfInternalImportEqualsDeclaration(node *ast.Node) bool { return ast.IsInternalModuleImportEqualsDeclaration(node.Parent) && node.Parent.AsImportEqualsDeclaration().ModuleReference == node } + +func createLspRangeFromNode(node *ast.Node, file *ast.SourceFile) *lsproto.Range { + return createLspRangeFromBounds(node.Pos(), node.End(), file) +} + +func createLspRangeFromBounds(start, end int, file *ast.SourceFile) *lsproto.Range { + // !!! needs converters access + return nil +} + +func quote(file *ast.SourceFile, preferences *UserPreferences, text string) string { + // Editors can pass in undefined or empty string - we want to infer the preference in those cases. + quotePreference := getQuotePreference(file, preferences) + quoted, _ := core.StringifyJson(text, "" /*prefix*/, "" /*indent*/) + if quotePreference == quotePreferenceSingle { + strings.ReplaceAll(strings.ReplaceAll(core.StripQuotes(quoted), "'", `\'`), `\"`, `"`) + } + return quoted +} + +type quotePreference int + +const ( + quotePreferenceSingle quotePreference = iota + quotePreferenceDouble +) + +func getQuotePreference(file *ast.SourceFile, preferences *UserPreferences) quotePreference { + // !!! + return quotePreferenceDouble +} + +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.GetLineAndCharacterOfPosition(file, node.End()) + endLine, _ := scanner.GetLineAndCharacterOfPosition(file, getStartOfNode(nextToken, file)) + return startLine != endLine +} + +func isNonContextualKeyword(token ast.Kind) bool { + return ast.IsKeywordKind(token) && !ast.IsContextualKeyword(token) +} diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 52a3e1e619..0ea3e73026 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -191,7 +191,7 @@ func (p *Parser) initializeState(fileName string, path tspath.Path, sourceText s p.sourceText = sourceText p.languageVersion = languageVersion p.scriptKind = ensureScriptKind(fileName, scriptKind) - p.languageVariant = getLanguageVariant(p.scriptKind) + p.languageVariant = ast.GetLanguageVariant(p.scriptKind) switch p.scriptKind { case core.ScriptKindJS, core.ScriptKindJSX: p.contextFlags = ast.NodeFlagsJavaScriptFile diff --git a/internal/parser/utilities.go b/internal/parser/utilities.go index 4692830b1a..b2487f9539 100644 --- a/internal/parser/utilities.go +++ b/internal/parser/utilities.go @@ -24,15 +24,6 @@ func ensureScriptKind(fileName string, scriptKind core.ScriptKind) core.ScriptKi return scriptKind } -func getLanguageVariant(scriptKind core.ScriptKind) core.LanguageVariant { - switch scriptKind { - case core.ScriptKindTSX, core.ScriptKindJSX, core.ScriptKindJS, core.ScriptKindJSON: - // .tsx and .jsx files are treated as jsx language variant. - return core.LanguageVariantJSX - } - return core.LanguageVariantStandard -} - func tokenIsIdentifierOrKeyword(token ast.Kind) bool { return token >= ast.KindIdentifier } diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index 62cfbd1b28..032c1d54c6 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -834,7 +834,7 @@ func (s *Scanner) Scan() ast.Kind { s.token = ast.KindAtToken case '\\': cp := s.peekUnicodeEscape() - if cp >= 0 && isIdentifierStart(cp, s.languageVersion) { + if cp >= 0 && IsIdentifierStart(cp, s.languageVersion) { s.tokenValue = string(s.scanUnicodeEscape(true)) + s.scanIdentifierParts() s.token = GetIdentifierToken(s.tokenValue) } else { @@ -857,7 +857,7 @@ func (s *Scanner) Scan() ast.Kind { if s.charAt(1) == '\\' { s.pos++ cp := s.peekUnicodeEscape() - if cp >= 0 && isIdentifierStart(cp, s.languageVersion) { + if cp >= 0 && IsIdentifierStart(cp, s.languageVersion) { s.tokenValue = "#" + string(s.scanUnicodeEscape(true)) + s.scanIdentifierParts() s.token = ast.KindPrivateIdentifier break @@ -1083,7 +1083,7 @@ func (s *Scanner) ReScanSlashToken() ast.Kind { s.pos++ for { ch, size := s.charAndSize() - if size == 0 || !isIdentifierPart(ch, s.languageVersion) { + if size == 0 || !IsIdentifierPart(ch, s.languageVersion) { break } s.pos += size @@ -1350,7 +1350,7 @@ func (s *Scanner) ScanJSDocToken() ast.Kind { case '\\': s.pos-- cp := s.peekUnicodeEscape() - if cp >= 0 && isIdentifierStart(cp, s.languageVersion) { + if cp >= 0 && IsIdentifierStart(cp, s.languageVersion) { s.tokenValue = string(s.scanUnicodeEscape(true)) + s.scanIdentifierParts() s.token = GetIdentifierToken(s.tokenValue) } else { @@ -1359,14 +1359,14 @@ func (s *Scanner) ScanJSDocToken() ast.Kind { return s.token } - if isIdentifierStart(ch, s.languageVersion) { + if IsIdentifierStart(ch, s.languageVersion) { char := ch for { if s.pos >= len(s.text) { break } char, size = s.charAndSize() - if !isIdentifierPart(char, s.languageVersion) && char != '-' { + if !IsIdentifierPart(char, s.languageVersion) && char != '-' { break } s.pos += size @@ -1403,11 +1403,11 @@ func (s *Scanner) scanIdentifier(prefixLength int) bool { s.pos = start + prefixLength } ch, size := s.charAndSize() - if isIdentifierStart(ch, s.languageVersion) { + if IsIdentifierStart(ch, s.languageVersion) { for { s.pos += size ch, size = s.charAndSize() - if !isIdentifierPart(ch, s.languageVersion) { + if !IsIdentifierPart(ch, s.languageVersion) { break } } @@ -1425,13 +1425,13 @@ func (s *Scanner) scanIdentifierParts() string { start := s.pos for { ch, size := s.charAndSize() - if isIdentifierPart(ch, s.languageVersion) { + if IsIdentifierPart(ch, s.languageVersion) { s.pos += size continue } if ch == '\\' { escaped := s.peekUnicodeEscape() - if escaped >= 0 && isIdentifierPart(escaped, s.languageVersion) { + if escaped >= 0 && IsIdentifierPart(escaped, s.languageVersion) { sb.WriteString(s.text[start:s.pos]) sb.WriteRune(s.scanUnicodeEscape(true)) start = s.pos @@ -1632,7 +1632,7 @@ func (s *Scanner) scanEscapeSequence(flags EscapeSequenceScanningFlags) string { // case CharacterCodes.paragraphSeparator !!! return "" default: - if flags&EscapeSequenceScanningFlagsAnyUnicodeMode != 0 || flags&EscapeSequenceScanningFlagsRegularExpression != 0 && flags&EscapeSequenceScanningFlagsAnnexB == 0 && isIdentifierPart(ch, s.languageVersion) { + if flags&EscapeSequenceScanningFlagsAnyUnicodeMode != 0 || flags&EscapeSequenceScanningFlagsRegularExpression != 0 && flags&EscapeSequenceScanningFlagsAnnexB == 0 && IsIdentifierPart(ch, s.languageVersion) { s.errorAt(diagnostics.This_character_cannot_be_escaped_in_a_regular_expression, s.pos-2, 2) } return string(ch) @@ -1775,7 +1775,7 @@ func (s *Scanner) scanNumber() ast.Kind { result = ast.KindNumericLiteral } ch, _ := s.charAndSize() - if isIdentifierStart(ch, s.languageVersion) { + if IsIdentifierStart(ch, s.languageVersion) { idStart := s.pos id := s.scanIdentifierParts() if result != ast.KindBigIntLiteral && len(id) == 1 && s.text[idStart] == 'n' { @@ -1949,7 +1949,7 @@ func IsValidIdentifier(s string, languageVersion core.ScriptTarget) bool { return false } for i, ch := range s { - if i == 0 && !isIdentifierStart(ch, languageVersion) || i != 0 && !isIdentifierPart(ch, languageVersion) { + if i == 0 && !IsIdentifierStart(ch, languageVersion) || i != 0 && !IsIdentifierPart(ch, languageVersion) { return false } } @@ -1961,11 +1961,11 @@ func isWordCharacter(ch rune) bool { return stringutil.IsASCIILetter(ch) || stringutil.IsDigit(ch) || ch == '_' } -func isIdentifierStart(ch rune, languageVersion core.ScriptTarget) bool { +func IsIdentifierStart(ch rune, languageVersion core.ScriptTarget) bool { return stringutil.IsASCIILetter(ch) || ch == '_' || ch == '$' || ch >= utf8.RuneSelf && isUnicodeIdentifierStart(ch, languageVersion) } -func isIdentifierPart(ch rune, languageVersion core.ScriptTarget) bool { +func IsIdentifierPart(ch rune, languageVersion core.ScriptTarget) bool { return isWordCharacter(ch) || ch == '$' || ch >= utf8.RuneSelf && isUnicodeIdentifierPart(ch, languageVersion) } @@ -2017,6 +2017,14 @@ func TokenToString(token ast.Kind) string { return tokenToText[token] } +func StringToToken(s string) ast.Kind { + kind, ok := textToToken[s] + if ok { + return kind + } + return ast.KindUnknown +} + func GetViableKeywordSuggestions() []string { result := make([]string, 0, len(textToKeyword)) for text := range textToKeyword { diff --git a/internal/scanner/utilities.go b/internal/scanner/utilities.go index 9cecd1385a..b225204942 100644 --- a/internal/scanner/utilities.go +++ b/internal/scanner/utilities.go @@ -48,12 +48,12 @@ func DeclarationNameToString(name *ast.Node) string { func IsIdentifierText(name string, languageVersion core.ScriptTarget) bool { ch, size := utf8.DecodeRuneInString(name) - if !isIdentifierStart(ch, languageVersion) { + if !IsIdentifierStart(ch, languageVersion) { return false } for i := size; i < len(name); { ch, size = utf8.DecodeRuneInString(name[i:]) - if !isIdentifierPart(ch, languageVersion) { + if !IsIdentifierPart(ch, languageVersion) { return false } i += size From 9d8edd1179a73e17610b8b1ccce61abe7a54b15a Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 10 Apr 2025 13:12:22 -0700 Subject: [PATCH 06/25] more completions: finish create completion entry --- internal/ast/ast.go | 1 + internal/checker/services.go | 4 + internal/ls/completions.go | 238 +++++++++++++++++++++++++++++++--- internal/ls/symbol_display.go | 125 ++++++++++++++++++ internal/ls/utilities.go | 48 +++++++ internal/lsp/server.go | 9 +- 6 files changed, 403 insertions(+), 22 deletions(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index b2d956b94b..071a36ec4b 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -1644,6 +1644,7 @@ type ( JsxOpeningFragmentNode = Node JsxClosingFragmentNode = Node SourceFileNode = Node + PropertyAccessExpressionNode = Node ) type ( diff --git a/internal/checker/services.go b/internal/checker/services.go index 34bd4c25e2..6e00923659 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -421,3 +421,7 @@ func (c *Checker) tryGetTarget(symbol *ast.Symbol) *ast.Symbol { } return target } + +func (c *Checker) GetExportSymbolOfSymbol(symbol *ast.Symbol) *ast.Symbol { + return c.getMergedSymbol(core.IfElse(symbol.ExportSymbol != nil, symbol.ExportSymbol, symbol)) +} diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 5f508afe01..ded4a80629 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -2,6 +2,7 @@ package ls import ( "fmt" + "maps" "slices" "strings" "unicode" @@ -14,6 +15,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -27,7 +29,7 @@ func (l *LanguageService) ProvideCompletion( if node.Kind == ast.KindSourceFile { return nil } - return l.getCompletionsAtPosition(program, file, position, context, nil /*preferences*/) // !!! get preferences + return l.getCompletionsAtPosition(program, file, position, context, nil /*preferences*/, clientOptions) // !!! get preferences } // !!! figure out other kinds of completion data return @@ -37,7 +39,7 @@ type completionData struct { completionKind CompletionKind isInSnippetScope bool // Note that the presence of this alone doesn't mean that we need a conversion. Only do that if the completion is not an ordinary identifier. - propertyAccessToConvert *ast.PropertyAccessExpression + propertyAccessToConvert *ast.PropertyAccessExpressionNode isNewIdentifierLocation bool location *ast.Node keywordFilters KeywordCompletionFilters @@ -238,7 +240,9 @@ func (l *LanguageService) getCompletionsAtPosition( file *ast.SourceFile, position int, context *lsproto.CompletionContext, - preferences *UserPreferences) *lsproto.CompletionList { + preferences *UserPreferences, + clientOptions *lsproto.ClientCompletionItemOptions, +) *lsproto.CompletionList { previousToken, _ := getRelevantTokens(position, file) if context.TriggerCharacter != nil && !isInString(file, position, previousToken) && !isValidTrigger(file, *context.TriggerCharacter, previousToken, position) { return nil @@ -259,7 +263,6 @@ func (l *LanguageService) getCompletionsAtPosition( } compilerOptions := program.GetCompilerOptions() - checker := program.GetTypeChecker() // !!! see if incomplete completion list and continue or clean @@ -275,7 +278,15 @@ func (l *LanguageService) getCompletionsAtPosition( // switch completionData.Kind // !!! other data cases // !!! transform data into completion list - response := completionInfoFromData(file, program, compilerOptions, completionData, preferences, position) + response := completionInfoFromData( + file, + program, + compilerOptions, + completionData, + preferences, + position, + clientOptions, + ) // !!! check if response is incomplete return response } @@ -304,7 +315,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position // Also determine whether we are trying to complete with members of that node // or attributes of a JSX tag. node := currentToken - var propertyAccessToConvert *ast.PropertyAccessExpression + var propertyAccessToConvert *ast.PropertyAccessExpressionNode isRightOfDot := false isRightOfQuestionDot := false isRightOfOpenTag := false @@ -328,8 +339,8 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position isRightOfQuestionDot = contextToken.Kind == ast.KindQuestionDotToken switch parent.Kind { case ast.KindPropertyAccessExpression: - propertyAccessToConvert = parent.AsPropertyAccessExpression() - node = propertyAccessToConvert.Expression + propertyAccessToConvert = parent + node = propertyAccessToConvert.Expression() leftMostAccessExpression := ast.GetLeftmostAccessExpression(parent) if ast.NodeIsMissing(leftMostAccessExpression) || ((ast.IsCallExpression(node) || ast.IsFunctionLike(node)) && @@ -752,6 +763,7 @@ func completionInfoFromData( data *completionData, preferences *UserPreferences, position int, + clientOptions *lsproto.ClientCompletionItemOptions, ) *lsproto.CompletionList { keywordFilters := data.keywordFilters symbols := data.symbols @@ -783,13 +795,30 @@ func completionInfoFromData( uniqueNames, sortedEntries := getCompletionEntriesFromSymbols( data, nil, /*replacementToken*/ + position, file, program, compilerOptions.GetEmitScriptTarget(), preferences, compilerOptions, + clientOptions, ) + if data.keywordFilters != KeywordCompletionFiltersNone { + keywordCompletions := getKeywordCompletions( + data.keywordFilters, + !data.insideJsDocTagTypeExpression && ast.IsSourceFileJs(sourceFile)) + for _, keywordEntry := keywordCompletions { + if data.isTypeOnlyLocation && isTypeKeyword(scanner.StringToToken(keywordEntry.name)) || + false { // !!! HERE HERE + + } + } + } + + + // !!! exhaustive case completions + // !!! here return nil } @@ -797,13 +826,16 @@ func completionInfoFromData( func getCompletionEntriesFromSymbols( data *completionData, replacementToken *ast.Node, + position int, file *ast.SourceFile, program *compiler.Program, target core.ScriptTarget, preferences *UserPreferences, compilerOptions *core.CompilerOptions, -) (uniqueNames uniqueNameSet, sortedEntries []*lsproto.CompletionItem) { + clientOptions *lsproto.ClientCompletionItemOptions, +) (uniqueNames *core.Set[string], sortedEntries []*lsproto.CompletionItem) { closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location) + useSemicolons := probablyUsesSemicolons(file) typeChecker := program.GetTypeChecker() // Tracks unique names. // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; @@ -837,11 +869,47 @@ func getCompletionEntriesFromSymbols( originalSortText = sortTextLocationPriority } sortText := core.IfElse(isDeprecated(symbol, typeChecker), deprecateSortText(originalSortText), originalSortText) + entry := createCompletionItem( + symbol, + sortText, + replacementToken, + data.contextToken, + data.location, + position, + file, + program, + name, + needsConvertPropertyAccess, + origin, + data.recommendedCompletion, + data.propertyAccessToConvert, + data.jsxInitializer, + data.importStatementCompletion, + useSemicolons, + compilerOptions, + preferences, + clientOptions, + data.completionKind, + data.isJsxIdentifierExpected, + data.isRightOfOpenTag, + ) + if entry == nil { + continue + } + /** True for locals; false for globals, module exports from other files, `this.` completions. */ + shouldShadowLaterSymbols := (origin == nil || originIsTypeOnlyAlias(origin)) && + !(symbol.Parent == nil && + !core.Some(symbol.Declarations, func(d *ast.Node) bool { return ast.GetSourceFileOfNode(d) == file })) + uniques[name] = shouldShadowLaterSymbols + core.InsertSorted(sortedEntries, entry, compareCompletionEntries) } - // !!! - return nil, nil + uniqueSet := core.NewSetWithSizeHint[string](len(uniques)) + for name := range maps.Keys(uniques) { + uniqueSet.Add(name) + } + return uniqueSet, sortedEntries } func createCompletionItem( @@ -861,7 +929,7 @@ func createCompletionItem( jsxInitializer jsxInitializer, importStatementCompletion any, useSemicolons bool, - options *core.CompilerOptions, + compilerOptions *core.CompilerOptions, preferences *UserPreferences, clientOptions *lsproto.ClientCompletionItemOptions, completionKind CompletionKind, @@ -869,7 +937,7 @@ func createCompletionItem( isRightOfOpenTag bool, ) *lsproto.CompletionItem { var insertText string - var filterText *string + var filterText string replacementSpan := getReplacementRangeForContextToken(file, replacementToken, position) var isSnippet, hasAction bool source := getSourceFromOrigin(origin) @@ -1058,7 +1126,7 @@ func createCompletionItem( parentNamedImportOrExport := ast.FindAncestor(location, isNamedImportsOrExports) if parentNamedImportOrExport != nil { - languageVersion := options.GetEmitScriptTarget() + languageVersion := compilerOptions.GetEmitScriptTarget() if !scanner.IsIdentifierText(name, languageVersion) { insertText = quotePropertyName(file, preferences, name) @@ -1087,15 +1155,93 @@ func createCompletionItem( if elementKind == ScriptElementKindWarning || elementKind == ScriptElementKindString { commitCharacters = []string{} } else { - commitCharacters = nil + commitCharacters = nil // Use the completion list default. + } + + kindModifiers := getSymbolModifiers(typeChecker, symbol) + var tags *[]lsproto.CompletionItemTag + var detail *string + // Copied from vscode ts extension. + if kindModifiers.Has(ScriptElementKindModifierOptional) { + if insertText == "" { + insertText = name + } + if filterText == "" { + filterText = name + } + name = name + "?" + } + if kindModifiers.Has(ScriptElementKindModifierDeprecated) { + tags = &[]lsproto.CompletionItemTag{lsproto.CompletionItemTagDeprecated} + } + if kind == lsproto.CompletionItemKindFile { + for _, extensionModifier := range fileExtensionKindModifiers { + if kindModifiers.Has(extensionModifier) { + if strings.HasSuffix(name, string(extensionModifier)) { + detail = ptrTo(name) + } else { + detail = ptrTo(name + string(extensionModifier)) + } + break + } + } + } + + if hasAction && source != "" { + // !!! adjust label like vscode does + } + + var insertTextFormat *lsproto.InsertTextFormat + if isSnippet { + insertTextFormat = ptrTo(lsproto.InsertTextFormatSnippet) + } else { + insertTextFormat = ptrTo(lsproto.InsertTextFormatPlainText) + } + + var textEdit *lsproto.TextEditOrInsertReplaceEdit + if replacementSpan != nil { + textEdit = &lsproto.TextEditOrInsertReplaceEdit{ + TextEdit: &lsproto.TextEdit{ + NewText: core.IfElse(insertText == "", name, insertText), + Range: *replacementSpan, + }, + } } return &lsproto.CompletionItem{ - SortText: ptrTo(string(sortText)), - FilterText: filterText, - InsertText: ptrTo(insertText), + Label: name, + LabelDetails: labelDetails, + Kind: &kind, + Tags: tags, + Detail: detail, + Preselect: boolToPtr(isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker)), + SortText: ptrTo(string(sortText)), + FilterText: strPtrTo(filterText), + InsertText: strPtrTo(insertText), + InsertTextFormat: insertTextFormat, + TextEdit: textEdit, + CommitCharacters: slicesPtrTo(commitCharacters), + Data: nil, // !!! auto-imports } - // !!! HERE +} + +func isRecommendedCompletionMatch(localSymbol *ast.Symbol, recommendedCompletion *ast.Symbol, typeChecker *checker.Checker) bool { + return localSymbol == recommendedCompletion || + localSymbol.Flags&ast.SymbolFlagsExportValue != 0 && typeChecker.GetExportSymbolOfSymbol(localSymbol) == recommendedCompletion +} + +func strPtrTo(v string) *string { + if v == "" { + return nil + } + return &v +} + +func slicesPtrTo[T any](s []T) *[]T { + if s == nil { + return nil + } + return &s } func ptrTo[T any](v T) *T { @@ -1109,6 +1255,13 @@ func ptrIsTrue(ptr *bool) bool { return *ptr } +func boolToPtr(v bool) *bool { + if v { + return ptrTo(true) + } + return nil +} + func getLineOfPosition(file *ast.SourceFile, pos int) int { line, _ := scanner.GetLineAndCharacterOfPosition(file, pos) return line @@ -1861,3 +2014,50 @@ func getCompletionsSymbolKind(kind ScriptElementKind) lsproto.CompletionItemKind } panic("Unhandled script element kind: " + kind) } + +// Editors will use the `sortText` and then fall back to `name` for sorting, but leave ties in response order. +// So, it's important that we sort those ties in the order we want them displayed if it matters. We don't +// strictly need to sort by name or SortText here since clients are going to do it anyway, but we have to +// do the work of comparing them so we can sort those ties appropriately; plus, it makes the order returned +// by the language service consistent with what TS Server does and what editors typically do. This also makes +// completions tests make more sense. We used to sort only alphabetically and only in the server layer, but +// this made tests really weird, since most fourslash tests don't use the server. +func compareCompletionEntries(entryInSlice *lsproto.CompletionItem, entryToInsert *lsproto.CompletionItem) int { + // !!! use locale-aware comparison + result := stringutil.CompareStringsCaseSensitive(*entryInSlice.SortText, *entryToInsert.SortText) + if result == stringutil.ComparisonEqual { + result = stringutil.CompareStringsCaseSensitive(entryInSlice.Label, entryToInsert.Label) + } + // !!! auto-imports + // if (result === Comparison.EqualTo && entryInArray.data?.moduleSpecifier && entryToInsert.data?.moduleSpecifier) { + // // Sort same-named auto-imports by module specifier + // result = compareNumberOfDirectorySeparators( + // (entryInArray.data as CompletionEntryDataResolved).moduleSpecifier, + // (entryToInsert.data as CompletionEntryDataResolved).moduleSpecifier, + // ); + // } + if result == stringutil.ComparisonEqual { + // Fall back to symbol order - if we return `EqualTo`, `insertSorted` will put later symbols first. + return stringutil.ComparisonLessThan + } + + return result +} + + +func getKeywordCompletions(keywordFilter KeywordCompletionFilters, filterOutTsOnlyKeywords bool) []*lsproto.CompletionItem { + if !filterOutTsOnlyKeywords { + return getTypescriptKeywordCompletions(keywordFilter) + } + + // !!! cache keyword list per filter + return core.Filter( + getTypescriptKeywordCompletions(keywordFilter), + func(ci *lsproto.CompletionItem) bool { + return !isTypeScriptOnlyKeyword(scanner.StringToToken(ci.Label)) + }) +} + +func getTypescriptKeywordCompletions(keywordFilter KeywordCompletionFilters) []*lsproto.CompletionItem { + // !!! cache keyword list per filter +} diff --git a/internal/ls/symbol_display.go b/internal/ls/symbol_display.go index 52240f771b..777eea8614 100644 --- a/internal/ls/symbol_display.go +++ b/internal/ls/symbol_display.go @@ -81,6 +81,48 @@ const ( ScriptElementKindLinkText ScriptElementKind = "link text" ) +type ScriptElementKindModifier string + +const ( + ScriptElementKindModifierNone ScriptElementKindModifier = "" + ScriptElementKindModifierPublic ScriptElementKindModifier = "public" + ScriptElementKindModifierPrivate ScriptElementKindModifier = "private" + ScriptElementKindModifierProtected ScriptElementKindModifier = "protected" + ScriptElementKindModifierExported ScriptElementKindModifier = "export" + ScriptElementKindModifierAmbient ScriptElementKindModifier = "declare" + ScriptElementKindModifierStatic ScriptElementKindModifier = "static" + ScriptElementKindModifierAbstract ScriptElementKindModifier = "abstract" + ScriptElementKindModifierOptional ScriptElementKindModifier = "optional" + ScriptElementKindModifierDeprecated ScriptElementKindModifier = "deprecated" + ScriptElementKindModifierDts ScriptElementKindModifier = ".d.ts" + ScriptElementKindModifierTs ScriptElementKindModifier = ".ts" + ScriptElementKindModifierTsx ScriptElementKindModifier = ".tsx" + ScriptElementKindModifierJs ScriptElementKindModifier = ".js" + ScriptElementKindModifierJsx ScriptElementKindModifier = ".jsx" + ScriptElementKindModifierJson ScriptElementKindModifier = ".json" + ScriptElementKindModifierDmts ScriptElementKindModifier = ".d.mts" + ScriptElementKindModifierMts ScriptElementKindModifier = ".mts" + ScriptElementKindModifierMjs ScriptElementKindModifier = ".mjs" + ScriptElementKindModifierDcts ScriptElementKindModifier = ".d.cts" + ScriptElementKindModifierCts ScriptElementKindModifier = ".cts" + ScriptElementKindModifierCjs ScriptElementKindModifier = ".cjs" +) + +var fileExtensionKindModifiers = []ScriptElementKindModifier{ + 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 { @@ -258,3 +300,86 @@ func isLocalVariableOrFunction(symbol *ast.Symbol) bool { } return false } + +func getSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) core.Set[ScriptElementKindModifier] { + if symbol == nil { + return core.Set[ScriptElementKindModifier]{} + } + + modifiers := getNormalizedSymbolModifiers(typeChecker, symbol) + if symbol.Flags&ast.SymbolFlagsAlias != 0 { + resolvedSymbol := typeChecker.GetAliasedSymbol(symbol) + if resolvedSymbol != symbol { + aliasModifiers := getNormalizedSymbolModifiers(typeChecker, resolvedSymbol) + for modifier := range aliasModifiers.Keys() { + modifiers.Add(modifier) + } + } + } + if symbol.Flags&ast.SymbolFlagsOptional != 0 { + modifiers.Add(ScriptElementKindModifierOptional) + } + + return modifiers +} + +func getNormalizedSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) core.Set[ScriptElementKindModifier] { + var modifierSet core.Set[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 && + typeChecker.IsDeprecatedDeclaration(declaration) && // !!! include jsdoc node flags + core.Some(declarations, func(d *ast.Node) bool { return !typeChecker.IsDeprecatedDeclaration(d) }) { + excludeFlags = ast.ModifierFlagsDeprecated + } else { + excludeFlags = ast.ModifierFlagsNone + } + modifierSet = getNodeModifiers(declaration, excludeFlags) + } + + return modifierSet +} + +func getNodeModifiers(node *ast.Node, excludeFlags ast.ModifierFlags) core.Set[ScriptElementKindModifier] { + var result core.Set[ScriptElementKindModifier] + var flags ast.ModifierFlags + if ast.IsDeclaration(node) { + flags = ast.GetCombinedModifierFlags(node) & ^excludeFlags // !!! include jsdoc node flags + } + + if flags&ast.ModifierFlagsPrivate != 0 { + result.Add(ScriptElementKindModifierPrivate) + } + if flags&ast.ModifierFlagsProtected != 0 { + result.Add(ScriptElementKindModifierProtected) + } + if flags&ast.ModifierFlagsPublic != 0 { + result.Add(ScriptElementKindModifierPublic) + } + if flags&ast.ModifierFlagsStatic != 0 { + result.Add(ScriptElementKindModifierStatic) + } + if flags&ast.ModifierFlagsAbstract != 0 { + result.Add(ScriptElementKindModifierAbstract) + } + if flags&ast.ModifierFlagsExport != 0 { + result.Add(ScriptElementKindModifierExported) + } + if flags&ast.ModifierFlagsDeprecated != 0 { + result.Add(ScriptElementKindModifierDeprecated) + } + if flags&ast.ModifierFlagsAmbient != 0 { + result.Add(ScriptElementKindModifierAmbient) + } + if node.Flags&ast.NodeFlagsAmbient != 0 { + result.Add(ScriptElementKindModifierAmbient) + } + if node.Kind == ast.KindExportAssignment { + result.Add(ScriptElementKindModifierExported) + } + + return result +} diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index b8c27ae063..5f188b20bd 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -265,3 +265,51 @@ func nodeIsASICandidate(node *ast.Node, file *ast.SourceFile) bool { func isNonContextualKeyword(token ast.Kind) bool { return ast.IsKeywordKind(token) && !ast.IsContextualKeyword(token) } + +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 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.GetLineAndCharacterOfPosition(file, getStartOfNode(lastToken, file)) + nextTokenLine, _ := scanner.GetLineAndCharacterOfPosition(file, scanner.GetRangeOfTokenAtPosition(file, lastToken.End()).Pos()) + // 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 + } + + // If even 2/5 places have a semicolon, the user probably wants semicolons + return withSemicolon/withoutSemicolon > 1/nStatementsToObserve +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 1f7aa73bb2..f98355ed07 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -381,9 +381,12 @@ func (s *Server) handleCompletion(req *lsproto.RequestMessage) error { return s.sendError(req.ID, err) } - list := project.LanguageService().ProvideCompletion(file.FileName(), pos, params.Context) - // !!! convert completion to `lsproto.CompletionList` - return s.sendResult(req.ID, nil) // !!! + list := project.LanguageService().ProvideCompletion( + file.FileName(), + pos, + params.Context, + s.initializeParams.Capabilities.TextDocument.Completion.CompletionItem) + return s.sendResult(req.ID, list) } func (s *Server) getFileAndProject(uri lsproto.DocumentUri) (*project.ScriptInfo, *project.Project) { From 75a781c27c81ec5bc2361485f6426de7f35423b5 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 11 Apr 2025 15:28:02 -0700 Subject: [PATCH 07/25] more completions --- internal/ast/ast.go | 15 +- internal/ast/utilities.go | 9 + internal/ls/completions.go | 413 ++++++++++++++++++++++++++++--------- internal/ls/utilities.go | 78 ++++++- internal/lsp/server.go | 2 +- internal/parser/parser.go | 10 +- 6 files changed, 419 insertions(+), 108 deletions(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 675e013e52..4201a142e1 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -724,6 +724,18 @@ func (n *Node) Children() *NodeList { panic("Unhandled case in Node.Children: " + n.Kind.String()) } +func (n *Node) ModuleSpecifier() *Expression { + switch n.Kind { + case KindImportDeclaration: + return n.AsImportDeclaration().ModuleSpecifier + case KindExportDeclaration: + return n.AsExportDeclaration().ModuleSpecifier + case KindJSDocImportTag: + return n.AsJSDocImportTag().ModuleSpecifier + } + panic("Unhandled case in Node.ModuleSpecifier: " + n.Kind.String()) +} + // Determines if `n` contains `descendant` by walking up the `Parent` pointers from `descendant`. This method panics if // `descendant` or one of its ancestors is not parented except when that node is a `SourceFile`. func (n *Node) Contains(descendant *Node) bool { @@ -1633,6 +1645,7 @@ type ( StringLiteralLike = Node // StringLiteral | NoSubstitutionTemplateLiteral AnyValidImportOrReExport = Node // (ImportDeclaration | ExportDeclaration | JSDocImportTag) & { moduleSpecifier: StringLiteral } | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteral }} | RequireOrImportCall | ValidImportTypeNode ValidImportTypeNode = Node // ImportTypeNode & { argument: LiteralTypeNode & { literal: StringLiteral } } + NumericOrStringLikeLiteral = Node // StringLiteralLike | NumericLiteral ) // Aliases for node singletons @@ -9407,7 +9420,7 @@ func (node *JSDocThisTag) Clone(f *NodeFactory) *Node { type JSDocImportTag struct { JSDocTagBase ImportClause *Declaration - ModuleSpecifier *Node + ModuleSpecifier *Expression Attributes *Node } diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 0fab89d490..867c32898f 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -2768,3 +2768,12 @@ func IsThisInTypeQuery(node *Node) bool { func IsLet(node *Node) bool { return GetCombinedNodeFlags(node)&NodeFlagsBlockScoped == NodeFlagsLet } + +func IsClassMemberModifier(token Kind) bool { + return IsParameterPropertyModifier(token) || token == KindStaticKeyword || + token == KindOverrideKeyword || token == KindAccessorKeyword +} + +func IsParameterPropertyModifier(kind Kind) bool { + return ModifierToFlag(kind)&ModifierFlagsParameterPropertyModifier != 0 +} diff --git a/internal/ls/completions.go b/internal/ls/completions.go index b026ed561a..8a0da52997 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -5,14 +5,17 @@ import ( "maps" "slices" "strings" + "sync" "unicode" "unicode/utf8" "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/compiler" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/jsnum" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" @@ -22,7 +25,7 @@ func (l *LanguageService) ProvideCompletion( fileName string, position int, context *lsproto.CompletionContext, - clientOptions *lsproto.ClientCompletionItemOptions) *lsproto.CompletionList { + clientOptions *lsproto.CompletionClientCapabilities) *lsproto.CompletionList { program, file := l.getProgramAndFile(fileName) node := astnav.GetTouchingPropertyName(file, position) if node.Kind == ast.KindSourceFile { @@ -42,7 +45,7 @@ type completionData struct { isNewIdentifierLocation bool location *ast.Node keywordFilters KeywordCompletionFilters - literals []any + literals []literalValue symbolToOriginInfoMap map[ast.SymbolId]*symbolOriginInfo symbolToSortTextMap map[ast.SymbolId]sortText recommendedCompletion *ast.Symbol @@ -232,7 +235,9 @@ const ( // Value is set to false for global variables or completions from external module exports, // true otherwise. -type uniqueNameSet = map[string]bool +type uniqueNamesMap = map[string]bool + +type literalValue any // string | jsnum.Number | PseudoBigInt func (l *LanguageService) getCompletionsAtPosition( program *compiler.Program, @@ -240,7 +245,7 @@ func (l *LanguageService) getCompletionsAtPosition( position int, context *lsproto.CompletionContext, preferences *UserPreferences, - clientOptions *lsproto.ClientCompletionItemOptions, + clientOptions *lsproto.CompletionClientCapabilities, ) *lsproto.CompletionList { previousToken, _ := getRelevantTokens(position, file) if context.TriggerCharacter != nil && !isInString(file, position, previousToken) && !isValidTrigger(file, *context.TriggerCharacter, previousToken, position) { @@ -254,7 +259,7 @@ func (l *LanguageService) getCompletionsAtPosition( return &lsproto.CompletionList{ IsIncomplete: true, ItemDefaults: &lsproto.CompletionItemDefaults{ // !!! do we need this if no entries? also, check if client supports item defaults - CommitCharacters: getDefaultCommitCharacters(true /*isNewIdentifierLocation*/), + CommitCharacters: ptrTo(getDefaultCommitCharacters(true /*isNewIdentifierLocation*/)), }, } } @@ -359,7 +364,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position node = parent case ast.KindMetaProperty: node = getFirstToken(parent, file) - if node.Kind != ast.KindImportKeyword || node.Kind != ast.KindNewKeyword { + if node.Kind != ast.KindImportKeyword && node.Kind != ast.KindNewKeyword { panic("Unexpected token kind: " + node.Kind.String()) } default: @@ -554,11 +559,11 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position addTypeProperties := func(t *checker.Type, insertAwait bool, insertQuestionDot bool) { if typeChecker.GetStringIndexType(t) != nil { isNewIdentifierLocation = true - defaultCommitCharacters = make([]string, 0) + defaultCommitCharacters = []string{} } if isRightOfQuestionDot && len(typeChecker.GetCallSignatures(t)) != 0 { isNewIdentifierLocation = true - if len(defaultCommitCharacters) == 0 { + if defaultCommitCharacters == nil { defaultCommitCharacters = slices.Clone(allCommitCharacters) // Only invalid commit character here would be `(`. } } @@ -615,7 +620,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position isNamespaceName := ast.IsModuleDeclaration(node.Parent) if isNamespaceName { isNewIdentifierLocation = true - defaultCommitCharacters = make([]string, 0) + defaultCommitCharacters = []string{} } symbol := typeChecker.GetSymbolAtLocation(node) if symbol != nil { @@ -706,7 +711,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position // exclude literal suggestions after (#51667) and after closing quote (#52675) // for strings getStringLiteralCompletions handles completions isLiteralExpected := !ast.IsStringLiteralLike(previousToken) && !isJsxIdentifierExpected - var literals []any + var literals []literalValue if isLiteralExpected { var types []*checker.Type if contextualType != nil && checker.IsUnion(contextualType) { @@ -714,7 +719,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position } else if contextualType != nil { types = []*checker.Type{contextualType} } - literals = core.MapNonNil(types, func(t *checker.Type) any { + literals = core.MapNonNil(types, func(t *checker.Type) literalValue { if isLiteral(t) && !checker.IsEnumLiteral(t) { return t.AsLiteralType().Value() } @@ -727,6 +732,10 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position recommendedCompletion = getRecommendedCompletion(previousToken, contextualType, typeChecker) } + if defaultCommitCharacters == nil { + defaultCommitCharacters = getDefaultCommitCharacters(isNewIdentifierLocation) + } + return &completionData{ symbols: symbols, completionKind: completionKind, @@ -753,6 +762,13 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position } } +func getDefaultCommitCharacters(isNewIdentifierLocation bool) []string { + if isNewIdentifierLocation { + return []string{} + } + return slices.Clone(allCommitCharacters) +} + func completionInfoFromData( file *ast.SourceFile, program *compiler.Program, @@ -760,14 +776,13 @@ func completionInfoFromData( data *completionData, preferences *UserPreferences, position int, - clientOptions *lsproto.ClientCompletionItemOptions, + clientOptions *lsproto.CompletionClientCapabilities, ) *lsproto.CompletionList { keywordFilters := data.keywordFilters symbols := data.symbols isNewIdentifierLocation := data.isNewIdentifierLocation contextToken := data.contextToken - // literals := data.literals // !!! undo - // typeChecker := program.GetTypeChecker() // !!! undo + literals := data.literals // Verify if the file is JSX language variant if ast.GetLanguageVariant(file.ScriptKind) == core.LanguageVariantJSX { @@ -807,20 +822,62 @@ func completionInfoFromData( !data.insideJsDocTagTypeExpression && ast.IsSourceFileJs(file)) for _, keywordEntry := range keywordCompletions { if data.isTypeOnlyLocation && isTypeKeyword(scanner.StringToToken(keywordEntry.Label)) || - false { // !!! HERE HERE - + !data.isTypeOnlyLocation && isContextualKeywordInAutoImportableExpressionSpace(keywordEntry.Label) || + !uniqueNames.Has(keywordEntry.Label) { + uniqueNames.Add(keywordEntry.Label) + sortedEntries = core.InsertSorted(sortedEntries, keywordEntry, compareCompletionEntries) } } } + for _, keywordEntry := range getContextualKeywords(file, contextToken, position) { + if !uniqueNames.Has(keywordEntry.Label) { + uniqueNames.Add(keywordEntry.Label) + sortedEntries = core.InsertSorted(sortedEntries, keywordEntry, compareCompletionEntries) + } + } + + for _, literal := range literals { + literalEntry := createCompletionItemForLiteral(file, preferences, literal) + uniqueNames.Add(literalEntry.Label) + sortedEntries = core.InsertSorted(sortedEntries, literalEntry, compareCompletionEntries) + } + + if !isChecked { + sortedEntries = getJSCompletionEntries( + file, + position, + uniqueNames, + compilerOptions.GetEmitScriptTarget(), + sortedEntries, + ) + } + // !!! exhaustive case completions - // !!! here - // !!! undo - if uniqueNames != nil && sortedEntries != nil { + // !!! revisit this when we decide what the protocol type should be + items := make([]lsproto.CompletionItem, len(sortedEntries)) + for i, entry := range sortedEntries { + items[i] = *entry + } + var defaultCommitCharacters *[]string + if supportsDefaultCommitCharacters(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.CommitCharactersSupport) { + defaultCommitCharacters = &data.defaultCommitCharacters + } + + var itemDefaults *lsproto.CompletionItemDefaults + if defaultCommitCharacters != nil { + itemDefaults = &lsproto.CompletionItemDefaults{ + CommitCharacters: defaultCommitCharacters, + } + } + + return &lsproto.CompletionList{ + IsIncomplete: data.hasUnresolvedAutoImports, + ItemDefaults: itemDefaults, + Items: items, } - return nil } func getCompletionEntriesFromSymbols( @@ -832,7 +889,7 @@ func getCompletionEntriesFromSymbols( target core.ScriptTarget, preferences *UserPreferences, compilerOptions *core.CompilerOptions, - clientOptions *lsproto.ClientCompletionItemOptions, + clientOptions *lsproto.CompletionClientCapabilities, ) (uniqueNames *core.Set[string], sortedEntries []*lsproto.CompletionItem) { closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location) useSemicolons := probablyUsesSemicolons(file) @@ -841,7 +898,7 @@ func getCompletionEntriesFromSymbols( // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; // true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports. // So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name. - uniques := make(uniqueNameSet) + uniques := make(uniqueNamesMap) for _, symbol := range data.symbols { symbolId := ast.GetSymbolId(symbol) origin := data.symbolToOriginInfoMap[symbolId] @@ -873,25 +930,17 @@ func getCompletionEntriesFromSymbols( symbol, sortText, replacementToken, - data.contextToken, - data.location, + data, position, file, program, name, needsConvertPropertyAccess, origin, - data.recommendedCompletion, - data.propertyAccessToConvert, - data.jsxInitializer, - data.importStatementCompletion, useSemicolons, compilerOptions, preferences, clientOptions, - data.completionKind, - data.isJsxIdentifierExpected, - data.isRightOfOpenTag, ) if entry == nil { continue @@ -902,7 +951,7 @@ func getCompletionEntriesFromSymbols( !(symbol.Parent == nil && !core.Some(symbol.Declarations, func(d *ast.Node) bool { return ast.GetSourceFileOfNode(d) == file })) uniques[name] = shouldShadowLaterSymbols - core.InsertSorted(sortedEntries, entry, compareCompletionEntries) + sortedEntries = core.InsertSorted(sortedEntries, entry, compareCompletionEntries) } uniqueSet := core.NewSetWithSizeHint[string](len(uniques)) @@ -912,30 +961,53 @@ func getCompletionEntriesFromSymbols( return uniqueSet, sortedEntries } +func completionNameForLiteral( + file *ast.SourceFile, + preferences *UserPreferences, + literal literalValue, +) string { + switch literal.(type) { + case string: + return quote(file, preferences, literal.(string)) + case jsnum.Number: + name, _ := core.StringifyJson(literal, "" /*prefix*/, "" /*suffix*/) + return name + case jsnum.PseudoBigInt: + return literal.(jsnum.PseudoBigInt).String() + "n" + } + panic(fmt.Sprintf("Unhandled literal value: %v", literal)) +} + +func createCompletionItemForLiteral( + file *ast.SourceFile, + preferences *UserPreferences, + literal literalValue, +) *lsproto.CompletionItem { + return &lsproto.CompletionItem{ + Label: completionNameForLiteral(file, preferences, literal), + Kind: ptrTo(lsproto.CompletionItemKindConstant), + SortText: ptrTo(string(sortTextLocationPriority)), + CommitCharacters: ptrTo([]string{}), + } +} + func createCompletionItem( symbol *ast.Symbol, sortText sortText, replacementToken *ast.Node, - contextToken *ast.Node, - location *ast.Node, + data *completionData, position int, file *ast.SourceFile, program *compiler.Program, name string, needsConvertPropertyAccess bool, origin *symbolOriginInfo, - recommendedCompletion *ast.Symbol, - propertyAccessToConvert *ast.Node, - jsxInitializer jsxInitializer, - importStatementCompletion any, useSemicolons bool, compilerOptions *core.CompilerOptions, preferences *UserPreferences, - clientOptions *lsproto.ClientCompletionItemOptions, - completionKind CompletionKind, - isJsxIdentifierExpected bool, - isRightOfOpenTag bool, + clientOptions *lsproto.CompletionClientCapabilities, ) *lsproto.CompletionItem { + contextToken := data.contextToken var insertText string var filterText string replacementSpan := getReplacementRangeForContextToken(file, replacementToken, position) @@ -958,7 +1030,7 @@ func createCompletionItem( core.IfElse(insertQuestionDot, "?.", ""), name) } - } else if propertyAccessToConvert != nil && (useBraces || insertQuestionDot) { + } else if data.propertyAccessToConvert != nil && (useBraces || insertQuestionDot) { // We should only have needsConvertPropertyAccess if there's a property access to convert. But see #21790. // Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro. if useBraces { @@ -971,13 +1043,13 @@ func createCompletionItem( insertText = name } - if insertQuestionDot || propertyAccessToConvert.AsPropertyAccessExpression().QuestionDotToken != nil { + if insertQuestionDot || data.propertyAccessToConvert.AsPropertyAccessExpression().QuestionDotToken != nil { insertText = fmt.Sprintf("?.%s", insertText) } - dot := findChildOfKind(propertyAccessToConvert, ast.KindDotToken, file) + dot := findChildOfKind(data.propertyAccessToConvert, ast.KindDotToken, file) if dot == nil { - dot = findChildOfKind(propertyAccessToConvert, ast.KindQuestionDotToken, file) + dot = findChildOfKind(data.propertyAccessToConvert, ast.KindQuestionDotToken, file) } if dot == nil { @@ -986,51 +1058,55 @@ func createCompletionItem( // If the text after the '.' starts with this name, write over it. Else, add new text. var end int - if strings.HasPrefix(name, propertyAccessToConvert.Name().Text()) { - end = propertyAccessToConvert.End() + if strings.HasPrefix(name, data.propertyAccessToConvert.Name().Text()) { + end = data.propertyAccessToConvert.End() } else { end = dot.End() } replacementSpan = createLspRangeFromBounds(getStartOfNode(dot, file), end, file) } - if jsxInitializer.isInitializer { + if data.jsxInitializer.isInitializer { if insertText == "" { insertText = name } insertText = fmt.Sprintf("{%s}", insertText) - if jsxInitializer.initializer != nil { - replacementSpan = createLspRangeFromNode(jsxInitializer.initializer, file) + if data.jsxInitializer.initializer != nil { + replacementSpan = createLspRangeFromNode(data.jsxInitializer.initializer, file) } } - if originIsPromise(origin) && propertyAccessToConvert != nil { + if originIsPromise(origin) && data.propertyAccessToConvert != nil { if insertText == "" { insertText = name } - precedingToken := astnav.FindPrecedingToken(file, propertyAccessToConvert.Pos()) + precedingToken := astnav.FindPrecedingToken(file, data.propertyAccessToConvert.Pos()) var awaitText string if precedingToken != nil && positionIsASICandidate(precedingToken.End(), precedingToken.Parent, file) { awaitText = ";" } - awaitText += "(await " + scanner.GetTextOfNode(propertyAccessToConvert.Expression()) + ")" + awaitText += "(await " + scanner.GetTextOfNode(data.propertyAccessToConvert.Expression()) + ")" if needsConvertPropertyAccess { insertText = awaitText + insertText } else { dotStr := core.IfElse(insertQuestionDot, "?.", ".") insertText = awaitText + dotStr + insertText } - isInAwaitExpression := ast.IsAwaitExpression(propertyAccessToConvert.Parent) - wrapNode := core.IfElse(isInAwaitExpression, propertyAccessToConvert.Parent, propertyAccessToConvert.Expression()) - replacementSpan = createLspRangeFromBounds(getStartOfNode(wrapNode, file), propertyAccessToConvert.End(), file) + isInAwaitExpression := ast.IsAwaitExpression(data.propertyAccessToConvert.Parent) + wrapNode := core.IfElse( + isInAwaitExpression, + data.propertyAccessToConvert.Parent, + data.propertyAccessToConvert.Expression(), + ) + replacementSpan = createLspRangeFromBounds(getStartOfNode(wrapNode, file), data.propertyAccessToConvert.End(), file) } if originIsResolvedExport(origin) { labelDetails = &lsproto.CompletionItemLabelDetails{ Description: &origin.asResolvedExport().moduleSpecifier, // !!! vscode @link support } - if importStatementCompletion != nil { + if data.importStatementCompletion != nil { // !!! auto-imports } } @@ -1050,7 +1126,7 @@ func createCompletionItem( // const cc: I = { a: "red" | } // // Completion should add a comma after "red" and provide completions for b - if completionKind == CompletionKindObjectPropertyDeclaration && + if data.completionKind == CompletionKindObjectPropertyDeclaration && contextToken != nil && !ast.NodeHasKind(astnav.FindPrecedingTokenEx(file, contextToken.Pos(), contextToken), ast.KindCommaToken) { if ast.IsMethodDeclaration(contextToken.Parent.Parent) || @@ -1066,8 +1142,8 @@ func createCompletionItem( } if preferences.includeCompletionsWithClassMemberSnippets && - completionKind == CompletionKindMemberLike && - isClassLikeMemberCompletion(symbol, location, file) { + data.completionKind == CompletionKindMemberLike && + isClassLikeMemberCompletion(symbol, data.location, file) { // !!! class member completions } @@ -1075,7 +1151,7 @@ func createCompletionItem( insertText = origin.asObjectLiteralMethod().insertText isSnippet = origin.asObjectLiteralMethod().isSnippet labelDetails = origin.asObjectLiteralMethod().labelDetails // !!! check if this can conflict with case above where we set label details - if !ptrIsTrue(clientOptions.LabelDetailsSupport) { + if !ptrIsTrue(clientOptions.CompletionItem.LabelDetailsSupport) { name = name + *origin.asObjectLiteralMethod().labelDetails.Detail labelDetails = nil } @@ -1083,13 +1159,13 @@ func createCompletionItem( sortText = sortBelow(sortText) } - if isJsxIdentifierExpected && - !isRightOfOpenTag && - ptrIsTrue(clientOptions.SnippetSupport) && + if data.isJsxIdentifierExpected && + !data.isRightOfOpenTag && + ptrIsTrue(clientOptions.CompletionItem.SnippetSupport) && preferences.jsxAttributeCompletionStyle != JsxAttributeCompletionStyleNone && - !(ast.IsJsxAttribute(location.Parent) && location.Parent.Initializer() != nil) { + !(ast.IsJsxAttribute(data.location.Parent) && data.location.Parent.Initializer() != nil) { useBraces := preferences.jsxAttributeCompletionStyle == JsxAttributeCompletionStyleBraces - t := typeChecker.GetTypeOfSymbolAtLocation(symbol, location) + t := typeChecker.GetTypeOfSymbolAtLocation(symbol, data.location) // If is boolean like or undefined, don't return a snippet, we want to return just the completion. if preferences.jsxAttributeCompletionStyle == JsxAttributeCompletionStyleAuto && @@ -1124,7 +1200,7 @@ func createCompletionItem( // hasAction = importStatementCompletion == nil } - parentNamedImportOrExport := ast.FindAncestor(location, isNamedImportsOrExports) + parentNamedImportOrExport := ast.FindAncestor(data.location, isNamedImportsOrExports) if parentNamedImportOrExport != nil { languageVersion := compilerOptions.GetEmitScriptTarget() if !scanner.IsIdentifierText(name, languageVersion) { @@ -1144,18 +1220,21 @@ func createCompletionItem( possibleToken := scanner.StringToToken(name) if possibleToken != ast.KindUnknown && (possibleToken == ast.KindAwaitKeyword || isNonContextualKeyword(possibleToken)) { - insertText = fmt.Sprintf("%s as %s_") + insertText = fmt.Sprintf("%s as %s_", name, name) } } } - elementKind := getSymbolKind(typeChecker, symbol, location) + elementKind := getSymbolKind(typeChecker, symbol, data.location) kind := getCompletionsSymbolKind(elementKind) - var commitCharacters []string - if elementKind == ScriptElementKindWarning || elementKind == ScriptElementKindString { - commitCharacters = []string{} - } else { - commitCharacters = nil // Use the completion list default. + var commitCharacters *[]string + if ptrIsTrue(clientOptions.CompletionItem.CommitCharactersSupport) { + if elementKind == ScriptElementKindWarning || elementKind == ScriptElementKindString { + commitCharacters = &[]string{} + } else if !supportsDefaultCommitCharacters(clientOptions) { + commitCharacters = ptrTo(data.defaultCommitCharacters) + } + // Otherwise use the completion list default. } kindModifiers := getSymbolModifiers(typeChecker, symbol) @@ -1207,6 +1286,7 @@ func createCompletionItem( }, } } + // !!! adjust text edit like vscode does when Strada's `isMemberCompletion` is true return &lsproto.CompletionItem{ Label: name, @@ -1214,17 +1294,22 @@ func createCompletionItem( Kind: &kind, Tags: tags, Detail: detail, - Preselect: boolToPtr(isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker)), + Preselect: boolToPtr(isRecommendedCompletionMatch(symbol, data.recommendedCompletion, typeChecker)), SortText: ptrTo(string(sortText)), FilterText: strPtrTo(filterText), InsertText: strPtrTo(insertText), InsertTextFormat: insertTextFormat, TextEdit: textEdit, - CommitCharacters: slicesPtrTo(commitCharacters), + CommitCharacters: commitCharacters, Data: nil, // !!! auto-imports } } +func supportsDefaultCommitCharacters(clientOptions *lsproto.CompletionClientCapabilities) bool { + return clientOptions.CompletionList.ItemDefaults != nil && + slices.Contains(*clientOptions.CompletionList.ItemDefaults, "commitCharacters") +} + func isRecommendedCompletionMatch(localSymbol *ast.Symbol, recommendedCompletion *ast.Symbol, typeChecker *checker.Checker) bool { return localSymbol == recommendedCompletion || localSymbol.Flags&ast.SymbolFlagsExportValue != 0 && typeChecker.GetExportSymbolOfSymbol(localSymbol) == recommendedCompletion @@ -1237,13 +1322,6 @@ func strPtrTo(v string) *string { return &v } -func slicesPtrTo[T any](s []T) *[]T { - if s == nil { - return nil - } - return &s -} - func ptrIsTrue(ptr *bool) bool { if ptr == nil { return false @@ -1547,11 +1625,6 @@ func binaryExpressionMayBeOpenTag(binaryExpression *ast.BinaryExpression) bool { return ast.NodeIsMissing(binaryExpression.Left) } -func getDefaultCommitCharacters(isNewIdentifierLocation bool) *[]string { - // !!! - return nil -} - func isCheckedFile(file *ast.SourceFile, compilerOptions *core.CompilerOptions) bool { return !ast.IsSourceFileJs(file) || ast.IsCheckJSEnabledForFile(file, compilerOptions) } @@ -1705,7 +1778,7 @@ func getContextualType(previousToken *ast.Node, position int, file *ast.SourceFi // argInfo := getArgumentInfoForCompletions(previousToken, position, file, typeChecker) // !!! signature help var argInfo *struct{} // !!! signature help if argInfo != nil { - // return typeChecker.GetContextualTypeForArgumentAtIndex() // !!! signature help + // !!! signature help return nil } else if isEqualityOperatorKind(previousToken.Kind) && ast.IsBinaryExpression(parent) && isEqualityOperatorKind(parent.AsBinaryExpression().OperatorToken.Kind) { // completion at `x ===/**/` @@ -2040,25 +2113,175 @@ func compareCompletionEntries(entryInSlice *lsproto.CompletionItem, entryToInser return result } +var keywordCompletionsCache = collections.SyncMap[KeywordCompletionFilters, []*lsproto.CompletionItem]{} +var allKeywordCompletions = sync.OnceValue(func() []*lsproto.CompletionItem { + result := make([]*lsproto.CompletionItem, 0, ast.KindLastKeyword-ast.KindFirstKeyword+1) + for i := ast.KindFirstKeyword; i <= ast.KindLastKeyword; i++ { + result = append(result, &lsproto.CompletionItem{ + Label: scanner.TokenToString(i), + Kind: ptrTo(lsproto.CompletionItemKindKeyword), + SortText: ptrTo(string(sortTextGlobalsOrKeywords)), + }) + } + return result +}) + func getKeywordCompletions(keywordFilter KeywordCompletionFilters, filterOutTsOnlyKeywords bool) []*lsproto.CompletionItem { if !filterOutTsOnlyKeywords { return getTypescriptKeywordCompletions(keywordFilter) } - // !!! cache keyword list per filter - return core.Filter( + index := keywordFilter + KeywordCompletionFiltersLast + 1 + if cached, ok := keywordCompletionsCache.Load(index); ok { + return cached + } + result := core.Filter( getTypescriptKeywordCompletions(keywordFilter), func(ci *lsproto.CompletionItem) bool { return !isTypeScriptOnlyKeyword(scanner.StringToToken(ci.Label)) }) + keywordCompletionsCache.Store(index, result) + return result } func getTypescriptKeywordCompletions(keywordFilter KeywordCompletionFilters) []*lsproto.CompletionItem { - // !!! cache keyword list per filter - // !!! here - return nil + if cached, ok := keywordCompletionsCache.Load(keywordFilter); ok { + return cached + } + result := core.Filter(allKeywordCompletions(), func(entry *lsproto.CompletionItem) bool { + kind := scanner.StringToToken(entry.Label) + switch keywordFilter { + case KeywordCompletionFiltersNone: + return false + case KeywordCompletionFiltersAll: + return isFunctionLikeBodyKeyword(kind) || + kind == ast.KindDeclareKeyword || + kind == ast.KindModuleKeyword || + kind == ast.KindTypeKeyword || + kind == ast.KindNamespaceKeyword || + kind == ast.KindAbstractKeyword || + isTypeKeyword(kind) && kind != ast.KindUndefinedKeyword + case KeywordCompletionFiltersFunctionLikeBodyKeywords: + return isFunctionLikeBodyKeyword(kind) + case KeywordCompletionFiltersClassElementKeywords: + return isClassMemberCompletionKeyword(kind) + case KeywordCompletionFiltersInterfaceElementKeywords: + return isInterfaceOrTypeLiteralCompletionKeyword(kind) + case KeywordCompletionFiltersConstructorParameterKeywords: + return ast.IsParameterPropertyModifier(kind) + case KeywordCompletionFiltersTypeAssertionKeywords: + return isTypeKeyword(kind) || kind == ast.KindConstKeyword + case KeywordCompletionFiltersTypeKeywords: + return isTypeKeyword(kind) + case KeywordCompletionFiltersTypeKeyword: + return kind == ast.KindTypeKeyword + default: + panic(fmt.Sprintf("Unknown keyword filter: %v", keywordFilter)) + } + }) + + keywordCompletionsCache.Store(keywordFilter, result) + return result } func isTypeScriptOnlyKeyword(kind ast.Kind) bool { return false // !!! here } + +func isFunctionLikeBodyKeyword(kind ast.Kind) bool { + return kind == ast.KindAsyncKeyword || + kind == ast.KindAwaitKeyword || + kind == ast.KindUsingKeyword || + kind == ast.KindAsKeyword || + kind == ast.KindAssertsKeyword || + kind == ast.KindTypeKeyword || + !ast.IsContextualKeyword(kind) && !isClassMemberCompletionKeyword(kind) +} + +func isClassMemberCompletionKeyword(kind ast.Kind) bool { + switch kind { + case ast.KindAbstractKeyword, ast.KindAccessorKeyword: + case ast.KindConstructorKeyword: + case ast.KindGetKeyword: + case ast.KindSetKeyword: + case ast.KindAsyncKeyword: + case ast.KindDeclareKeyword: + case ast.KindOverrideKeyword: + return true + default: + return ast.IsClassMemberModifier(kind) + } + panic("unreachable") +} + +func isInterfaceOrTypeLiteralCompletionKeyword(kind ast.Kind) bool { + return kind == ast.KindReadonlyKeyword +} + +func isContextualKeywordInAutoImportableExpressionSpace(keyword string) bool { + return keyword == "abstract" || + keyword == "async" || + keyword == "await" || + keyword == "declare" || + keyword == "module" || + keyword == "namespace" || + keyword == "type" || + keyword == "satisfies" || + keyword == "as" +} + +func getContextualKeywords(file *ast.SourceFile, contextToken *ast.Node, position int) []*lsproto.CompletionItem { + var entries []*lsproto.CompletionItem + // An `AssertClause` can come after an import declaration: + // import * from "foo" | + // import "foo" | + // or after a re-export declaration that has a module specifier: + // export { foo } from "foo" | + // Source: https://tc39.es/proposal-import-assertions/ + if contextToken != nil { + parent := contextToken.Parent + tokenLine, _ := scanner.GetLineAndCharacterOfPosition(file, contextToken.End()) + currentLine, _ := scanner.GetLineAndCharacterOfPosition(file, position) + if (ast.IsImportDeclaration(parent) || + ast.IsExportDeclaration(parent) && parent.AsExportDeclaration().ModuleSpecifier != nil) && + contextToken == parent.ModuleSpecifier() && + tokenLine == currentLine { + entries = append(entries, &lsproto.CompletionItem{ + Label: scanner.TokenToString(ast.KindAssertKeyword), + Kind: ptrTo(lsproto.CompletionItemKindKeyword), + SortText: ptrTo(string(sortTextGlobalsOrKeywords)), + }) + } + } + return entries +} + +func getJSCompletionEntries( + file *ast.SourceFile, + position int, + uniqueNames *core.Set[string], + target core.ScriptTarget, + sortedEntries []*lsproto.CompletionItem, +) []*lsproto.CompletionItem { + nameTable := getNameTable(file) + for name, pos := range nameTable { + // Skip identifiers produced only from the current location + if pos == position { + continue + } + if !uniqueNames.Has(name) && scanner.IsIdentifierText(name, target) { + uniqueNames.Add(name) + sortedEntries = core.InsertSorted( + sortedEntries, + &lsproto.CompletionItem{ + Label: name, + Kind: ptrTo(lsproto.CompletionItemKindText), + SortText: ptrTo(string(sortTextJavascriptIdentifiers)), + CommitCharacters: ptrTo([]string{}), + }, + compareCompletionEntries, + ) + } + } + return sortedEntries +} diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index ec661c4b29..d8286b83b8 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -1,6 +1,7 @@ package ls import ( + "slices" "strings" "github.com/microsoft/typescript-go/internal/ast" @@ -314,7 +315,80 @@ func probablyUsesSemicolons(file *ast.SourceFile) bool { return withSemicolon/withoutSemicolon > 1/nStatementsToObserve } -// !!! here +var typeKeywords []ast.Kind = []ast.Kind{ + ast.KindAnyKeyword, + ast.KindAssertsKeyword, + ast.KindBigIntKeyword, + ast.KindBooleanKeyword, + ast.KindFalseKeyword, + ast.KindInferKeyword, + ast.KindKeyOfKeyword, + ast.KindNeverKeyword, + ast.KindNullKeyword, + ast.KindNumberKeyword, + ast.KindObjectKeyword, + ast.KindReadonlyKeyword, + ast.KindStringKeyword, + ast.KindSymbolKeyword, + ast.KindTypeOfKeyword, + ast.KindTrueKeyword, + ast.KindVoidKeyword, + ast.KindUndefinedKeyword, + ast.KindUniqueKeyword, + ast.KindUnknownKeyword, +} + func isTypeKeyword(kind ast.Kind) bool { - return false + return slices.Contains(typeKeywords, kind) +} + +// Returns a map of all names in the file to their positions. +// !!! cache this +func getNameTable(file *ast.SourceFile) map[string]int { + nameTable := make(map[string]int) + var walk func(node *ast.Node) bool + + walk = func(node *ast.Node) bool { + if ast.IsIdentifier(node) && !isTagName(node) && node.Text() != "" || + ast.IsStringOrNumericLiteralLike(node) && literalIsName(node) || + ast.IsPrivateIdentifier(node) { + text := node.Text() + if _, ok := nameTable[text]; ok { + nameTable[text] = -1 + } else { + nameTable[text] = node.Pos() + } + } + + node.ForEachChild(walk) + jsdocNodes := node.JSDoc(file) + for _, jsdoc := range jsdocNodes { + jsdoc.ForEachChild(walk) + } + return false + } + + file.ForEachChild(walk) + return nameTable +} + +// We want to store any numbers/strings if they were a name that could be +// related to a declaration. So, if we have 'import x = require("something")' +// then we want 'something' to be in the name table. Similarly, if we have +// "a['propname']" then we want to store "propname" in the name table. +func literalIsName(node *ast.NumericOrStringLikeLiteral) bool { + return ast.IsDeclarationName(node) || + node.Parent.Kind == ast.KindExternalModuleReference || + isArgumentOfElementAccessExpression(node) || + ast.IsLiteralComputedPropertyDeclarationName(node) +} + +func isArgumentOfElementAccessExpression(node *ast.Node) bool { + return node != nil && node.Parent != nil && + node.Parent.Kind == ast.KindElementAccessExpression && + node.Parent.AsElementAccessExpression().ArgumentExpression == node +} + +func isTagName(node *ast.Node) bool { + return node.Parent != nil && ast.IsJSDocTag(node.Parent) && node.Parent.TagName() == node } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index bddaef0d6a..f6f408ddc9 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -390,7 +390,7 @@ func (s *Server) handleCompletion(req *lsproto.RequestMessage) error { file.FileName(), pos, params.Context, - s.initializeParams.Capabilities.TextDocument.Completion.CompletionItem) + s.initializeParams.Capabilities.TextDocument.Completion) return s.sendResult(req.ID, list) } diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 57889d3688..e7e0117f7b 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -5918,7 +5918,7 @@ func (p *Parser) scanClassMemberStart() bool { // public foo() ... // true // public @dec blah ... // true; we will then report an error later // export public ... // true; we will then report an error later - if isClassMemberModifier(idToken) { + if ast.IsClassMemberModifier(idToken) { return true } p.nextToken() @@ -6329,14 +6329,6 @@ func (p *Parser) skipRangeTrivia(textRange core.TextRange) core.TextRange { return core.NewTextRange(scanner.SkipTrivia(p.sourceText, textRange.Pos()), textRange.End()) } -func isClassMemberModifier(token ast.Kind) bool { - return isParameterPropertyModifier(token) || token == ast.KindStaticKeyword || token == ast.KindOverrideKeyword || token == ast.KindAccessorKeyword -} - -func isParameterPropertyModifier(kind ast.Kind) bool { - return ast.ModifierToFlag(kind)&ast.ModifierFlagsParameterPropertyModifier != 0 -} - func isKeyword(token ast.Kind) bool { return ast.KindFirstKeyword <= token && token <= ast.KindLastKeyword } From bb922556984548ddf8a17d9dcf68d5690656fe3c Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 11 Apr 2025 15:29:11 -0700 Subject: [PATCH 08/25] format --- internal/astnav/tokens.go | 4 ++-- internal/checker/checker.go | 3 ++- internal/ls/completions.go | 32 ++++++++++++++++++-------------- internal/ls/utilities.go | 1 - 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go index adeac22520..f194dee07f 100644 --- a/internal/astnav/tokens.go +++ b/internal/astnav/tokens.go @@ -265,7 +265,7 @@ func FindPrecedingToken(sourceFile *ast.SourceFile, position int) *ast.Node { return 1 } if node.End()+1 == position { - //prevSubtree = node + // prevSubtree = node return 0 } return 0 @@ -309,7 +309,7 @@ func FindPrecedingToken(sourceFile *ast.SourceFile, position int) *ast.Node { if nodeList != nil && len(nodeList.Nodes) > 0 && next == nil { if nodeList.End() == position { left = nodeList.End() - //prevSubtree = nodeList.Nodes[len(nodeList.Nodes)-1] + // prevSubtree = nodeList.Nodes[len(nodeList.Nodes)-1] } else if nodeList.End() <= position { left = nodeList.End() } else if nodeList.Pos() <= position { diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 26ad19ffaa..97500a0332 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -29773,6 +29773,7 @@ func (c *Checker) GetAccessibleSymbolChain( symbol *ast.Symbol, enclosingDeclaration *ast.Node, meaning ast.SymbolFlags, - useOnlyExternalAliasing bool) []*ast.Symbol { + useOnlyExternalAliasing bool, +) []*ast.Symbol { return nil } diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 8a0da52997..d3aac4b006 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -25,7 +25,8 @@ func (l *LanguageService) ProvideCompletion( fileName string, position int, context *lsproto.CompletionContext, - clientOptions *lsproto.CompletionClientCapabilities) *lsproto.CompletionList { + clientOptions *lsproto.CompletionClientCapabilities, +) *lsproto.CompletionList { program, file := l.getProgramAndFile(fileName) node := astnav.GetTouchingPropertyName(file, position) if node.Kind == ast.KindSourceFile { @@ -1358,7 +1359,8 @@ func shouldIncludeSymbol( closestSymbolDeclaration *ast.Declaration, file *ast.SourceFile, typeChecker *checker.Checker, - compilerOptions *core.CompilerOptions) bool { + compilerOptions *core.CompilerOptions, +) bool { allFlags := symbol.Flags location := data.location if !ast.IsSourceFile(location) { @@ -2113,18 +2115,20 @@ func compareCompletionEntries(entryInSlice *lsproto.CompletionItem, entryToInser return result } -var keywordCompletionsCache = collections.SyncMap[KeywordCompletionFilters, []*lsproto.CompletionItem]{} -var allKeywordCompletions = sync.OnceValue(func() []*lsproto.CompletionItem { - result := make([]*lsproto.CompletionItem, 0, ast.KindLastKeyword-ast.KindFirstKeyword+1) - for i := ast.KindFirstKeyword; i <= ast.KindLastKeyword; i++ { - result = append(result, &lsproto.CompletionItem{ - Label: scanner.TokenToString(i), - Kind: ptrTo(lsproto.CompletionItemKindKeyword), - SortText: ptrTo(string(sortTextGlobalsOrKeywords)), - }) - } - return result -}) +var ( + keywordCompletionsCache = collections.SyncMap[KeywordCompletionFilters, []*lsproto.CompletionItem]{} + allKeywordCompletions = sync.OnceValue(func() []*lsproto.CompletionItem { + result := make([]*lsproto.CompletionItem, 0, ast.KindLastKeyword-ast.KindFirstKeyword+1) + for i := ast.KindFirstKeyword; i <= ast.KindLastKeyword; i++ { + result = append(result, &lsproto.CompletionItem{ + Label: scanner.TokenToString(i), + Kind: ptrTo(lsproto.CompletionItemKindKeyword), + SortText: ptrTo(string(sortTextGlobalsOrKeywords)), + }) + } + return result + }) +) func getKeywordCompletions(keywordFilter KeywordCompletionFilters, filterOutTsOnlyKeywords bool) []*lsproto.CompletionItem { if !filterOutTsOnlyKeywords { diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index d8286b83b8..e10431b937 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -19,7 +19,6 @@ func isInString(file *ast.SourceFile, position int, previousToken *ast.Node) boo } if previousToken != nil && isStringTextContainingNode(previousToken) { - } return false From d515d6c95c31e1301d0be1ff461d12a09b0a3cb8 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 11 Apr 2025 16:10:09 -0700 Subject: [PATCH 09/25] sketch of unit test --- internal/ls/completions.go | 2 ++ internal/ls/completions_test.go | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 internal/ls/completions_test.go diff --git a/internal/ls/completions.go b/internal/ls/completions.go index d3aac4b006..e90789e804 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -32,6 +32,7 @@ func (l *LanguageService) ProvideCompletion( if node.Kind == ast.KindSourceFile { return nil } + // !!! get user preferences return l.getCompletionsAtPosition(program, file, position, context, nil /*preferences*/, clientOptions) // !!! get preferences } @@ -874,6 +875,7 @@ func completionInfoFromData( } } + // !!! port behavior of other strada fields of CompletionInfo that are non-LSP return &lsproto.CompletionList{ IsIncomplete: data.hasUnresolvedAutoImports, ItemDefaults: itemDefaults, diff --git a/internal/ls/completions_test.go b/internal/ls/completions_test.go new file mode 100644 index 0000000000..18f719975d --- /dev/null +++ b/internal/ls/completions_test.go @@ -0,0 +1,36 @@ +package ls_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/project" + "gotest.tools/v3/assert" +) + +func TestCompletions(t *testing.T) { + files := map[string]string{ + "index.ts": "", + } + ls := createLanguageService("/", files) + var context *lsproto.CompletionContext + var capabilities *lsproto.CompletionClientCapabilities + completionList := ls.ProvideCompletion("index.ts", 0, context, capabilities) + assert.Assert(t, completionList != nil) +} + +func createLanguageService(cd string, files map[string]string) *ls.LanguageService { + // !!! TODO: replace with service_test.go's `setup` + projectServiceHost := newProjectServiceHost(files) + projectService := project.NewService(projectServiceHost, project.ServiceOptions{}) + compilerOptions := &core.CompilerOptions{} + project := project.NewInferredProject(compilerOptions, cd, "/", projectService) + return project.LanguageService() +} + +func newProjectServiceHost(files map[string]string) project.ServiceHost { + // !!! TODO: import from service_test.go + return nil +} From 71fce1a4cc202869d3ef8e2aea9bb38f1c3410b9 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 14 Apr 2025 09:43:18 -0700 Subject: [PATCH 10/25] move service test setup to its own pacakge for reuse --- internal/project/service_test.go | 94 +++---------------- .../projecttestutil/projecttestutil.go | 72 ++++++++++++++ 2 files changed, 87 insertions(+), 79 deletions(-) create mode 100644 internal/testutil/projecttestutil/projecttestutil.go diff --git a/internal/project/service_test.go b/internal/project/service_test.go index 25fa07f83d..02bd34c484 100644 --- a/internal/project/service_test.go +++ b/internal/project/service_test.go @@ -1,19 +1,14 @@ package project_test import ( - "fmt" - "io" "maps" - "strings" - "sync" "testing" "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/project" - "github.com/microsoft/typescript-go/internal/vfs" - "github.com/microsoft/typescript-go/internal/vfs/vfstest" + "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" "gotest.tools/v3/assert" ) @@ -41,7 +36,7 @@ func TestService(t *testing.T) { t.Parallel() t.Run("create configured project", func(t *testing.T) { t.Parallel() - service, _ := setup(files) + service, _ := projecttestutil.Setup(files) assert.Equal(t, len(service.Projects()), 0) service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") assert.Equal(t, len(service.Projects()), 1) @@ -54,7 +49,7 @@ func TestService(t *testing.T) { t.Run("create inferred project", func(t *testing.T) { t.Parallel() - service, _ := setup(files) + service, _ := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/config.ts", files["/home/projects/TS/p1/config.ts"], core.ScriptKindTS, "") // Find tsconfig, load, notice config.ts is not included, create inferred project assert.Equal(t, len(service.Projects()), 2) @@ -64,7 +59,7 @@ func TestService(t *testing.T) { t.Run("inferred project for in-memory files", func(t *testing.T) { t.Parallel() - service, _ := setup(files) + service, _ := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/config.ts", files["/home/projects/TS/p1/config.ts"], core.ScriptKindTS, "") service.OpenFile("^/untitled/ts-nul-authority/Untitled-1", "x", core.ScriptKindTS, "") service.OpenFile("^/untitled/ts-nul-authority/Untitled-2", "y", core.ScriptKindTS, "") @@ -81,7 +76,7 @@ func TestService(t *testing.T) { t.Parallel() t.Run("update script info eagerly and program lazily", func(t *testing.T) { t.Parallel() - service, _ := setup(files) + service, _ := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") info, proj := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/x.ts") programBefore := proj.GetProgram() @@ -94,7 +89,7 @@ func TestService(t *testing.T) { t.Run("unchanged source files are reused", func(t *testing.T) { t.Parallel() - service, _ := setup(files) + service, _ := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") _, proj := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/x.ts") programBefore := proj.GetProgram() @@ -107,7 +102,7 @@ func TestService(t *testing.T) { t.Parallel() filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/y.ts"] = `export const y = 2;` - service, _ := setup(filesCopy) + service, _ := projecttestutil.Setup(filesCopy) service.OpenFile("/home/projects/TS/p1/src/index.ts", filesCopy["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") assert.Check(t, service.GetScriptInfo("/home/projects/TS/p1/y.ts") == nil) @@ -122,14 +117,14 @@ func TestService(t *testing.T) { t.Parallel() t.Run("delete a file, close it, recreate it", func(t *testing.T) { t.Parallel() - service, host := setup(files) + service, host := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") assert.Equal(t, service.SourceFileCount(), 2) filesCopy := maps.Clone(files) delete(filesCopy, "/home/projects/TS/p1/src/x.ts") - host.replaceFS(filesCopy) + host.ReplaceFS(filesCopy) service.CloseFile("/home/projects/TS/p1/src/x.ts") assert.Check(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts") == nil) @@ -137,7 +132,7 @@ func TestService(t *testing.T) { assert.Equal(t, service.SourceFileCount(), 1) filesCopy["/home/projects/TS/p1/src/x.ts"] = `` - host.replaceFS(filesCopy) + host.ReplaceFS(filesCopy) service.OpenFile("/home/projects/TS/p1/src/x.ts", filesCopy["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") assert.Equal(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts").Text(), "") assert.Check(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil) @@ -151,19 +146,19 @@ func TestService(t *testing.T) { t.Parallel() filesCopy := maps.Clone(files) delete(filesCopy, "/home/projects/TS/p1/tsconfig.json") - service, host := setup(filesCopy) + service, host := projecttestutil.Setup(filesCopy) service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") delete(filesCopy, "/home/projects/TS/p1/src/x.ts") - host.replaceFS(filesCopy) + host.ReplaceFS(filesCopy) service.CloseFile("/home/projects/TS/p1/src/x.ts") assert.Check(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts") == nil) assert.Check(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil) filesCopy["/home/projects/TS/p1/src/x.ts"] = `` - host.replaceFS(filesCopy) + host.ReplaceFS(filesCopy) service.OpenFile("/home/projects/TS/p1/src/x.ts", filesCopy["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") assert.Equal(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts").Text(), "") assert.Check(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil) @@ -186,7 +181,7 @@ func TestService(t *testing.T) { }, }` filesCopy["/home/projects/TS/p2/src/index.ts"] = `import { x } from "../../p1/src/x";` - service, _ := setup(filesCopy) + service, _ := projecttestutil.Setup(filesCopy) service.OpenFile("/home/projects/TS/p1/src/index.ts", filesCopy["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") service.OpenFile("/home/projects/TS/p2/src/index.ts", filesCopy["/home/projects/TS/p2/src/index.ts"], core.ScriptKindTS, "") assert.Equal(t, len(service.Projects()), 2) @@ -209,7 +204,7 @@ func TestService(t *testing.T) { } }` filesCopy["/home/projects/TS/p2/src/index.ts"] = `import { x } from "../../p1/src/x";` - service, _ := setup(filesCopy) + service, _ := projecttestutil.Setup(filesCopy) service.OpenFile("/home/projects/TS/p1/src/index.ts", filesCopy["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") service.OpenFile("/home/projects/TS/p2/src/index.ts", filesCopy["/home/projects/TS/p2/src/index.ts"], core.ScriptKindTS, "") assert.Equal(t, len(service.Projects()), 2) @@ -222,62 +217,3 @@ func TestService(t *testing.T) { }) }) } - -func setup(files map[string]string) (*project.Service, *projectServiceHost) { - host := newProjectServiceHost(files) - service := project.NewService(host, project.ServiceOptions{ - Logger: host.logger, - }) - return service, host -} - -type projectServiceHost struct { - fs vfs.FS - mu sync.Mutex - defaultLibraryPath string - output strings.Builder - logger *project.Logger -} - -func newProjectServiceHost(files map[string]string) *projectServiceHost { - fs := bundled.WrapFS(vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/)) - host := &projectServiceHost{ - fs: fs, - defaultLibraryPath: bundled.LibPath(), - } - host.logger = project.NewLogger([]io.Writer{&host.output}, project.LogLevelVerbose) - return host -} - -// DefaultLibraryPath implements project.ProjectServiceHost. -func (p *projectServiceHost) DefaultLibraryPath() string { - return p.defaultLibraryPath -} - -// FS implements project.ProjectServiceHost. -func (p *projectServiceHost) FS() vfs.FS { - return p.fs -} - -// GetCurrentDirectory implements project.ProjectServiceHost. -func (p *projectServiceHost) GetCurrentDirectory() string { - return "/" -} - -// Log implements project.ProjectServiceHost. -func (p *projectServiceHost) Log(msg ...any) { - p.mu.Lock() - defer p.mu.Unlock() - fmt.Fprintln(&p.output, msg...) -} - -// NewLine implements project.ProjectServiceHost. -func (p *projectServiceHost) NewLine() string { - return "\n" -} - -func (p *projectServiceHost) replaceFS(files map[string]string) { - p.fs = bundled.WrapFS(vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/)) -} - -var _ project.ServiceHost = (*projectServiceHost)(nil) diff --git a/internal/testutil/projecttestutil/projecttestutil.go b/internal/testutil/projecttestutil/projecttestutil.go new file mode 100644 index 0000000000..f81776fb34 --- /dev/null +++ b/internal/testutil/projecttestutil/projecttestutil.go @@ -0,0 +1,72 @@ +package projecttestutil + +import ( + "fmt" + "io" + "strings" + "sync" + + "github.com/microsoft/typescript-go/internal/bundled" + "github.com/microsoft/typescript-go/internal/project" + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/vfstest" +) + +type ProjectServiceHost struct { + fs vfs.FS + mu sync.Mutex + defaultLibraryPath string + output strings.Builder + logger *project.Logger +} + +// DefaultLibraryPath implements project.ProjectServiceHost. +func (p *ProjectServiceHost) DefaultLibraryPath() string { + return p.defaultLibraryPath +} + +// FS implements project.ProjectServiceHost. +func (p *ProjectServiceHost) FS() vfs.FS { + return p.fs +} + +// GetCurrentDirectory implements project.ProjectServiceHost. +func (p *ProjectServiceHost) GetCurrentDirectory() string { + return "/" +} + +// Log implements project.ProjectServiceHost. +func (p *ProjectServiceHost) Log(msg ...any) { + p.mu.Lock() + defer p.mu.Unlock() + fmt.Fprintln(&p.output, msg...) +} + +// NewLine implements project.ProjectServiceHost. +func (p *ProjectServiceHost) NewLine() string { + return "\n" +} + +func (p *ProjectServiceHost) ReplaceFS(files map[string]string) { + p.fs = bundled.WrapFS(vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/)) +} + +var _ project.ServiceHost = (*ProjectServiceHost)(nil) + +func Setup(files map[string]string) (*project.Service, *ProjectServiceHost) { + host := newProjectServiceHost(files) + service := project.NewService(host, project.ServiceOptions{ + Logger: host.logger, + }) + return service, host +} + +func newProjectServiceHost(files map[string]string) *ProjectServiceHost { + fs := bundled.WrapFS(vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/)) + host := &ProjectServiceHost{ + fs: fs, + defaultLibraryPath: bundled.LibPath(), + } + host.logger = project.NewLogger([]io.Writer{&host.output}, project.LogLevelVerbose) + return host +} From 9d729a2efd5551fb7ae43015575ee1f392f806fd Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 16 Apr 2025 12:31:56 -0700 Subject: [PATCH 11/25] WIP: findPrecedingToken --- internal/ast/ast.go | 2 +- internal/astnav/tokens.go | 284 ++++++++++++++++++++------------- internal/astnav/tokens_test.go | 95 +++++++++-- 3 files changed, 258 insertions(+), 123 deletions(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 4201a142e1..cda9a047b6 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -9919,7 +9919,7 @@ func (node *SourceFile) GetOrCreateToken( panic(fmt.Sprintf("Token cache mismatch: %v != %v", token.Kind, kind)) } if token.Parent != parent { - panic("Token cache mismatch: parent") + panic(fmt.Sprintf("Token cache mismatch: parent. Expected parent of kind %v, got %v", token.Parent.Kind, parent.Kind)) } return token } diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go index f194dee07f..681a6e8a8d 100644 --- a/internal/astnav/tokens.go +++ b/internal/astnav/tokens.go @@ -247,138 +247,206 @@ func visitEachChildAndJSDoc(node *ast.Node, sourceFile *ast.SourceFile, visitor node.VisitEachChild(visitor) } +const ( + comparisonLessThan = -1 + comparisonEqualTo = 0 + comparisonGreaterThan = 1 +) + // !!! Shared (placeholder) -// Finds the rightmost token satisfying `token.end <= position`, +// Finds the leftmost token satisfying `position < token.End()`. +// If the leftmost token satisfying `position < token.End()` is invalid, +// we will find the rightmost valid token with `token.End() <= position`. func FindPrecedingToken(sourceFile *ast.SourceFile, position int) *ast.Node { - var next *ast.Node - current := sourceFile.AsNode() - left := 0 + return FindPrecedingTokenEx(sourceFile, position, nil) +} - testNode := func(node *ast.Node) int { - if node.Pos() == position { - return 1 - } - if node.End() < position { - return -1 - } - if getPosition(node, sourceFile, false) > position || node.Pos() == position+1 { - return 1 - } - if node.End()+1 == position { - // prevSubtree = node - return 0 +func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *ast.Node) *ast.Node { + var find func(node *ast.Node) *ast.Node + find = func(n *ast.Node) *ast.Node { + if ast.IsNonWhitespaceToken(n) { + return n } - return 0 - } - visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { - // We can't abort visiting children, so once a match is found, we set `next` - // and do nothing on subsequent visits. - if node != nil && next == nil { - switch testNode(node) { - case -1: - if !ast.IsJSDocKind(node.Kind) { - // We can't move the left boundary into or beyond JSDoc, - // because we may end up returning the token after this JSDoc, - // constructing it with the scanner, and we need to include - // all its leading trivia in its position. - left = node.End() + // `foundChild` is the leftmost node that contains the target position. + // `lastValidChild` is the rightmost valid (non-whitespace) node that precedes `foundChild` if it is set. + // `prevChild` is the last visited child of the current node. + var foundChild, prevChild, lastValidChild *ast.Node + visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { + // skip synthesized nodes (that will exist now because of jsdoc handling) + if node == nil || node.Flags&ast.NodeFlagsReparsed != 0 { + return node + } + if foundChild != nil { // We cannot abort visiting children, so once the desired child is found, we do nothing. + return node + } + if position < node.End() && (prevChild == nil || prevChild.End() <= position) { + foundChild = node + } else { + if isValidPrecedingNode(node, sourceFile) { + lastValidChild = node } - case 0: - next = node + prevChild = node } + return node } - return node - } + visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { + if foundChild != nil { + return nodeList + } + if nodeList != nil && len(nodeList.Nodes) > 0 { + nodes := nodeList.Nodes + index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, _ *ast.Node) int { + // synthetic jsdoc nodes should have jsdocNode.End() <= n.Pos() + if nodes[middle].Flags&ast.NodeFlagsReparsed != 0 { + return comparisonLessThan + } + if position < nodes[middle].End() { + if middle == 0 || position >= nodes[middle-1].End() { + return comparisonEqualTo + } + return comparisonGreaterThan + } + return comparisonLessThan + }) - visitNodes := func(nodes []*ast.Node) { - index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, node *ast.Node) int { - cmp := testNode(node) - if cmp < 0 { - left = node.End() + if match { + foundChild = nodes[index] + } + + validLookupIndex := core.IfElse(match, index-1, len(nodes)-1) + for i := validLookupIndex; i >= 0; i-- { + if nodes[i].Flags&ast.NodeFlagsReparsed != 0 { + continue + } + if prevChild == nil { + prevChild = nodes[i] + } + if isValidPrecedingNode(nodes[i], sourceFile) { + lastValidChild = nodes[i] + break + } + } } - return cmp + return nodeList + } + nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ + VisitNode: visitNode, + VisitToken: visitNode, + VisitNodes: visitNodes, + VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { + if modifiers != nil { + visitNodes(&modifiers.NodeList, visitor) + } + return modifiers + }, }) + visitEachChildAndJSDoc(n, sourceFile, nodeVisitor) - if match { - next = nodes[index] + if foundChild != nil { + // Note that the span of a node's tokens is [getStartOfNode(node, ...), node.end). + // Given that `position < child.end` and child has constituent tokens, we distinguish these cases: + // 1) `position` precedes `child`'s tokens or `child` has no tokens (ie: in a comment or whitespace preceding `child`): + // we need to find the last token in a previous child node or child tokens. + // 2) `position` is within the same span: we recurse on `child`. + start := getStartOfNode(foundChild, sourceFile) + lookInPreviousChild := start >= position || // cursor in the leading trivia or preceding tokens + !isValidPrecedingNode(foundChild, sourceFile) + if lookInPreviousChild { + var startPos int + // Nodes that could be the parent of the tokens in the range [startPos, foundChild.Pos()). + var possibleNodes []*ast.Node + if lastValidChild != nil { + startPos = lastValidChild.Pos() + possibleNodes = []*ast.Node{lastValidChild, n} + } else { + startPos = n.Pos() + possibleNodes = []*ast.Node{n} + } + if position >= foundChild.Pos() { + return findRightmostValidToken(startPos, foundChild.Pos(), sourceFile, possibleNodes, -1 /*position*/) + } else { // Answer is in tokens between two visited children. + return findRightmostValidToken(startPos, foundChild.Pos(), sourceFile, possibleNodes, position) + } + // !!! JSDoc case + } else { + // position is in [foundChild.getStart(), foundChild.End): recur. + return find(foundChild) + } } - } - visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { - if nodeList != nil && len(nodeList.Nodes) > 0 && next == nil { - if nodeList.End() == position { - left = nodeList.End() - // prevSubtree = nodeList.Nodes[len(nodeList.Nodes)-1] - } else if nodeList.End() <= position { - left = nodeList.End() - } else if nodeList.Pos() <= position { - visitNodes(nodeList.Nodes) - } + // We have two cases here: either the position is at the end of the file, + // or the desired token is in the unvisited trailing tokens of the current node. + var startPos int + var possibleNodes []*ast.Node + if prevChild != nil { + startPos = prevChild.Pos() + possibleNodes = []*ast.Node{prevChild, n} + } else { + startPos = n.Pos() + possibleNodes = []*ast.Node{n} + } + if position >= n.End() { + return findRightmostValidToken(startPos, n.End(), sourceFile, possibleNodes, -1 /*position*/) + } else { + return findRightmostValidToken(startPos, n.End(), sourceFile, possibleNodes, position) } - return nodeList } - nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: visitNode, - VisitToken: visitNode, - VisitNodes: visitNodeList, - VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { - if modifiers != nil { - visitNodeList(&modifiers.NodeList, visitor) - } - return modifiers - }, - }) + var node *ast.Node + if startNode != nil { + node = startNode + } else { + node = sourceFile.AsNode() + } + result := find(node) + if result != nil && (!ast.IsTokenKind(result.Kind) || ast.IsWhitespaceOnlyJsxText(result)) { + panic("Expected result to be a non-whitespace token.") + } + return result +} - for { - visitEachChildAndJSDoc(current, sourceFile, nodeVisitor) +func isValidPrecedingNode(node *ast.Node, sourceFile *ast.SourceFile) bool { + start := getStartOfNode(node, sourceFile) + width := node.End() - start + return !(ast.IsWhitespaceOnlyJsxText(node) || width == 0) +} - // No node was found that contains the target position, so we've gone as deep as - // we can in the AST. We've either found a token, or we need to run the scanner - // to construct one that isn't stored in the AST. - if next == nil { - if ast.IsTokenKind(current.Kind) || ast.IsJSDocCommentContainingNode(current) { - return current - } - scanner := scanner.GetScannerForSourceFile(sourceFile, left) - for left < current.End() { - token := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenStart := scanner.TokenStart() - tokenEnd := scanner.TokenEnd() - if tokenStart <= position && (position < tokenEnd) { - if token == ast.KindIdentifier || !ast.IsTokenKind(token) { - if ast.IsJSDocKind(current.Kind) { - return current - } - panic(fmt.Sprintf("did not expect %s to have %s in its trivia", current.Kind.String(), token.String())) - } - return sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, current) - } - if tokenStart <= position && (tokenEnd == position || tokenEnd == position-1) { - return sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, current) - } - // if includePrecedingTokenAtEndPosition != nil && tokenEnd == position { - // prevToken := sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, current) - // if includePrecedingTokenAtEndPosition(prevToken) { - // return prevToken - // } - // } - left = tokenEnd - scanner.Scan() +// Looks for rightmost valid token in the range [startPos, endPos). +// If position is >= 0, looks for rightmost valid token that preceeds or touches that position. +func findRightmostValidToken(startPos, endPos int, sourceFile *ast.SourceFile, possibleNodes []*ast.Node, position int) *ast.Node { + scanner := scanner.GetScannerForSourceFile(sourceFile, startPos) + var tokens []*ast.Node + for startPos < endPos { + token := scanner.Token() + tokenFullStart := scanner.TokenFullStart() + tokenEnd := scanner.TokenEnd() + startPos = tokenEnd + parent := core.Find(possibleNodes, func(node *ast.Node) bool { return node.Pos() <= tokenFullStart && tokenEnd <= node.End() }) + tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, parent)) + scanner.Scan() + } + lastToken := len(tokens) - 1 + if position >= 0 { // Look for preceding token. + lastToken = -1 + for i := range tokens { + if position < tokens[i].End() && (i == 0 || tokens[i-1].End() <= position) { + lastToken = i + break } - return current } - current = next - left = current.Pos() - next = nil } + // Find preceding valid token. + for i := lastToken; i >= 0; i-- { + if !ast.IsWhitespaceOnlyJsxText(tokens[i]) { + return tokens[i] + } + } + return nil } -// !!! -func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *ast.Node) *ast.Node { - return FindPrecedingToken(sourceFile, position) +func getStartOfNode(node *ast.Node, file *ast.SourceFile) int { + return scanner.GetTokenPosOfNode(node, file, false /*includeJSDoc*/) // !!! includeJSDoc } // !!! diff --git a/internal/astnav/tokens_test.go b/internal/astnav/tokens_test.go index c6afcabe9d..3c025a7f62 100644 --- a/internal/astnav/tokens_test.go +++ b/internal/astnav/tokens_test.go @@ -15,6 +15,7 @@ import ( "github.com/microsoft/typescript-go/internal/parser" "github.com/microsoft/typescript-go/internal/repo" "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/testutil" "github.com/microsoft/typescript-go/internal/testutil/baseline" "github.com/microsoft/typescript-go/internal/testutil/jstest" "gotest.tools/v3/assert" @@ -36,10 +37,11 @@ func TestGetTokenAtPosition(t *testing.T) { baselineTokens( t, "GetTokenAtPosition", - func(fileText string, positions []int) []tokenInfo { + false, /*includeEOF*/ + func(fileText string, positions []int) []*tokenInfo { return tsGetTokensAtPositions(t, fileText, positions) }, - func(file *ast.SourceFile, pos int) tokenInfo { + func(file *ast.SourceFile, pos int) *tokenInfo { return toTokenInfo(astnav.GetTokenAtPosition(file, pos)) }, ) @@ -65,16 +67,17 @@ func TestGetTouchingPropertyName(t *testing.T) { baselineTokens( t, "GetTouchingPropertyName", - func(fileText string, positions []int) []tokenInfo { + false, /*includeEOF*/ + func(fileText string, positions []int) []*tokenInfo { return tsGetTouchingPropertyName(t, fileText, positions) }, - func(file *ast.SourceFile, pos int) tokenInfo { + func(file *ast.SourceFile, pos int) *tokenInfo { return toTokenInfo(astnav.GetTouchingPropertyName(file, pos)) }, ) } -func baselineTokens(t *testing.T, testName string, getTSTokens func(fileText string, positions []int) []tokenInfo, getGoToken func(file *ast.SourceFile, pos int) tokenInfo) { +func baselineTokens(t *testing.T, testName string, includeEOF bool, getTSTokens func(fileText string, positions []int) []*tokenInfo, getGoToken func(file *ast.SourceFile, pos int) *tokenInfo) { for _, fileName := range testFiles { t.Run(filepath.Base(fileName), func(t *testing.T) { t.Parallel() @@ -93,11 +96,12 @@ func baselineTokens(t *testing.T, testName string, getTSTokens func(fileText str currentDiff := tokenDiff{} for pos, tsToken := range tsTokens { + defer testutil.RecoverAndFail(t, fmt.Sprintf("pos: %d", pos)) goToken := getGoToken(file, pos) diff := tokenDiff{goToken: goToken, tsToken: tsToken} if currentDiff != diff { - if currentDiff.goToken != currentDiff.tsToken { + if currentDiff.goToken != currentDiff.tsToken { // !!! fix writeRangeDiff(&output, file, currentDiff, currentRange) } currentDiff = diff @@ -123,8 +127,8 @@ func baselineTokens(t *testing.T, testName string, getTSTokens func(fileText str } type tokenDiff struct { - goToken tokenInfo - tsToken tokenInfo + goToken *tokenInfo + tsToken *tokenInfo } type tokenInfo struct { @@ -133,20 +137,23 @@ type tokenInfo struct { End int `json:"end"` } -func toTokenInfo(node *ast.Node) tokenInfo { +func toTokenInfo(node *ast.Node) *tokenInfo { + if node == nil { + return nil + } kind := strings.Replace(node.Kind.String(), "Kind", "", 1) switch kind { case "EndOfFile": kind = "EndOfFileToken" } - return tokenInfo{ + return &tokenInfo{ Kind: kind, Pos: node.Pos(), End: node.End(), } } -func tsGetTokensAtPositions(t testing.TB, fileText string, positions []int) []tokenInfo { +func tsGetTokensAtPositions(t testing.TB, fileText string, positions []int) []*tokenInfo { dir := t.TempDir() err := os.WriteFile(filepath.Join(dir, "file.ts"), []byte(fileText), 0o644) assert.NilError(t, err) @@ -178,12 +185,12 @@ func tsGetTokensAtPositions(t testing.TB, fileText string, positions []int) []to }); };` - info, err := jstest.EvalNodeScriptWithTS[[]tokenInfo](t, script, dir, "") + info, err := jstest.EvalNodeScriptWithTS[[]*tokenInfo](t, script, dir, "") assert.NilError(t, err) return info } -func tsGetTouchingPropertyName(t testing.TB, fileText string, positions []int) []tokenInfo { +func tsGetTouchingPropertyName(t testing.TB, fileText string, positions []int) []*tokenInfo { dir := t.TempDir() err := os.WriteFile(filepath.Join(dir, "file.ts"), []byte(fileText), 0o644) assert.NilError(t, err) @@ -215,7 +222,7 @@ func tsGetTouchingPropertyName(t testing.TB, fileText string, positions []int) [ }); };` - info, err := jstest.EvalNodeScriptWithTS[[]tokenInfo](t, script, dir, "") + info, err := jstest.EvalNodeScriptWithTS[[]*tokenInfo](t, script, dir, "") assert.NilError(t, err) return info } @@ -292,3 +299,63 @@ func writeRangeDiff(output *strings.Builder, file *ast.SourceFile, diff tokenDif } } } + +func TestFindPrecedingToken(t *testing.T) { + t.Parallel() + repo.SkipIfNoTypeScriptSubmodule(t) + jstest.SkipIfNoNodeJS(t) + + t.Run("baseline", func(t *testing.T) { + t.Parallel() + baselineTokens( + t, + "FindPrecedingToken", + true, /*includeEOF*/ + func(fileText string, positions []int) []*tokenInfo { + return tsFindPrecedingTokens(t, fileText, positions) + }, + func(file *ast.SourceFile, pos int) *tokenInfo { + return toTokenInfo(astnav.FindPrecedingToken(file, pos)) + }, + ) + }) +} + +func tsFindPrecedingTokens(t *testing.T, fileText string, positions []int) []*tokenInfo { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "file.ts"), []byte(fileText), 0o644) + assert.NilError(t, err) + + err = os.WriteFile(filepath.Join(dir, "positions.json"), []byte(core.Must(core.StringifyJson(positions, "", ""))), 0o644) + assert.NilError(t, err) + + script := ` + import fs from "fs"; + export default (ts) => { + const positions = JSON.parse(fs.readFileSync("positions.json", "utf8")); + const fileText = fs.readFileSync("file.ts", "utf8"); + const file = ts.createSourceFile( + "file.ts", + fileText, + { languageVersion: ts.ScriptTarget.Latest, jsDocParsingMode: ts.JSDocParsingMode.ParseAll }, + /*setParentNodes*/ true + ); + return positions.map(position => { + let token = ts.findPrecedingToken(position, file); + if (token === undefined) { + return undefined; + } + if (token.kind === ts.SyntaxKind.SyntaxList) { + token = token.parent; + } + return { + kind: ts.Debug.formatSyntaxKind(token.kind), + pos: token.pos, + end: token.end, + }; + }); + };` + info, err := jstest.EvalNodeScriptWithTS[[]*tokenInfo](t, script, dir, "") + assert.NilError(t, err) + return info +} From df5086ea8f79468522dfd0f0e4a02ed654b76077 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 17 Apr 2025 12:09:42 -0700 Subject: [PATCH 12/25] find preceding token --- internal/astnav/tokens.go | 225 +- internal/astnav/tokens_test.go | 59 +- ...FindPrecedingToken.mapCode.ts.baseline.txt | 2002 +++++++++++++++++ 3 files changed, 2199 insertions(+), 87 deletions(-) create mode 100644 testdata/baselines/reference/astnav/FindPrecedingToken.mapCode.ts.baseline.txt diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go index 681a6e8a8d..f420b8fa36 100644 --- a/internal/astnav/tokens.go +++ b/internal/astnav/tokens.go @@ -253,15 +253,15 @@ const ( comparisonGreaterThan = 1 ) -// !!! Shared (placeholder) // Finds the leftmost token satisfying `position < token.End()`. -// If the leftmost token satisfying `position < token.End()` is invalid, +// If the leftmost token satisfying `position < token.End()` is invalid, or if position +// is in the trivia of that leftmost token, // we will find the rightmost valid token with `token.End() <= position`. func FindPrecedingToken(sourceFile *ast.SourceFile, position int) *ast.Node { - return FindPrecedingTokenEx(sourceFile, position, nil) + return FindPrecedingTokenEx(sourceFile, position, nil, false) } -func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *ast.Node) *ast.Node { +func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *ast.Node, excludeJSDoc bool) *ast.Node { var find func(node *ast.Node) *ast.Node find = func(n *ast.Node) *ast.Node { if ast.IsNonWhitespaceToken(n) { @@ -269,9 +269,8 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a } // `foundChild` is the leftmost node that contains the target position. - // `lastValidChild` is the rightmost valid (non-whitespace) node that precedes `foundChild` if it is set. // `prevChild` is the last visited child of the current node. - var foundChild, prevChild, lastValidChild *ast.Node + var foundChild, prevChild *ast.Node visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { // skip synthesized nodes (that will exist now because of jsdoc handling) if node == nil || node.Flags&ast.NodeFlagsReparsed != 0 { @@ -283,9 +282,6 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a if position < node.End() && (prevChild == nil || prevChild.End() <= position) { foundChild = node } else { - if isValidPrecedingNode(node, sourceFile) { - lastValidChild = node - } prevChild = node } return node @@ -296,6 +292,9 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a } if nodeList != nil && len(nodeList.Nodes) > 0 { nodes := nodeList.Nodes + if isJSDocSingleCommentNodeList(n, nodeList) { + return nodeList + } index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, _ *ast.Node) int { // synthetic jsdoc nodes should have jsdocNode.End() <= n.Pos() if nodes[middle].Flags&ast.NodeFlagsReparsed != 0 { @@ -322,10 +321,6 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a if prevChild == nil { prevChild = nodes[i] } - if isValidPrecedingNode(nodes[i], sourceFile) { - lastValidChild = nodes[i] - break - } } } return nodeList @@ -349,26 +344,31 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a // 1) `position` precedes `child`'s tokens or `child` has no tokens (ie: in a comment or whitespace preceding `child`): // we need to find the last token in a previous child node or child tokens. // 2) `position` is within the same span: we recurse on `child`. - start := getStartOfNode(foundChild, sourceFile) + start := getStartOfNode(foundChild, sourceFile, !excludeJSDoc /*includeJSDoc*/) lookInPreviousChild := start >= position || // cursor in the leading trivia or preceding tokens !isValidPrecedingNode(foundChild, sourceFile) if lookInPreviousChild { - var startPos int - // Nodes that could be the parent of the tokens in the range [startPos, foundChild.Pos()). - var possibleNodes []*ast.Node - if lastValidChild != nil { - startPos = lastValidChild.Pos() - possibleNodes = []*ast.Node{lastValidChild, n} - } else { - startPos = n.Pos() - possibleNodes = []*ast.Node{n} - } if position >= foundChild.Pos() { - return findRightmostValidToken(startPos, foundChild.Pos(), sourceFile, possibleNodes, -1 /*position*/) + // Find jsdoc preceding the foundChild. + var jsDoc *ast.Node + nodeJSDoc := n.JSDoc(sourceFile) + for i := len(nodeJSDoc) - 1; i >= 0; i-- { + if nodeJSDoc[i].Pos() >= foundChild.Pos() { + jsDoc = nodeJSDoc[i] + break + } + } + if jsDoc != nil { + if !excludeJSDoc { + return find(jsDoc) + } else { + return findRightmostValidToken(jsDoc.End(), sourceFile, n, position, excludeJSDoc) + } + } + return findRightmostValidToken(foundChild.Pos(), sourceFile, n, -1 /*position*/, excludeJSDoc) } else { // Answer is in tokens between two visited children. - return findRightmostValidToken(startPos, foundChild.Pos(), sourceFile, possibleNodes, position) + return findRightmostValidToken(foundChild.Pos(), sourceFile, n, position, excludeJSDoc) } - // !!! JSDoc case } else { // position is in [foundChild.getStart(), foundChild.End): recur. return find(foundChild) @@ -377,19 +377,10 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a // We have two cases here: either the position is at the end of the file, // or the desired token is in the unvisited trailing tokens of the current node. - var startPos int - var possibleNodes []*ast.Node - if prevChild != nil { - startPos = prevChild.Pos() - possibleNodes = []*ast.Node{prevChild, n} - } else { - startPos = n.Pos() - possibleNodes = []*ast.Node{n} - } if position >= n.End() { - return findRightmostValidToken(startPos, n.End(), sourceFile, possibleNodes, -1 /*position*/) + return findRightmostValidToken(n.End(), sourceFile, n, -1 /*position*/, excludeJSDoc) } else { - return findRightmostValidToken(startPos, n.End(), sourceFile, possibleNodes, position) + return findRightmostValidToken(n.End(), sourceFile, n, position, excludeJSDoc) } } @@ -400,56 +391,146 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a node = sourceFile.AsNode() } result := find(node) - if result != nil && (!ast.IsTokenKind(result.Kind) || ast.IsWhitespaceOnlyJsxText(result)) { + if result != nil && ast.IsWhitespaceOnlyJsxText(result) { panic("Expected result to be a non-whitespace token.") } return result } func isValidPrecedingNode(node *ast.Node, sourceFile *ast.SourceFile) bool { - start := getStartOfNode(node, sourceFile) + start := getStartOfNode(node, sourceFile, false /*includeJSDoc*/) width := node.End() - start return !(ast.IsWhitespaceOnlyJsxText(node) || width == 0) } +func getStartOfNode(node *ast.Node, file *ast.SourceFile, includeJSDoc bool) int { + return scanner.GetTokenPosOfNode(node, file, includeJSDoc) +} + +// If this is a single comment JSDoc, we do not visit the comment node. +func isJSDocSingleCommentNodeList(parent *ast.Node, nodeList *ast.NodeList) bool { + return parent.Kind == ast.KindJSDoc && nodeList == parent.AsJSDoc().Comment && nodeList != nil && len(nodeList.Nodes) == 1 +} + +// !!! +func FindNextToken(previousToken *ast.Node, parent *ast.Node, file *ast.SourceFile) *ast.Node { + return nil +} + // Looks for rightmost valid token in the range [startPos, endPos). // If position is >= 0, looks for rightmost valid token that preceeds or touches that position. -func findRightmostValidToken(startPos, endPos int, sourceFile *ast.SourceFile, possibleNodes []*ast.Node, position int) *ast.Node { - scanner := scanner.GetScannerForSourceFile(sourceFile, startPos) - var tokens []*ast.Node - for startPos < endPos { - token := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - startPos = tokenEnd - parent := core.Find(possibleNodes, func(node *ast.Node) bool { return node.Pos() <= tokenFullStart && tokenEnd <= node.End() }) - tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, parent)) - scanner.Scan() +func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingNode *ast.Node, position int, excludeJSDoc bool) *ast.Node { + if position == -1 { + position = containingNode.End() } - lastToken := len(tokens) - 1 - if position >= 0 { // Look for preceding token. - lastToken = -1 - for i := range tokens { - if position < tokens[i].End() && (i == 0 || tokens[i-1].End() <= position) { - lastToken = i - break + var find func(n *ast.Node) *ast.Node + find = func(n *ast.Node) *ast.Node { + if n == nil { + return nil + } + if ast.IsNonWhitespaceToken(n) { + return n + } + + var rightmostValidNode *ast.Node + var rightmostVisitedNode *ast.Node + hasChildren := false + test := func(node *ast.Node) bool { + if node.Flags&ast.NodeFlagsReparsed != 0 || + node.End() > endPos || getStartOfNode(node, sourceFile, !excludeJSDoc /*includeJSDoc*/) >= position { + return false + } + rightmostVisitedNode = node + if isValidPrecedingNode(node, sourceFile) { + rightmostValidNode = node + return true } + return false } - } - // Find preceding valid token. - for i := lastToken; i >= 0; i-- { - if !ast.IsWhitespaceOnlyJsxText(tokens[i]) { - return tokens[i] + visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { + if node == nil { + return node + } + hasChildren = true + test(node) + return node } - } - return nil -} + visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { + if nodeList != nil && len(nodeList.Nodes) > 0 { + if isJSDocSingleCommentNodeList(n, nodeList) { + return nodeList + } + hasChildren = true + index, _ := core.BinarySearchUniqueFunc(nodeList.Nodes, func(middle int, node *ast.Node) int { + if node.End() > endPos { + return comparisonGreaterThan + } + return comparisonLessThan + }) + for i := index - 1; i >= 0; i-- { + if test(nodeList.Nodes[i]) { + break + } + } + } + return nodeList + } + nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ + VisitNode: visitNode, + VisitToken: visitNode, + VisitNodes: visitNodes, + VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { + if modifiers != nil { + visitNodes(&modifiers.NodeList, visitor) + } + return modifiers + }, + }) + visitEachChildAndJSDoc(n, sourceFile, nodeVisitor) -func getStartOfNode(node *ast.Node, file *ast.SourceFile) int { - return scanner.GetTokenPosOfNode(node, file, false /*includeJSDoc*/) // !!! includeJSDoc -} + // Three cases: + // 1. The answer is a token of `rightmostValidNode`. + // 2. The answer is one of the unvisited tokens that occur after the last visited node. + // 3. The current node is a childless, token-less node. The answer is the current node. -// !!! -func FindNextToken(previousToken *ast.Node, parent *ast.Node, file *ast.SourceFile) *ast.Node { - return nil + // Case 2: Look at trailing tokens. + if !ast.IsJSDocCommentContainingNode(n) { // JSDoc nodes don't include trivia tokens as children. + var startPos int + if rightmostVisitedNode != nil { + startPos = rightmostVisitedNode.End() + } else { + startPos = n.Pos() + } + scanner := scanner.GetScannerForSourceFile(sourceFile, startPos) + var tokens []*ast.Node + for startPos < min(endPos, position) { + tokenStart := scanner.TokenStart() + if tokenStart >= position { + break + } + token := scanner.Token() + tokenFullStart := scanner.TokenFullStart() + tokenEnd := scanner.TokenEnd() + startPos = tokenEnd + tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, n)) + scanner.Scan() + } + lastToken := len(tokens) - 1 + // Find preceding valid token. + for i := lastToken; i >= 0; i-- { + if !ast.IsWhitespaceOnlyJsxText(tokens[i]) { + return tokens[i] + } + } + } + + // Case 3: childless node. + if !hasChildren { + return n + } + // Case 1: recur on rightmostValidNode. + return find(rightmostValidNode) + } + + return find(containingNode) } diff --git a/internal/astnav/tokens_test.go b/internal/astnav/tokens_test.go index 3c025a7f62..d3c6a6ecb0 100644 --- a/internal/astnav/tokens_test.go +++ b/internal/astnav/tokens_test.go @@ -101,8 +101,8 @@ func baselineTokens(t *testing.T, testName string, includeEOF bool, getTSTokens diff := tokenDiff{goToken: goToken, tsToken: tsToken} if currentDiff != diff { - if currentDiff.goToken != currentDiff.tsToken { // !!! fix - writeRangeDiff(&output, file, currentDiff, currentRange) + if !tokensEqual(currentDiff) { + writeRangeDiff(&output, file, currentDiff, currentRange, pos) } currentDiff = diff currentRange = core.NewTextRange(pos, pos) @@ -110,8 +110,8 @@ func baselineTokens(t *testing.T, testName string, includeEOF bool, getTSTokens currentRange = currentRange.WithEnd(pos) } - if currentDiff.goToken != currentDiff.tsToken { - writeRangeDiff(&output, file, currentDiff, currentRange) + if !tokensEqual(currentDiff) { + writeRangeDiff(&output, file, currentDiff, currentRange, len(tsTokens)-1) } baseline.Run( @@ -153,6 +153,13 @@ func toTokenInfo(node *ast.Node) *tokenInfo { } } +func tokensEqual(diff tokenDiff) bool { + if diff.goToken == nil || diff.tsToken == nil { + return diff.goToken == diff.tsToken + } + return *diff.goToken == *diff.tsToken +} + func tsGetTokensAtPositions(t testing.TB, fileText string, positions []int) []*tokenInfo { dir := t.TempDir() err := os.WriteFile(filepath.Join(dir, "file.ts"), []byte(fileText), 0o644) @@ -227,12 +234,26 @@ func tsGetTouchingPropertyName(t testing.TB, fileText string, positions []int) [ return info } -func writeRangeDiff(output *strings.Builder, file *ast.SourceFile, diff tokenDiff, rng core.TextRange) { +func writeRangeDiff(output *strings.Builder, file *ast.SourceFile, diff tokenDiff, rng core.TextRange, position int) { lines := file.LineMap() - tsStartLine, _ := core.PositionToLineAndCharacter(diff.tsToken.Pos, lines) - tsEndLine, _ := core.PositionToLineAndCharacter(diff.tsToken.End, lines) - goStartLine, _ := core.PositionToLineAndCharacter(diff.goToken.Pos, lines) - goEndLine, _ := core.PositionToLineAndCharacter(diff.goToken.End, lines) + + tsTokenPos := position + goTokenPos := position + tsTokenEnd := position + goTokenEnd := position + if diff.tsToken != nil { + tsTokenPos = diff.tsToken.Pos + tsTokenEnd = diff.tsToken.End + } + if diff.goToken != nil { + goTokenPos = diff.goToken.Pos + goTokenEnd = diff.goToken.End + } + tsStartLine, _ := core.PositionToLineAndCharacter(tsTokenPos, lines) + tsEndLine, _ := core.PositionToLineAndCharacter(tsTokenEnd, lines) + goStartLine, _ := core.PositionToLineAndCharacter(goTokenPos, lines) + goEndLine, _ := core.PositionToLineAndCharacter(goTokenEnd, lines) + contextLines := 2 startLine := min(tsStartLine, goStartLine) endLine := max(tsEndLine, goEndLine) @@ -260,8 +281,16 @@ func writeRangeDiff(output *strings.Builder, file *ast.SourceFile, diff tokenDif } output.WriteString(fmt.Sprintf("〚Positions: [%d, %d]〛\n", rng.Pos(), rng.End())) - output.WriteString(fmt.Sprintf("【TS: %s [%d, %d)】\n", diff.tsToken.Kind, diff.tsToken.Pos, diff.tsToken.End)) - output.WriteString(fmt.Sprintf("《Go: %s [%d, %d)》\n", diff.goToken.Kind, diff.goToken.Pos, diff.goToken.End)) + if diff.tsToken != nil { + output.WriteString(fmt.Sprintf("【TS: %s [%d, %d)】\n", diff.tsToken.Kind, tsTokenPos, tsTokenEnd)) + } else { + output.WriteString("【TS: nil】\n") + } + if diff.goToken != nil { + output.WriteString(fmt.Sprintf("《Go: %s [%d, %d)》\n", diff.goToken.Kind, goTokenPos, goTokenEnd)) + } else { + output.WriteString("《Go: nil》\n") + } for line := contextStart; line <= contextEnd; line++ { if truncate, skipTo := shouldTruncate(line); truncate { output.WriteString(fmt.Sprintf("%s │........ %d lines omitted ........\n", strings.Repeat(" ", digits), skipTo-line+1)) @@ -276,17 +305,17 @@ func writeRangeDiff(output *strings.Builder, file *ast.SourceFile, diff tokenDif if pos == rng.End()+1 { output.WriteString("〛") } - if pos == diff.tsToken.End { + if diff.tsToken != nil && pos == tsTokenEnd { output.WriteString("】") } - if pos == diff.goToken.End { + if diff.goToken != nil && pos == goTokenEnd { output.WriteString("》") } - if pos == diff.goToken.Pos { + if diff.goToken != nil && pos == goTokenPos { output.WriteString("《") } - if pos == diff.tsToken.Pos { + if diff.tsToken != nil && pos == tsTokenPos { output.WriteString("【") } if pos == rng.Pos() { diff --git a/testdata/baselines/reference/astnav/FindPrecedingToken.mapCode.ts.baseline.txt b/testdata/baselines/reference/astnav/FindPrecedingToken.mapCode.ts.baseline.txt new file mode 100644 index 0000000000..0893234510 --- /dev/null +++ b/testdata/baselines/reference/astnav/FindPrecedingToken.mapCode.ts.baseline.txt @@ -0,0 +1,2002 @@ +〚Positions: [1612, 1612]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/〚*〛* +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1613, 1613]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/*〚*〛 +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1614, 1614]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/**〚 〛 +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1615, 1615]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** 〚 +69 │〛 * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1616, 1616]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │〚 〛* Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1617, 1617]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ 〚*〛 Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1618, 1618]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ *〚 〛Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1619, 1619]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * 〚T〛ries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1620, 1620]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * T〚r〛ies to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1621, 1621]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tr〚i〛es to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1622, 1622]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tri〚e〛s to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1623, 1623]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Trie〚s〛 to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1624, 1624]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries〚 〛to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1625, 1625]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries 〚t〛o parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1626, 1626]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries t〚o〛 parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1627, 1627]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to〚 〛parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1628, 1628]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to 〚p〛arse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1629, 1629]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to p〚a〛rse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1630, 1630]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to pa〚r〛se something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1631, 1631]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to par〚s〛e something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1632, 1632]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to pars〚e〛 something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1633, 1633]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse〚 〛something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1634, 1634]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse 〚s〛omething into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1635, 1635]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse s〚o〛mething into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1636, 1636]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse so〚m〛ething into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1637, 1637]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse som〚e〛thing into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1638, 1638]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse some〚t〛hing into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1639, 1639]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse somet〚h〛ing into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1640, 1640]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse someth〚i〛ng into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1641, 1641]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse somethi〚n〛g into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1642, 1642]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse somethin〚g〛 into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1643, 1643]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something〚 〛into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1644, 1644]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something 〚i〛nto either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1645, 1645]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something i〚n〛to either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1646, 1646]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something in〚t〛o either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1647, 1647]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something int〚o〛 either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1648, 1648]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into〚 〛either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1649, 1649]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into 〚e〛ither "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1650, 1650]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into e〚i〛ther "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1651, 1651]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into ei〚t〛her "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1652, 1652]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into eit〚h〛er "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1653, 1653]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into eith〚e〛r "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1654, 1654]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into eithe〚r〛 "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1655, 1655]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either〚 〛"top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1656, 1656]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either 〚"〛top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1657, 1657]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "〚t〛op-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1658, 1658]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "t〚o〛p-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1659, 1659]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "to〚p〛-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1660, 1660]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top〚-〛level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1661, 1661]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-〚l〛evel" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1662, 1662]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-l〚e〛vel" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1663, 1663]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-le〚v〛el" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1664, 1664]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-lev〚e〛l" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1665, 1665]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-leve〚l〛" statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1666, 1666]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level〚"〛 statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1667, 1667]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level"〚 〛statements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1668, 1668]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" 〚s〛tatements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1669, 1669]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" s〚t〛atements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1670, 1670]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" st〚a〛tements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1671, 1671]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" sta〚t〛ements, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1672, 1672]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" stat〚e〛ments, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1673, 1673]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" state〚m〛ents, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1674, 1674]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statem〚e〛nts, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1675, 1675]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" stateme〚n〛ts, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1676, 1676]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statemen〚t〛s, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1677, 1677]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statement〚s〛, or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1678, 1678]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements〚,〛 or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1679, 1679]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements,〚 〛or into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1680, 1680]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, 〚o〛r into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1681, 1681]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, o〚r〛 into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1682, 1682]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or〚 〛into blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1683, 1683]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or 〚i〛nto blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1684, 1684]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or i〚n〛to blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1685, 1685]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or in〚t〛o blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1686, 1686]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or int〚o〛 blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1687, 1687]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into〚 〛blocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1688, 1688]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into 〚b〛locks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1689, 1689]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into b〚l〛ocks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1690, 1690]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into bl〚o〛cks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1691, 1691]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blo〚c〛ks +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1692, 1692]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into bloc〚k〛s +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1693, 1693]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into block〚s〛 +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1694, 1694]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks〚 〛 +70 │ * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1695, 1695]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks 〚 +70 │〛 * of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1696, 1696]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │〚 〛* of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1697, 1697]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ 〚*〛 of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1698, 1698]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ *〚 〛of class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1699, 1699]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * 〚o〛f class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1700, 1700]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * o〚f〛 class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1701, 1701]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of〚 〛class-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1702, 1702]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of 〚c〛lass-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1703, 1703]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of c〚l〛ass-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1704, 1704]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of cl〚a〛ss-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1705, 1705]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of cla〚s〛s-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1706, 1706]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of clas〚s〛-context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1707, 1707]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class〚-〛context definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1708, 1708]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-〚c〛ontext definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1709, 1709]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-c〚o〛ntext definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1710, 1710]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-co〚n〛text definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1711, 1711]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-con〚t〛ext definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1712, 1712]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-cont〚e〛xt definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1713, 1713]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-conte〚x〛t definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1714, 1714]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-contex〚t〛 definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1715, 1715]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context〚 〛definitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1716, 1716]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context 〚d〛efinitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1717, 1717]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context d〚e〛finitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1718, 1718]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context de〚f〛initions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1719, 1719]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context def〚i〛nitions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1720, 1720]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context defi〚n〛itions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1721, 1721]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context defin〚i〛tions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1722, 1722]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context defini〚t〛ions. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1723, 1723]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definit〚i〛ons. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1724, 1724]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definiti〚o〛ns. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1725, 1725]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitio〚n〛s. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1726, 1726]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definition〚s〛. +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1727, 1727]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions〚.〛 +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1728, 1728]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions.〚 〛 +71 │ */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1729, 1729]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. 〚 +71 │〛 */》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1730, 1730]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │〚 〛*/》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1731, 1731]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ 〚*〛/》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1732, 1732]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ *〚/〛》 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1733, 1733]〛 +【TS: JSDoc [1611, 1733)】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │【/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */】》〚 〛 +72 │function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1734, 1734]〛 +【TS: JSDoc [1611, 1733)】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │【/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */】》 〚 +72 │〛function parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1735, 1735]〛 +【TS: JSDoc [1611, 1733)】 +《Go: JSDoc [1607, 1733)》 +64 │ }, +65 │ ); +66 │}《 +67 │ +68 │【/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */】》 +72 │〚f〛unction parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see + + +〚Positions: [1736, 1736]〛 +【TS: FunctionKeyword [1607, 1743)】 +《Go: FunctionKeyword [1733, 1743)》 +64 │ }, +65 │ ); +66 │}【 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */《 +72 │f〚u〛nction】》 parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see +74 │ // which one makes the most sense, and grab the NodeArray from there. Do + + +〚Positions: [1737, 1737]〛 +【TS: FunctionKeyword [1607, 1743)】 +《Go: FunctionKeyword [1733, 1743)》 +64 │ }, +65 │ ); +66 │}【 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */《 +72 │fu〚n〛ction】》 parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see +74 │ // which one makes the most sense, and grab the NodeArray from there. Do + + +〚Positions: [1738, 1738]〛 +【TS: FunctionKeyword [1607, 1743)】 +《Go: FunctionKeyword [1733, 1743)》 +64 │ }, +65 │ ); +66 │}【 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */《 +72 │fun〚c〛tion】》 parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see +74 │ // which one makes the most sense, and grab the NodeArray from there. Do + + +〚Positions: [1739, 1739]〛 +【TS: FunctionKeyword [1607, 1743)】 +《Go: FunctionKeyword [1733, 1743)》 +64 │ }, +65 │ ); +66 │}【 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */《 +72 │func〚t〛ion】》 parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see +74 │ // which one makes the most sense, and grab the NodeArray from there. Do + + +〚Positions: [1740, 1740]〛 +【TS: FunctionKeyword [1607, 1743)】 +《Go: FunctionKeyword [1733, 1743)》 +64 │ }, +65 │ ); +66 │}【 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */《 +72 │funct〚i〛on】》 parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see +74 │ // which one makes the most sense, and grab the NodeArray from there. Do + + +〚Positions: [1741, 1741]〛 +【TS: FunctionKeyword [1607, 1743)】 +《Go: FunctionKeyword [1733, 1743)》 +64 │ }, +65 │ ); +66 │}【 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */《 +72 │functi〚o〛n】》 parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see +74 │ // which one makes the most sense, and grab the NodeArray from there. Do + + +〚Positions: [1742, 1742]〛 +【TS: FunctionKeyword [1607, 1743)】 +《Go: FunctionKeyword [1733, 1743)》 +64 │ }, +65 │ ); +66 │}【 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */《 +72 │functio〚n〛】》 parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see +74 │ // which one makes the most sense, and grab the NodeArray from there. Do + + +〚Positions: [1743, 1743]〛 +【TS: FunctionKeyword [1607, 1743)】 +《Go: FunctionKeyword [1733, 1743)》 +64 │ }, +65 │ ); +66 │}【 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */《 +72 │function】》〚 〛parse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see +74 │ // which one makes the most sense, and grab the NodeArray from there. Do + + +〚Positions: [1744, 1744]〛 +【TS: FunctionKeyword [1607, 1743)】 +《Go: FunctionKeyword [1733, 1743)》 +64 │ }, +65 │ ); +66 │}【 +67 │ +68 │/** +69 │ * Tries to parse something into either "top-level" statements, or into blocks +70 │ * of class-context definitions. +71 │ */《 +72 │function】》 〚p〛arse(sourceFile: SourceFile, content: string): NodeArray { +73 │ // We're going to speculatively parse different kinds of contexts to see +74 │ // which one makes the most sense, and grab the NodeArray from there. Do From b4cc421836dd1fb234a8de08d3900f227810becc Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 21 Apr 2025 11:09:41 -0700 Subject: [PATCH 13/25] completion basic member test passing --- internal/ast/utilities.go | 1 + internal/astnav/tokens.go | 10 +-- internal/astnav/tokens_test.go | 2 - internal/checker/checker.go | 19 +++-- internal/checker/utilities.go | 2 +- internal/ls/completions.go | 137 ++++++++++++++++++++------------ internal/ls/completions_test.go | 98 +++++++++++++++++++---- internal/ls/types.go | 11 ++- internal/lsp/server.go | 6 +- 9 files changed, 194 insertions(+), 92 deletions(-) diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 867c32898f..4b2e34a034 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -1962,6 +1962,7 @@ func TryGetTextOfPropertyName(name *Node) (string, bool) { return "", false } +// True if node is of a JSDoc kind that may contain comment text. func IsJSDocCommentContainingNode(node *Node) bool { return node.Kind == KindJSDoc || node.Kind == KindJSDocText || diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go index f420b8fa36..78393105c1 100644 --- a/internal/astnav/tokens.go +++ b/internal/astnav/tokens.go @@ -412,11 +412,6 @@ func isJSDocSingleCommentNodeList(parent *ast.Node, nodeList *ast.NodeList) bool return parent.Kind == ast.KindJSDoc && nodeList == parent.AsJSDoc().Comment && nodeList != nil && len(nodeList.Nodes) == 1 } -// !!! -func FindNextToken(previousToken *ast.Node, parent *ast.Node, file *ast.SourceFile) *ast.Node { - return nil -} - // Looks for rightmost valid token in the range [startPos, endPos). // If position is >= 0, looks for rightmost valid token that preceeds or touches that position. func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingNode *ast.Node, position int, excludeJSDoc bool) *ast.Node { @@ -534,3 +529,8 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN return find(containingNode) } + +// !!! +func FindNextToken(previousToken *ast.Node, parent *ast.Node, file *ast.SourceFile) *ast.Node { + return nil +} diff --git a/internal/astnav/tokens_test.go b/internal/astnav/tokens_test.go index d3c6a6ecb0..498f3aa7c2 100644 --- a/internal/astnav/tokens_test.go +++ b/internal/astnav/tokens_test.go @@ -15,7 +15,6 @@ import ( "github.com/microsoft/typescript-go/internal/parser" "github.com/microsoft/typescript-go/internal/repo" "github.com/microsoft/typescript-go/internal/scanner" - "github.com/microsoft/typescript-go/internal/testutil" "github.com/microsoft/typescript-go/internal/testutil/baseline" "github.com/microsoft/typescript-go/internal/testutil/jstest" "gotest.tools/v3/assert" @@ -96,7 +95,6 @@ func baselineTokens(t *testing.T, testName string, includeEOF bool, getTSTokens currentDiff := tokenDiff{} for pos, tsToken := range tsTokens { - defer testutil.RecoverAndFail(t, fmt.Sprintf("pos: %d", pos)) goToken := getGoToken(file, pos) diff := tokenDiff{goToken: goToken, tsToken: tsToken} diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 97500a0332..a976be87b0 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -1407,7 +1407,7 @@ func (c *Checker) checkAndReportErrorForInvalidInitializer(errorLocation *ast.No } func (c *Checker) checkAndReportErrorForMissingPrefix(errorLocation *ast.Node, name string) bool { - if !ast.IsIdentifier(errorLocation) || errorLocation.Text() != name || isTypeReferenceIdentifier(errorLocation) || isInTypeQuery(errorLocation) { + if !ast.IsIdentifier(errorLocation) || errorLocation.Text() != name || isTypeReferenceIdentifier(errorLocation) || IsInTypeQuery(errorLocation) { return false } container := c.getThisContainer(errorLocation, false /*includeArrowFunctions*/, false /*includeClassComputedPropertyName*/) @@ -1765,7 +1765,7 @@ func (c *Checker) isBlockScopedNameDeclaredBeforeUse(declaration *ast.Node, usag useFile := ast.GetSourceFileOfNode(usage) declContainer := ast.GetEnclosingBlockScopeContainer(declaration) if declarationFile != useFile { - if (c.moduleKind != core.ModuleKindNone && (declarationFile.ExternalModuleIndicator != nil || useFile.ExternalModuleIndicator != nil)) || c.compilerOptions.OutFile == "" || isInTypeQuery(usage) || declaration.Flags&ast.NodeFlagsAmbient != 0 { + if (c.moduleKind != core.ModuleKindNone && (declarationFile.ExternalModuleIndicator != nil || useFile.ExternalModuleIndicator != nil)) || c.compilerOptions.OutFile == "" || IsInTypeQuery(usage) || declaration.Flags&ast.NodeFlagsAmbient != 0 { // nodes are in different files and order cannot be determined return true } @@ -1778,7 +1778,7 @@ func (c *Checker) isBlockScopedNameDeclaredBeforeUse(declaration *ast.Node, usag return slices.Index(sourceFiles, declarationFile) <= slices.Index(sourceFiles, useFile) } // deferred usage in a type context is always OK regardless of the usage position: - if usage.Flags&ast.NodeFlagsJSDoc != 0 || isInTypeQuery(usage) || c.isInAmbientOrTypeNode(usage) { + if usage.Flags&ast.NodeFlagsJSDoc != 0 || IsInTypeQuery(usage) || c.isInAmbientOrTypeNode(usage) { return true } if declaration.Pos() <= usage.Pos() && !(ast.IsPropertyDeclaration(declaration) && isThisProperty(usage.Parent) && declaration.Initializer() == nil && !isExclamationToken(declaration.AsPropertyDeclaration().PostfixToken)) { @@ -10454,7 +10454,7 @@ func (c *Checker) checkIdentifier(node *ast.Node, checkMode CheckMode) *Type { isSpreadDestructuringAssignmentTarget || isModuleExports || c.isSameScopedBindingElement(node, declaration) || - t != c.autoType && t != c.autoArrayType && (!c.strictNullChecks || t.flags&(TypeFlagsAnyOrUnknown|TypeFlagsVoid) != 0 || isInTypeQuery(node) || c.isInAmbientOrTypeNode(node) || node.Parent.Kind == ast.KindExportSpecifier) || + t != c.autoType && t != c.autoArrayType && (!c.strictNullChecks || t.flags&(TypeFlagsAnyOrUnknown|TypeFlagsVoid) != 0 || IsInTypeQuery(node) || c.isInAmbientOrTypeNode(node) || node.Parent.Kind == ast.KindExportSpecifier) || ast.IsNonNullExpression(node.Parent) || ast.IsVariableDeclaration(declaration) && declaration.AsVariableDeclaration().ExclamationToken != nil || declaration.Flags&ast.NodeFlagsAmbient != 0 @@ -11382,7 +11382,7 @@ func (c *Checker) checkThisExpression(node *ast.Node) *Type { // do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks } } - t := c.tryGetThisTypeAtEx(node, true /*includeGlobalThis*/, container) + t := c.TryGetThisTypeAtEx(node, true /*includeGlobalThis*/, container) if c.noImplicitThis { globalThisType := c.getTypeOfSymbol(c.globalThisSymbol) if t == globalThisType && capturedByArrowFunction { @@ -11405,10 +11405,13 @@ func (c *Checker) checkThisExpression(node *ast.Node) *Type { } func (c *Checker) tryGetThisTypeAt(node *ast.Node) *Type { - return c.tryGetThisTypeAtEx(node, true /*includeGlobalThis*/, c.getThisContainer(node, false /*includeArrowFunctions*/, false /*includeClassComputedPropertyName*/)) + return c.TryGetThisTypeAtEx(node, true /*includeGlobalThis*/, nil /*container*/) } -func (c *Checker) tryGetThisTypeAtEx(node *ast.Node, includeGlobalThis bool, container *ast.Node) *Type { +func (c *Checker) TryGetThisTypeAtEx(node *ast.Node, includeGlobalThis bool, container *ast.Node) *Type { + if container == nil { + container = c.getThisContainer(node, false /*includeArrowFunctions*/, false /*includeClassComputedPropertyName*/) + } if ast.IsFunctionLike(container) && (!c.isInParameterInitializerBeforeContainingFunction(node) || getThisParameter(container) != nil) { thisType := c.getThisTypeOfDeclaration(container) // Note: a parameter initializer should refer to class-this unless function-this is explicitly annotated. @@ -26546,7 +26549,7 @@ func (c *Checker) markAliasReferenced(symbol *ast.Symbol, location *ast.Node) { if !c.canCollectSymbolAliasAccessibilityData { return } - if ast.IsNonLocalAlias(symbol, ast.SymbolFlagsValue /*excludes*/) && !isInTypeQuery(location) { + if ast.IsNonLocalAlias(symbol, ast.SymbolFlagsValue /*excludes*/) && !IsInTypeQuery(location) { target := c.resolveAlias(symbol) if c.getSymbolFlagsEx(symbol, true /*excludeTypeOnlyMeanings*/, false /*excludeLocalMeanings*/)&(ast.SymbolFlagsValue|ast.SymbolFlagsExportValue) != 0 { // An alias resolving to a const enum cannot be elided if (1) 'isolatedModules' is enabled diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go index 84d98a53e5..0df68646bb 100644 --- a/internal/checker/utilities.go +++ b/internal/checker/utilities.go @@ -224,7 +224,7 @@ func isTypeReferenceIdentifier(node *ast.Node) bool { return ast.IsTypeReferenceNode(node.Parent) } -func isInTypeQuery(node *ast.Node) bool { +func IsInTypeQuery(node *ast.Node) bool { // TypeScript 1.0 spec (April 2014): 3.6.3 // A type query consists of the keyword typeof followed by an expression. // The expression is restricted to a single identifier or a sequence of identifiers separated by periods diff --git a/internal/ls/completions.go b/internal/ls/completions.go index e90789e804..eb12bd27ba 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -26,14 +26,10 @@ func (l *LanguageService) ProvideCompletion( position int, context *lsproto.CompletionContext, clientOptions *lsproto.CompletionClientCapabilities, + preferences *UserPreferences, ) *lsproto.CompletionList { program, file := l.getProgramAndFile(fileName) - node := astnav.GetTouchingPropertyName(file, position) - if node.Kind == ast.KindSourceFile { - return nil - } - // !!! get user preferences - return l.getCompletionsAtPosition(program, file, position, context, nil /*preferences*/, clientOptions) // !!! get preferences + return l.getCompletionsAtPosition(program, file, position, context, preferences, clientOptions) } // !!! figure out other kinds of completion data return @@ -113,14 +109,14 @@ type sortText string const ( SortTextLocalDeclarationPriority sortText = "10" - sortTextLocationPriority sortText = "11" - sortTextOptionalMember sortText = "12" - sortTextMemberDeclaredBySpreadAssignment sortText = "13" - sortTextSuggestedClassMembers sortText = "14" - sortTextGlobalsOrKeywords sortText = "15" - sortTextAutoImportSuggestions sortText = "16" - sortTextClassMemberSnippets sortText = "17" - sortTextJavascriptIdentifiers sortText = "18" + SortTextLocationPriority sortText = "11" + SortTextOptionalMember sortText = "12" + SortTextMemberDeclaredBySpreadAssignment sortText = "13" + SortTextSuggestedClassMembers sortText = "14" + SortTextGlobalsOrKeywords sortText = "15" + SortTextAutoImportSuggestions sortText = "16" + SortTextClassMemberSnippets sortText = "17" + SortTextJavascriptIdentifiers sortText = "18" ) func deprecateSortText(original sortText) sortText { @@ -254,9 +250,9 @@ func (l *LanguageService) getCompletionsAtPosition( return nil } - if *context.TriggerCharacter == " " { + if context.TriggerCharacter != nil && *context.TriggerCharacter == " " { // `isValidTrigger` ensures we are at `import |` - if preferences.includeCompletionsForImportStatements { + if ptrIsTrue(preferences.includeCompletionsForImportStatements) { // !!! isMemberCompletion return &lsproto.CompletionList{ IsIncomplete: true, @@ -677,7 +673,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position insertQuestionDot := false if typeChecker.IsNullableType(t) { canCorrectToQuestionDot := isRightOfDot && !isRightOfQuestionDot && - preferences.includeAutomaticOptionalChainCompletions + !ptrIsFalse(preferences.includeAutomaticOptionalChainCompletions) if canCorrectToQuestionDot || isRightOfQuestionDot { t = typeChecker.GetNonNullableType(t) if canCorrectToQuestionDot { @@ -687,7 +683,35 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position } addTypeProperties(t, node.Flags&ast.NodeFlagsAwaitContext != 0, insertQuestionDot) } + + return + } + } + } + + if !isTypeLocation || checker.IsInTypeQuery(node) { + // GH#39946. Pulling on the type of a node inside of a function with a contextual `this` parameter can result in a circularity + // if the `node` is part of the exprssion of a `yield` or `return`. This circularity doesn't exist at compile time because + // we will check (and cache) the type of `this` *before* checking the type of the node. + typeChecker.TryGetThisTypeAtEx(node, false /*includeGlobalThis*/, nil) + t := typeChecker.GetNonOptionalType(typeChecker.GetTypeAtLocation(node)) + + if !isTypeLocation { + insertQuestionDot := false + if typeChecker.IsNullableType(t) { + canCorrectToQuestionDot := isRightOfDot && !isRightOfQuestionDot && + !ptrIsFalse(preferences.includeAutomaticOptionalChainCompletions) + + if canCorrectToQuestionDot || isRightOfQuestionDot { + t = typeChecker.GetNonNullableType(t) + if canCorrectToQuestionDot { + insertQuestionDot = true + } + } } + addTypeProperties(t, node.Flags&ast.NodeFlagsAwaitContext != 0, insertQuestionDot) + } else { + addTypeProperties(typeChecker.GetNonNullableType(t), false /*insertAwait*/, false /*insertQuestionDot*/) } } } @@ -926,7 +950,7 @@ func getCompletionEntriesFromSymbols( originalSortText := data.symbolToSortTextMap[ast.GetSymbolId(symbol)] if originalSortText == "" { - originalSortText = sortTextLocationPriority + originalSortText = SortTextLocationPriority } sortText := core.IfElse(isDeprecated(symbol, typeChecker), deprecateSortText(originalSortText), originalSortText) entry := createCompletionItem( @@ -989,7 +1013,7 @@ func createCompletionItemForLiteral( return &lsproto.CompletionItem{ Label: completionNameForLiteral(file, preferences, literal), Kind: ptrTo(lsproto.CompletionItemKindConstant), - SortText: ptrTo(string(sortTextLocationPriority)), + SortText: ptrTo(string(SortTextLocationPriority)), CommitCharacters: ptrTo([]string{}), } } @@ -1131,7 +1155,7 @@ func createCompletionItem( // Completion should add a comma after "red" and provide completions for b if data.completionKind == CompletionKindObjectPropertyDeclaration && contextToken != nil && - !ast.NodeHasKind(astnav.FindPrecedingTokenEx(file, contextToken.Pos(), contextToken), ast.KindCommaToken) { + !ast.NodeHasKind(astnav.FindPrecedingTokenEx(file, contextToken.Pos(), contextToken, false /*excludeJSDoc*/), ast.KindCommaToken) { if ast.IsMethodDeclaration(contextToken.Parent.Parent) || ast.IsGetAccessorDeclaration(contextToken.Parent.Parent) || ast.IsSetAccessorDeclaration(contextToken.Parent.Parent) || @@ -1144,7 +1168,7 @@ func createCompletionItem( } } - if preferences.includeCompletionsWithClassMemberSnippets && + if ptrIsTrue(preferences.includeCompletionsWithClassMemberSnippets) && data.completionKind == CompletionKindMemberLike && isClassLikeMemberCompletion(symbol, data.location, file) { // !!! class member completions @@ -1165,13 +1189,13 @@ func createCompletionItem( if data.isJsxIdentifierExpected && !data.isRightOfOpenTag && ptrIsTrue(clientOptions.CompletionItem.SnippetSupport) && - preferences.jsxAttributeCompletionStyle != JsxAttributeCompletionStyleNone && + !jsxAttributeCompletionStyleIs(preferences.jsxAttributeCompletionStyle, JsxAttributeCompletionStyleNone) && !(ast.IsJsxAttribute(data.location.Parent) && data.location.Parent.Initializer() != nil) { - useBraces := preferences.jsxAttributeCompletionStyle == JsxAttributeCompletionStyleBraces + useBraces := jsxAttributeCompletionStyleIs(preferences.jsxAttributeCompletionStyle, JsxAttributeCompletionStyleBraces) t := typeChecker.GetTypeOfSymbolAtLocation(symbol, data.location) // If is boolean like or undefined, don't return a snippet, we want to return just the completion. - if preferences.jsxAttributeCompletionStyle == JsxAttributeCompletionStyleAuto && + if jsxAttributeCompletionStyleIs(preferences.jsxAttributeCompletionStyle, JsxAttributeCompletionStyleAuto) && t.Flags()&checker.TypeFlagsBooleanLike == 0 && !(t.Flags()&checker.TypeFlagsUnion != 0 && core.Some(t.Types(), func(t *checker.Type) bool { return t.Flags()&checker.TypeFlagsBooleanLike != 0 })) { if t.Flags()&checker.TypeFlagsStringLike != 0 || @@ -1332,6 +1356,13 @@ func ptrIsTrue(ptr *bool) bool { return *ptr } +func ptrIsFalse(ptr *bool) bool { + if ptr == nil { + return false + } + return !*ptr +} + func boolToPtr(v bool) *bool { if v { return ptrTo(true) @@ -1339,6 +1370,13 @@ func boolToPtr(v bool) *bool { return nil } +func jsxAttributeCompletionStyleIs(preferenceStyle *JsxAttributeCompletionStyle, style JsxAttributeCompletionStyle) bool { + if preferenceStyle == nil { + return false + } + return *preferenceStyle == style +} + func getLineOfPosition(file *ast.SourceFile, pos int) int { line, _ := scanner.GetLineAndCharacterOfPosition(file, pos) return line @@ -1423,9 +1461,9 @@ func shouldIncludeSymbol( // Auto Imports are not available for scripts so this conditional is always false. if file.AsSourceFile().ExternalModuleIndicator != nil && compilerOptions.AllowUmdGlobalAccess != core.TSTrue && - data.symbolToSortTextMap[ast.GetSymbolId(symbol)] == sortTextGlobalsOrKeywords && - (data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == sortTextAutoImportSuggestions || - data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == sortTextLocationPriority) { + data.symbolToSortTextMap[ast.GetSymbolId(symbol)] == SortTextGlobalsOrKeywords && + (data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == SortTextAutoImportSuggestions || + data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == SortTextLocationPriority) { return false } @@ -1457,7 +1495,12 @@ func getCompletionEntryDisplayNameForSymbol( return "", false } - name := core.IfElse(originIncludesSymbolName(origin), origin.symbolName(), symbol.Name) + var name string + if originIncludesSymbolName(origin) { + name = origin.symbolName() + } else { + name = symbol.Name + } if name == "" || // If the symbol is external module, don't show it in the completion list // (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there) @@ -2027,30 +2070,21 @@ func generateIdentifierForArbitraryString(text string, languageVersion core.Scri // Copied from vscode TS extension. func getCompletionsSymbolKind(kind ScriptElementKind) lsproto.CompletionItemKind { switch kind { - case ScriptElementKindPrimitiveType: - case ScriptElementKindKeyword: + case ScriptElementKindPrimitiveType, ScriptElementKindKeyword: return lsproto.CompletionItemKindKeyword - case ScriptElementKindConstElement: - case ScriptElementKindLetElement: - case ScriptElementKindVariableElement: - case ScriptElementKindLocalVariableElement: - case ScriptElementKindAlias: - case ScriptElementKindParameterElement: + case ScriptElementKindConstElement, ScriptElementKindLetElement, ScriptElementKindVariableElement, + ScriptElementKindLocalVariableElement, ScriptElementKindAlias, ScriptElementKindParameterElement: return lsproto.CompletionItemKindVariable - case ScriptElementKindMemberVariableElement: - case ScriptElementKindMemberGetAccessorElement: - case ScriptElementKindMemberSetAccessorElement: + case ScriptElementKindMemberVariableElement, ScriptElementKindMemberGetAccessorElement, + ScriptElementKindMemberSetAccessorElement: return lsproto.CompletionItemKindField - case ScriptElementKindFunctionElement: - case ScriptElementKindLocalFunctionElement: + case ScriptElementKindFunctionElement, ScriptElementKindLocalFunctionElement: return lsproto.CompletionItemKindFunction - case ScriptElementKindMemberFunctionElement: - case ScriptElementKindConstructSignatureElement: - case ScriptElementKindCallSignatureElement: - case ScriptElementKindIndexSignatureElement: + case ScriptElementKindMemberFunctionElement, ScriptElementKindConstructSignatureElement, + ScriptElementKindCallSignatureElement, ScriptElementKindIndexSignatureElement: return lsproto.CompletionItemKindMethod case ScriptElementKindEnumElement: @@ -2059,12 +2093,10 @@ func getCompletionsSymbolKind(kind ScriptElementKind) lsproto.CompletionItemKind case ScriptElementKindEnumMemberElement: return lsproto.CompletionItemKindEnumMember - case ScriptElementKindModuleElement: - case ScriptElementKindExternalModuleName: + case ScriptElementKindModuleElement, ScriptElementKindExternalModuleName: return lsproto.CompletionItemKindModule - case ScriptElementKindClassElement: - case ScriptElementKindTypeElement: + case ScriptElementKindClassElement, ScriptElementKindTypeElement: return lsproto.CompletionItemKindClass case ScriptElementKindInterfaceElement: @@ -2085,7 +2117,6 @@ func getCompletionsSymbolKind(kind ScriptElementKind) lsproto.CompletionItemKind default: return lsproto.CompletionItemKindProperty } - panic("Unhandled script element kind: " + kind) } // Editors will use the `sortText` and then fall back to `name` for sorting, but leave ties in response order. @@ -2125,7 +2156,7 @@ var ( result = append(result, &lsproto.CompletionItem{ Label: scanner.TokenToString(i), Kind: ptrTo(lsproto.CompletionItemKindKeyword), - SortText: ptrTo(string(sortTextGlobalsOrKeywords)), + SortText: ptrTo(string(SortTextGlobalsOrKeywords)), }) } return result @@ -2255,7 +2286,7 @@ func getContextualKeywords(file *ast.SourceFile, contextToken *ast.Node, positio entries = append(entries, &lsproto.CompletionItem{ Label: scanner.TokenToString(ast.KindAssertKeyword), Kind: ptrTo(lsproto.CompletionItemKindKeyword), - SortText: ptrTo(string(sortTextGlobalsOrKeywords)), + SortText: ptrTo(string(SortTextGlobalsOrKeywords)), }) } } @@ -2282,7 +2313,7 @@ func getJSCompletionEntries( &lsproto.CompletionItem{ Label: name, Kind: ptrTo(lsproto.CompletionItemKindText), - SortText: ptrTo(string(sortTextJavascriptIdentifiers)), + SortText: ptrTo(string(SortTextJavascriptIdentifiers)), CommitCharacters: ptrTo([]string{}), }, compareCompletionEntries, diff --git a/internal/ls/completions_test.go b/internal/ls/completions_test.go index 18f719975d..6f5e8f3afe 100644 --- a/internal/ls/completions_test.go +++ b/internal/ls/completions_test.go @@ -6,31 +6,97 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/lsp/lsproto" - "github.com/microsoft/typescript-go/internal/project" + "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" "gotest.tools/v3/assert" ) +var defaultCommitCharacters = []string{".", ",", ";"} + +type testCase struct { + name string + content string + position int + expected *lsproto.CompletionList +} + func TestCompletions(t *testing.T) { + testCases := []testCase{ + { + name: "basicInterfaceMembers", + content: `export {}; +interface Point { + x: number; + y: number; +} +declare const p: Point; +p.`, + position: 87, + expected: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: &lsproto.CompletionItemDefaults{ + CommitCharacters: &defaultCommitCharacters, + }, + Items: []lsproto.CompletionItem{ + { + Label: "x", + Kind: ptrTo(lsproto.CompletionItemKindField), + SortText: ptrTo(string(ls.SortTextLocationPriority)), + InsertTextFormat: ptrTo(lsproto.InsertTextFormatPlainText), + }, + { + Label: "y", + Kind: ptrTo(lsproto.CompletionItemKindField), + SortText: ptrTo(string(ls.SortTextLocationPriority)), + InsertTextFormat: ptrTo(lsproto.InsertTextFormatPlainText), + }, + }, + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + run_test(t, testCase.content, testCase.position, testCase.expected) + }) + } +} + +func run_test(t *testing.T, content string, position int, expected *lsproto.CompletionList) { files := map[string]string{ - "index.ts": "", + "/index.ts": content, + } + languageService := createLanguageService("/index.ts", files) + context := &lsproto.CompletionContext{ + TriggerKind: lsproto.CompletionTriggerKindInvoked, + } + capabilities := &lsproto.CompletionClientCapabilities{ + CompletionItem: &lsproto.ClientCompletionItemOptions{ + SnippetSupport: ptrTo(true), + CommitCharactersSupport: ptrTo(true), + PreselectSupport: ptrTo(true), + LabelDetailsSupport: ptrTo(true), + }, + CompletionList: &lsproto.CompletionListCapabilities{ + ItemDefaults: &[]string{"commitCharacters"}, + }, } - ls := createLanguageService("/", files) - var context *lsproto.CompletionContext - var capabilities *lsproto.CompletionClientCapabilities - completionList := ls.ProvideCompletion("index.ts", 0, context, capabilities) - assert.Assert(t, completionList != nil) + preferences := &ls.UserPreferences{} + completionList := languageService.ProvideCompletion( + "/index.ts", + position, + context, + capabilities, + preferences) + assert.DeepEqual(t, completionList, expected) } -func createLanguageService(cd string, files map[string]string) *ls.LanguageService { - // !!! TODO: replace with service_test.go's `setup` - projectServiceHost := newProjectServiceHost(files) - projectService := project.NewService(projectServiceHost, project.ServiceOptions{}) - compilerOptions := &core.CompilerOptions{} - project := project.NewInferredProject(compilerOptions, cd, "/", projectService) +func createLanguageService(fileName string, files map[string]string) *ls.LanguageService { + projectService, _ := projecttestutil.Setup(files) + projectService.OpenFile(fileName, files[fileName], core.ScriptKindTS, "") + project := projectService.Projects()[0] return project.LanguageService() } -func newProjectServiceHost(files map[string]string) project.ServiceHost { - // !!! TODO: import from service_test.go - return nil +func ptrTo[T any](v T) *T { + return &v } diff --git a/internal/ls/types.go b/internal/ls/types.go index 9d1f00ee47..a888266df3 100644 --- a/internal/ls/types.go +++ b/internal/ls/types.go @@ -25,27 +25,26 @@ const ( ) type UserPreferences struct { - // !!! use *bool?? // Enables auto-import-style completions on partially-typed import statements. E.g., allows // `import write|` to be completed to `import { writeFile } from "fs"`. - includeCompletionsForImportStatements bool + includeCompletionsForImportStatements *bool // 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 bool + includeAutomaticOptionalChainCompletions *bool // 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 bool + includeCompletionsWithClassMemberSnippets *bool // 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 bool + includeCompletionsWithObjectLiteralMethodSnippets *bool - jsxAttributeCompletionStyle JsxAttributeCompletionStyle + jsxAttributeCompletionStyle *JsxAttributeCompletionStyle } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index f6f408ddc9..1d03a9573a 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -194,6 +194,8 @@ func (s *Server) handleMessage(req *lsproto.RequestMessage) error { return s.handleHover(req) case *lsproto.DefinitionParams: return s.handleDefinition(req) + case *lsproto.CompletionParams: + return s.handleCompletion(req) default: switch req.Method { case lsproto.MethodShutdown: @@ -386,11 +388,13 @@ func (s *Server) handleCompletion(req *lsproto.RequestMessage) error { return s.sendError(req.ID, err) } + // !!! get user preferences list := project.LanguageService().ProvideCompletion( file.FileName(), pos, params.Context, - s.initializeParams.Capabilities.TextDocument.Completion) + s.initializeParams.Capabilities.TextDocument.Completion, + &ls.UserPreferences{}) return s.sendResult(req.ID, list) } From 1aa96a4f6158c96bf44ecde668c5ec301d97a92d Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 21 Apr 2025 12:39:32 -0700 Subject: [PATCH 14/25] refactoring and fixes after merge --- internal/ast/utilities.go | 13 +-- internal/astnav/tokens.go | 8 +- internal/checker/utilities.go | 8 -- internal/core/core.go | 8 -- internal/core/set.go | 9 ++ internal/ls/completions.go | 65 ++++++----- internal/ls/types.go | 10 +- internal/ls/utilities.go | 59 ++++------ ...FindPrecedingToken.mapCode.ts.baseline.txt | 28 ++--- .../compiler/checkJsFiles2.errors.txt | 9 -- .../compiler/checkJsFiles3.errors.txt | 9 -- .../compiler/checkJsFiles4.errors.txt | 9 -- ...kJsObjectLiteralHasCheckedKeyof.errors.txt | 18 --- .../checkJsdocOptionalParamOrder.errors.txt | 14 --- .../checkJsdocReturnTag1.errors.txt | 28 ----- .../checkJsdocReturnTag2.errors.txt | 29 ----- .../conformance/checkJsdocTypeTag1.errors.txt | 60 ---------- .../conformance/checkJsdocTypeTag2.errors.txt | 50 --------- ...ckJsdocTypeTagOnObjectProperty1.errors.txt | 31 ----- ...ckJsdocTypeTagOnObjectProperty2.errors.txt | 36 ------ .../checkJsdocTypedefInParamTag1.errors.txt | 56 --------- ...checkJsdocTypedefOnlySourceFile.errors.txt | 24 ---- .../conformance/jsdocTypeTagCast.errors.txt | 106 ------------------ ...ocTypeTagOnObjectProperty1.errors.txt.diff | 36 ------ ...eckJsdocTypedefInParamTag1.errors.txt.diff | 61 ---------- 25 files changed, 90 insertions(+), 694 deletions(-) delete mode 100644 testdata/baselines/reference/submodule/compiler/checkJsFiles2.errors.txt delete mode 100644 testdata/baselines/reference/submodule/compiler/checkJsFiles3.errors.txt delete mode 100644 testdata/baselines/reference/submodule/compiler/checkJsFiles4.errors.txt delete mode 100644 testdata/baselines/reference/submodule/compiler/checkJsObjectLiteralHasCheckedKeyof.errors.txt delete mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocOptionalParamOrder.errors.txt delete mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag1.errors.txt delete mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag2.errors.txt delete mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag1.errors.txt delete mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag2.errors.txt delete mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt delete mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty2.errors.txt delete mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypedefInParamTag1.errors.txt delete mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypedefOnlySourceFile.errors.txt delete mode 100644 testdata/baselines/reference/submodule/conformance/jsdocTypeTagCast.errors.txt delete mode 100644 testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt.diff delete mode 100644 testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypedefInParamTag1.errors.txt.diff diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 960b10d90a..7f027b5aa3 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -2661,10 +2661,9 @@ func GetPragmaArgument(pragma *Pragma, name string) string { } func IsCheckJSEnabledForFile(sourceFile *SourceFile, compilerOptions *core.CompilerOptions) bool { - // !!! - // if sourceFile.CheckJsDirective != nil { - // return sourceFile.CheckJsDirective.Enabled - // } + if sourceFile.CheckJsDirective != nil { + return sourceFile.CheckJsDirective.Enabled + } return compilerOptions.CheckJs == core.TSTrue } @@ -2675,10 +2674,6 @@ func GetLeftmostAccessExpression(expr *Node) *Node { return expr } -func IsSourceFileJs(file *SourceFile) bool { - return IsInJSFile(file.AsNode()) -} - func IsTypeOnlyImportDeclaration(node *Node) bool { switch node.Kind { case KindImportSpecifier: @@ -2741,7 +2736,6 @@ func GetLanguageVariant(scriptKind core.ScriptKind) core.LanguageVariant { return core.LanguageVariantStandard } -// !!! Shared? func IsCallLikeExpression(node *Node) bool { switch node.Kind { case KindJsxOpeningElement, KindJsxSelfClosingElement, KindCallExpression, KindNewExpression, @@ -2751,7 +2745,6 @@ func IsCallLikeExpression(node *Node) bool { return false } -// !!! Shared? func IsCallLikeOrFunctionLikeExpression(node *Node) bool { return IsCallLikeExpression(node) || IsFunctionExpressionOrArrowFunction(node) } diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go index 40b5d4e749..8e47eb1e75 100644 --- a/internal/astnav/tokens.go +++ b/internal/astnav/tokens.go @@ -344,7 +344,7 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a // 1) `position` precedes `child`'s tokens or `child` has no tokens (ie: in a comment or whitespace preceding `child`): // we need to find the last token in a previous child node or child tokens. // 2) `position` is within the same span: we recurse on `child`. - start := getStartOfNode(foundChild, sourceFile, !excludeJSDoc /*includeJSDoc*/) + start := GetStartOfNode(foundChild, sourceFile, !excludeJSDoc /*includeJSDoc*/) lookInPreviousChild := start >= position || // cursor in the leading trivia or preceding tokens !isValidPrecedingNode(foundChild, sourceFile) if lookInPreviousChild { @@ -398,12 +398,12 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a } func isValidPrecedingNode(node *ast.Node, sourceFile *ast.SourceFile) bool { - start := getStartOfNode(node, sourceFile, false /*includeJSDoc*/) + start := GetStartOfNode(node, sourceFile, false /*includeJSDoc*/) width := node.End() - start return !(ast.IsWhitespaceOnlyJsxText(node) || width == 0) } -func getStartOfNode(node *ast.Node, file *ast.SourceFile, includeJSDoc bool) int { +func GetStartOfNode(node *ast.Node, file *ast.SourceFile, includeJSDoc bool) int { return scanner.GetTokenPosOfNode(node, file, includeJSDoc) } @@ -432,7 +432,7 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN hasChildren := false test := func(node *ast.Node) bool { if node.Flags&ast.NodeFlagsReparsed != 0 || - node.End() > endPos || getStartOfNode(node, sourceFile, !excludeJSDoc /*includeJSDoc*/) >= position { + node.End() > endPos || GetStartOfNode(node, sourceFile, !excludeJSDoc /*includeJSDoc*/) >= position { return false } rightmostVisitedNode = node diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go index 24598d7c4c..7d079abc73 100644 --- a/internal/checker/utilities.go +++ b/internal/checker/utilities.go @@ -1722,14 +1722,6 @@ func canIncludeBindAndCheckDiagnostics(sourceFile *ast.SourceFile, options *core return isPlainJS || isCheckJS || sourceFile.ScriptKind == core.ScriptKindDeferred } -func isCheckJsEnabledForFile(sourceFile *ast.SourceFile, compilerOptions *core.CompilerOptions) bool { - // !!! - // if sourceFile.CheckJsDirective != nil { - // return sourceFile.CheckJsDirective.Enabled - // } - return compilerOptions.CheckJs == core.TSTrue -} - func isPlainJSFile(file *ast.SourceFile, checkJs core.Tristate) bool { return file != nil && (file.ScriptKind == core.ScriptKindJS || file.ScriptKind == core.ScriptKindJSX) && file.CheckJsDirective == nil && checkJs == core.TSUnknown } diff --git a/internal/core/core.go b/internal/core/core.go index cfdb3e530d..811922c0fc 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -540,14 +540,6 @@ func Identity[T any](t T) T { return t } -func AddIfAbsent[K comparable](seen Set[K], key K) bool { - if seen.Has(key) { - return false - } - seen.Add(key) - return true -} - func CheckEachDefined[S any](s []*S, msg string) []*S { for _, value := range s { if value == nil { diff --git a/internal/core/set.go b/internal/core/set.go index 54e261109c..3d094cc2ac 100644 --- a/internal/core/set.go +++ b/internal/core/set.go @@ -39,6 +39,15 @@ func (s *Set[T]) Clear() { clear(s.M) } +// Returns true if the key was not already present in the set. +func (s *Set[T]) AddIfAbsent(key T) bool { + if s.Has(key) { + return false + } + s.Add(key) + return true +} + func NewSetFromItems[T comparable](items ...T) *Set[T] { s := &Set[T]{} for _, item := range items { diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 21c7e60182..d76fe410e2 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -252,7 +252,7 @@ func (l *LanguageService) getCompletionsAtPosition( if context.TriggerCharacter != nil && *context.TriggerCharacter == " " { // `isValidTrigger` ensures we are at `import |` - if ptrIsTrue(preferences.includeCompletionsForImportStatements) { + if ptrIsTrue(preferences.IncludeCompletionsForImportStatements) { // !!! isMemberCompletion return &lsproto.CompletionList{ IsIncomplete: true, @@ -280,7 +280,7 @@ func (l *LanguageService) getCompletionsAtPosition( // switch completionData.Kind // !!! other data cases // !!! transform data into completion list - response := completionInfoFromData( + response := l.completionInfoFromData( file, program, compilerOptions, @@ -310,7 +310,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position // The decision to provide completion depends on the contextToken, which is determined through the previousToken. // Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file - // isJSOnlyLocation := !insideJsDocTagTypeExpression && !insideJsDocImportTag && ast.IsSourceFileJs(file) + // isJSOnlyLocation := !insideJsDocTagTypeExpression && !insideJsDocImportTag && ast.IsSourceFileJS(file) previousToken, contextToken := getRelevantTokens(position, file) // Find the node where completion is requested on. @@ -460,7 +460,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position addSymbolOriginInfo := func(symbol *ast.Symbol, insertQuestionDot bool, insertAwait bool) { symbolId := ast.GetSymbolId(symbol) - if insertAwait && core.AddIfAbsent(seenPropertySymbols, symbolId) { + if insertAwait && seenPropertySymbols.AddIfAbsent(symbolId) { symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindPromise, insertQuestionDot)} } else if insertQuestionDot { symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{kind: symbolOriginInfoKindNullable} @@ -501,7 +501,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position if firstAccessibleSymbol != nil { firstAccessibleSymbolId = ast.GetSymbolId(firstAccessibleSymbol) } - if firstAccessibleSymbolId != 0 && core.AddIfAbsent(seenPropertySymbols, firstAccessibleSymbolId) { + if firstAccessibleSymbolId != 0 && seenPropertySymbols.AddIfAbsent(firstAccessibleSymbolId) { symbols = append(symbols, firstAccessibleSymbol) moduleSymbol := firstAccessibleSymbol.Parent if moduleSymbol == nil || @@ -673,7 +673,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position insertQuestionDot := false if typeChecker.IsNullableType(t) { canCorrectToQuestionDot := isRightOfDot && !isRightOfQuestionDot && - !ptrIsFalse(preferences.includeAutomaticOptionalChainCompletions) + !ptrIsFalse(preferences.IncludeAutomaticOptionalChainCompletions) if canCorrectToQuestionDot || isRightOfQuestionDot { t = typeChecker.GetNonNullableType(t) if canCorrectToQuestionDot { @@ -700,7 +700,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position insertQuestionDot := false if typeChecker.IsNullableType(t) { canCorrectToQuestionDot := isRightOfDot && !isRightOfQuestionDot && - !ptrIsFalse(preferences.includeAutomaticOptionalChainCompletions) + !ptrIsFalse(preferences.IncludeAutomaticOptionalChainCompletions) if canCorrectToQuestionDot || isRightOfQuestionDot { t = typeChecker.GetNonNullableType(t) @@ -795,7 +795,7 @@ func getDefaultCommitCharacters(isNewIdentifierLocation bool) []string { return slices.Clone(allCommitCharacters) } -func completionInfoFromData( +func (l *LanguageService) completionInfoFromData( file *ast.SourceFile, program *compiler.Program, compilerOptions *core.CompilerOptions, @@ -830,7 +830,7 @@ func completionInfoFromData( return nil } - uniqueNames, sortedEntries := getCompletionEntriesFromSymbols( + uniqueNames, sortedEntries := l.getCompletionEntriesFromSymbols( data, nil, /*replacementToken*/ position, @@ -845,7 +845,7 @@ func completionInfoFromData( if data.keywordFilters != KeywordCompletionFiltersNone { keywordCompletions := getKeywordCompletions( data.keywordFilters, - !data.insideJsDocTagTypeExpression && ast.IsSourceFileJs(file)) + !data.insideJsDocTagTypeExpression && ast.IsSourceFileJS(file)) for _, keywordEntry := range keywordCompletions { if data.isTypeOnlyLocation && isTypeKeyword(scanner.StringToToken(keywordEntry.Label)) || !data.isTypeOnlyLocation && isContextualKeywordInAutoImportableExpressionSpace(keywordEntry.Label) || @@ -901,7 +901,7 @@ func completionInfoFromData( } } -func getCompletionEntriesFromSymbols( +func (l *LanguageService) getCompletionEntriesFromSymbols( data *completionData, replacementToken *ast.Node, position int, @@ -938,7 +938,7 @@ func getCompletionEntriesFromSymbols( } // When in a value location in a JS file, ignore symbols that definitely seem to be type-only. - if !data.isTypeOnlyLocation && ast.IsSourceFileJs(file) && symbolAppearsToBeTypeOnly(symbol, typeChecker) { + if !data.isTypeOnlyLocation && ast.IsSourceFileJS(file) && symbolAppearsToBeTypeOnly(symbol, typeChecker) { continue } @@ -947,7 +947,7 @@ func getCompletionEntriesFromSymbols( originalSortText = SortTextLocationPriority } sortText := core.IfElse(isDeprecated(symbol, typeChecker), deprecateSortText(originalSortText), originalSortText) - entry := createCompletionItem( + entry := l.createCompletionItem( symbol, sortText, replacementToken, @@ -1012,7 +1012,7 @@ func createCompletionItemForLiteral( } } -func createCompletionItem( +func (l *LanguageService) createCompletionItem( symbol *ast.Symbol, sortText sortText, replacementToken *ast.Node, @@ -1031,7 +1031,7 @@ func createCompletionItem( contextToken := data.contextToken var insertText string var filterText string - replacementSpan := getReplacementRangeForContextToken(file, replacementToken, position) + replacementSpan := l.getReplacementRangeForContextToken(file, replacementToken, position) var isSnippet, hasAction bool source := getSourceFromOrigin(origin) var labelDetails *lsproto.CompletionItemLabelDetails @@ -1084,7 +1084,7 @@ func createCompletionItem( } else { end = dot.End() } - replacementSpan = createLspRangeFromBounds(getStartOfNode(dot, file), end, file) + replacementSpan = l.createLspRangeFromBounds(astnav.GetStartOfNode(dot, file, false /*includeJSDoc*/), end, file) } if data.jsxInitializer.isInitializer { @@ -1093,7 +1093,7 @@ func createCompletionItem( } insertText = fmt.Sprintf("{%s}", insertText) if data.jsxInitializer.initializer != nil { - replacementSpan = createLspRangeFromNode(data.jsxInitializer.initializer, file) + replacementSpan = l.createLspRangeFromNode(data.jsxInitializer.initializer, file) } } @@ -1120,7 +1120,10 @@ func createCompletionItem( data.propertyAccessToConvert.Parent, data.propertyAccessToConvert.Expression(), ) - replacementSpan = createLspRangeFromBounds(getStartOfNode(wrapNode, file), data.propertyAccessToConvert.End(), file) + replacementSpan = l.createLspRangeFromBounds( + astnav.GetStartOfNode(wrapNode, file, false /*includeJSDoc*/), + data.propertyAccessToConvert.End(), + file) } if originIsResolvedExport(origin) { @@ -1162,7 +1165,7 @@ func createCompletionItem( } } - if ptrIsTrue(preferences.includeCompletionsWithClassMemberSnippets) && + if ptrIsTrue(preferences.IncludeCompletionsWithClassMemberSnippets) && data.completionKind == CompletionKindMemberLike && isClassLikeMemberCompletion(symbol, data.location, file) { // !!! class member completions @@ -1183,13 +1186,13 @@ func createCompletionItem( if data.isJsxIdentifierExpected && !data.isRightOfOpenTag && ptrIsTrue(clientOptions.CompletionItem.SnippetSupport) && - !jsxAttributeCompletionStyleIs(preferences.jsxAttributeCompletionStyle, JsxAttributeCompletionStyleNone) && + !jsxAttributeCompletionStyleIs(preferences.JsxAttributeCompletionStyle, JsxAttributeCompletionStyleNone) && !(ast.IsJsxAttribute(data.location.Parent) && data.location.Parent.Initializer() != nil) { - useBraces := jsxAttributeCompletionStyleIs(preferences.jsxAttributeCompletionStyle, JsxAttributeCompletionStyleBraces) + useBraces := jsxAttributeCompletionStyleIs(preferences.JsxAttributeCompletionStyle, JsxAttributeCompletionStyleBraces) t := typeChecker.GetTypeOfSymbolAtLocation(symbol, data.location) // If is boolean like or undefined, don't return a snippet, we want to return just the completion. - if jsxAttributeCompletionStyleIs(preferences.jsxAttributeCompletionStyle, JsxAttributeCompletionStyleAuto) && + if jsxAttributeCompletionStyleIs(preferences.JsxAttributeCompletionStyle, JsxAttributeCompletionStyleAuto) && t.Flags()&checker.TypeFlagsBooleanLike == 0 && !(t.Flags()&checker.TypeFlagsUnion != 0 && core.Some(t.Types(), func(t *checker.Type) bool { return t.Flags()&checker.TypeFlagsBooleanLike != 0 })) { if t.Flags()&checker.TypeFlagsStringLike != 0 || @@ -1628,7 +1631,7 @@ func isValidTrigger(file *ast.SourceFile, triggerCharacter CompletionsTriggerCha // Only automatically bring up completions if this is an opening quote. return contextToken != nil && isStringLiteralOrTemplate(contextToken) && - position == getStartOfNode(contextToken, file)+1 + position == astnav.GetStartOfNode(contextToken, file, false /*includeJSDoc*/)+1 case "#": return contextToken != nil && ast.IsPrivateIdentifier(contextToken) && @@ -1667,7 +1670,7 @@ func binaryExpressionMayBeOpenTag(binaryExpression *ast.BinaryExpression) bool { } func isCheckedFile(file *ast.SourceFile, compilerOptions *core.CompilerOptions) bool { - return !ast.IsSourceFileJs(file) || ast.IsCheckJSEnabledForFile(file, compilerOptions) + return !ast.IsSourceFileJS(file) || ast.IsCheckJSEnabledForFile(file, compilerOptions) } func isContextTokenValueLocation(contextToken *ast.Node) bool { @@ -1722,7 +1725,7 @@ func symbolCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checke func nonAliasCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules core.Set[ast.SymbolId]) bool { return symbol.Flags&ast.SymbolFlagsType != 0 || typeChecker.IsUnknownSymbol(symbol) || - symbol.Flags&ast.SymbolFlagsModule != 0 && core.AddIfAbsent(seenModules, ast.GetSymbolId(symbol)) && + symbol.Flags&ast.SymbolFlagsModule != 0 && seenModules.AddIfAbsent(ast.GetSymbolId(symbol)) && core.Some( typeChecker.GetExportsOfModule(symbol), func(e *ast.Symbol) bool { return symbolCanBeReferencedAtTypeLocation(e, typeChecker, seenModules) }) @@ -1963,7 +1966,7 @@ func isDeprecated(symbol *ast.Symbol, typeChecker *checker.Checker) bool { return len(declarations) > 0 && core.Every(declarations, func(decl *ast.Declaration) bool { return typeChecker.IsDeprecatedDeclaration(decl) }) } -func getReplacementRangeForContextToken(file *ast.SourceFile, contextToken *ast.Node, position int) *lsproto.Range { +func (l *LanguageService) getReplacementRangeForContextToken(file *ast.SourceFile, contextToken *ast.Node, position int) *lsproto.Range { if contextToken == nil { return nil } @@ -1971,15 +1974,15 @@ func getReplacementRangeForContextToken(file *ast.SourceFile, contextToken *ast. // !!! ensure range is single line switch contextToken.Kind { case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral: - return createRangeFromStringLiteralLikeContent(file, contextToken, position) + return l.createRangeFromStringLiteralLikeContent(file, contextToken, position) default: - return createLspRangeFromNode(contextToken, file) + return l.createLspRangeFromNode(contextToken, file) } } -func createRangeFromStringLiteralLikeContent(file *ast.SourceFile, node *ast.StringLiteralLike, position int) *lsproto.Range { +func (l *LanguageService) createRangeFromStringLiteralLikeContent(file *ast.SourceFile, node *ast.StringLiteralLike, position int) *lsproto.Range { replacementEnd := node.End() - 1 - nodeStart := getStartOfNode(node, file) + nodeStart := astnav.GetStartOfNode(node, file, false /*includeJSDoc*/) if ast.IsUnterminatedLiteral(node) { // we return no replacement range only if unterminated string is empty if nodeStart == replacementEnd { @@ -1987,7 +1990,7 @@ func createRangeFromStringLiteralLikeContent(file *ast.SourceFile, node *ast.Str } replacementEnd = min(position, node.End()) } - return createLspRangeFromBounds(nodeStart+1, replacementEnd, file) + return l.createLspRangeFromBounds(nodeStart+1, replacementEnd, file) } func quotePropertyName(file *ast.SourceFile, preferences *UserPreferences, name string) string { diff --git a/internal/ls/types.go b/internal/ls/types.go index a888266df3..67c5028d80 100644 --- a/internal/ls/types.go +++ b/internal/ls/types.go @@ -27,24 +27,24 @@ const ( type UserPreferences struct { // Enables auto-import-style completions on partially-typed import statements. E.g., allows // `import write|` to be completed to `import { writeFile } from "fs"`. - includeCompletionsForImportStatements *bool + IncludeCompletionsForImportStatements *bool // 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 *bool + IncludeAutomaticOptionalChainCompletions *bool // 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 *bool + IncludeCompletionsWithClassMemberSnippets *bool // 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 *bool + IncludeCompletionsWithObjectLiteralMethodSnippets *bool - jsxAttributeCompletionStyle *JsxAttributeCompletionStyle + JsxAttributeCompletionStyle *JsxAttributeCompletionStyle } diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index e10431b937..a82d2d3584 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -12,28 +12,11 @@ import ( "github.com/microsoft/typescript-go/internal/scanner" ) -// !!! Shared (placeholder) +// !!! func isInString(file *ast.SourceFile, position int, previousToken *ast.Node) bool { - if previousToken == nil { - previousToken = astnav.FindPrecedingToken(file, position) - } - - if previousToken != nil && isStringTextContainingNode(previousToken) { - } - return false } -// !!! Shared (placeholder) -func isStringTextContainingNode(node *ast.Node) bool { - return true -} - -// !!! Shared (placeholder) -func getStartOfNode(node *ast.Node, file *ast.SourceFile) int { - return node.Pos() -} - func tryGetImportFromModuleSpecifier(node *ast.StringLiteralLike) *ast.Node { switch node.Parent.Kind { case ast.KindImportDeclaration, ast.KindExportDeclaration, ast.KindJSDocImportTag: @@ -57,18 +40,11 @@ func tryGetImportFromModuleSpecifier(node *ast.StringLiteralLike) *ast.Node { return nil } -// !!! Shared (placeholder) +// !!! func isInComment(file *ast.SourceFile, position int, tokenAtPosition *ast.Node) *ast.CommentRange { return nil } -// // Returns a non-nil comment range if the cursor at position in sourceFile is within a comment. -// // tokenAtPosition: must equal `getTokenAtPosition(sourceFile, position)` -// // predicate: additional predicate to test on the comment range. -// func isInComment(file *ast.SourceFile, position int, tokenAtPosition *ast.Node) *ast.CommentRange { -// return getRangeOfEnclosingComment(file, position, nil /*precedingToken*/, tokenAtPosition) -// } - // !!! // Replaces last(node.getChildren(sourceFile)) func getLastChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { @@ -110,18 +86,18 @@ func findChildOfKind(node *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) return nil } -// !!! Shared: placeholder +// !!! type PossibleTypeArgumentInfo struct { called *ast.IdentifierNode nTypeArguments int } -// !!! Shared: placeholder +// !!! func getPossibleTypeArgumentsInfo(tokenIn *ast.Node, sourceFile *ast.SourceFile) *PossibleTypeArgumentInfo { return nil } -// !!! Shared: placeholder +// !!! func getPossibleGenericSignatures(called *ast.Expression, typeArgumentCount int, checker *checker.Checker) []*checker.Signature { return nil } @@ -134,13 +110,16 @@ func isInRightSideOfInternalImportEqualsDeclaration(node *ast.Node) bool { return ast.IsInternalModuleImportEqualsDeclaration(node.Parent) && node.Parent.AsImportEqualsDeclaration().ModuleReference == node } -func createLspRangeFromNode(node *ast.Node, file *ast.SourceFile) *lsproto.Range { - return createLspRangeFromBounds(node.Pos(), node.End(), file) +func (l *LanguageService) createLspRangeFromNode(node *ast.Node, file *ast.SourceFile) *lsproto.Range { + return l.createLspRangeFromBounds(node.Pos(), node.End(), file) } -func createLspRangeFromBounds(start, end int, file *ast.SourceFile) *lsproto.Range { - // !!! needs converters access - return nil +func (l *LanguageService) createLspRangeFromBounds(start, end int, file *ast.SourceFile) *lsproto.Range { + lspRange, err := l.converters.ToLSPRange(file.FileName(), core.NewTextRange(start, end)) + if err != nil { + panic(err) + } + return &lspRange } func quote(file *ast.SourceFile, preferences *UserPreferences, text string) string { @@ -160,8 +139,8 @@ const ( quotePreferenceDouble ) +// !!! func getQuotePreference(file *ast.SourceFile, preferences *UserPreferences) quotePreference { - // !!! return quotePreferenceDouble } @@ -258,7 +237,7 @@ func nodeIsASICandidate(node *ast.Node, file *ast.SourceFile) bool { } startLine, _ := scanner.GetLineAndCharacterOfPosition(file, node.End()) - endLine, _ := scanner.GetLineAndCharacterOfPosition(file, getStartOfNode(nextToken, file)) + endLine, _ := scanner.GetLineAndCharacterOfPosition(file, astnav.GetStartOfNode(nextToken, file, false /*includeJSDoc*/)) return startLine != endLine } @@ -285,8 +264,12 @@ func probablyUsesSemicolons(file *ast.SourceFile) bool { if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken { withSemicolon++ } else if lastToken != nil && lastToken.Kind != ast.KindCommaToken { - lastTokenLine, _ := scanner.GetLineAndCharacterOfPosition(file, getStartOfNode(lastToken, file)) - nextTokenLine, _ := scanner.GetLineAndCharacterOfPosition(file, scanner.GetRangeOfTokenAtPosition(file, lastToken.End()).Pos()) + lastTokenLine, _ := scanner.GetLineAndCharacterOfPosition( + file, + astnav.GetStartOfNode(lastToken, file, false /*includeJSDoc*/)) + nextTokenLine, _ := scanner.GetLineAndCharacterOfPosition( + file, + scanner.GetRangeOfTokenAtPosition(file, lastToken.End()).Pos()) // Avoid counting missing semicolon in single-line objects: // `function f(p: { x: string /*no semicolon here is insignificant*/ }) {` if lastTokenLine != nextTokenLine { diff --git a/testdata/baselines/reference/astnav/FindPrecedingToken.mapCode.ts.baseline.txt b/testdata/baselines/reference/astnav/FindPrecedingToken.mapCode.ts.baseline.txt index b8d2ff1c48..6ac134a860 100644 --- a/testdata/baselines/reference/astnav/FindPrecedingToken.mapCode.ts.baseline.txt +++ b/testdata/baselines/reference/astnav/FindPrecedingToken.mapCode.ts.baseline.txt @@ -1,6 +1,6 @@ -〚Positions: [1612, 1732]〛 -【TS: nil】 -《Go: JSDoc [1607, 1733)》 +〚Positions: [1612, 1732]〛 +【TS: nil】 +《Go: JSDoc [1607, 1733)》 64 │ }, 65 │ ); 66 │}《 @@ -11,11 +11,11 @@ 71 │ */〛》 72 │function parse(sourceFile: SourceFile, content: string): NodeArray { 73 │ // We're going to speculatively parse different kinds of contexts to see - - -〚Positions: [1733, 1735]〛 -【TS: JSDoc [1611, 1733)】 -《Go: JSDoc [1607, 1733)》 + + +〚Positions: [1733, 1735]〛 +【TS: JSDoc [1611, 1733)】 +《Go: JSDoc [1607, 1733)》 64 │ }, 65 │ ); 66 │}《 @@ -26,11 +26,11 @@ 71 │ */】》〚 72 │f〛unction parse(sourceFile: SourceFile, content: string): NodeArray { 73 │ // We're going to speculatively parse different kinds of contexts to see - - -〚Positions: [1736, 1744]〛 -【TS: FunctionKeyword [1607, 1743)】 -《Go: FunctionKeyword [1733, 1743)》 + + +〚Positions: [1736, 1744]〛 +【TS: FunctionKeyword [1607, 1743)】 +《Go: FunctionKeyword [1733, 1743)》 64 │ }, 65 │ ); 66 │}【 @@ -41,4 +41,4 @@ 71 │ */《 72 │f〚unction】》 p〛arse(sourceFile: SourceFile, content: string): NodeArray { 73 │ // We're going to speculatively parse different kinds of contexts to see -74 │ // which one makes the most sense, and grab the NodeArray from there. Do \ No newline at end of file +74 │ // which one makes the most sense, and grab the NodeArray from there. Do diff --git a/testdata/baselines/reference/submodule/compiler/checkJsFiles2.errors.txt b/testdata/baselines/reference/submodule/compiler/checkJsFiles2.errors.txt deleted file mode 100644 index f106054306..0000000000 --- a/testdata/baselines/reference/submodule/compiler/checkJsFiles2.errors.txt +++ /dev/null @@ -1,9 +0,0 @@ -a.js(3,1): error TS2322: Type 'number' is not assignable to type 'string'. - - -==== a.js (1 errors) ==== - // @ts-check - var x = "string"; - x = 0; - ~ -!!! error TS2322: Type 'number' is not assignable to type 'string'. \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/checkJsFiles3.errors.txt b/testdata/baselines/reference/submodule/compiler/checkJsFiles3.errors.txt deleted file mode 100644 index f106054306..0000000000 --- a/testdata/baselines/reference/submodule/compiler/checkJsFiles3.errors.txt +++ /dev/null @@ -1,9 +0,0 @@ -a.js(3,1): error TS2322: Type 'number' is not assignable to type 'string'. - - -==== a.js (1 errors) ==== - // @ts-check - var x = "string"; - x = 0; - ~ -!!! error TS2322: Type 'number' is not assignable to type 'string'. \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/checkJsFiles4.errors.txt b/testdata/baselines/reference/submodule/compiler/checkJsFiles4.errors.txt deleted file mode 100644 index f106054306..0000000000 --- a/testdata/baselines/reference/submodule/compiler/checkJsFiles4.errors.txt +++ /dev/null @@ -1,9 +0,0 @@ -a.js(3,1): error TS2322: Type 'number' is not assignable to type 'string'. - - -==== a.js (1 errors) ==== - // @ts-check - var x = "string"; - x = 0; - ~ -!!! error TS2322: Type 'number' is not assignable to type 'string'. \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/checkJsObjectLiteralHasCheckedKeyof.errors.txt b/testdata/baselines/reference/submodule/compiler/checkJsObjectLiteralHasCheckedKeyof.errors.txt deleted file mode 100644 index 50e261254c..0000000000 --- a/testdata/baselines/reference/submodule/compiler/checkJsObjectLiteralHasCheckedKeyof.errors.txt +++ /dev/null @@ -1,18 +0,0 @@ -file.js(11,1): error TS2322: Type '"z"' is not assignable to type '"x" | "y"'. - - -==== file.js (1 errors) ==== - // @ts-check - const obj = { - x: 1, - y: 2 - }; - - /** - * @type {keyof typeof obj} - */ - let selected = "x"; - selected = "z"; // should fail - ~~~~~~~~ -!!! error TS2322: Type '"z"' is not assignable to type '"x" | "y"'. - \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocOptionalParamOrder.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocOptionalParamOrder.errors.txt deleted file mode 100644 index b29c988e17..0000000000 --- a/testdata/baselines/reference/submodule/conformance/checkJsdocOptionalParamOrder.errors.txt +++ /dev/null @@ -1,14 +0,0 @@ -0.js(7,20): error TS1016: A required parameter cannot follow an optional parameter. - - -==== 0.js (1 errors) ==== - // @ts-check - /** - * @param {number} a - * @param {number} [b] - * @param {number} c - */ - function foo(a, b, c) {} - ~ -!!! error TS1016: A required parameter cannot follow an optional parameter. - \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag1.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag1.errors.txt deleted file mode 100644 index 47f1a3209d..0000000000 --- a/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag1.errors.txt +++ /dev/null @@ -1,28 +0,0 @@ -returns.js(20,12): error TS2872: This kind of expression is always truthy. - - -==== returns.js (1 errors) ==== - // @ts-check - /** - * @returns {string} This comment is not currently exposed - */ - function f() { - return "hello"; - } - - /** - * @returns {string=} This comment is not currently exposed - */ - function f1() { - return "hello world"; - } - - /** - * @returns {string|number} This comment is not currently exposed - */ - function f2() { - return 5 || "hello"; - ~ -!!! error TS2872: This kind of expression is always truthy. - } - \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag2.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag2.errors.txt deleted file mode 100644 index c5ab204ed8..0000000000 --- a/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag2.errors.txt +++ /dev/null @@ -1,29 +0,0 @@ -returns.js(6,5): error TS2322: Type 'number' is not assignable to type 'string'. -returns.js(13,5): error TS2322: Type 'number | boolean' is not assignable to type 'string | number'. - Type 'boolean' is not assignable to type 'string | number'. -returns.js(13,12): error TS2872: This kind of expression is always truthy. - - -==== returns.js (3 errors) ==== - // @ts-check - /** - * @returns {string} This comment is not currently exposed - */ - function f() { - return 5; - ~~~~~~ -!!! error TS2322: Type 'number' is not assignable to type 'string'. - } - - /** - * @returns {string | number} This comment is not currently exposed - */ - function f1() { - return 5 || true; - ~~~~~~ -!!! error TS2322: Type 'number | boolean' is not assignable to type 'string | number'. -!!! error TS2322: Type 'boolean' is not assignable to type 'string | number'. - ~ -!!! error TS2872: This kind of expression is always truthy. - } - \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag1.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag1.errors.txt deleted file mode 100644 index beeb1723a4..0000000000 --- a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag1.errors.txt +++ /dev/null @@ -1,60 +0,0 @@ -0.js(20,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? -0.js(24,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? -0.js(28,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? -0.js(40,5): error TS2403: Subsequent variable declarations must have the same type. Variable 'props' must be of type 'object', but here has type 'Object'. - - -==== 0.js (4 errors) ==== - // @ts-check - /** @type {String} */ - var S = "hello world"; - - /** @type {number} */ - var n = 10; - - /** @type {*} */ - var anyT = 2; - anyT = "hello"; - - /** @type {?} */ - var anyT1 = 2; - anyT1 = "hi"; - - /** @type {Function} */ - const x = (a) => a + 1; - x(1); - - /** @type {function} */ - ~~~~~~~~ -!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? -!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. - const y = (a) => a + 1; - y(1); - - /** @type {function (number)} */ - ~~~~~~~~ -!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? -!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. - const x1 = (a) => a + 1; - x1(0); - - /** @type {function (number): number} */ - ~~~~~~~~ -!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? -!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. - const x2 = (a) => a + 1; - x2(0); - - /** - * @type {object} - */ - var props = {}; - - /** - * @type {Object} - */ - var props = {}; - ~~~~~ -!!! error TS2403: Subsequent variable declarations must have the same type. Variable 'props' must be of type 'object', but here has type 'Object'. -!!! related TS6203 0.js:35:5: 'props' was also declared here. - \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag2.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag2.errors.txt deleted file mode 100644 index 76933dc06a..0000000000 --- a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag2.errors.txt +++ /dev/null @@ -1,50 +0,0 @@ -0.js(3,5): error TS2322: Type 'boolean' is not assignable to type 'String'. -0.js(6,5): error TS2322: Type 'string' is not assignable to type 'number'. -0.js(8,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? -0.js(12,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? -0.js(19,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? -0.js(23,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? - - -==== 0.js (6 errors) ==== - // @ts-check - /** @type {String} */ - var S = true; - ~ -!!! error TS2322: Type 'boolean' is not assignable to type 'String'. - - /** @type {number} */ - var n = "hello"; - ~ -!!! error TS2322: Type 'string' is not assignable to type 'number'. - - /** @type {function (number)} */ - ~~~~~~~~ -!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? -!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. - const x1 = (a) => a + 1; - x1("string"); - - /** @type {function (number): number} */ - ~~~~~~~~ -!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? -!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. - const x2 = (a) => a + 1; - - /** @type {string} */ - var a; - a = x2(0); - - /** @type {function (number): number} */ - ~~~~~~~~ -!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? -!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. - const x3 = (a) => a.concat("hi"); - x3(0); - - /** @type {function (number): string} */ - ~~~~~~~~ -!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? -!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. - const x4 = (a) => a + 1; - x4(0); \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt deleted file mode 100644 index 35c7d11f52..0000000000 --- a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt +++ /dev/null @@ -1,31 +0,0 @@ -0.js(16,14): error TS2552: Cannot find name 'function'. Did you mean 'Function'? - - -==== 0.js (1 errors) ==== - // @ts-check - var lol = "hello Lol" - const obj = { - /** @type {string|undefined} */ - foo: undefined, - /** @type {string|undefined} */ - bar: "42", - /** @type {function(number): number} */ - method1(n1) { - return n1 + 42; - }, - /** @type {string} */ - lol, - /** @type {number} */ - ['b' + 'ar1']: 42, - /** @type {function(number): number} */ - ~~~~~~~~ -!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? -!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. - arrowFunc: (num) => num + 42 - } - obj.foo = 'string' - obj.lol - obj.bar = undefined; - var k = obj.method1(0); - obj.bar1 = "42"; - obj.arrowFunc(0); \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty2.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty2.errors.txt deleted file mode 100644 index 579a296c34..0000000000 --- a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty2.errors.txt +++ /dev/null @@ -1,36 +0,0 @@ -0.js(5,8): error TS2352: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. -0.js(10,14): error TS2552: Cannot find name 'function'. Did you mean 'Function'? -0.js(12,14): error TS2552: Cannot find name 'function'. Did you mean 'Function'? - - -==== 0.js (3 errors) ==== - // @ts-check - var lol; - const obj = { - /** @type {string|undefined} */ - bar: 42, - ~~ -!!! error TS2352: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. - /** @type {function(number): number} */ - method1(n1) { - return "42"; - }, - /** @type {function(number): number} */ - ~~~~~~~~ -!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? -!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. - method2: (n1) => "lol", - /** @type {function(number): number} */ - ~~~~~~~~ -!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? -!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. - arrowFunc: (num="0") => num + 42, - /** @type {string} */ - lol - } - lol = "string" - /** @type {string} */ - var s = obj.method1(0); - - /** @type {string} */ - var s1 = obj.method2("0"); \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefInParamTag1.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefInParamTag1.errors.txt deleted file mode 100644 index 5c9948e856..0000000000 --- a/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefInParamTag1.errors.txt +++ /dev/null @@ -1,56 +0,0 @@ -0.js(15,5): error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts': y, z, w -0.js(28,6): error TS2741: Property 'anotherY' is missing in type '{ anotherX: string; }' but required in type 'AnotherOpts'. -0.js(42,6): error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts1': y, z, w - - -==== 0.js (3 errors) ==== - // @ts-check - /** - * @typedef {Object} Opts - * @property {string} x - * @property {string=} y - * @property {string} [z] - * @property {string} [w="hi"] - * - * @param {Opts} opts - */ - function foo(opts) { - opts.x; - } - - foo({x: 'abc'}); - ~~~~~~~~~~ -!!! error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts': y, z, w - - /** - * @typedef {Object} AnotherOpts - * @property anotherX {string} - * @property anotherY {string=} - * - * @param {AnotherOpts} opts - */ - function foo1(opts) { - opts.anotherX; - } - - foo1({anotherX: "world"}); - ~~~~~~~~~~~~~~~~~~~ -!!! error TS2741: Property 'anotherY' is missing in type '{ anotherX: string; }' but required in type 'AnotherOpts'. -!!! related TS2728 0.js:20:14: 'anotherY' is declared here. - - /** - * @typedef {object} Opts1 - * @property {string} x - * @property {string=} y - * @property {string} [z] - * @property {string} [w="hi"] - * - * @param {Opts1} opts - */ - function foo2(opts) { - opts.x; - } - foo2({x: 'abc'}); - ~~~~~~~~~~ -!!! error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts1': y, z, w - \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefOnlySourceFile.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefOnlySourceFile.errors.txt deleted file mode 100644 index 7cb22b9170..0000000000 --- a/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefOnlySourceFile.errors.txt +++ /dev/null @@ -1,24 +0,0 @@ -0.js(3,5): error TS2441: Duplicate identifier 'exports'. Compiler reserves name 'exports' in top level scope of a module. -0.js(8,9): error TS2339: Property 'SomeName' does not exist on type '{}'. -0.js(10,12): error TS2503: Cannot find namespace 'exports'. - - -==== 0.js (3 errors) ==== - // @ts-check - - var exports = {}; - ~~~~~~~ -!!! error TS2441: Duplicate identifier 'exports'. Compiler reserves name 'exports' in top level scope of a module. - - /** - * @typedef {string} - */ - exports.SomeName; - ~~~~~~~~ -!!! error TS2339: Property 'SomeName' does not exist on type '{}'. - - /** @type {exports.SomeName} */ - ~~~~~~~ -!!! error TS2503: Cannot find namespace 'exports'. - const myString = 'str'; - \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/jsdocTypeTagCast.errors.txt b/testdata/baselines/reference/submodule/conformance/jsdocTypeTagCast.errors.txt deleted file mode 100644 index 2cac730ae2..0000000000 --- a/testdata/baselines/reference/submodule/conformance/jsdocTypeTagCast.errors.txt +++ /dev/null @@ -1,106 +0,0 @@ -b.js(4,31): error TS2352: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. -b.js(17,14): error TS2339: Property 'p' does not exist on type 'SomeBase'. -b.js(23,14): error TS2339: Property 'x' does not exist on type 'SomeDerived'. -b.js(28,14): error TS2339: Property 'q' does not exist on type 'SomeOther'. -b.js(66,15): error TS1228: A type predicate is only allowed in return type position for functions and methods. -b.js(66,38): error TS2454: Variable 'numOrStr' is used before being assigned. -b.js(67,2): error TS2322: Type 'string | number' is not assignable to type 'string'. - Type 'number' is not assignable to type 'string'. -b.js(67,8): error TS2454: Variable 'numOrStr' is used before being assigned. - - -==== a.ts (0 errors) ==== - var W: string; - -==== b.js (8 errors) ==== - // @ts-check - var W = /** @type {string} */(/** @type {*} */ (4)); - - var W = /** @type {string} */(4); // Error - ~ -!!! error TS2352: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. - - /** @type {*} */ - var a; - - /** @type {string} */ - var s; - - var a = /** @type {*} */("" + 4); - var s = "" + /** @type {*} */(4); - - class SomeBase { - constructor() { - this.p = 42; - ~ -!!! error TS2339: Property 'p' does not exist on type 'SomeBase'. - } - } - class SomeDerived extends SomeBase { - constructor() { - super(); - this.x = 42; - ~ -!!! error TS2339: Property 'x' does not exist on type 'SomeDerived'. - } - } - class SomeOther { - constructor() { - this.q = 42; - ~ -!!! error TS2339: Property 'q' does not exist on type 'SomeOther'. - } - } - - function SomeFakeClass() { - /** @type {string|number} */ - this.p = "bar"; - } - - // Type assertion should check for assignability in either direction - var someBase = new SomeBase(); - var someDerived = new SomeDerived(); - var someOther = new SomeOther(); - var someFakeClass = new SomeFakeClass(); - - someBase = /** @type {SomeBase} */(someDerived); - someBase = /** @type {SomeBase} */(someBase); - someBase = /** @type {SomeBase} */(someOther); // Error - - someDerived = /** @type {SomeDerived} */(someDerived); - someDerived = /** @type {SomeDerived} */(someBase); - someDerived = /** @type {SomeDerived} */(someOther); // Error - - someOther = /** @type {SomeOther} */(someDerived); // Error - someOther = /** @type {SomeOther} */(someBase); // Error - someOther = /** @type {SomeOther} */(someOther); - - someFakeClass = someBase; - someFakeClass = someDerived; - - someBase = someFakeClass; // Error - someBase = /** @type {SomeBase} */(someFakeClass); - - // Type assertion cannot be a type-predicate type - /** @type {number | string} */ - var numOrStr; - /** @type {string} */ - var str; - if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error - ~~~~~~~~~~~~~~~~~~ -!!! error TS1228: A type predicate is only allowed in return type position for functions and methods. - ~~~~~~~~ -!!! error TS2454: Variable 'numOrStr' is used before being assigned. - str = numOrStr; // Error, no narrowing occurred - ~~~ -!!! error TS2322: Type 'string | number' is not assignable to type 'string'. -!!! error TS2322: Type 'number' is not assignable to type 'string'. - ~~~~~~~~ -!!! error TS2454: Variable 'numOrStr' is used before being assigned. - } - - - var asConst1 = /** @type {const} */(1); - var asConst2 = /** @type {const} */({ - x: 1 - }); \ No newline at end of file diff --git a/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt.diff b/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt.diff deleted file mode 100644 index 7aa3c6a048..0000000000 --- a/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt.diff +++ /dev/null @@ -1,36 +0,0 @@ ---- old.checkJsdocTypeTagOnObjectProperty1.errors.txt -+++ new.checkJsdocTypeTagOnObjectProperty1.errors.txt -@@= skipped -0, +-1 lines =@@ -- -@@= skipped --1, +1 lines =@@ -+0.js(16,14): error TS2552: Cannot find name 'function'. Did you mean 'Function'? -+ -+ -+==== 0.js (1 errors) ==== -+ // @ts-check -+ var lol = "hello Lol" -+ const obj = { -+ /** @type {string|undefined} */ -+ foo: undefined, -+ /** @type {string|undefined} */ -+ bar: "42", -+ /** @type {function(number): number} */ -+ method1(n1) { -+ return n1 + 42; -+ }, -+ /** @type {string} */ -+ lol, -+ /** @type {number} */ -+ ['b' + 'ar1']: 42, -+ /** @type {function(number): number} */ -+ ~~~~~~~~ -+!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? -+!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. -+ arrowFunc: (num) => num + 42 -+ } -+ obj.foo = 'string' -+ obj.lol -+ obj.bar = undefined; -+ var k = obj.method1(0); -+ obj.bar1 = "42"; -+ obj.arrowFunc(0); diff --git a/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypedefInParamTag1.errors.txt.diff b/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypedefInParamTag1.errors.txt.diff deleted file mode 100644 index d1d0f17779..0000000000 --- a/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypedefInParamTag1.errors.txt.diff +++ /dev/null @@ -1,61 +0,0 @@ ---- old.checkJsdocTypedefInParamTag1.errors.txt -+++ new.checkJsdocTypedefInParamTag1.errors.txt -@@= skipped -0, +-1 lines =@@ -- -@@= skipped --1, +1 lines =@@ -+0.js(15,5): error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts': y, z, w -+0.js(28,6): error TS2741: Property 'anotherY' is missing in type '{ anotherX: string; }' but required in type 'AnotherOpts'. -+0.js(42,6): error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts1': y, z, w -+ -+ -+==== 0.js (3 errors) ==== -+ // @ts-check -+ /** -+ * @typedef {Object} Opts -+ * @property {string} x -+ * @property {string=} y -+ * @property {string} [z] -+ * @property {string} [w="hi"] -+ * -+ * @param {Opts} opts -+ */ -+ function foo(opts) { -+ opts.x; -+ } -+ -+ foo({x: 'abc'}); -+ ~~~~~~~~~~ -+!!! error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts': y, z, w -+ -+ /** -+ * @typedef {Object} AnotherOpts -+ * @property anotherX {string} -+ * @property anotherY {string=} -+ * -+ * @param {AnotherOpts} opts -+ */ -+ function foo1(opts) { -+ opts.anotherX; -+ } -+ -+ foo1({anotherX: "world"}); -+ ~~~~~~~~~~~~~~~~~~~ -+!!! error TS2741: Property 'anotherY' is missing in type '{ anotherX: string; }' but required in type 'AnotherOpts'. -+!!! related TS2728 0.js:20:14: 'anotherY' is declared here. -+ -+ /** -+ * @typedef {object} Opts1 -+ * @property {string} x -+ * @property {string=} y -+ * @property {string} [z] -+ * @property {string} [w="hi"] -+ * -+ * @param {Opts1} opts -+ */ -+ function foo2(opts) { -+ opts.x; -+ } -+ foo2({x: 'abc'}); -+ ~~~~~~~~~~ -+!!! error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts1': y, z, w -+ From 62b46e17b96c125b9ec1b64062131bfc858fd268 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 21 Apr 2025 13:55:59 -0700 Subject: [PATCH 15/25] re-add baselines --- .../compiler/checkJsFiles2.errors.txt | 9 ++ .../compiler/checkJsFiles3.errors.txt | 9 ++ .../compiler/checkJsFiles4.errors.txt | 9 ++ ...kJsObjectLiteralHasCheckedKeyof.errors.txt | 18 +++ .../checkJsdocOptionalParamOrder.errors.txt | 14 +++ .../checkJsdocReturnTag1.errors.txt | 28 +++++ .../checkJsdocReturnTag2.errors.txt | 29 +++++ .../conformance/checkJsdocTypeTag1.errors.txt | 60 ++++++++++ .../conformance/checkJsdocTypeTag2.errors.txt | 50 +++++++++ ...ckJsdocTypeTagOnObjectProperty1.errors.txt | 31 +++++ ...ckJsdocTypeTagOnObjectProperty2.errors.txt | 36 ++++++ .../checkJsdocTypedefInParamTag1.errors.txt | 56 +++++++++ ...checkJsdocTypedefOnlySourceFile.errors.txt | 24 ++++ .../conformance/jsdocTypeTagCast.errors.txt | 106 ++++++++++++++++++ ...ocTypeTagOnObjectProperty1.errors.txt.diff | 36 ++++++ ...eckJsdocTypedefInParamTag1.errors.txt.diff | 61 ++++++++++ 16 files changed, 576 insertions(+) create mode 100644 testdata/baselines/reference/submodule/compiler/checkJsFiles2.errors.txt create mode 100644 testdata/baselines/reference/submodule/compiler/checkJsFiles3.errors.txt create mode 100644 testdata/baselines/reference/submodule/compiler/checkJsFiles4.errors.txt create mode 100644 testdata/baselines/reference/submodule/compiler/checkJsObjectLiteralHasCheckedKeyof.errors.txt create mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocOptionalParamOrder.errors.txt create mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag1.errors.txt create mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag2.errors.txt create mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag1.errors.txt create mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag2.errors.txt create mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt create mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty2.errors.txt create mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypedefInParamTag1.errors.txt create mode 100644 testdata/baselines/reference/submodule/conformance/checkJsdocTypedefOnlySourceFile.errors.txt create mode 100644 testdata/baselines/reference/submodule/conformance/jsdocTypeTagCast.errors.txt create mode 100644 testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt.diff create mode 100644 testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypedefInParamTag1.errors.txt.diff diff --git a/testdata/baselines/reference/submodule/compiler/checkJsFiles2.errors.txt b/testdata/baselines/reference/submodule/compiler/checkJsFiles2.errors.txt new file mode 100644 index 0000000000..f106054306 --- /dev/null +++ b/testdata/baselines/reference/submodule/compiler/checkJsFiles2.errors.txt @@ -0,0 +1,9 @@ +a.js(3,1): error TS2322: Type 'number' is not assignable to type 'string'. + + +==== a.js (1 errors) ==== + // @ts-check + var x = "string"; + x = 0; + ~ +!!! error TS2322: Type 'number' is not assignable to type 'string'. \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/checkJsFiles3.errors.txt b/testdata/baselines/reference/submodule/compiler/checkJsFiles3.errors.txt new file mode 100644 index 0000000000..f106054306 --- /dev/null +++ b/testdata/baselines/reference/submodule/compiler/checkJsFiles3.errors.txt @@ -0,0 +1,9 @@ +a.js(3,1): error TS2322: Type 'number' is not assignable to type 'string'. + + +==== a.js (1 errors) ==== + // @ts-check + var x = "string"; + x = 0; + ~ +!!! error TS2322: Type 'number' is not assignable to type 'string'. \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/checkJsFiles4.errors.txt b/testdata/baselines/reference/submodule/compiler/checkJsFiles4.errors.txt new file mode 100644 index 0000000000..f106054306 --- /dev/null +++ b/testdata/baselines/reference/submodule/compiler/checkJsFiles4.errors.txt @@ -0,0 +1,9 @@ +a.js(3,1): error TS2322: Type 'number' is not assignable to type 'string'. + + +==== a.js (1 errors) ==== + // @ts-check + var x = "string"; + x = 0; + ~ +!!! error TS2322: Type 'number' is not assignable to type 'string'. \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/checkJsObjectLiteralHasCheckedKeyof.errors.txt b/testdata/baselines/reference/submodule/compiler/checkJsObjectLiteralHasCheckedKeyof.errors.txt new file mode 100644 index 0000000000..50e261254c --- /dev/null +++ b/testdata/baselines/reference/submodule/compiler/checkJsObjectLiteralHasCheckedKeyof.errors.txt @@ -0,0 +1,18 @@ +file.js(11,1): error TS2322: Type '"z"' is not assignable to type '"x" | "y"'. + + +==== file.js (1 errors) ==== + // @ts-check + const obj = { + x: 1, + y: 2 + }; + + /** + * @type {keyof typeof obj} + */ + let selected = "x"; + selected = "z"; // should fail + ~~~~~~~~ +!!! error TS2322: Type '"z"' is not assignable to type '"x" | "y"'. + \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocOptionalParamOrder.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocOptionalParamOrder.errors.txt new file mode 100644 index 0000000000..b29c988e17 --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/checkJsdocOptionalParamOrder.errors.txt @@ -0,0 +1,14 @@ +0.js(7,20): error TS1016: A required parameter cannot follow an optional parameter. + + +==== 0.js (1 errors) ==== + // @ts-check + /** + * @param {number} a + * @param {number} [b] + * @param {number} c + */ + function foo(a, b, c) {} + ~ +!!! error TS1016: A required parameter cannot follow an optional parameter. + \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag1.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag1.errors.txt new file mode 100644 index 0000000000..47f1a3209d --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag1.errors.txt @@ -0,0 +1,28 @@ +returns.js(20,12): error TS2872: This kind of expression is always truthy. + + +==== returns.js (1 errors) ==== + // @ts-check + /** + * @returns {string} This comment is not currently exposed + */ + function f() { + return "hello"; + } + + /** + * @returns {string=} This comment is not currently exposed + */ + function f1() { + return "hello world"; + } + + /** + * @returns {string|number} This comment is not currently exposed + */ + function f2() { + return 5 || "hello"; + ~ +!!! error TS2872: This kind of expression is always truthy. + } + \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag2.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag2.errors.txt new file mode 100644 index 0000000000..c5ab204ed8 --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/checkJsdocReturnTag2.errors.txt @@ -0,0 +1,29 @@ +returns.js(6,5): error TS2322: Type 'number' is not assignable to type 'string'. +returns.js(13,5): error TS2322: Type 'number | boolean' is not assignable to type 'string | number'. + Type 'boolean' is not assignable to type 'string | number'. +returns.js(13,12): error TS2872: This kind of expression is always truthy. + + +==== returns.js (3 errors) ==== + // @ts-check + /** + * @returns {string} This comment is not currently exposed + */ + function f() { + return 5; + ~~~~~~ +!!! error TS2322: Type 'number' is not assignable to type 'string'. + } + + /** + * @returns {string | number} This comment is not currently exposed + */ + function f1() { + return 5 || true; + ~~~~~~ +!!! error TS2322: Type 'number | boolean' is not assignable to type 'string | number'. +!!! error TS2322: Type 'boolean' is not assignable to type 'string | number'. + ~ +!!! error TS2872: This kind of expression is always truthy. + } + \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag1.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag1.errors.txt new file mode 100644 index 0000000000..beeb1723a4 --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag1.errors.txt @@ -0,0 +1,60 @@ +0.js(20,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? +0.js(24,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? +0.js(28,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? +0.js(40,5): error TS2403: Subsequent variable declarations must have the same type. Variable 'props' must be of type 'object', but here has type 'Object'. + + +==== 0.js (4 errors) ==== + // @ts-check + /** @type {String} */ + var S = "hello world"; + + /** @type {number} */ + var n = 10; + + /** @type {*} */ + var anyT = 2; + anyT = "hello"; + + /** @type {?} */ + var anyT1 = 2; + anyT1 = "hi"; + + /** @type {Function} */ + const x = (a) => a + 1; + x(1); + + /** @type {function} */ + ~~~~~~~~ +!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? +!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. + const y = (a) => a + 1; + y(1); + + /** @type {function (number)} */ + ~~~~~~~~ +!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? +!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. + const x1 = (a) => a + 1; + x1(0); + + /** @type {function (number): number} */ + ~~~~~~~~ +!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? +!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. + const x2 = (a) => a + 1; + x2(0); + + /** + * @type {object} + */ + var props = {}; + + /** + * @type {Object} + */ + var props = {}; + ~~~~~ +!!! error TS2403: Subsequent variable declarations must have the same type. Variable 'props' must be of type 'object', but here has type 'Object'. +!!! related TS6203 0.js:35:5: 'props' was also declared here. + \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag2.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag2.errors.txt new file mode 100644 index 0000000000..76933dc06a --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTag2.errors.txt @@ -0,0 +1,50 @@ +0.js(3,5): error TS2322: Type 'boolean' is not assignable to type 'String'. +0.js(6,5): error TS2322: Type 'string' is not assignable to type 'number'. +0.js(8,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? +0.js(12,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? +0.js(19,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? +0.js(23,12): error TS2552: Cannot find name 'function'. Did you mean 'Function'? + + +==== 0.js (6 errors) ==== + // @ts-check + /** @type {String} */ + var S = true; + ~ +!!! error TS2322: Type 'boolean' is not assignable to type 'String'. + + /** @type {number} */ + var n = "hello"; + ~ +!!! error TS2322: Type 'string' is not assignable to type 'number'. + + /** @type {function (number)} */ + ~~~~~~~~ +!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? +!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. + const x1 = (a) => a + 1; + x1("string"); + + /** @type {function (number): number} */ + ~~~~~~~~ +!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? +!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. + const x2 = (a) => a + 1; + + /** @type {string} */ + var a; + a = x2(0); + + /** @type {function (number): number} */ + ~~~~~~~~ +!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? +!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. + const x3 = (a) => a.concat("hi"); + x3(0); + + /** @type {function (number): string} */ + ~~~~~~~~ +!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? +!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. + const x4 = (a) => a + 1; + x4(0); \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt new file mode 100644 index 0000000000..35c7d11f52 --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt @@ -0,0 +1,31 @@ +0.js(16,14): error TS2552: Cannot find name 'function'. Did you mean 'Function'? + + +==== 0.js (1 errors) ==== + // @ts-check + var lol = "hello Lol" + const obj = { + /** @type {string|undefined} */ + foo: undefined, + /** @type {string|undefined} */ + bar: "42", + /** @type {function(number): number} */ + method1(n1) { + return n1 + 42; + }, + /** @type {string} */ + lol, + /** @type {number} */ + ['b' + 'ar1']: 42, + /** @type {function(number): number} */ + ~~~~~~~~ +!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? +!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. + arrowFunc: (num) => num + 42 + } + obj.foo = 'string' + obj.lol + obj.bar = undefined; + var k = obj.method1(0); + obj.bar1 = "42"; + obj.arrowFunc(0); \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty2.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty2.errors.txt new file mode 100644 index 0000000000..579a296c34 --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/checkJsdocTypeTagOnObjectProperty2.errors.txt @@ -0,0 +1,36 @@ +0.js(5,8): error TS2352: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. +0.js(10,14): error TS2552: Cannot find name 'function'. Did you mean 'Function'? +0.js(12,14): error TS2552: Cannot find name 'function'. Did you mean 'Function'? + + +==== 0.js (3 errors) ==== + // @ts-check + var lol; + const obj = { + /** @type {string|undefined} */ + bar: 42, + ~~ +!!! error TS2352: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. + /** @type {function(number): number} */ + method1(n1) { + return "42"; + }, + /** @type {function(number): number} */ + ~~~~~~~~ +!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? +!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. + method2: (n1) => "lol", + /** @type {function(number): number} */ + ~~~~~~~~ +!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? +!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. + arrowFunc: (num="0") => num + 42, + /** @type {string} */ + lol + } + lol = "string" + /** @type {string} */ + var s = obj.method1(0); + + /** @type {string} */ + var s1 = obj.method2("0"); \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefInParamTag1.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefInParamTag1.errors.txt new file mode 100644 index 0000000000..5c9948e856 --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefInParamTag1.errors.txt @@ -0,0 +1,56 @@ +0.js(15,5): error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts': y, z, w +0.js(28,6): error TS2741: Property 'anotherY' is missing in type '{ anotherX: string; }' but required in type 'AnotherOpts'. +0.js(42,6): error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts1': y, z, w + + +==== 0.js (3 errors) ==== + // @ts-check + /** + * @typedef {Object} Opts + * @property {string} x + * @property {string=} y + * @property {string} [z] + * @property {string} [w="hi"] + * + * @param {Opts} opts + */ + function foo(opts) { + opts.x; + } + + foo({x: 'abc'}); + ~~~~~~~~~~ +!!! error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts': y, z, w + + /** + * @typedef {Object} AnotherOpts + * @property anotherX {string} + * @property anotherY {string=} + * + * @param {AnotherOpts} opts + */ + function foo1(opts) { + opts.anotherX; + } + + foo1({anotherX: "world"}); + ~~~~~~~~~~~~~~~~~~~ +!!! error TS2741: Property 'anotherY' is missing in type '{ anotherX: string; }' but required in type 'AnotherOpts'. +!!! related TS2728 0.js:20:14: 'anotherY' is declared here. + + /** + * @typedef {object} Opts1 + * @property {string} x + * @property {string=} y + * @property {string} [z] + * @property {string} [w="hi"] + * + * @param {Opts1} opts + */ + function foo2(opts) { + opts.x; + } + foo2({x: 'abc'}); + ~~~~~~~~~~ +!!! error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts1': y, z, w + \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefOnlySourceFile.errors.txt b/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefOnlySourceFile.errors.txt new file mode 100644 index 0000000000..7cb22b9170 --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/checkJsdocTypedefOnlySourceFile.errors.txt @@ -0,0 +1,24 @@ +0.js(3,5): error TS2441: Duplicate identifier 'exports'. Compiler reserves name 'exports' in top level scope of a module. +0.js(8,9): error TS2339: Property 'SomeName' does not exist on type '{}'. +0.js(10,12): error TS2503: Cannot find namespace 'exports'. + + +==== 0.js (3 errors) ==== + // @ts-check + + var exports = {}; + ~~~~~~~ +!!! error TS2441: Duplicate identifier 'exports'. Compiler reserves name 'exports' in top level scope of a module. + + /** + * @typedef {string} + */ + exports.SomeName; + ~~~~~~~~ +!!! error TS2339: Property 'SomeName' does not exist on type '{}'. + + /** @type {exports.SomeName} */ + ~~~~~~~ +!!! error TS2503: Cannot find namespace 'exports'. + const myString = 'str'; + \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/jsdocTypeTagCast.errors.txt b/testdata/baselines/reference/submodule/conformance/jsdocTypeTagCast.errors.txt new file mode 100644 index 0000000000..2cac730ae2 --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/jsdocTypeTagCast.errors.txt @@ -0,0 +1,106 @@ +b.js(4,31): error TS2352: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. +b.js(17,14): error TS2339: Property 'p' does not exist on type 'SomeBase'. +b.js(23,14): error TS2339: Property 'x' does not exist on type 'SomeDerived'. +b.js(28,14): error TS2339: Property 'q' does not exist on type 'SomeOther'. +b.js(66,15): error TS1228: A type predicate is only allowed in return type position for functions and methods. +b.js(66,38): error TS2454: Variable 'numOrStr' is used before being assigned. +b.js(67,2): error TS2322: Type 'string | number' is not assignable to type 'string'. + Type 'number' is not assignable to type 'string'. +b.js(67,8): error TS2454: Variable 'numOrStr' is used before being assigned. + + +==== a.ts (0 errors) ==== + var W: string; + +==== b.js (8 errors) ==== + // @ts-check + var W = /** @type {string} */(/** @type {*} */ (4)); + + var W = /** @type {string} */(4); // Error + ~ +!!! error TS2352: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. + + /** @type {*} */ + var a; + + /** @type {string} */ + var s; + + var a = /** @type {*} */("" + 4); + var s = "" + /** @type {*} */(4); + + class SomeBase { + constructor() { + this.p = 42; + ~ +!!! error TS2339: Property 'p' does not exist on type 'SomeBase'. + } + } + class SomeDerived extends SomeBase { + constructor() { + super(); + this.x = 42; + ~ +!!! error TS2339: Property 'x' does not exist on type 'SomeDerived'. + } + } + class SomeOther { + constructor() { + this.q = 42; + ~ +!!! error TS2339: Property 'q' does not exist on type 'SomeOther'. + } + } + + function SomeFakeClass() { + /** @type {string|number} */ + this.p = "bar"; + } + + // Type assertion should check for assignability in either direction + var someBase = new SomeBase(); + var someDerived = new SomeDerived(); + var someOther = new SomeOther(); + var someFakeClass = new SomeFakeClass(); + + someBase = /** @type {SomeBase} */(someDerived); + someBase = /** @type {SomeBase} */(someBase); + someBase = /** @type {SomeBase} */(someOther); // Error + + someDerived = /** @type {SomeDerived} */(someDerived); + someDerived = /** @type {SomeDerived} */(someBase); + someDerived = /** @type {SomeDerived} */(someOther); // Error + + someOther = /** @type {SomeOther} */(someDerived); // Error + someOther = /** @type {SomeOther} */(someBase); // Error + someOther = /** @type {SomeOther} */(someOther); + + someFakeClass = someBase; + someFakeClass = someDerived; + + someBase = someFakeClass; // Error + someBase = /** @type {SomeBase} */(someFakeClass); + + // Type assertion cannot be a type-predicate type + /** @type {number | string} */ + var numOrStr; + /** @type {string} */ + var str; + if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error + ~~~~~~~~~~~~~~~~~~ +!!! error TS1228: A type predicate is only allowed in return type position for functions and methods. + ~~~~~~~~ +!!! error TS2454: Variable 'numOrStr' is used before being assigned. + str = numOrStr; // Error, no narrowing occurred + ~~~ +!!! error TS2322: Type 'string | number' is not assignable to type 'string'. +!!! error TS2322: Type 'number' is not assignable to type 'string'. + ~~~~~~~~ +!!! error TS2454: Variable 'numOrStr' is used before being assigned. + } + + + var asConst1 = /** @type {const} */(1); + var asConst2 = /** @type {const} */({ + x: 1 + }); \ No newline at end of file diff --git a/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt.diff b/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt.diff new file mode 100644 index 0000000000..7aa3c6a048 --- /dev/null +++ b/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypeTagOnObjectProperty1.errors.txt.diff @@ -0,0 +1,36 @@ +--- old.checkJsdocTypeTagOnObjectProperty1.errors.txt ++++ new.checkJsdocTypeTagOnObjectProperty1.errors.txt +@@= skipped -0, +-1 lines =@@ +- +@@= skipped --1, +1 lines =@@ ++0.js(16,14): error TS2552: Cannot find name 'function'. Did you mean 'Function'? ++ ++ ++==== 0.js (1 errors) ==== ++ // @ts-check ++ var lol = "hello Lol" ++ const obj = { ++ /** @type {string|undefined} */ ++ foo: undefined, ++ /** @type {string|undefined} */ ++ bar: "42", ++ /** @type {function(number): number} */ ++ method1(n1) { ++ return n1 + 42; ++ }, ++ /** @type {string} */ ++ lol, ++ /** @type {number} */ ++ ['b' + 'ar1']: 42, ++ /** @type {function(number): number} */ ++ ~~~~~~~~ ++!!! error TS2552: Cannot find name 'function'. Did you mean 'Function'? ++!!! related TS2728 lib.es5.d.ts:--:--: 'Function' is declared here. ++ arrowFunc: (num) => num + 42 ++ } ++ obj.foo = 'string' ++ obj.lol ++ obj.bar = undefined; ++ var k = obj.method1(0); ++ obj.bar1 = "42"; ++ obj.arrowFunc(0); diff --git a/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypedefInParamTag1.errors.txt.diff b/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypedefInParamTag1.errors.txt.diff new file mode 100644 index 0000000000..d1d0f17779 --- /dev/null +++ b/testdata/baselines/reference/submoduleAccepted/conformance/checkJsdocTypedefInParamTag1.errors.txt.diff @@ -0,0 +1,61 @@ +--- old.checkJsdocTypedefInParamTag1.errors.txt ++++ new.checkJsdocTypedefInParamTag1.errors.txt +@@= skipped -0, +-1 lines =@@ +- +@@= skipped --1, +1 lines =@@ ++0.js(15,5): error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts': y, z, w ++0.js(28,6): error TS2741: Property 'anotherY' is missing in type '{ anotherX: string; }' but required in type 'AnotherOpts'. ++0.js(42,6): error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts1': y, z, w ++ ++ ++==== 0.js (3 errors) ==== ++ // @ts-check ++ /** ++ * @typedef {Object} Opts ++ * @property {string} x ++ * @property {string=} y ++ * @property {string} [z] ++ * @property {string} [w="hi"] ++ * ++ * @param {Opts} opts ++ */ ++ function foo(opts) { ++ opts.x; ++ } ++ ++ foo({x: 'abc'}); ++ ~~~~~~~~~~ ++!!! error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts': y, z, w ++ ++ /** ++ * @typedef {Object} AnotherOpts ++ * @property anotherX {string} ++ * @property anotherY {string=} ++ * ++ * @param {AnotherOpts} opts ++ */ ++ function foo1(opts) { ++ opts.anotherX; ++ } ++ ++ foo1({anotherX: "world"}); ++ ~~~~~~~~~~~~~~~~~~~ ++!!! error TS2741: Property 'anotherY' is missing in type '{ anotherX: string; }' but required in type 'AnotherOpts'. ++!!! related TS2728 0.js:20:14: 'anotherY' is declared here. ++ ++ /** ++ * @typedef {object} Opts1 ++ * @property {string} x ++ * @property {string=} y ++ * @property {string} [z] ++ * @property {string} [w="hi"] ++ * ++ * @param {Opts1} opts ++ */ ++ function foo2(opts) { ++ opts.x; ++ } ++ foo2({x: 'abc'}); ++ ~~~~~~~~~~ ++!!! error TS2739: Type '{ x: string; }' is missing the following properties from type 'Opts1': y, z, w ++ From 29ec6ac31fb0484fbe8b5c177fe54e448bf83489 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 21 Apr 2025 14:18:24 -0700 Subject: [PATCH 16/25] fix lint --- internal/checker/services.go | 1 - internal/ls/completions.go | 23 ++++------------------- internal/ls/completions_test.go | 1 + 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/internal/checker/services.go b/internal/checker/services.go index 6e00923659..e867a8461e 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -331,7 +331,6 @@ func runWithInferenceBlockedFromSourceNode[T any](c *Checker, node *ast.Node, fn return result } -// !!! Shared, made generic func runWithoutResolvedSignatureCaching[T any](c *Checker, node *ast.Node, fn func() T) T { ancestorNode := ast.FindAncestor(node, func(n *ast.Node) bool { return ast.IsCallLikeOrFunctionLikeExpression(n) diff --git a/internal/ls/completions.go b/internal/ls/completions.go index d76fe410e2..493d72daef 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -127,8 +127,6 @@ func sortBelow(original sortText) sortText { return original + "1" } -// !!! sort text transformations - type symbolOriginInfoKind int const ( @@ -155,11 +153,6 @@ type symbolOriginInfo struct { data any } -// type originData interface { -// symbolName() string -// } - -// !!! origin info func (s *symbolOriginInfo) symbolName() string { switch s.data.(type) { case *symbolOriginInfoExport: @@ -1065,7 +1058,7 @@ func (l *LanguageService) createCompletionItem( } if insertQuestionDot || data.propertyAccessToConvert.AsPropertyAccessExpression().QuestionDotToken != nil { - insertText = fmt.Sprintf("?.%s", insertText) + insertText = "?." + insertText } dot := findChildOfKind(data.propertyAccessToConvert, ast.KindDotToken, file) @@ -1213,7 +1206,7 @@ func (l *LanguageService) createCompletionItem( } if useBraces { - insertText = fmt.Sprintf("%s={$1}", escapeSnippetText(name)) + insertText = escapeSnippetText(name) + "={$1}" isSnippet = true } } @@ -1837,8 +1830,6 @@ func getContextualType(previousToken *ast.Node, position int, file *ast.SourceFi } } -// TODO: this is also used by string completions originally, but passing a flag `ContextFlagsCompletions`. -// What difference does it make? func getContextualTypeFromParent(node *ast.Expression, typeChecker *checker.Checker, contextFlags checker.ContextFlags) *checker.Type { parent := ast.WalkUpParenthesizedExpressions(node.Parent) switch parent.Kind { @@ -2234,18 +2225,12 @@ func isFunctionLikeBodyKeyword(kind ast.Kind) bool { func isClassMemberCompletionKeyword(kind ast.Kind) bool { switch kind { - case ast.KindAbstractKeyword, ast.KindAccessorKeyword: - case ast.KindConstructorKeyword: - case ast.KindGetKeyword: - case ast.KindSetKeyword: - case ast.KindAsyncKeyword: - case ast.KindDeclareKeyword: - case ast.KindOverrideKeyword: + case ast.KindAbstractKeyword, ast.KindAccessorKeyword, ast.KindConstructorKeyword, ast.KindGetKeyword, + ast.KindSetKeyword, ast.KindAsyncKeyword, ast.KindDeclareKeyword, ast.KindOverrideKeyword: return true default: return ast.IsClassMemberModifier(kind) } - panic("unreachable") } func isInterfaceOrTypeLiteralCompletionKeyword(kind ast.Kind) bool { diff --git a/internal/ls/completions_test.go b/internal/ls/completions_test.go index 83294e7c90..a9c9e9b40e 100644 --- a/internal/ls/completions_test.go +++ b/internal/ls/completions_test.go @@ -20,6 +20,7 @@ type testCase struct { } func TestCompletions(t *testing.T) { + t.Parallel() testCases := []testCase{ { name: "basicInterfaceMembers", From c9ce06edc16dc915b7380090679501d8aee232fd Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 21 Apr 2025 14:30:20 -0700 Subject: [PATCH 17/25] skip completion server test if no embeded --- internal/ls/completions_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/ls/completions_test.go b/internal/ls/completions_test.go index a9c9e9b40e..facff56947 100644 --- a/internal/ls/completions_test.go +++ b/internal/ls/completions_test.go @@ -3,6 +3,7 @@ package ls_test import ( "testing" + "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -21,6 +22,11 @@ type testCase struct { func TestCompletions(t *testing.T) { t.Parallel() + if !bundled.Embedded { + // Without embedding, we'd need to read all of the lib files out from disk into the MapFS. + // Just skip this for now. + t.Skip("bundled files are not embedded") + } testCases := []testCase{ { name: "basicInterfaceMembers", From 1514cfe545fabf1d8c9c34ebe50a4af63fb21ff2 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 21 Apr 2025 15:53:15 -0700 Subject: [PATCH 18/25] address CR --- internal/ls/completions.go | 22 ++++++++++++++-------- internal/ls/completions_test.go | 4 ++-- internal/ls/utilities.go | 11 ++++++----- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 493d72daef..5574ab8186 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -683,7 +683,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position } if !isTypeLocation || checker.IsInTypeQuery(node) { - // GH#39946. Pulling on the type of a node inside of a function with a contextual `this` parameter can result in a circularity + // microsoft/TypeScript#39946. Pulling on the type of a node inside of a function with a contextual `this` parameter can result in a circularity // if the `node` is part of the exprssion of a `yield` or `return`. This circularity doesn't exist at compile time because // we will check (and cache) the type of `this` *before* checking the type of the node. typeChecker.TryGetThisTypeAtEx(node, false /*includeGlobalThis*/, nil) @@ -727,7 +727,7 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position contextualType = getContextualType(previousToken, position, file, typeChecker) } - // exclude literal suggestions after (#51667) and after closing quote (#52675) + // exclude literal suggestions after microsoft/TypeScript#51667) and after closing quote (microsoft/TypeScript#52675) // for strings getStringLiteralCompletions handles completions isLiteralExpected := !ast.IsStringLiteralLike(previousToken) && !isJsxIdentifierExpected var literals []literalValue @@ -939,7 +939,13 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( if originalSortText == "" { originalSortText = SortTextLocationPriority } - sortText := core.IfElse(isDeprecated(symbol, typeChecker), deprecateSortText(originalSortText), originalSortText) + + var sortText sortText + if isDeprecated(symbol, typeChecker) { + sortText = deprecateSortText(originalSortText) + } else { + sortText = originalSortText + } entry := l.createCompletionItem( symbol, sortText, @@ -980,14 +986,14 @@ func completionNameForLiteral( preferences *UserPreferences, literal literalValue, ) string { - switch literal.(type) { + switch literal := literal.(type) { case string: - return quote(file, preferences, literal.(string)) + return quote(file, preferences, literal) case jsnum.Number: name, _ := core.StringifyJson(literal, "" /*prefix*/, "" /*suffix*/) return name case jsnum.PseudoBigInt: - return literal.(jsnum.PseudoBigInt).String() + "n" + return literal.String() + "n" } panic(fmt.Sprintf("Unhandled literal value: %v", literal)) } @@ -1045,7 +1051,7 @@ func (l *LanguageService) createCompletionItem( name) } } else if data.propertyAccessToConvert != nil && (useBraces || insertQuestionDot) { - // We should only have needsConvertPropertyAccess if there's a property access to convert. But see #21790. + // We should only have needsConvertPropertyAccess if there's a property access to convert. But see microsoft/TypeScript#21790. // Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro. if useBraces { if needsConvertPropertyAccess { @@ -1518,7 +1524,7 @@ func getCompletionEntryDisplayNameForSymbol( } return "", false case CompletionKindObjectPropertyDeclaration: - // TODO: GH#18169 + // TODO: microsoft/TypeScript#18169 escapedName, _ := core.StringifyJson(name, "", "") return escapedName, false case CompletionKindPropertyAccess, CompletionKindGlobal: diff --git a/internal/ls/completions_test.go b/internal/ls/completions_test.go index facff56947..838c74722b 100644 --- a/internal/ls/completions_test.go +++ b/internal/ls/completions_test.go @@ -63,12 +63,12 @@ p.`, for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { t.Parallel() - run_test(t, testCase.content, testCase.position, testCase.expected) + runTest(t, testCase.content, testCase.position, testCase.expected) }) } } -func run_test(t *testing.T, content string, position int, expected *lsproto.CompletionList) { +func runTest(t *testing.T, content string, position int, expected *lsproto.CompletionList) { files := map[string]string{ "/index.ts": content, } diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index a82d2d3584..ac01e02164 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -1,7 +1,6 @@ package ls import ( - "slices" "strings" "github.com/microsoft/typescript-go/internal/ast" @@ -12,6 +11,8 @@ import ( "github.com/microsoft/typescript-go/internal/scanner" ) +var quoteReplacer = strings.NewReplacer("'", `\'`, `\"`, `"`) + // !!! func isInString(file *ast.SourceFile, position int, previousToken *ast.Node) bool { return false @@ -127,7 +128,7 @@ func quote(file *ast.SourceFile, preferences *UserPreferences, text string) stri quotePreference := getQuotePreference(file, preferences) quoted, _ := core.StringifyJson(text, "" /*prefix*/, "" /*indent*/) if quotePreference == quotePreferenceSingle { - strings.ReplaceAll(strings.ReplaceAll(core.StripQuotes(quoted), "'", `\'`), `\"`, `"`) + quoteReplacer.Replace(core.StripQuotes(quoted)) } return quoted } @@ -297,7 +298,7 @@ func probablyUsesSemicolons(file *ast.SourceFile) bool { return withSemicolon/withoutSemicolon > 1/nStatementsToObserve } -var typeKeywords []ast.Kind = []ast.Kind{ +var typeKeywords *core.Set[ast.Kind] = core.NewSetFromItems( ast.KindAnyKeyword, ast.KindAssertsKeyword, ast.KindBigIntKeyword, @@ -318,10 +319,10 @@ var typeKeywords []ast.Kind = []ast.Kind{ ast.KindUndefinedKeyword, ast.KindUniqueKeyword, ast.KindUnknownKeyword, -} +) func isTypeKeyword(kind ast.Kind) bool { - return slices.Contains(typeKeywords, kind) + return typeKeywords.Has(kind) } // Returns a map of all names in the file to their positions. From 4b2821fb1678f32eff2ddeb5a899fe25c3e7ea28 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 21 Apr 2025 16:11:36 -0700 Subject: [PATCH 19/25] more refactoring --- internal/checker/services.go | 28 ---------------------------- internal/checker/types.go | 36 ++++++++++++++++++++++++++++++++++++ internal/ls/completions.go | 31 +++++++++++++++++++------------ 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/internal/checker/services.go b/internal/checker/services.go index e867a8461e..2feb7945e4 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -244,34 +244,6 @@ func (c *Checker) getAugmentedPropertiesOfType(t *Type) []*ast.Symbol { return c.getNamedMembers(propsByName) } -func IsUnion(t *Type) bool { - return t.flags&TypeFlagsUnion != 0 -} - -func IsStringLiteral(t *Type) bool { - return t.flags&TypeFlagsStringLiteral != 0 -} - -func IsNumberLiteral(t *Type) bool { - return t.flags&TypeFlagsNumberLiteral != 0 -} - -func IsBigIntLiteral(t *Type) bool { - return t.flags&TypeFlagsBigIntLiteral != 0 -} - -func IsEnumLiteral(t *Type) bool { - return t.flags&TypeFlagsEnumLiteral != 0 -} - -func IsBooleanLike(t *Type) bool { - return t.flags&TypeFlagsBooleanLike != 0 -} - -func IsStringLike(t *Type) bool { - return t.flags&TypeFlagsStringLike != 0 -} - func (c *Checker) TryGetMemberInModuleExportsAndProperties(memberName string, moduleSymbol *ast.Symbol) *ast.Symbol { symbol := c.TryGetMemberInModuleExports(memberName, moduleSymbol) if symbol != nil { diff --git a/internal/checker/types.go b/internal/checker/types.go index 71ef804aa7..9da2867153 100644 --- a/internal/checker/types.go +++ b/internal/checker/types.go @@ -640,6 +640,42 @@ func (t *Type) Symbol() *ast.Symbol { return t.symbol } +func (t *Type) IsUnion() bool { + return t.flags&TypeFlagsUnion != 0 +} + +func (t *Type) IsString() bool { + return t.flags&TypeFlagsString != 0 +} + +func (t *Type) IsIntersection() bool { + return t.flags&TypeFlagsIntersection != 0 +} + +func (t *Type) IsStringLiteral() bool { + return t.flags&TypeFlagsStringLiteral != 0 +} + +func (t *Type) IsNumberLiteral() bool { + return t.flags&TypeFlagsNumberLiteral != 0 +} + +func (t *Type) IsBigIntLiteral() bool { + return t.flags&TypeFlagsBigIntLiteral != 0 +} + +func (t *Type) IsEnumLiteral() bool { + return t.flags&TypeFlagsEnumLiteral != 0 +} + +func (t *Type) IsBooleanLike() bool { + return t.flags&TypeFlagsBooleanLike != 0 +} + +func (t *Type) IsStringLike() bool { + return t.flags&TypeFlagsStringLike != 0 +} + // TypeData type TypeData interface { diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 5574ab8186..c085193a32 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -733,13 +733,13 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position var literals []literalValue if isLiteralExpected { var types []*checker.Type - if contextualType != nil && checker.IsUnion(contextualType) { + if contextualType != nil && contextualType.IsUnion() { types = contextualType.Types() } else if contextualType != nil { types = []*checker.Type{contextualType} } literals = core.MapNonNil(types, func(t *checker.Type) literalValue { - if isLiteral(t) && !checker.IsEnumLiteral(t) { + if isLiteral(t) && !t.IsEnumLiteral() { return t.AsLiteralType().Value() } return nil @@ -1192,10 +1192,10 @@ func (l *LanguageService) createCompletionItem( // If is boolean like or undefined, don't return a snippet, we want to return just the completion. if jsxAttributeCompletionStyleIs(preferences.JsxAttributeCompletionStyle, JsxAttributeCompletionStyleAuto) && - t.Flags()&checker.TypeFlagsBooleanLike == 0 && - !(t.Flags()&checker.TypeFlagsUnion != 0 && core.Some(t.Types(), func(t *checker.Type) bool { return t.Flags()&checker.TypeFlagsBooleanLike != 0 })) { - if t.Flags()&checker.TypeFlagsStringLike != 0 || - t.Flags()&checker.TypeFlagsUnion != 0 && + !t.IsBooleanLike() && + !(t.IsUnion() && core.Some(t.Types(), func(t *checker.Type) bool { return t.IsBooleanLike() })) { + if t.IsStringLike() || + t.IsUnion() && core.Every( t.Types(), func(t *checker.Type) bool { @@ -1733,7 +1733,7 @@ func nonAliasCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *chec // Gets all properties on a type, but if that type is a union of several types, // excludes array-like types or callable/constructable types. func getPropertiesForCompletion(t *checker.Type, typeChecker *checker.Checker) []*ast.Symbol { - if checker.IsUnion(t) { + if t.IsUnion() { return core.CheckEachDefined(typeChecker.GetAllPossiblePropertiesOfTypes(t.Types()), "getAllPossiblePropertiesOfTypes() should all be defined.") } else { return core.CheckEachDefined(typeChecker.GetApparentProperties(t), "getApparentProperties() should all be defined.") @@ -1843,7 +1843,8 @@ func getContextualTypeFromParent(node *ast.Expression, typeChecker *checker.Chec return typeChecker.GetContextualType(parent, contextFlags) case ast.KindBinaryExpression: if isEqualityOperatorKind(parent.AsBinaryExpression().OperatorToken.Kind) { - return typeChecker.GetTypeAtLocation(core.IfElse(node == parent.AsBinaryExpression().Right, parent.AsBinaryExpression().Left, parent.AsBinaryExpression().Right)) + return typeChecker.GetTypeAtLocation( + core.IfElse(node == parent.AsBinaryExpression().Right, parent.AsBinaryExpression().Left, parent.AsBinaryExpression().Right)) } return typeChecker.GetContextualType(node, contextFlags) case ast.KindCaseClause: @@ -1868,13 +1869,19 @@ func isEqualityOperatorKind(kind ast.Kind) bool { } func isLiteral(t *checker.Type) bool { - return checker.IsStringLiteral(t) || checker.IsNumberLiteral(t) || checker.IsBigIntLiteral(t) + return t.IsStringLiteral() || t.IsNumberLiteral() || t.IsBigIntLiteral() } func getRecommendedCompletion(previousToken *ast.Node, contextualType *checker.Type, typeChecker *checker.Checker) *ast.Symbol { + var types []*checker.Type + if contextualType.IsUnion() { + types = contextualType.Types() + } else { + types = []*checker.Type{contextualType} + } // For a union, return the first one with a recommended completion. return core.FirstNonNil( - core.IfElse(checker.IsUnion(contextualType), contextualType.Types(), []*checker.Type{contextualType}), + types, func(t *checker.Type) *ast.Symbol { symbol := t.Symbol() // Don't make a recommended completion for an abstract class. @@ -2002,7 +2009,7 @@ func quotePropertyName(file *ast.SourceFile, preferences *UserPreferences, name // is not reduced by the checker as a special case used for supporting string literal completions // for string type. func isStringAndEmptyAnonymousObjectIntersection(typeChecker *checker.Checker, t *checker.Type) bool { - if t.Flags()&checker.TypeFlagsIntersection == 0 { + if !t.IsIntersection() { return false } @@ -2012,7 +2019,7 @@ func isStringAndEmptyAnonymousObjectIntersection(typeChecker *checker.Checker, t } func areIntersectedTypesAvoidingStringReduction(typeChecker *checker.Checker, t1 *checker.Type, t2 *checker.Type) bool { - return t1.Flags()&checker.TypeFlagsString != 0 && typeChecker.IsEmptyAnonymousObjectType(t2) + return t1.IsString() && typeChecker.IsEmptyAnonymousObjectType(t2) } func escapeSnippetText(text string) string { From 34f5b8355db7c1d072ce89e638d3107e40b48d53 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 22 Apr 2025 08:37:14 -0700 Subject: [PATCH 20/25] more fixes --- internal/ls/completions.go | 2 +- internal/ls/utilities.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ls/completions.go b/internal/ls/completions.go index c085193a32..5092ee113a 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -1193,7 +1193,7 @@ func (l *LanguageService) createCompletionItem( // If is boolean like or undefined, don't return a snippet, we want to return just the completion. if jsxAttributeCompletionStyleIs(preferences.JsxAttributeCompletionStyle, JsxAttributeCompletionStyleAuto) && !t.IsBooleanLike() && - !(t.IsUnion() && core.Some(t.Types(), func(t *checker.Type) bool { return t.IsBooleanLike() })) { + !(t.IsUnion() && core.Some(t.Types(), (*checker.Type).IsBooleanLike)) { if t.IsStringLike() || t.IsUnion() && core.Every( diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index ac01e02164..460fbbd1b0 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -128,7 +128,7 @@ func quote(file *ast.SourceFile, preferences *UserPreferences, text string) stri quotePreference := getQuotePreference(file, preferences) quoted, _ := core.StringifyJson(text, "" /*prefix*/, "" /*indent*/) if quotePreference == quotePreferenceSingle { - quoteReplacer.Replace(core.StripQuotes(quoted)) + quoted = quoteReplacer.Replace(core.StripQuotes(quoted)) } return quoted } From 3b005d17d74f8c63ae45959a9402975e2f62e366 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 22 Apr 2025 10:23:41 -0700 Subject: [PATCH 21/25] add panic recovery, and send completion provider in lsp initialize --- internal/ls/completions.go | 2 ++ internal/lsp/server.go | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 5092ee113a..27d8e6f8d9 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -99,6 +99,8 @@ const ( CompletionKindString ) +var TriggerCharacters = []string{".", `"`, "'", "`", "/", "@", "<", "#", " "} + // All commit characters, valid when `isNewIdentifierLocation` is false. var allCommitCharacters = []string{".", ",", ";"} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 865a2bb904..d5e3c2b72c 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "runtime/debug" "slices" "strings" "time" @@ -252,6 +253,10 @@ func (s *Server) handleInitialize(req *lsproto.RequestMessage) error { InterFileDependencies: true, }, }, + CompletionProvider: &lsproto.CompletionOptions{ + TriggerCharacters: &ls.TriggerCharacters, + // !!! other options + }, }, }) } @@ -379,7 +384,7 @@ func (s *Server) handleDefinition(req *lsproto.RequestMessage) error { return s.sendResult(req.ID, &lsproto.Definition{Locations: &lspLocations}) } -func (s *Server) handleCompletion(req *lsproto.RequestMessage) error { +func (s *Server) handleCompletion(req *lsproto.RequestMessage) (messageErr error) { params := req.Params.(*lsproto.CompletionParams) file, project := s.getFileAndProject(params.TextDocument.Uri) pos, err := s.converters.LineAndCharacterToPositionForFile(params.Position, file.FileName()) @@ -387,6 +392,13 @@ func (s *Server) handleCompletion(req *lsproto.RequestMessage) error { return s.sendError(req.ID, err) } + // !!! remove this after completions is fully ported/tested + defer func() { + if r := recover(); r != nil { + stack := debug.Stack() + messageErr = s.sendError(req.ID, fmt.Errorf("panic obtaining completions: %v\n%s", r, string(stack))) + } + }() // !!! get user preferences list := project.LanguageService().ProvideCompletion( file.FileName(), From fa9885a7dab0208e9590ae973d0ad726597f822c Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 22 Apr 2025 12:04:07 -0700 Subject: [PATCH 22/25] fix infinite loop --- internal/checker/services.go | 2 ++ internal/ls/completions_test.go | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/internal/checker/services.go b/internal/checker/services.go index 2feb7945e4..9b5863d2ae 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -384,6 +384,8 @@ func (c *Checker) tryGetTarget(symbol *ast.Symbol) *ast.Symbol { next = c.valueSymbolLinks.Get(next).target } else if c.exportTypeLinks.Has(next) { next = c.exportTypeLinks.Get(next).target + } else { + next = nil } if next == nil { break diff --git a/internal/ls/completions_test.go b/internal/ls/completions_test.go index 838c74722b..883844835f 100644 --- a/internal/ls/completions_test.go +++ b/internal/ls/completions_test.go @@ -59,6 +59,27 @@ p.`, }, }, }, + { + name: "objectLiteralType", + content: `export {}; +let x = { foo: 123 }; +x.`, + position: 35, + expected: &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: &lsproto.CompletionItemDefaults{ + CommitCharacters: &defaultCommitCharacters, + }, + Items: []*lsproto.CompletionItem{ + { + Label: "foo", + Kind: ptrTo(lsproto.CompletionItemKindField), + SortText: ptrTo(string(ls.SortTextLocationPriority)), + InsertTextFormat: ptrTo(lsproto.InsertTextFormatPlainText), + }, + }, + }, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { From 967fb76b4155e4b348ae675989668dfff35de688 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 22 Apr 2025 14:55:42 -0700 Subject: [PATCH 23/25] fix map --- internal/ls/completions.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 27d8e6f8d9..605ff714f7 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -441,8 +441,8 @@ func getCompletionData(program *compiler.Program, file *ast.SourceFile, position hasUnresolvedAutoImports := false // This also gets mutated in nested-functions after the return var symbols []*ast.Symbol - var symbolToOriginInfoMap map[ast.SymbolId]*symbolOriginInfo - var symbolToSortTextMap map[ast.SymbolId]sortText + symbolToOriginInfoMap := map[ast.SymbolId]*symbolOriginInfo{} + symbolToSortTextMap := map[ast.SymbolId]sortText{} // var importSpecifierResolver any // !!! import var seenPropertySymbols core.Set[ast.SymbolId] isTypeOnlyLocation := insideJsDocTagTypeExpression || insideJsDocImportTag || From 3cc279cfcb64a394d59f7685d834076b8d4be9b6 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 23 Apr 2025 08:28:53 -0700 Subject: [PATCH 24/25] print panic --- internal/lsp/server.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index d5e3c2b72c..9cc5dc60df 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -396,7 +396,8 @@ func (s *Server) handleCompletion(req *lsproto.RequestMessage) (messageErr error defer func() { if r := recover(); r != nil { stack := debug.Stack() - messageErr = s.sendError(req.ID, fmt.Errorf("panic obtaining completions: %v\n%s", r, string(stack))) + fmt.Printf("panic obtaining completions: %v\n%s", r, string(stack)) + messageErr = s.sendResult(req.ID, &lsproto.CompletionList{}) } }() // !!! get user preferences From 0e929aa618cefa934bc08ed76eaae0dbd4e2d06f Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 23 Apr 2025 16:29:11 -0700 Subject: [PATCH 25/25] Update internal/lsp/server.go Co-authored-by: Jake Bailey <5341706+jakebailey@users.noreply.github.com> --- internal/lsp/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 9cc5dc60df..14f2957cbc 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -396,7 +396,7 @@ func (s *Server) handleCompletion(req *lsproto.RequestMessage) (messageErr error defer func() { if r := recover(); r != nil { stack := debug.Stack() - fmt.Printf("panic obtaining completions: %v\n%s", r, string(stack)) + s.Log("panic obtaining completions:", r, string(stack)) messageErr = s.sendResult(req.ID, &lsproto.CompletionList{}) } }()