fun listDeclarations()

in gdscript/src/main/kotlin/gdscript/psi/utils/GdClassMemberUtil.kt [50:246]


    fun listDeclarations(
        element: PsiElement,
        searchFor: String? = null,
        onlyLocalScope: Boolean = false,
        ignoreParents: Boolean = false,
        ignoreGlobalScope: Boolean = false,
        allowResource: Boolean = false,
    ): Array<Any> {
        var static: Boolean? = false
        val project = element.project

        val result = mutableListOf<Any>()
        var calledOn: String? = GdKeywords.SELF

        val calledOnPsi: GdExpr? = calledUpon(element)
        val calledOnPsiName = calledOnPsi?.text ?: ""
        if (calledOnPsi != null && calledOnPsiName != GdKeywords.SELF) {
            // Check if there is an assertion check 'if (node is Node3D):'
            var isCheckedSuccess = false
            val isChecked = findIsTypeCheck(element)
            if (isChecked != null) {
                val isExpr = isChecked.expr.firstChild
                if (isExpr is GdRefIdRef && isExpr.text == calledOnPsiName) {
                    calledOn = PsiGdExprUtil.fromTyped(isChecked.typedVal)
                    isCheckedSuccess = true
                }
            }

            if (!isCheckedSuccess) {
                calledOn = calledOnPsi.getReturnTypeOrRes(allowResource)
            }

            if (calledOn != null) {
                // Only apply this heuristic if we haven't already determined a static context.
                // The idea: if the inferred type name of the qualifier matches the qualifier text itself
                // (e.g., "Outer"), or a fully qualified form like "FileOrOuter.Outer", then we consider
                // the access to be static on a class, unless _Global has a variable with the same name.
                static = isStaticAccessByName(element, calledOnPsi, calledOn)
                // If the qualifier resolves to a script resource path (e.g. "res://Something.gd"),
                // do not assume either static or instance context. Resource-based references can represent
                // unnamed scripts or external files where we cannot reliably infer static-ness from the type
                // string alone. Setting `static = null` prevents premature filtering of members.
                if (calledOn.endsWith(".gd")) {
                    static = null
                }
                // Hybrid handling for _GlobalScope singletons shadowing class names (e.g., `Input`).
                // If the qualifier text looks like a class name (matches the inferred type name) AND
                // there exists a variable with the same name in _GlobalScope, expose both static and
                // instance members by switching to tri-state `static = null`.
                run {
                    val qualifierText = calledOnPsi.text
                    val fullOwnerId = GdClassUtil.getFullClassId(calledOnPsi)
                    val looksLikeClassName = (calledOn == qualifierText) || (calledOn == "$fullOwnerId.$qualifierText")
                    val globalHasVarWithSameName = !checkGlobalStaticMatch(element, qualifierText)
                    if (looksLikeClassName && globalHasVarWithSameName) {
                        static = null
                    }
                }
            }
        }

        if (static == false && (calledOn == null || calledOn == GdKeywords.SELF)) {
            when (val ownerMethod = PsiTreeUtil.getParentOfType(
                calledOnPsi ?: element,
                GdMethodDeclTl::class.java,
            )) {
                is GdMethodDeclTl -> {
                    static = ownerMethod.isStatic
                }
            }
        }

        when (calledOn) {
            GdKeywords.SELF -> calledOn = null
            GdKeywords.SUPER -> calledOn = GdInheritanceUtil.getExtendedClassId(element)
        }

        var parent: PsiElement?

        // If it's stand-alone ref_id, adds also _Global & ClassNames - Classes are added as last due to matching name of some GlobalVars with class_name
        if (calledOn == null && !ignoreGlobalScope) {
            arrayOf(GdKeywords.GLOBAL_SCOPE, GdKeywords.GLOBAL_GD_SCRIPT).forEach {
                val globalParent = getClassIdElement(it, element, project)
                if (globalParent != null) {
                    val local = addsParentDeclarations(
                        GdClassUtil.getOwningClassElement(globalParent),
                        result,
                        null,
                        searchFor
                    )
                    if (searchFor != null && local != null) return arrayOf(local)
                }
            }
        }

        val hitLocal = BoolVal.new()
        // Checks locals only when it's not attribute/call expression moving declaration possibly outside
        if (calledOn == null) {
            val locals = listLocalDeclarationsUpward(element, onlyLocalScope, hitLocal)
            if (searchFor != null && locals.containsKey(searchFor)) return arrayOf(locals[searchFor]!!)
            result.addAll(locals.values)

            // This class is already scanned via localDecl - so move to extended one
            parent = if (onlyLocalScope) {
                GdInheritanceUtil.getExtendedElement(element)
            } else {
                GdClassUtil.getOwningClassElement(element)
            }
        } else {
            // Normalize typed Array[K] to base Array
            if (calledOn.startsWith("Array[")) calledOn = "Array"

            // Normalize typed Dictionary[K, V] to base Dictionary
            if (calledOn.startsWith("Dictionary[")) {
                val firstChild = PsiTreeUtil.collectElementsOfType(calledOnPsi, GdRefIdRef::class.java).lastOrNull()
                if (firstChild != null) {
                    val dictDecl = findDeclaration(firstChild)
                    if (dictDecl is GdEnumDeclTl) {
                        if (searchFor != null) {
                            val localVal = dictDecl.enumValueList.find { eval -> eval.enumValueNmi.name == searchFor }
                            if (localVal != null) return arrayOf(localVal)
                        }
                        result.addAll(dictDecl.enumValueList)
                    }
                }
                calledOn = "Dictionary"
            }

            // If qualifier resolves to a named enum, expose its values for member lookup (e.g., Class.Enum.VALUE)
            run {
                // First, try resolving the entire qualifier expression directly
                val direct = findDeclaration(calledOnPsi!!)
                val enumDecl = when (direct) {
                    is GdEnumDeclTl -> direct
                    else -> {
                        // If that failed, try resolving the right-most identifier within the qualifier
                        val refs = PsiTreeUtil.findChildrenOfType(calledOnPsi, GdRefIdRef::class.java)
                            .sortedBy { it.textRange.startOffset }
                        val lastRef = refs.lastOrNull()
                        val lastDecl = lastRef?.let { findDeclaration(it) }
                        lastDecl as? GdEnumDeclTl
                    }
                }
                if (enumDecl != null) {
                    if (searchFor != null) {
                        val localVal = enumDecl.enumValueList.find { eval -> eval.enumValueNmi.name == searchFor }
                        if (localVal != null) return arrayOf(localVal)
                    }
                    result.addAll(enumDecl.enumValueList)
                    if (searchFor == null) return result.toTypedArray()
                }
            }

            parent = getClassIdElement(calledOn, element, project)
            if (parent == null) {
                val classId = GdClassUtil.getFullClassId(element)
                parent = getClassIdElement("$classId.${calledOn}", element, project)
                // Try autoload classes
                if (parent == null) {
                    parent = ProjectAutoloadUtil.findFromAlias(calledOn, element)
                    if (parent != null) static = false
                }
            }

            if (parent != null) {
                parent = GdClassUtil.getOwningClassElement(parent)
            }
        }

        // Recursively iterate over all extended classes
        if (!ignoreParents && !hitLocal.value) {
            // Important nuance: for unqualified completion (calledOn == null), do not suppress
            // file-level declarations by passing a concrete static flag; keep it nullable so that
            // listClassMemberDeclarations won't early-return for PsiFile owners. This preserves
            // SDK/_Global and file-root members in plain completion, while still restricting them
            // for qualified completion (calledOn != null).
            val staticForParents = if (calledOn == null && parent is PsiFile) null else static
            val local = collectFromParents(parent, result, project, staticForParents, searchFor)
            if (local != null) return arrayOf(local)
        }

        if (calledOn == null) {
            val autoLoads = ProjectAutoloadUtil.listGlobals(project)
            if (searchFor != null) {
                val localClass = GdClassNamingIndex.INSTANCE.getGlobally(searchFor, element).firstOrNull()
                if (localClass != null) return arrayOf(localClass)
                val autoLoaded = autoLoads.find { it.key == searchFor }
                if (autoLoaded != null) return arrayOf(autoLoaded)
            }
            // todo: this call is time-consuming and it is useful for completion of base types, which we have from LSP anyway
            // result.addAll(GdClassNamingIndex.INSTANCE.getAllValues(project))
            result.addAll(autoLoads)
        }

        if (searchFor != null) return emptyArray() // this one seems weird, but removing it brakes tests
        return result.toTypedArray()
    }