Skip to content

Commit 0b6c670

Browse files
authored
Merge pull request #3833 from joshuataylor/feature/requires-read-lock
Add @RequiresReadLock annotations to PSI-accessing methods
2 parents c5e503c + 11e05f7 commit 0b6c670

15 files changed

Lines changed: 168 additions & 10 deletions

File tree

.github/actions/setup-env/action.yml

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@ runs:
4949
large-packages: false
5050
docker-images: ${{ inputs.free-docker-images == 'true' }}
5151

52-
- name: Cache JetBrains JDK
53-
id: cache-jdk
54-
uses: actions/cache@v5
52+
- name: Restore JetBrains Runtime cache
53+
id: cache-jbr
54+
uses: actions/cache/restore@v5
5555
with:
56-
path: /opt/hostedtoolcache/Java_JetBrains_*
57-
key: jdk-${{ runner.os }}-${{ runner.arch }}-jetbrains-21
56+
path: ${{ runner.tool_cache }}/Java_JetBrains_*
57+
key: jbr-${{ runner.os }}-${{ runner.arch }}-21
58+
restore-keys: |
59+
jbr-${{ runner.os }}-${{ runner.arch }}-
5860
5961
- name: Setup JBR 21
6062
id: setup-java
@@ -67,6 +69,14 @@ runs:
6769
env:
6870
GITHUB_TOKEN: ${{ inputs.github-token }}
6971

72+
- name: Save JetBrains Runtime cache
73+
id: cache-jbr-save
74+
if: always() && steps.cache-jbr.outputs.cache-hit != 'true' && steps.setup-java.outcome == 'success'
75+
uses: actions/cache/save@v5
76+
with:
77+
path: ${{ runner.tool_cache }}/Java_JetBrains_*
78+
key: jbr-${{ runner.os }}-${{ runner.arch }}-${{ steps.setup-java.outputs.version }}
79+
7080
# This sets project properties, allowing us to override gradle.properties via environment variables,
7181
# applying to all Gradle tasks. Means we don't need to do like `./gradlew -PplatformVersion=xxx`
7282
# Needed because we can't just use env, as we're in a composite action, and env set here won't propagate to
@@ -133,7 +143,7 @@ runs:
133143
134144
- name: Save Elixir cache
135145
id: cache-elixir-save
136-
if: always() && steps.cache-elixir-restore.outputs.cache-hit != 'true' && inputs.setup-elixir == 'true'
146+
if: always() && steps.cache-elixir-restore.outputs.cache-hit != 'true' && inputs.setup-elixir == 'true' && steps.setup-elixir.outcome == 'success'
137147
uses: actions/cache/save@v5
138148
with:
139149
path: |

.github/workflows/shared-test.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
os: [ ubuntu-22.04, windows-2025 ]
4444
idea-version:
4545
- '2026.1.2'
46-
- '2025.3.2'
46+
- '2025.3.5'
4747
# 2026.2 upgrades to Java 25, won't work in CI until we resolve issues. https://github.com/JetBrains/intellij-community/commit/aa1cfa9632ab
4848
# - 'LATEST-EAP-SNAPSHOT'
4949

@@ -146,6 +146,12 @@ jobs:
146146
- 'rubymine:2026.1.2'
147147

148148
steps:
149+
- name: Sanitise IDE version for artifact names
150+
id: sanitise
151+
run: echo "ide-version=${IDE_VERSION//:/-}" >> "$GITHUB_OUTPUT"
152+
env:
153+
IDE_VERSION: ${{ matrix.ide-version }}
154+
149155
- name: Download Plugin Zip
150156
id: download-plugin-zip
151157
uses: actions/download-artifact@v6
@@ -192,7 +198,7 @@ jobs:
192198
uses: actions/upload-artifact@v6
193199
if: ${{ always() && inputs.upload-reports }}
194200
with:
195-
name: verify-plugin-reports-${{ matrix.ide-version }}
201+
name: verify-plugin-reports-${{ steps.sanitise.outputs.ide-version }}
196202
path: |
197203
${{ steps.verify-plugin.outputs.verification-output-log-filename }}
198204
${{ steps.verify-plugin.outputs.verification-reports-dir }}
@@ -202,6 +208,6 @@ jobs:
202208
id: upload-plugin
203209
uses: actions/upload-artifact@v6
204210
with:
205-
name: plugin-artifact
211+
name: plugin-artifact-${{ steps.sanitise.outputs.ide-version }}
206212
path: build/distributions/*.zip
207213
retention-days: 1

CHANGELOG.md

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

3+
## v23.8.2
4+
5+
### Enhancements
6+
* Added `@RequiresReadLock` annotations to PSI-accessing methods across 10 files (`CallDefinitionClause`, `Definition`, `Implementation`, `Module`, `Protocol`, `ElixirPsiImplUtil`, `PsiElementImpl`, `PsiNamedElementImpl`, `CallImpl`, `CanonicallyNamedImpl`). Enables the `ThreadingConcurrency` inspection to statically detect callers that don't hold the read lock. - [@joshuataylor](https://github.com/joshuataylor)
7+
8+
### Build
9+
* [#3830](https://github.com/KronicDeth/intellij-elixir/pull/3830) - [@joshuataylor](https://github.com/joshuataylor)
10+
* Bumped IntelliJ 2026.1.x target to 2026.1.2.
11+
* Bumped Gradle to 9.5.1. - [@joshuataylor](https://github.com/joshuataylor)
12+
* Bumped intellij-platform plugin to 2.16.0. - [@joshuataylor](https://github.com/joshuataylor)
13+
314
## v23.8.1
415

516
### Bug Fixes

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.8.1
9+
pluginVersion=23.8.2
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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
<html>
22
<body>
33

4+
<h1>v23.8.2</h1>
5+
<ul>
6+
<li>
7+
<p>Enhancements</p>
8+
<ul>
9+
<li>Added static threading annotations to PSI-accessing methods, enabling compile-time detection of missing read locks via the <code>ThreadingConcurrency</code> inspection.</li>
10+
</ul>
11+
</li>
12+
<li>
13+
<p>Build</p>
14+
<ul>
15+
<li>Bumped IntelliJ 2026.1.x target to 2026.1.2, Gradle to 9.5.1, and intellij-platform plugin to 2.16.0.</li>
16+
</ul>
17+
</li>
18+
</ul>
19+
420
<h1>v23.8.1</h1>
521
<ul>
622
<li>

src/org/elixir_lang/psi/CallDefinitionClause.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.intellij.psi.ElementDescriptionLocation
44
import com.intellij.psi.PsiElement
55
import com.intellij.psi.ResolveState
66
import com.intellij.usageView.UsageViewTypeLocation
7+
import com.intellij.util.concurrency.annotations.RequiresReadLock
78
import org.elixir_lang.NameArityInterval
89
import org.elixir_lang.find_usages.Provider
910
import org.elixir_lang.psi.call.Call
@@ -19,6 +20,7 @@ object CallDefinitionClause {
1920
*
2021
* @param call a def(macro)?p?
2122
*/
23+
@RequiresReadLock
2224
@JvmStatic
2325
fun enclosingModularMacroCall(call: Call): Call? {
2426
var enclosedCall = call
@@ -60,30 +62,42 @@ object CallDefinitionClause {
6062
* @param call a call that [.is].
6163
* @return element for `name(arg, ...) when ...` in `def* name(arg, ...) when ...`
6264
*/
65+
@RequiresReadLock
6366
@JvmStatic
6467
fun head(call: Call): PsiElement? = call.primaryArguments()?.firstOrNull()
6568

69+
@RequiresReadLock
6670
@JvmStatic
6771
fun `is`(call: Call): Boolean = isFunction(call) || isMacro(call) || isGuard(call)
6872

73+
@RequiresReadLock
6974
@JvmStatic
7075
fun isFunction(call: Call): Boolean = isPrivateFunction(call) || isPublicFunction(call)
76+
@RequiresReadLock
7177
@JvmStatic
7278
fun isPublicFunction(call: Call): Boolean =
7379
isCallingKernelMacroOrHead(call, DEF) || isCallingKernelMacroOrHead(call, DEFMEMO)
80+
@RequiresReadLock
7481
fun isPrivateFunction(call: Call): Boolean =
7582
isCallingKernelMacroOrHead(call, DEFP) || isCallingKernelMacroOrHead(call, DEFMEMOP)
7683

84+
@RequiresReadLock
7785
@JvmStatic
7886
fun isMacro(call: Call): Boolean = isPrivateMacro(call) || isPublicMacro(call)
87+
@RequiresReadLock
7988
@JvmStatic
8089
fun isPublicMacro(call: Call): Boolean = isCallingKernelMacroOrHead(call, DEFMACRO)
90+
@RequiresReadLock
8191
fun isPrivateMacro(call: Call): Boolean = isCallingKernelMacroOrHead(call, DEFMACROP)
8292

93+
@RequiresReadLock
8394
fun isGuard(call: Call): Boolean = isPrivateGuard(call) || isPublicGuard(call)
95+
@RequiresReadLock
8496
fun isPublicGuard(call: Call): Boolean = isCallingKernelMacroOrHead(call, DEFGUARD)
97+
@RequiresReadLock
8598
fun isPrivateGuard(call: Call): Boolean = isCallingKernelMacroOrHead(call, DEFGUARDP)
8699

100+
@RequiresReadLock
87101
fun isPublic(call: Call): Boolean = isPublicFunction(call) || isPublicMacro(call) || isPublicGuard(call)
88102

89103
/**
@@ -94,10 +108,12 @@ object CallDefinitionClause {
94108
* default arguments are used, which produces an arity for each default argument that is turned on and off.
95109
* @see Call.resolvedFinalArityInterval
96110
*/
111+
@RequiresReadLock
97112
@JvmStatic
98113
fun nameArityInterval(call: Call, state: ResolveState): NameArityInterval? =
99114
head(call)?.let { CallDefinitionHead.nameArityInterval(it, state) }
100115

116+
@RequiresReadLock
101117
fun nameIdentifier(call: Call): PsiElement? = head(call)?.let { CallDefinitionHead.nameIdentifier(it) }
102118

103119
private fun functionElementDescription(

src/org/elixir_lang/psi/Definition.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.elixir_lang.psi
22

3+
import com.intellij.util.concurrency.annotations.RequiresReadLock
34
import org.elixir_lang.psi.call.Call
45
import org.elixir_lang.psi.call.name.Function.*
56
import org.elixir_lang.psi.call.name.Module.KERNEL
@@ -27,6 +28,7 @@ enum class Definition(val type: Type) {
2728
/**
2829
* What kind of definition is defined by the [call]
2930
*/
31+
@RequiresReadLock
3032
fun definition(call: Call): Definition? =
3133
definition(call.resolvedModuleName(), call.functionName(), call.resolvedFinalArity(), call.hasDoBlockOrKeyword())
3234

src/org/elixir_lang/psi/Implementation.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.intellij.psi.search.GlobalSearchScope
77
import com.intellij.psi.stubs.StubIndex
88
import com.intellij.usageView.UsageViewTypeLocation
99
import com.intellij.util.Processor
10+
import com.intellij.util.concurrency.annotations.RequiresReadLock
1011
import org.elixir_lang.psi.call.Call
1112
import org.elixir_lang.psi.call.name.Function
1213
import org.elixir_lang.psi.impl.call.finalArguments
@@ -17,6 +18,7 @@ import org.elixir_lang.structure_view.element.CallDefinitionClause
1718
import org.elixir_lang.structure_view.element.modular.Modular
1819

1920
object Implementation {
21+
@RequiresReadLock
2022
@JvmStatic
2123
fun `is`(call: Call): Boolean {
2224
return call.isCallingMacro(org.elixir_lang.psi.call.name.Module.KERNEL, Function.DEFIMPL, 2) ||
@@ -27,6 +29,7 @@ object Implementation {
2729
* @return `null` if protocol or module for the implementation cannot be derived or if the `for` argument is a
2830
* list.
2931
*/
32+
@RequiresReadLock
3033
fun name(call: Call): String? = nameCollection(CallDefinitionClause.enclosingModular(call), call)?.singleOrNull()
3134

3235
private fun nameCollection(enclosingModular: Modular?, call: Call): Collection<String>? {
@@ -53,6 +56,7 @@ object Implementation {
5356
private fun forNameCollection(forNameElement: ElixirList): Collection<String> =
5457
forNameCollection(forNameElement.children)
5558

59+
@RequiresReadLock
5660
fun forNameCollection(forNameElement: PsiElement): Collection<String>? = when (forNameElement) {
5761
is ElixirAccessExpression -> forNameCollection(forNameElement)
5862
is ElixirList -> forNameCollection(forNameElement)
@@ -70,6 +74,7 @@ object Implementation {
7074
private fun forNameCollection(forNameElement: QualifiableAlias): Collection<String>? =
7175
forNameElement.name?.let { listOf(it) }
7276

77+
@RequiresReadLock
7378
fun forNameCollection(enclosingModular: Modular?, call: Call): Collection<String>? {
7479
val forNameElement = forNameElement(call)
7580
return when {
@@ -85,6 +90,7 @@ object Implementation {
8590
}
8691
}
8792

93+
@RequiresReadLock
8894
fun forNameElement(call: Call): PsiElement? =
8995
call.finalArguments()?.lastOrNull()?.let { it as? QuotableKeywordList }?.keywordValue(Function.FOR)
9096

@@ -105,9 +111,11 @@ object Implementation {
105111
}
106112
}
107113

114+
@RequiresReadLock
108115
@JvmStatic
109116
fun protocolName(call: Call): String? = protocolNameElement(call)?.let { protocolName(it) }
110117

118+
@RequiresReadLock
111119
fun protocolNameElement(call: Call): QualifiableAlias? {
112120
val finalArguments = call.finalArguments()
113121

src/org/elixir_lang/psi/Module.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import com.intellij.openapi.util.Computable
55
import org.elixir_lang.psi.call.Call
66
import org.elixir_lang.psi.call.name.Function
77
import org.elixir_lang.psi.call.name.Module
8+
import com.intellij.util.concurrency.annotations.RequiresReadLock
89
import org.jetbrains.annotations.Contract
910

1011
object Module {
12+
@RequiresReadLock
1113
@JvmStatic
1214
fun `is`(call: Call): Boolean =
1315
(call.isCallingMacro(Module.KERNEL, Function.DEFMODULE, 2) &&
@@ -25,6 +27,7 @@ object Module {
2527
}) != true) ||
2628
call.isCalling(Module.MODULE, Function.CREATE, 3)
2729

30+
@RequiresReadLock
2831
@Contract(pure = true)
2932
fun name(call: Call): String = call.primaryArguments()!!.first()!!.text
3033
}

src/org/elixir_lang/psi/Protocol.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.intellij.psi.ResolveState
66
import com.intellij.psi.search.GlobalSearchScope
77
import com.intellij.psi.stubs.StubIndex
88
import com.intellij.util.Processor
9+
import com.intellij.util.concurrency.annotations.RequiresReadLock
910
import org.elixir_lang.beam.psi.impl.CallDefinitionImpl
1011
import org.elixir_lang.beam.psi.impl.ModuleImpl
1112
import org.elixir_lang.beam.psi.stubs.ModuleStub
@@ -15,6 +16,7 @@ import org.elixir_lang.psi.call.name.Function
1516
import org.elixir_lang.psi.stub.index.ImplementedProtocolName
1617

1718
object Protocol {
19+
@RequiresReadLock
1820
@JvmStatic
1921
fun `is`(call: Call): Boolean =
2022
call.isCallingMacro(org.elixir_lang.psi.call.name.Module.KERNEL, Function.DEFPROTOCOL, 2)

0 commit comments

Comments
 (0)