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()
}