diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 5b1a6fb905..7b83f275f3 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -749,6 +749,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 { @@ -1655,6 +1667,10 @@ 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 } } + NumericOrStringLikeLiteral = Node // StringLiteralLike | NumericLiteral ) // Aliases for node singletons @@ -1673,6 +1689,7 @@ type ( CatchClauseNode = Node CaseBlockNode = Node CaseOrDefaultClauseNode = Node + CaseClauseNode = Node VariableDeclarationNode = Node VariableDeclarationListNode = Node BindingElementNode = Node @@ -1695,6 +1712,7 @@ type ( JsxOpeningFragmentNode = Node JsxClosingFragmentNode = Node SourceFileNode = Node + PropertyAccessExpressionNode = Node ) type ( @@ -8219,6 +8237,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 { @@ -9446,7 +9468,7 @@ func (node *JSDocThisTag) Clone(f NodeFactoryCoercible) *Node { type JSDocImportTag struct { JSDocTagBase ImportClause *Declaration - ModuleSpecifier *Node + ModuleSpecifier *Expression Attributes *Node } @@ -9947,7 +9969,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/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 9f6c560b98..7f027b5aa3 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 { @@ -927,6 +927,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 { @@ -1966,6 +1973,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 || @@ -2651,3 +2659,127 @@ func GetPragmaArgument(pragma *Pragma, name string) string { } return "" } + +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 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) +} + +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) +} + +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 +} + +func IsCallLikeExpression(node *Node) bool { + switch node.Kind { + case KindJsxOpeningElement, KindJsxSelfClosingElement, KindCallExpression, KindNewExpression, + KindTaggedTemplateExpression, KindDecorator: + return true + } + return false +} + +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 +} + +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/astnav/tokens.go b/internal/astnav/tokens.go index 6bccfe0f16..49bc960a0c 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) } @@ -433,7 +433,7 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN shouldVisitNode := func(node *ast.Node) bool { // Node is synthetic or out of the desired range: don't visit it. return !(node.Flags&ast.NodeFlagsReparsed != 0 || - node.End() > endPos || getStartOfNode(node, sourceFile, !excludeJSDoc /*includeJSDoc*/) >= position) + node.End() > endPos || GetStartOfNode(node, sourceFile, !excludeJSDoc /*includeJSDoc*/) >= position) } visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { if node == nil { @@ -560,3 +560,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/checker/checker.go b/internal/checker/checker.go index e0501bca70..8dee49403e 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -823,6 +823,7 @@ type Checker struct { emitResolverOnce sync.Once _jsxNamespace string _jsxFactoryEntity *ast.Node + skipDirectInferenceNodes core.Set[*ast.Node] } func NewChecker(program Program) *Checker { @@ -1156,7 +1157,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 @@ -1406,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*/) @@ -1718,7 +1719,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) } } @@ -1764,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 } @@ -1777,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)) { @@ -4134,7 +4135,7 @@ func (c *Checker) checkBaseTypeAccessibility(t *Type, node *ast.Node) { if len(signatures) != 0 { declaration := signatures[0].declaration if declaration != nil && hasModifier(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)) } @@ -4227,7 +4228,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 @@ -4288,7 +4289,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 @@ -4824,7 +4825,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: @@ -5027,7 +5028,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 { @@ -6276,12 +6277,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 { @@ -6330,7 +6331,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)) { @@ -6887,7 +6888,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 } @@ -7070,7 +7071,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 } @@ -8136,7 +8137,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 && hasModifier(valueDecl, ast.ModifierFlagsAbstract) { c.error(node, diagnostics.Cannot_create_an_instance_of_an_abstract_class) return c.resolveErrorCall(node) @@ -8175,7 +8176,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) { @@ -8907,7 +8908,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) } @@ -9939,7 +9940,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 } } @@ -9960,13 +9961,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 { @@ -10366,7 +10367,7 @@ func (c *Checker) checkSyntheticExpression(node *ast.Node) *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) @@ -10479,7 +10480,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 @@ -10498,7 +10499,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) } @@ -10671,7 +10672,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 { @@ -10847,7 +10848,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)) @@ -10987,11 +10988,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 @@ -11123,7 +11124,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()) @@ -11138,7 +11139,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))) @@ -11353,7 +11354,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) } @@ -11407,7 +11408,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 { @@ -11430,10 +11431,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. @@ -11777,7 +11781,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) @@ -11789,7 +11793,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) @@ -12335,7 +12339,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)) }) } @@ -13142,7 +13146,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 } @@ -13167,12 +13171,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 { @@ -13576,7 +13580,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())) } @@ -13923,7 +13927,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") } @@ -14185,7 +14189,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 @@ -14206,7 +14210,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 @@ -16338,7 +16342,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) } @@ -17090,20 +17094,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 } @@ -17682,7 +17686,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) } @@ -18154,7 +18158,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 { @@ -18775,7 +18779,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 } @@ -19224,7 +19228,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) @@ -20448,13 +20452,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*/) } @@ -24436,7 +24433,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) @@ -24478,7 +24475,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) } @@ -24644,7 +24641,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) } @@ -24889,7 +24886,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)) } } @@ -24966,7 +24963,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 @@ -25541,7 +25538,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) } @@ -25945,7 +25942,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 } } @@ -26160,7 +26157,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 } @@ -26419,7 +26416,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) } } @@ -26578,7 +26575,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 @@ -26648,7 +26645,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 { @@ -26691,7 +26688,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) } @@ -26839,7 +26836,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: @@ -29351,7 +29348,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 @@ -29799,3 +29796,13 @@ 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/flow.go b/internal/checker/flow.go index aba89e9764..58dcf30b9c 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) @@ -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) } @@ -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/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 0874e542a5..907307ed7b 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 { @@ -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) @@ -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) } } @@ -4885,10 +4885,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 new file mode 100644 index 0000000000..9b5863d2ae --- /dev/null +++ b/internal/checker/services.go @@ -0,0 +1,400 @@ +package checker + +import ( + "maps" + "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 { + 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 +} + +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) +} + +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) 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) +} + +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 +} + +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 + } else { + next = nil + } + if next == nil { + break + } + target = next + } + 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/checker/types.go b/internal/checker/types.go index a99e7f7c43..9da2867153 100644 --- a/internal/checker/types.go +++ b/internal/checker/types.go @@ -636,6 +636,46 @@ func (t *Type) TargetTupleType() *TupleType { return t.AsTypeReference().target.AsTupleType() } +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 { @@ -680,6 +720,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 ca369921b6..7d079abc73 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" @@ -110,20 +111,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() @@ -230,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 @@ -245,37 +239,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: @@ -460,7 +423,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 } @@ -483,26 +446,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 { @@ -957,20 +902,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*/) } @@ -1248,7 +1179,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) } @@ -1294,15 +1225,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) } @@ -1790,7 +1712,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 @@ -1800,13 +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 } @@ -2157,3 +2072,50 @@ 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 +} + +// 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 +} + +// 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 5456fb3077..811922c0fc 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -539,3 +539,21 @@ func levenshteinWithMax(s1 []rune, s2 []rune, maxValue float64) float64 { func Identity[T any](t T) T { return t } + +func CheckEachDefined[S any](s []*S, msg string) []*S { + for _, value := range s { + if value == nil { + panic(msg) + } + } + 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/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 new file mode 100644 index 0000000000..605ff714f7 --- /dev/null +++ b/internal/ls/completions.go @@ -0,0 +1,2321 @@ +package ls + +import ( + "fmt" + "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" +) + +func (l *LanguageService) ProvideCompletion( + fileName string, + position int, + context *lsproto.CompletionContext, + clientOptions *lsproto.CompletionClientCapabilities, + preferences *UserPreferences, +) *lsproto.CompletionList { + program, file := l.getProgramAndFile(fileName) + return l.getCompletionsAtPosition(program, file, position, context, preferences, clientOptions) +} + +// !!! 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.PropertyAccessExpressionNode + isNewIdentifierLocation bool + location *ast.Node + keywordFilters KeywordCompletionFilters + literals []literalValue + 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 { + // !!! +} + +// 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.IdentifierNode +} + +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 +) + +var TriggerCharacters = []string{".", `"`, "'", "`", "/", "@", "<", "#", " "} + +// 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{".", ";"} + +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" +) + +func deprecateSortText(original sortText) sortText { + return "z" + original +} + +func sortBelow(original sortText) sortText { + return original + "1" +} + +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 + data any +} + +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 uniqueNamesMap = map[string]bool + +type literalValue any // string | jsnum.Number | PseudoBigInt + +func (l *LanguageService) getCompletionsAtPosition( + program *compiler.Program, + file *ast.SourceFile, + position int, + context *lsproto.CompletionContext, + preferences *UserPreferences, + clientOptions *lsproto.CompletionClientCapabilities, +) *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 != nil && *context.TriggerCharacter == " " { + // `isValidTrigger` ensures we are at `import |` + if ptrIsTrue(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: ptrTo(getDefaultCommitCharacters(true /*isNewIdentifierLocation*/)), + }, + } + } + return nil + } + + compilerOptions := program.GetCompilerOptions() + + // !!! see if incomplete completion list and continue or clean + + // !!! string literal completions + + // !!! label completions + + completionData := getCompletionData(program, file, position, preferences) + if completionData == nil { + return nil + } + + // switch completionData.Kind // !!! other data cases + // !!! transform data into completion list + + response := l.completionInfoFromData( + file, + program, + compilerOptions, + completionData, + preferences, + position, + clientOptions, + ) + // !!! check if response is incomplete + return response +} + +func getCompletionData(program *compiler.Program, file *ast.SourceFile, position int, preferences *UserPreferences) *completionData { + typeChecker := program.GetTypeChecker() + inCheckedFile := isCheckedFile(file, program.GetCompilerOptions()) + + currentToken := astnav.GetTokenAtPosition(file, position) + + 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.PropertyAccessExpressionNode + 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 + 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 + } + + // 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 + } + } + } + + 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 + } + } + } + } + } + } + + completionKind := CompletionKindNone + hasUnresolvedAutoImports := false + // This also gets mutated in nested-functions after the return + var symbols []*ast.Symbol + symbolToOriginInfoMap := map[ast.SymbolId]*symbolOriginInfo{} + symbolToSortTextMap := map[ast.SymbolId]sortText{} + // var importSpecifierResolver any // !!! 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 // !!! auto import + + addSymbolOriginInfo := func(symbol *ast.Symbol, insertQuestionDot bool, insertAwait bool) { + symbolId := ast.GetSymbolId(symbol) + if insertAwait && seenPropertySymbols.AddIfAbsent(symbolId) { + 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 { + name := ast.GetNameOfDeclaration(decl) + if name != nil && name.Kind == ast.KindComputedPropertyName { + return name + } + return nil + }) + + 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 && seenPropertySymbols.AddIfAbsent(firstAccessibleSymbolId) { + 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 { + // !!! imports + // var fileName string + // if tspath.IsExternalModuleNameRelative(core.StripQuotes(moduleSymbol.Name)) { + // fileName = ast.GetSourceFileOfModule(moduleSymbol).FileName() + // } + // if importSpecifierResolver == nil { + // 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 firstAccessibleSymbolId == 0 || !seenPropertySymbols.Has(firstAccessibleSymbolId) { + 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) { + if typeChecker.GetStringIndexType(t) != nil { + isNewIdentifierLocation = true + defaultCommitCharacters = []string{} + } + if isRightOfQuestionDot && len(typeChecker.GetCallSignatures(t)) != 0 { + isNewIdentifierLocation = true + if defaultCommitCharacters == nil { + defaultCommitCharacters = slices.Clone(allCommitCharacters) // Only invalid commit character here would be `(`. + } + } + + 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 = []string{} + } + 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, core.Set[ast.SymbolId]{}) + } + 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 && + !ptrIsFalse(preferences.IncludeAutomaticOptionalChainCompletions) + if canCorrectToQuestionDot || isRightOfQuestionDot { + t = typeChecker.GetNonNullableType(t) + if canCorrectToQuestionDot { + insertQuestionDot = true + } + } + } + addTypeProperties(t, node.Flags&ast.NodeFlagsAwaitContext != 0, insertQuestionDot) + } + + return + } + } + } + + if !isTypeLocation || checker.IsInTypeQuery(node) { + // 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) + 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*/) + } + } + } + + 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 + } + + var contextualType *checker.Type + if previousToken != nil { + contextualType = getContextualType(previousToken, position, file, typeChecker) + } + + // 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 + if isLiteralExpected { + var types []*checker.Type + 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) && !t.IsEnumLiteral() { + return t.AsLiteralType().Value() + } + return nil + }) + } + + var recommendedCompletion *ast.Symbol + if previousToken != nil && contextualType != nil { + recommendedCompletion = getRecommendedCompletion(previousToken, contextualType, typeChecker) + } + + if defaultCommitCharacters == nil { + defaultCommitCharacters = getDefaultCommitCharacters(isNewIdentifierLocation) + } + + 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 getDefaultCommitCharacters(isNewIdentifierLocation bool) []string { + if isNewIdentifierLocation { + return []string{} + } + return slices.Clone(allCommitCharacters) +} + +func (l *LanguageService) completionInfoFromData( + file *ast.SourceFile, + program *compiler.Program, + compilerOptions *core.CompilerOptions, + data *completionData, + preferences *UserPreferences, + position int, + clientOptions *lsproto.CompletionClientCapabilities, +) *lsproto.CompletionList { + keywordFilters := data.keywordFilters + symbols := data.symbols + isNewIdentifierLocation := data.isNewIdentifierLocation + contextToken := data.contextToken + literals := data.literals + + // 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 := l.getCompletionEntriesFromSymbols( + data, + nil, /*replacementToken*/ + position, + file, + program, + compilerOptions.GetEmitScriptTarget(), + preferences, + compilerOptions, + clientOptions, + ) + + if data.keywordFilters != KeywordCompletionFiltersNone { + keywordCompletions := getKeywordCompletions( + data.keywordFilters, + !data.insideJsDocTagTypeExpression && ast.IsSourceFileJS(file)) + for _, keywordEntry := range keywordCompletions { + if data.isTypeOnlyLocation && isTypeKeyword(scanner.StringToToken(keywordEntry.Label)) || + !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 + + 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, + } + } + + // !!! port behavior of other strada fields of CompletionInfo that are non-LSP + return &lsproto.CompletionList{ + IsIncomplete: data.hasUnresolvedAutoImports, + ItemDefaults: itemDefaults, + Items: sortedEntries, + } +} + +func (l *LanguageService) getCompletionEntriesFromSymbols( + data *completionData, + replacementToken *ast.Node, + position int, + file *ast.SourceFile, + program *compiler.Program, + target core.ScriptTarget, + preferences *UserPreferences, + compilerOptions *core.CompilerOptions, + clientOptions *lsproto.CompletionClientCapabilities, +) (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; + // 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(uniqueNamesMap) + 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 + } + + var sortText sortText + if isDeprecated(symbol, typeChecker) { + sortText = deprecateSortText(originalSortText) + } else { + sortText = originalSortText + } + entry := l.createCompletionItem( + symbol, + sortText, + replacementToken, + data, + position, + file, + program, + name, + needsConvertPropertyAccess, + origin, + useSemicolons, + compilerOptions, + preferences, + clientOptions, + ) + 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 + sortedEntries = core.InsertSorted(sortedEntries, entry, compareCompletionEntries) + } + + uniqueSet := core.NewSetWithSizeHint[string](len(uniques)) + for name := range maps.Keys(uniques) { + uniqueSet.Add(name) + } + return uniqueSet, sortedEntries +} + +func completionNameForLiteral( + file *ast.SourceFile, + preferences *UserPreferences, + literal literalValue, +) string { + switch literal := literal.(type) { + case string: + return quote(file, preferences, literal) + case jsnum.Number: + name, _ := core.StringifyJson(literal, "" /*prefix*/, "" /*suffix*/) + return name + case jsnum.PseudoBigInt: + return literal.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 (l *LanguageService) createCompletionItem( + symbol *ast.Symbol, + sortText sortText, + replacementToken *ast.Node, + data *completionData, + position int, + file *ast.SourceFile, + program *compiler.Program, + name string, + needsConvertPropertyAccess bool, + origin *symbolOriginInfo, + useSemicolons bool, + compilerOptions *core.CompilerOptions, + preferences *UserPreferences, + clientOptions *lsproto.CompletionClientCapabilities, +) *lsproto.CompletionItem { + contextToken := data.contextToken + var insertText string + var filterText string + replacementSpan := l.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 data.propertyAccessToConvert != nil && (useBraces || insertQuestionDot) { + // 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 { + insertText = fmt.Sprintf("[%s]", quotePropertyName(file, preferences, name)) + } else { + insertText = fmt.Sprintf("[%s]", name) + } + } else { + insertText = name + } + + if insertQuestionDot || data.propertyAccessToConvert.AsPropertyAccessExpression().QuestionDotToken != nil { + insertText = "?." + insertText + } + + dot := findChildOfKind(data.propertyAccessToConvert, ast.KindDotToken, file) + if dot == nil { + dot = findChildOfKind(data.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, data.propertyAccessToConvert.Name().Text()) { + end = data.propertyAccessToConvert.End() + } else { + end = dot.End() + } + replacementSpan = l.createLspRangeFromBounds(astnav.GetStartOfNode(dot, file, false /*includeJSDoc*/), end, file) + } + + if data.jsxInitializer.isInitializer { + if insertText == "" { + insertText = name + } + insertText = fmt.Sprintf("{%s}", insertText) + if data.jsxInitializer.initializer != nil { + replacementSpan = l.createLspRangeFromNode(data.jsxInitializer.initializer, file) + } + } + + if originIsPromise(origin) && data.propertyAccessToConvert != nil { + if insertText == "" { + insertText = name + } + precedingToken := astnav.FindPrecedingToken(file, data.propertyAccessToConvert.Pos()) + var awaitText string + if precedingToken != nil && positionIsASICandidate(precedingToken.End(), precedingToken.Parent, file) { + awaitText = ";" + } + + awaitText += "(await " + scanner.GetTextOfNode(data.propertyAccessToConvert.Expression()) + ")" + if needsConvertPropertyAccess { + insertText = awaitText + insertText + } else { + dotStr := core.IfElse(insertQuestionDot, "?.", ".") + insertText = awaitText + dotStr + insertText + } + isInAwaitExpression := ast.IsAwaitExpression(data.propertyAccessToConvert.Parent) + wrapNode := core.IfElse( + isInAwaitExpression, + data.propertyAccessToConvert.Parent, + data.propertyAccessToConvert.Expression(), + ) + replacementSpan = l.createLspRangeFromBounds( + astnav.GetStartOfNode(wrapNode, file, false /*includeJSDoc*/), + data.propertyAccessToConvert.End(), + file) + } + + if originIsResolvedExport(origin) { + labelDetails = &lsproto.CompletionItemLabelDetails{ + Description: &origin.asResolvedExport().moduleSpecifier, // !!! vscode @link support + } + if data.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 data.completionKind == CompletionKindObjectPropertyDeclaration && + contextToken != nil && + !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) || + 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 ptrIsTrue(preferences.IncludeCompletionsWithClassMemberSnippets) && + data.completionKind == CompletionKindMemberLike && + isClassLikeMemberCompletion(symbol, data.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.CompletionItem.LabelDetailsSupport) { + name = name + *origin.asObjectLiteralMethod().labelDetails.Detail + labelDetails = nil + } + source = string(completionSourceObjectLiteralMethodSnippet) + sortText = sortBelow(sortText) + } + + if data.isJsxIdentifierExpected && + !data.isRightOfOpenTag && + ptrIsTrue(clientOptions.CompletionItem.SnippetSupport) && + !jsxAttributeCompletionStyleIs(preferences.JsxAttributeCompletionStyle, JsxAttributeCompletionStyleNone) && + !(ast.IsJsxAttribute(data.location.Parent) && data.location.Parent.Initializer() != nil) { + 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) && + !t.IsBooleanLike() && + !(t.IsUnion() && core.Some(t.Types(), (*checker.Type).IsBooleanLike)) { + if t.IsStringLike() || + t.IsUnion() && + 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 = escapeSnippetText(name) + "={$1}" + isSnippet = true + } + } + + if originIsExport(origin) || originIsResolvedExport(origin) { + // !!! auto-imports + // data = originToCompletionEntryData(origin) + // hasAction = importStatementCompletion == nil + } + + parentNamedImportOrExport := ast.FindAncestor(data.location, isNamedImportsOrExports) + if parentNamedImportOrExport != nil { + languageVersion := compilerOptions.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_", name, name) + } + } + } + + elementKind := getSymbolKind(typeChecker, symbol, data.location) + kind := getCompletionsSymbolKind(elementKind) + 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) + 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, + }, + } + } + // !!! adjust text edit like vscode does when Strada's `isMemberCompletion` is true + + return &lsproto.CompletionItem{ + Label: name, + LabelDetails: labelDetails, + Kind: &kind, + Tags: tags, + Detail: detail, + Preselect: boolToPtr(isRecommendedCompletionMatch(symbol, data.recommendedCompletion, typeChecker)), + SortText: ptrTo(string(sortText)), + FilterText: strPtrTo(filterText), + InsertText: strPtrTo(insertText), + InsertTextFormat: insertTextFormat, + TextEdit: textEdit, + 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 +} + +func strPtrTo(v string) *string { + if v == "" { + return nil + } + return &v +} + +func ptrIsTrue(ptr *bool) bool { + if ptr == nil { + return false + } + return *ptr +} + +func ptrIsFalse(ptr *bool) bool { + if ptr == nil { + return false + } + return !*ptr +} + +func boolToPtr(v bool) *bool { + if v { + return ptrTo(true) + } + 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 +} + +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 + } + + 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) + 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: microsoft/TypeScript#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. +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 == astnav.GetStartOfNode(contextToken, file, false /*includeJSDoc*/)+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 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 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) || + nonAliasCanBeReferencedAtTypeLocation( + checker.SkipAlias(core.IfElse(symbol.ExportSymbol != nil, symbol.ExportSymbol, symbol), typeChecker), + typeChecker, + seenModules, + ) +} + +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 && seenModules.AddIfAbsent(ast.GetSymbolId(symbol)) && + 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 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.") + } +} + +// 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 && + symbol.ValueDeclaration.ModifierFlags()&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 { + // !!! 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) + } + } +} + +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 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( + types, + 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 (l *LanguageService) 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 l.createRangeFromStringLiteralLikeContent(file, contextToken, position) + default: + return l.createLspRangeFromNode(contextToken, file) + } +} + +func (l *LanguageService) createRangeFromStringLiteralLikeContent(file *ast.SourceFile, node *ast.StringLiteralLike, position int) *lsproto.Range { + replacementEnd := node.End() - 1 + 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 { + return nil + } + replacementEnd = min(position, node.End()) + } + return l.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.IsIntersection() { + 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.IsString() && 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, ScriptElementKindKeyword: + return lsproto.CompletionItemKindKeyword + case ScriptElementKindConstElement, ScriptElementKindLetElement, ScriptElementKindVariableElement, + ScriptElementKindLocalVariableElement, ScriptElementKindAlias, ScriptElementKindParameterElement: + return lsproto.CompletionItemKindVariable + + case ScriptElementKindMemberVariableElement, ScriptElementKindMemberGetAccessorElement, + ScriptElementKindMemberSetAccessorElement: + return lsproto.CompletionItemKindField + + case ScriptElementKindFunctionElement, ScriptElementKindLocalFunctionElement: + return lsproto.CompletionItemKindFunction + + case ScriptElementKindMemberFunctionElement, ScriptElementKindConstructSignatureElement, + ScriptElementKindCallSignatureElement, ScriptElementKindIndexSignatureElement: + return lsproto.CompletionItemKindMethod + + case ScriptElementKindEnumElement: + return lsproto.CompletionItemKindEnum + + case ScriptElementKindEnumMemberElement: + return lsproto.CompletionItemKindEnumMember + + case ScriptElementKindModuleElement, ScriptElementKindExternalModuleName: + return lsproto.CompletionItemKindModule + + case ScriptElementKindClassElement, 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 + } +} + +// 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 +} + +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 { + return getTypescriptKeywordCompletions(keywordFilter) + } + + 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 { + 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, ast.KindConstructorKeyword, ast.KindGetKeyword, + ast.KindSetKeyword, ast.KindAsyncKeyword, ast.KindDeclareKeyword, ast.KindOverrideKeyword: + return true + default: + return ast.IsClassMemberModifier(kind) + } +} + +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/completions_test.go b/internal/ls/completions_test.go new file mode 100644 index 0000000000..883844835f --- /dev/null +++ b/internal/ls/completions_test.go @@ -0,0 +1,130 @@ +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" + "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) { + 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", + 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), + }, + }, + }, + }, + { + 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) { + t.Parallel() + runTest(t, testCase.content, testCase.position, testCase.expected) + }) + } +} + +func runTest(t *testing.T, content string, position int, expected *lsproto.CompletionList) { + files := map[string]string{ + "/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"}, + }, + } + preferences := &ls.UserPreferences{} + completionList := languageService.ProvideCompletion( + "/index.ts", + position, + context, + capabilities, + preferences) + assert.DeepEqual(t, completionList, expected) +} + +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 ptrTo[T any](v T) *T { + return &v +} diff --git a/internal/ls/symbol_display.go b/internal/ls/symbol_display.go new file mode 100644 index 0000000000..777eea8614 --- /dev/null +++ b/internal/ls/symbol_display.go @@ -0,0 +1,385 @@ +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" +) + +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 { + 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 +} + +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/types.go b/internal/ls/types.go index 830a6bfc3d..67c5028d80 100644 --- a/internal/ls/types.go +++ b/internal/ls/types.go @@ -15,3 +15,36 @@ type Location struct { FileName string Range core.TextRange } + +type JsxAttributeCompletionStyle string + +const ( + JsxAttributeCompletionStyleAuto JsxAttributeCompletionStyle = "auto" + JsxAttributeCompletionStyleBraces JsxAttributeCompletionStyle = "braces" + JsxAttributeCompletionStyleNone JsxAttributeCompletionStyle = "none" +) + +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 + + // 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 new file mode 100644 index 0000000000..460fbbd1b0 --- /dev/null +++ b/internal/ls/utilities.go @@ -0,0 +1,377 @@ +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" +) + +var quoteReplacer = strings.NewReplacer("'", `\'`, `\"`, `"`) + +// !!! +func isInString(file *ast.SourceFile, position int, previousToken *ast.Node) bool { + return false +} + +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 +} + +// !!! +func isInComment(file *ast.SourceFile, position int, tokenAtPosition *ast.Node) *ast.CommentRange { + return nil +} + +// !!! +// 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 { + if node == nil { + return nil + } + + assertHasRealPosition(node) + + lastChild := getLastChild(node, sourceFile) + if lastChild == nil { + return nil + } + + if lastChild.Kind < ast.KindFirstNode { + return lastChild + } else { + return getLastToken(lastChild, sourceFile) + } +} + +// !!! +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 +} + +// !!! +type PossibleTypeArgumentInfo struct { + called *ast.IdentifierNode + nTypeArguments int +} + +// !!! +func getPossibleTypeArgumentsInfo(tokenIn *ast.Node, sourceFile *ast.SourceFile) *PossibleTypeArgumentInfo { + return nil +} + +// !!! +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 +} + +func (l *LanguageService) createLspRangeFromNode(node *ast.Node, file *ast.SourceFile) *lsproto.Range { + return l.createLspRangeFromBounds(node.Pos(), node.End(), file) +} + +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 { + // 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 { + quoted = quoteReplacer.Replace(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, astnav.GetStartOfNode(nextToken, file, false /*includeJSDoc*/)) + return startLine != endLine +} + +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, + 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 { + 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 +} + +var typeKeywords *core.Set[ast.Kind] = core.NewSetFromItems( + 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 typeKeywords.Has(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 e8373bed21..9cc5dc60df 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "runtime/debug" "slices" "strings" "time" @@ -194,6 +195,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: @@ -250,6 +253,10 @@ func (s *Server) handleInitialize(req *lsproto.RequestMessage) error { InterFileDependencies: true, }, }, + CompletionProvider: &lsproto.CompletionOptions{ + TriggerCharacters: &ls.TriggerCharacters, + // !!! other options + }, }, }) } @@ -377,6 +384,32 @@ func (s *Server) handleDefinition(req *lsproto.RequestMessage) error { return s.sendResult(req.ID, &lsproto.Definition{Locations: &lspLocations}) } +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()) + if err != nil { + return s.sendError(req.ID, err) + } + + // !!! remove this after completions is fully ported/tested + defer func() { + if r := recover(); r != nil { + stack := debug.Stack() + fmt.Printf("panic obtaining completions: %v\n%s", r, string(stack)) + messageErr = s.sendResult(req.ID, &lsproto.CompletionList{}) + } + }() + // !!! get user preferences + list := project.LanguageService().ProvideCompletion( + file.FileName(), + pos, + params.Context, + s.initializeParams.Capabilities.TextDocument.Completion, + &ls.UserPreferences{}) + return s.sendResult(req.ID, list) +} + func (s *Server) getFileAndProject(uri lsproto.DocumentUri) (*project.ScriptInfo, *project.Project) { fileName := ls.DocumentURIToFileName(uri) return s.projectService.EnsureDefaultProjectForFile(fileName) diff --git a/internal/parser/parser.go b/internal/parser/parser.go index c0282c2949..8858d17c31 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -199,7 +199,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 @@ -5931,7 +5931,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() @@ -6342,14 +6342,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 } 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/project/service_test.go b/internal/project/service_test.go index 90952bb5a7..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/scanner/scanner.go b/internal/scanner/scanner.go index 99641e26f0..21b8d13603 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 @@ -1082,7 +1082,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 @@ -1349,7 +1349,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 { @@ -1358,14 +1358,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 @@ -1402,11 +1402,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 } } @@ -1424,13 +1424,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 @@ -1631,7 +1631,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) @@ -1774,7 +1774,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' { @@ -1948,7 +1948,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 } } @@ -1960,11 +1960,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) } @@ -2016,6 +2016,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 91e883fc91..b397d67640 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 diff --git a/internal/testutil/projecttestutil/projecttestutil.go b/internal/testutil/projecttestutil/projecttestutil.go new file mode 100644 index 0000000000..c2e324ee4d --- /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 +}