Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Improved Kotlin Inline Variable: local `var` declarations with one initializer and no subsequent writes can now be inlined from either a declaration or selected usage; assignments and increment/decrement writes are rejected before changing the source with the conflicting expression identified.
- Improved Kotlin Introduce Type Alias: selecting a complete generic type now creates a concrete alias, while selecting only its type constructor creates a parameterized alias that preserves nested and qualified type structures and each same-file usage's own type arguments; star projections and local type parameters that cannot form a valid top-level alias are rejected.
- Improved Kotlin hierarchy-aware refactorings: Pull Members Up, Push Members Down, and compatible IDEA refactoring paths now discover Kotlin override chains and direct supertypes across all source files in the active Maven or Gradle build; Java/library and full IDEA-index conflict scenarios remain unavailable.
- Improved Kotlin Extract Interface and Extract Superclass: source and target Kotlin files now roll back together if an update fails, generated supertypes remain correctly separated from existing target declarations, and Undo Last Refactoring precisely restores an existing target or removes a generated one.
Expand Down
2 changes: 1 addition & 1 deletion CoreImpl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.228-SNAPSHOT</version>
<version>0.11.231-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-core-impl</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinCompilerCliBase/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.228-SNAPSHOT</version>
<version>0.11.231-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-compiler-cli-base</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinFixesImpl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.228-SNAPSHOT</version>
<version>0.11.231-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-fixes-impl</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinFormatter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.228-SNAPSHOT</version>
<version>0.11.231-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-formatter</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinHighlighting/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.228-SNAPSHOT</version>
<version>0.11.231-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-highlighting</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinIcons/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.228-SNAPSHOT</version>
<version>0.11.231-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-icons</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinIntentionUtils/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.228-SNAPSHOT</version>
<version>0.11.231-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-intention-utils</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinRefactoring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.228-SNAPSHOT</version>
<version>0.11.231-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-refactoring</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ import org.jetbrains.kotlin.idea.k2.refactoring.inline.codeInliner.PropertyUsage
import org.jetbrains.kotlin.idea.refactoring.inline.AbstractKotlinInlinePropertyProcessor
import org.jetbrains.kotlin.idea.refactoring.inline.codeInliner.UsageReplacementStrategy
import org.jetbrains.kotlin.idea.refactoring.inline.createReplacementStrategyForProperty
import org.jetbrains.kotlin.idea.references.ReadWriteAccessChecker
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtReferenceExpression
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.KtTreeVisitorVoid

/**
Expand Down Expand Up @@ -69,14 +72,16 @@ data class KaInlineVariableError(
* starting the IDEA `BaseRefactoringProcessor` pipeline.
*
* Steps performed by [compute]:
* 1. Locate the [KtProperty] at the cursor (parent walk from `findElementAt(offset)`).
* 2. Run IDEA's `AbstractKotlinInlinePropertyProcessor.extractInitialization` — returns either
* 1. Locate the [KtProperty] at the cursor (K2 reference resolution first, then a parent walk).
* 2. For a declaration-initialized `var`, reject any K2-resolved Kotlin write/read-write usage so
* only a single definition can be inlined in the standalone session.
* 3. Run IDEA's `AbstractKotlinInlinePropertyProcessor.extractInitialization` — returns either
* the initializer expression or an error message (e.g. "property has no initializer").
* 3. Run IDEA's top-level `createReplacementStrategyForProperty(...)` — this builds the
* 4. Run IDEA's top-level `createReplacementStrategyForProperty(...)` — this builds the
* `PropertyUsageReplacementStrategy` (which wraps a `CodeInliner`) that knows how to inline
* the initializer at any reference site, with proper handling of parentheses, qualified calls,
* imports and type arguments.
* 4. Scan every `KtFile` in [projectKtFiles] for `KtReferenceExpression`s whose `mainReference`
* 5. Scan every `KtFile` in [projectKtFiles] for `KtReferenceExpression`s whose `mainReference`
* resolves to [property] — these are the usages to inline.
*
* The actual PSI mutation (`strategy.createReplacer(usage).invoke()` + `property.delete()`) is
Expand Down Expand Up @@ -120,8 +125,8 @@ class KaInlineVariableComputer(
* (handles the cursor being on the declaration keyword/name itself).
*
* @return [Outcome.NotApplicable] when the cursor is not on a `KtProperty`,
* [Outcome.Error] when the property cannot be inlined (missing initializer, etc.),
* [Outcome.Ready] with the prepared plan otherwise
* [Outcome.Error] when the property cannot be inlined (missing initializer, multiple
* definitions of a mutable local, etc.), [Outcome.Ready] with the prepared plan otherwise
*/
fun compute(): Outcome {
val element = cursorKtFile.findElementAt(offset) ?: return Outcome.NotApplicable
Expand All @@ -130,6 +135,16 @@ class KaInlineVariableComputer(
?: return Outcome.NotApplicable
val declarationName = property.name ?: return Outcome.NotApplicable

findWriteUsage(property)?.let { writeExpression ->
return Outcome.Error(
KaInlineVariableError(
"Cannot inline '$declarationName': '${writeExpression.text}' changes its value, " +
"so it has no single definition to inline.",
declarationName,
),
)
}

// IDEA's extractInitialization validates the property and returns either the initializer
// expression or an error (e.g. no initializer, multiple assignments without a dominator).
val init = AbstractKotlinInlinePropertyProcessor.extractInitialization(property)
Expand Down Expand Up @@ -176,6 +191,42 @@ class KaInlineVariableComputer(
)
}

/**
* Finds the first resolved Kotlin write or read-write usage of [property] in the active
* standalone session.
*
* IDEA normally obtains this information through an indexed [com.intellij.psi.search.PsiSearchHelper],
* but the standalone session deliberately provides a no-op implementation. Returning the full
* access expression, rather than a Boolean, lets the UI explain the exact assignment or
* increment that prevents a mutable local from having one definition.
*/
private fun findWriteUsage(property: KtProperty): KtExpression? {
val checker = ReadWriteAccessChecker.getInstance(project)
for (file in projectKtFiles) {
var conflict: KtExpression? = null
runCatching {
analyze(file) {
file.accept(object : KtTreeVisitorVoid() {
override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) {
if (conflict != null) return
super.visitSimpleNameExpression(expression)
val symbol = runCatching {
expression.mainReference?.resolveToSymbol()
}.getOrNull() ?: return
if (symbol.psi != property) return
val access = checker.readWriteAccessWithFullExpression(expression, true)
if (access.first.isWrite) {
conflict = access.second ?: expression
}
}
})
}
}
conflict?.let { return it }
}
return null
}

/**
* If [element] is (or is inside) a [KtNameReferenceExpression], resolves it via K2 and
* returns the [KtProperty] that the reference points to. Returns `null` when [element] is
Expand Down
2 changes: 1 addition & 1 deletion Nbm/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.228-SNAPSHOT</version>
<version>0.11.231-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-nbm</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ package io.github.nbplugins.kotlin.nbm.refactoring

import com.intellij.psi.util.PsiTreeUtil
import io.github.nbplugins.kotlin.nbm.resolve.KotlinAnalysisAPISession
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.log.KotlinLogger
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.utils.ProjectUtils
import org.netbeans.editor.BaseAction
import org.netbeans.modules.refactoring.spi.ui.UI
Expand Down Expand Up @@ -56,71 +62,93 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR

override fun actionPerformed(evt: ActionEvent, target: JTextComponent) {
val doc = target.document as? StyledDocument ?: return
val caretOffset = target.caretPosition
val selectionStart = target.selectionStart
val selectionEnd = target.selectionEnd
val targetOffset = selectionStart

runCatching {
// Dispatch: function first, then property. The plugin's prepare() returns a fatal
// Problem when the symbol is not inlineable, so the action is always safe to open.
val functionName = resolveFunctionNameAt(doc, caretOffset)
if (functionName != null) {
UI.openRefactoringUI(
KotlinInlineFunctionUI(functionName, KotlinInlineFunctionRefactoring(doc, caretOffset)),
when (val targetDeclaration = resolveTargetAt(doc, selectionStart, selectionEnd)) {
is KtNamedFunction -> UI.openRefactoringUI(
KotlinInlineFunctionUI(targetDeclaration.name.orEmpty(), KotlinInlineFunctionRefactoring(doc, targetOffset)),
TopComponent.getRegistry().activated,
)
return@runCatching
is KtProperty -> UI.openRefactoringUI(
KotlinInlineVariableUI(targetDeclaration.name.orEmpty(), KotlinInlineVariableRefactoring(doc, targetOffset)),
TopComponent.getRegistry().activated,
)
null -> return@runCatching
}

val propertyName = resolvePropertyNameAt(doc, caretOffset) ?: ""
UI.openRefactoringUI(
KotlinInlineVariableUI(propertyName, KotlinInlineVariableRefactoring(doc, caretOffset)),
TopComponent.getRegistry().activated,
)
}.onFailure { e ->
KotlinLogger.INSTANCE.logException("KotlinInlineVariableAction failed", e)
}
}

/**
* Looks up the [KtNamedFunction] at [offset] in [doc] and returns its short name, or `null`
* when the caret is not on a function declaration or a call to a named function.
* Resolves the directly selected inline declaration.
*
* A reference resolves through K2 to its declaration; a declaration-position caret uses the
* directly selected property/function token. Unlike the former function-first lookup, this
* never treats an arbitrary child of an enclosing function as the function itself.
*
* @param doc the document under the caret
* @param offset caret offset within [doc]
* @return the function's simple name, or `null`
* @param doc Kotlin editor document containing the selection
* @param selectionStart first selected offset, or the caret offset when empty
* @param selectionEnd offset immediately after the selection, or [selectionStart] when empty
* @return the target property/function, or `null` when the selection has no inline target
*/
private fun resolveFunctionNameAt(doc: StyledDocument, offset: Int): String? {
private fun resolveTargetAt(
doc: StyledDocument,
selectionStart: Int,
selectionEnd: Int,
): KtNamedDeclaration? {
val fo = ProjectUtils.getFileObjectForDocument(doc) ?: return null
val project = ProjectUtils.getKotlinProjectForFileObject(fo)
?: ProjectUtils.getValidProject()
?: return null
return runCatching {
val session = KotlinAnalysisAPISession.getSession(project)
val ktFile = session.getKtFileForPath(fo.path) ?: return@runCatching null
val element = ktFile.findElementAt(offset) ?: return@runCatching null
PsiTreeUtil.getParentOfType(element, KtNamedFunction::class.java, false)?.name
resolveTargetAt(ktFile, selectionStart, selectionEnd)
}.getOrNull()
}

/** Resolves a direct caret target from a session-managed Kotlin file. */
internal fun resolveTargetAt(ktFile: KtFile, offset: Int): KtNamedDeclaration? =
resolveTargetAt(ktFile, offset, offset)

/**
* Looks up the `KtProperty` at [offset] in [doc] and returns its short name for the dialog
* title, or `null` when the caret is not on a property (the framework then shows a generic
* "Inline Variable" title).
* Resolves the property/function covered by a caret or selection in a session-managed Kotlin file.
*
* @param doc the document under the caret
* @param offset caret offset within [doc]
* @return the property's simple name, or `null`
* @param selectionStart first selected offset, or the caret offset for an empty selection
* @param selectionEnd offset immediately after the selection, or [selectionStart] when empty
* @return directly selected inline target, or `null` when the range selects no inlineable declaration
*/
private fun resolvePropertyNameAt(doc: StyledDocument, offset: Int): String? {
val fo = ProjectUtils.getFileObjectForDocument(doc) ?: return null
val project = ProjectUtils.getKotlinProjectForFileObject(fo)
?: ProjectUtils.getValidProject()
internal fun resolveTargetAt(
ktFile: KtFile,
selectionStart: Int,
selectionEnd: Int,
): KtNamedDeclaration? {
val offsets = if (selectionStart < selectionEnd) {
intArrayOf(selectionStart, selectionEnd - 1)
} else {
intArrayOf(selectionStart)
}
for (offset in offsets) {
val element = ktFile.findElementAt(offset) ?: continue
resolveReferenceTarget(element)?.let { return it }
((element.parent as? KtProperty) ?: (element.parent as? KtNamedFunction))?.let { return it }
}
return null
}

/** Resolves a direct Kotlin reference and retains only declarations supported by Inline. */
private fun resolveReferenceTarget(element: PsiElement): KtNamedDeclaration? {
val reference = PsiTreeUtil.getParentOfType(element, KtNameReferenceExpression::class.java, false)
?: return null
return runCatching {
val session = KotlinAnalysisAPISession.getSession(project)
val ktFile = session.getKtFileForPath(fo.path) ?: return@runCatching null
val element = ktFile.findElementAt(offset) ?: return@runCatching null
PsiTreeUtil.getParentOfType(element, KtProperty::class.java, false)?.name
}.getOrNull()
analyze(reference) {
reference.mainReference?.resolveToSymbol()?.psi as? KtNamedDeclaration
}
}.getOrNull()?.takeIf { it is KtProperty || it is KtNamedFunction }
}

companion object {
Expand Down
Loading
Loading