Skip to content

Commit 249744a

Browse files
authored
Merge pull request #3818 from sh41/split/pr3808-5-psi-resolver
PSI / Resolver / Code Insight Fixes
2 parents 57d8997 + c05dc77 commit 249744a

56 files changed

Lines changed: 1376 additions & 438 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
## v23.2.0
4+
5+
### Enhancements
6+
* [#3818](https://github.com/KronicDeth/intellij-elixir/pull/3818) - [@sh41](https://github.com/sh41)
7+
* Code completion deduplication: multi-clause functions (e.g. `Enum.map_every` with 5 clauses) now appear once in completion results instead of once per clause head. Shared `PreferFunctionHead` logic selects bare function heads over implementation clauses.
8+
* Parameter info deduplication: parameter hints grouped by `(name, arity)` -- separate arities still show distinct hints, but multiple clauses of the same arity no longer produce duplicate entries.
9+
* Completion prefers source-defined modulars over BEAM stubs when both are available, eliminating duplicate completion entries.
10+
* Transitive alias resolution: when stub-index lookup finds nothing for a module name that is itself a `QualifiableAlias`, resolution now follows alias chains transitively. Possibly addresses [#1806](https://github.com/KronicDeth/intellij-elixir/issues/1806).
11+
* `DefinitionsScopedSearch` cancellation: added `ProgressManager.checkCanceled()` at loop boundaries and honour `Processor.process()` return value for early-exit, preventing hangs during large-project searches.
12+
13+
### Bug Fixes
14+
* [#3818](https://github.com/KronicDeth/intellij-elixir/pull/3818) - [@sh41](https://github.com/sh41)
15+
* **Breaking change**: removed `nameArityInAnyModule` global fallback from resolver. Previously, when `resolveInScope` found no results, the resolver fell back to a global stub-index search returning every function with a matching name from every module (all marked `validResult=false`). This polluted parameter hints with unrelated modules (e.g. hovering `Enum.map()` showed hints from `Stream.Reducers`, `Ecto`, `Phoenix`), caused Go-to-Definition to navigate to wrong-module definitions, and filled the resolution cache with irrelevant results. Calls that were previously "resolved" to functions in unrelated modules will now correctly appear as unresolved references.
16+
* Infinite loop in `UnaliasedName.up` when resolving `QualifiedMultipleAliases` -- function overload ordering caused mutual recursion.
17+
* Infinite recursion and NPE prevention in PSI resolve/tree-walk paths via `RecursionManager.doPreventingRecursion()` and null-safe `VISITED_ELEMENT_SET` access in `ResolveState`.
18+
* `@spec` line marker grouping checked arity equality before name equality -- specs for different functions with the same arity were incorrectly grouped together.
19+
* Gutter icons anchored to leaf `PsiElement`s per the `LineMarkerProvider` contract. Non-leaf elements caused markers to blink or appear in wrong positions after edits.
20+
* Removed redundant `computeReadAction`/`runReadAction` wrappers from `CallImpl` getters (`functionName`, `moduleName`, `resolvedPrimaryArity`) and `PsiNamedElementImpl` name getters. These trivial PSI reads were called from paths already holding a read lock; under 2025.3+ writer-preference locking, re-acquiring blocks the EDT when a write action is pending. Partially fixes [#3790](https://github.com/KronicDeth/intellij-elixir/issues/3790).
21+
* `Elixir.` prefix stripping for module name resolution -- stub index stores names without the prefix, so `Elixir.Enum` lookups now match correctly.
22+
323
## v23.1.0
424

525
### Enhancements

gen/org/elixir_lang/psi/scope/module/MultiResolve.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.intellij.psi.util.PsiTreeUtil
88
import org.elixir_lang.Module.concat
99
import org.elixir_lang.Module.split
1010
import org.elixir_lang.psi.NamedElement
11+
import org.elixir_lang.psi.QualifiableAlias
1112
import org.elixir_lang.psi.call.Named
1213
import org.elixir_lang.psi.impl.ElixirPsiImplUtil.ENTRANCE
1314
import org.elixir_lang.psi.impl.call.finalArguments
@@ -67,7 +68,7 @@ class MultiResolve internal constructor(private val name: String, private val in
6768
)
6869
}
6970
}
70-
} else if (requireCall != null) {
71+
} else {
7172
resolveResultOrderedSet.add(match, requireCall.text, true, state.visitedElementSet())
7273
}
7374
} else {
@@ -106,6 +107,8 @@ class MultiResolve internal constructor(private val name: String, private val in
106107
val project = match.project
107108

108109
if (!DumbService.isDumb(project)) {
110+
var found = false
111+
109112
StubIndex
110113
.getInstance()
111114
.processElements(
@@ -115,9 +118,29 @@ class MultiResolve internal constructor(private val name: String, private val in
115118
GlobalSearchScope.allScope(project),
116119
NamedElement::class.java) {
117120
resolveResultOrderedSet.add(it, unaliasedName, true, visitedElementSet)
121+
found = true
118122

119123
true
120124
}
125+
126+
// Transitive alias fallback: if stub lookup found nothing and match is a QualifiableAlias,
127+
// resolve it through the module scope to follow alias chains
128+
// (e.g., `alias MyNamespace.Referenced; alias Referenced, as: Refd` — resolving `Refd` needs
129+
// to follow Referenced → MyNamespace.Referenced transitively)
130+
if (!found && match is QualifiableAlias) {
131+
val transitiveResults = resolveResults(match.fullyQualifiedName(), incompleteCode, match)
132+
for (result in transitiveResults) {
133+
val resolvedElement = result.element
134+
if (resolvedElement is NamedElement) {
135+
resolveResultOrderedSet.add(
136+
resolvedElement,
137+
resolvedElement.name ?: unaliasedName,
138+
result.isValidResult,
139+
visitedElementSet + result.visitedElementSet
140+
)
141+
}
142+
}
143+
}
121144
}
122145
}
123146

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# --- Plugin Metadata ---
77
pluginGroup=org.elixir_lang
88
pluginName=Elixir
9-
pluginVersion=23.1.0
9+
pluginVersion=23.2.0
1010
pluginRepositoryUrl=https://github.com/KronicDeth/intellij-elixir/
1111
vendorName=Elle Imhoff
1212
vendorEmail=Kronic.Deth@gmail.com

resources/META-INF/changelog.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
<html>
22
<body>
33

4+
<h1>v23.2.0</h1>
5+
<ul>
6+
<li>
7+
<p>Enhancements</p>
8+
<ul>
9+
<li>Code completion no longer shows duplicate entries for functions with multiple clauses or guards (e.g. <code>Enum.map_every</code> previously appeared 5 times).</li>
10+
<li>Parameter hints deduplicated -- hovering a function call shows one set of hints per arity, not one per clause.</li>
11+
<li>Module alias resolution now follows alias chains transitively, improving Go-to-Declaration and completion for aliased modules.</li>
12+
<li>Large-project searches (Find Usages, Go-to-Definition) can now be cancelled mid-search and exit early when results are found, preventing UI hangs.</li>
13+
</ul>
14+
</li>
15+
<li>
16+
<p>Bug Fixes</p>
17+
<ul>
18+
<li><b>Breaking change</b>: parameter hints and Go-to-Definition no longer show results from unrelated modules. Previously, unresolvable calls fell back to a global search returning every function with the same name across the entire project. Calls that cannot be resolved now correctly show no results instead of misleading ones.</li>
19+
<li>Fixed infinite loop when resolving multi-aliases (e.g. <code>alias Foo.{Bar, Baz}</code>).</li>
20+
<li>Fixed infinite recursion and potential crashes in reference resolution.</li>
21+
<li>Fixed <code>@spec</code> gutter icons grouping specs for different functions together when they shared the same arity.</li>
22+
<li>Fixed gutter icons (line markers) blinking or appearing in wrong positions after editing code.</li>
23+
<li>Reduced UI freezes caused by redundant read-lock acquisition in frequently called PSI accessors under IntelliJ 2025.3+ writer-preference locking.</li>
24+
</ul>
25+
</li>
26+
</ul>
27+
428
<h1>v23.1.0</h1>
529
<ul>
630
<li>

src/org/elixir_lang/DefinitionsScopedSearch.kt

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package org.elixir_lang
22

3-
import com.intellij.openapi.application.ApplicationManager
43
import com.intellij.openapi.application.QueryExecutorBase
4+
import com.intellij.openapi.progress.ProgressManager
55
import com.intellij.psi.PsiElement
66
import com.intellij.psi.ResolveState
77
import com.intellij.psi.search.searches.DefinitionsScopedSearch
@@ -23,23 +23,21 @@ class DefinitionsScopedSearch :
2323
queryParameters: DefinitionsScopedSearch.SearchParameters,
2424
consumer: Processor<in PsiElement>
2525
) {
26-
ApplicationManager.getApplication().assertReadAccessAllowed()
2726
when (val element = queryParameters.element) {
2827
is Call -> processQuery(element, consumer)
2928
is QualifiableAlias -> processQuery(element, consumer)
3029
}
3130
}
3231

3332
private fun processQuery(qualifiableAlias: QualifiableAlias, consumer: Processor<in PsiElement>) {
34-
ApplicationManager.getApplication().assertReadAccessAllowed()
33+
ProgressManager.checkCanceled()
3534
qualifiableAlias.outerMostQualifiableAlias().maybeModularNameToModulars(qualifiableAlias.containingFile)
3635
.map { modular ->
3736
processQuery(modular, consumer)
3837
}
3938
}
4039

4140
private fun processQuery(psiElement: PsiElement, consumer: Processor<in PsiElement>) {
42-
ApplicationManager.getApplication().assertReadAccessAllowed()
4341
when (psiElement) {
4442
is Call -> processQuery(psiElement, consumer)
4543
is ModuleImpl<*> -> processQuery(psiElement, consumer)
@@ -48,28 +46,63 @@ class DefinitionsScopedSearch :
4846
}
4947

5048
private fun processQuery(call: Call, consumer: Processor<in PsiElement>) {
51-
ApplicationManager.getApplication().assertReadAccessAllowed()
5249
if (Protocol.`is`(call)) {
5350
Protocol.processImplementations(call, consumer)
5451
} else if (CallDefinitionClause.`is`(call)) {
5552
enclosingModularMacroCall(call)?.let { modularCall ->
5653
CallDefinitionClause.nameArityInterval(call, ResolveState.initial())?.let { protocolNameArityInterval ->
5754
if (Protocol.`is`(modularCall)) {
5855
Protocol.processImplementations(modularCall) { defimpl ->
59-
for (defimplChild in (defimpl as Call).macroChildCallList()) {
60-
if (CallDefinitionClause.`is`(defimplChild)) {
61-
CallDefinitionClause.nameArityInterval(defimplChild, ResolveState.initial())
62-
?.let { implNameArityInterval ->
63-
if (implNameArityInterval.name == protocolNameArityInterval.name &&
64-
implNameArityInterval.arityInterval.overlaps(protocolNameArityInterval.arityInterval)
65-
) {
66-
consumer.process(defimplChild)
56+
ProgressManager.checkCanceled()
57+
58+
var continueProcessing = true
59+
60+
when (defimpl) {
61+
is Call -> {
62+
for (defimplChild in defimpl.macroChildCallList()) {
63+
ProgressManager.checkCanceled()
64+
65+
if (CallDefinitionClause.`is`(defimplChild)) {
66+
CallDefinitionClause.nameArityInterval(defimplChild, ResolveState.initial())
67+
?.let { implNameArityInterval ->
68+
if (implNameArityInterval.name == protocolNameArityInterval.name &&
69+
implNameArityInterval.arityInterval.overlaps(protocolNameArityInterval.arityInterval)
70+
) {
71+
if (!consumer.process(defimplChild)) {
72+
continueProcessing = false
73+
}
74+
}
75+
}
76+
}
77+
78+
if (!continueProcessing) {
79+
break
80+
}
81+
}
82+
}
83+
84+
is ModuleImpl<*> -> {
85+
for (callDefinition in defimpl.callDefinitions()) {
86+
ProgressManager.checkCanceled()
87+
88+
val implNameArityInterval = callDefinition.nameArityInterval
89+
90+
if (implNameArityInterval.name == protocolNameArityInterval.name &&
91+
implNameArityInterval.arityInterval.overlaps(protocolNameArityInterval.arityInterval)
92+
) {
93+
if (!consumer.process(callDefinition)) {
94+
continueProcessing = false
6795
}
6896
}
97+
98+
if (!continueProcessing) {
99+
break
100+
}
101+
}
69102
}
70103
}
71104

72-
true
105+
continueProcessing
73106
}
74107
}
75108
}
@@ -78,49 +111,67 @@ class DefinitionsScopedSearch :
78111
}
79112

80113
private fun processQuery(moduleImpl: ModuleImpl<*>, consumer: Processor<in PsiElement>) {
81-
ApplicationManager.getApplication().assertReadAccessAllowed()
82114
if (Protocol.`is`(moduleImpl)) {
83115
Protocol.processImplementations(moduleImpl, consumer)
84116
}
85117
}
86118

87119
private fun processQuery(callDefinitionImpl: CallDefinitionImpl<*>, consumer: Processor<in PsiElement>) {
88-
ApplicationManager.getApplication().assertReadAccessAllowed()
89120
val moduleImpl = callDefinitionImpl.parent
90121

91122
if (Protocol.`is`(moduleImpl)) {
92123
val name = callDefinitionImpl.name
93124
val arity = callDefinitionImpl.exportedArity(ResolveState.initial())
94125

95126
Protocol.processImplementations(moduleImpl) { defimpl ->
127+
ProgressManager.checkCanceled()
128+
129+
var continueProcessing = true
130+
96131
when (defimpl) {
97132
is Call -> {
98133
for (defimplChild in defimpl.macroChildCallList()) {
134+
ProgressManager.checkCanceled()
135+
99136
if (CallDefinitionClause.`is`(defimplChild)) {
100137
CallDefinitionClause.nameArityInterval(defimplChild, ResolveState.initial())
101138
?.let { implNameArityInterval ->
102139
if (implNameArityInterval.name == name &&
103140
implNameArityInterval.arityInterval.contains(arity)
104141
) {
105-
consumer.process(defimplChild)
142+
if (!consumer.process(defimplChild)) {
143+
continueProcessing = false
144+
}
106145
}
107146
}
108147
}
148+
149+
if (!continueProcessing) {
150+
break
151+
}
109152
}
110153
}
111154
is ModuleImpl<*> ->
112155
for (callDefinition in defimpl.callDefinitions()) {
156+
ProgressManager.checkCanceled()
157+
113158
val implNameArityInterval = callDefinition.nameArityInterval
114159

115160
if (implNameArityInterval.name == name &&
116161
implNameArityInterval.arityInterval.contains(arity)
117162
) {
118-
consumer.process(callDefinition)
163+
if (!consumer.process(callDefinition)) {
164+
continueProcessing = false
165+
}
166+
}
167+
168+
if (!continueProcessing) {
169+
break
119170
}
120171
}
121172
}
122173

123-
true
174+
continueProcessing
124175
}
125176
}
126177
}

src/org/elixir_lang/beam/psi/Modular.java

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/org/elixir_lang/code_insight/ParameterInfo.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class ParameterInfo : ParameterInfoHandler<Arguments, Any> {
2020

2121
override fun showParameterInfo(element: Arguments, context: CreateParameterInfoContext) {
2222
PsiTreeUtil.getParentOfType(element, Call::class.java)?.let { call ->
23-
val itemToShowList = call.references.flatMap { reference ->
23+
val allClauses = call.references.flatMap { reference ->
2424
if (reference is PsiPolyVariantReference) {
2525
reference.multiResolve(true).flatMap { resolveResult ->
2626
resolveResult.element?.let {
@@ -38,10 +38,15 @@ class ParameterInfo : ParameterInfoHandler<Arguments, Any> {
3838
} else {
3939
null
4040
}
41-
} ?: emptyList<Any>()
41+
} ?: emptyList()
4242
}
4343
}
4444

45+
// Deduplicate by (name, arity), preferring bare function heads (no do block)
46+
// over implementation clauses. Uses arity-aware grouping so that
47+
// genuinely different arities (e.g. foo/1 vs foo/2) each get their own hint.
48+
val itemToShowList = preferFunctionHeadsByArity(allClauses)
49+
4550
if (itemToShowList.isNotEmpty()) {
4651
context.itemsToShow = itemToShowList.toTypedArray()
4752
context.showHint(element, element.textRange.startOffset, this)
@@ -115,9 +120,6 @@ class ParameterInfo : ParameterInfoHandler<Arguments, Any> {
115120
}
116121
}
117122

118-
private fun findArguments(context: ParameterInfoContext): Arguments? {
119-
val elementAtOffset = context.file.findElementAt(context.offset)
120-
121-
return PsiTreeUtil.getParentOfType<Arguments>(elementAtOffset, Arguments::class.java)
122-
}
123+
private fun findArguments(context: ParameterInfoContext): Arguments? =
124+
ParameterInfoUtils.findParentOfType(context.file, context.offset, Arguments::class.java)
123125
}

0 commit comments

Comments
 (0)