diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e7dcbf..269fef6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Added structural conversion hints for Kotlin: with the caret on an operator or convention-named call you can now convert `a.plus(b)` to `a + b`, `a.get(i)`/`a.set(i, v)` to `a[i]`/`a[i] = v`, `a.contains(b)` to `b in a`, `a.invoke(b)` to `a(b)`, `a.not()`/`a.unaryPlus()` to `!a`/`+a`, and `f() {}` to `f {}` directly from the hint popup. # 0.11.54 (2026-10-12) - Added **New → Kotlin Package** for **Source Packages**, **Test Packages**, **Kotlin Source Packages**, and **Kotlin Test Packages**, creating the requested nested package hierarchy exclusively beneath the selected source or test root in Maven, Gradle, and Ant/J2SE projects. diff --git a/CoreImpl/pom.xml b/CoreImpl/pom.xml index 8feb779a..d1390a6a 100644 --- a/CoreImpl/pom.xml +++ b/CoreImpl/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.282-SNAPSHOT + 0.11.284-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-core-impl diff --git a/KotlinCodeInsightImpl/pom.xml b/KotlinCodeInsightImpl/pom.xml new file mode 100644 index 00000000..80b1ad9b --- /dev/null +++ b/KotlinCodeInsightImpl/pom.xml @@ -0,0 +1,318 @@ + + + 4.0.0 + + io.github.nbplugins + netbeans-plugin-kotlin + 0.11.284-SNAPSHOT + ../pom.xml + + netbeans-plugin-kotlin-code-insight-impl + jar + Netbeans Plugin Kotlin Code Insight Impl + + Structural-conversion inspections and intentions compiled from + submodules/IntellijCommunity (code-insight/inspections-k2, + code-insight/intentions-k2, code-insight/inspections-shared). + These are the IDEA implementations of operator/function conversions + (a + b <-> a.plus(b), a[i] <-> a.get(i), a in b <-> b.contains(a), + f {} <-> f() {}) consumed by the NetBeans adapter layer in the Nbm module. + + + + + ${project.build.directory}/generated-sources/code-insight + + + + org.apache.maven.plugins + maven-resources-plugin + + + + + copy-inspections-k2 + initialize + copy-resources + + ${project.build.directory}/generated-sources/code-insight + true + + + ${basedir}/../submodules/IntellijCommunity/plugins/kotlin/code-insight/inspections-k2/src + false + + org/jetbrains/kotlin/idea/k2/codeinsight/inspections/expressions/ReplaceCallWithBinaryOperatorInspection.kt + org/jetbrains/kotlin/idea/k2/codeinsight/inspections/ReplaceGetOrSetInspection.kt + org/jetbrains/kotlin/idea/k2/codeinsight/inspections/ReplaceContainsInspection.kt + + + + + + + + + copy-intentions-k2 + initialize + copy-resources + + ${project.build.directory}/generated-sources/code-insight + true + + + ${basedir}/../submodules/IntellijCommunity/plugins/kotlin/code-insight/intentions-k2/src + false + + org/jetbrains/kotlin/idea/k2/codeinsight/intentions/ReplaceInvokeIntention.kt + org/jetbrains/kotlin/idea/k2/codeinsight/intentions/ReplaceCallWithUnaryOperatorIntention.kt + + + + + + + + + copy-inspections-shared + initialize + copy-resources + + ${project.build.directory}/generated-sources/code-insight + true + + + ${basedir}/../submodules/IntellijCommunity/plugins/kotlin/code-insight/inspections-shared/src + false + + org/jetbrains/kotlin/idea/codeInsight/inspections/shared/RemoveEmptyParenthesesFromLambdaCallInspection.kt + + + + + + + + + copy-local-code-insight + initialize + copy-resources + + ${project.build.directory}/generated-sources/code-insight + true + + + ${basedir}/src/main/kotlin + false + + + + + + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + patch-code-insight-impl + generate-sources + execute + + + + + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + + ${kotlin.runtime.languageVersion} + ${kotlin.runtime.languageVersion} + 17 + + -Xskip-metadata-version-check + -Xcontext-parameters + -opt-in=kotlin.ExperimentalContextParameters + -opt-in=kotlin.RequiresOptIn + -opt-in=org.jetbrains.kotlin.analysis.api.KaExperimentalApi + -opt-in=org.jetbrains.kotlin.analysis.api.KaIdeApi + -opt-in=org.jetbrains.kotlin.analysis.api.KaContextParameterApi + + + ${project.build.directory}/generated-sources/code-insight + + + + + compile + process-sources + compile + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + -proc:none + + + + default-compile + compile + compile + + + ${project.build.directory}/generated-sources/code-insight + + + + + + + + + + + + com.jetbrains.intellij.platform + core + provided + + + com.jetbrains.intellij.platform + analysis + provided + + + com.jetbrains.intellij.platform + extensions + provided + + + com.jetbrains.intellij.platform + core-impl + 193.7288.26 + provided + + ** + + + + + + org.jetbrains.kotlin + kotlin-compiler-ir-for-ide + provided + + ** + + + + org.jetbrains.kotlin + kotlin-compiler-common-for-ide + provided + + ** + + + + + + org.jetbrains.kotlin + analysis-api-for-ide + provided + + ** + + + + org.jetbrains.kotlin + analysis-api-impl-base-for-ide + provided + + ** + + + + + + com.jetbrains.intellij.java + java-psi + ${intellij.platform.253.version} + provided + + ** + + + + + + io.github.nbplugins + netbeans-plugin-kotlin-fixes-impl + + + + + io.github.nbplugins + netbeans-plugin-kotlin-intention-utils + + + + + io.github.nbplugins + netbeans-plugin-kotlin-refactoring + + + + org.jetbrains + annotations + provided + + + diff --git a/KotlinCompilerCliBase/pom.xml b/KotlinCompilerCliBase/pom.xml index 6bb4d10c..f77043d0 100644 --- a/KotlinCompilerCliBase/pom.xml +++ b/KotlinCompilerCliBase/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.282-SNAPSHOT + 0.11.284-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-compiler-cli-base diff --git a/KotlinFixesImpl/pom.xml b/KotlinFixesImpl/pom.xml index 07c8e769..36f0c5a4 100644 --- a/KotlinFixesImpl/pom.xml +++ b/KotlinFixesImpl/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.282-SNAPSHOT + 0.11.284-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-fixes-impl @@ -105,6 +105,31 @@ + + + copy-codeinsight-api-framework + initialize + copy-resources + + ${project.build.directory}/generated-sources/fixes + true + + + ${basedir}/../submodules/IntellijCommunity/plugins/kotlin/code-insight/api/src + false + + org/jetbrains/kotlin/idea/codeinsight/api/applicators/ApplicabilityRange.kt + org/jetbrains/kotlin/idea/codeinsight/api/applicable/ApplicableRangesProvider.kt + org/jetbrains/kotlin/idea/codeinsight/api/applicable/ContextProvider.kt + org/jetbrains/kotlin/idea/codeinsight/api/applicable/inspections/KotlinApplicableInspectionBase.kt + org/jetbrains/kotlin/idea/codeinsight/api/applicable/inspections/KotlinModCommandQuickFix.kt + org/jetbrains/kotlin/idea/codeinsight/api/applicable/intentions/KotlinApplicableModCommandAction.kt + + + + + + copy-local-fixes @@ -152,25 +177,20 @@ def patches = [ new_: 'public abstract fun invoke('], ] -// Remove ClassBased and Contextless inner classes from KotlinPsiUpdateModCommandAction — -// we only use ElementBased. ClassBased imports ContextProvider/getElementContext/KtElement -// which are not in our classpath; Contextless also uses KtElement. +// Remove the Contextless inner class from KotlinPsiUpdateModCommandAction — +// we only use ElementBased and ClassBased. ClassBased is kept because +// KotlinApplicableModCommandAction extends it; its ContextProvider / +// getElementContext / KtElement references are now satisfiable because +// ContextProvider.kt is compiled in this module (see the patch section below). def modCommandFile = new File(srcDir, 'org/jetbrains/kotlin/idea/codeinsight/api/applicable/intentions/KotlinPsiUpdateModCommandAction.kt') if (modCommandFile.exists()) { def lines = modCommandFile.readLines() def out = [] - def skipImports = [ - 'import org.jetbrains.kotlin.idea.codeinsight.api.applicable.ContextProvider', - 'import org.jetbrains.kotlin.idea.codeinsight.api.applicable.getElementContext', - 'import org.jetbrains.kotlin.psi.KtElement', - // Note: kotlin.reflect.KClass is kept — used in the sealed class constructor signature - ] - def skipClassMarkers = ['abstract class ClassBased', 'abstract class Contextless'] + def skipClassMarkers = ['abstract class Contextless'] def inSkippedClass = false def braceDepth = 0 lines.each { line -> - if (skipImports.any { line.startsWith(it) }) return if (!inSkippedClass && skipClassMarkers.any { line.contains(it) }) { inSkippedClass = true braceDepth = 0 @@ -183,7 +203,7 @@ if (modCommandFile.exists()) { out << line } modCommandFile.text = out.join('\n') - log.info("Removed ClassBased and Contextless from KotlinPsiUpdateModCommandAction.kt") + log.info("Removed Contextless from KotlinPsiUpdateModCommandAction.kt") } patches.each { p -> @@ -256,6 +276,91 @@ if (addReturnExprFile.exists()) { addReturnExprFile.text = text log.info("Patched AddReturnExpressionFix.kt: removed safeAs/endOffset, simplified invoke body") } + +// --------------------------------------------------------------------------- +// code-insight/api applicable/** framework (used by the K2 inspections and +// intentions in the KotlinCodeInsightImpl module). +// +// The framework is compiled standalone here, without the surrounding +// code-insight modules. Two kinds of references are absent from this +// classpath and are patched out: +// * org.jetbrains.kotlin.idea.codeinsight.api.applicable.extensions.* +// (KaDanglingFileResolutionModeProvider) and analyzeCopy / copyOrigin — +// only needed for injected/dangling files, which this standalone +// container never produces. +// * org.jetbrains.kotlin.analysis.api.permissions.forbidAnalysis — +// a development-time assertion helper; inlining its body keeps the +// "no analysis on EDT" guard semantics without the dependency. +// --------------------------------------------------------------------------- + +// ApplicabilityRange.kt: textRangeIn lives in base/psi, which is not a dependency here. +def applicabilityRangeFile = new File(srcDir, + 'org/jetbrains/kotlin/idea/codeinsight/api/applicators/ApplicabilityRange.kt') +if (applicabilityRangeFile.exists()) { + def text = applicabilityRangeFile.text + text = text.replace('import org.jetbrains.kotlin.idea.base.psi.textRangeIn\n', '') + text = text.replace('it.textRangeIn(element)', + 'com.intellij.openapi.util.TextRange(it.textRange.startOffset - element.textRange.startOffset, it.textRange.endOffset - element.textRange.startOffset)') + applicabilityRangeFile.text = text + log.info("Patched ApplicabilityRange.kt: inlined textRangeIn") +} + +// ContextProvider.kt: drop analyzeCopy/copyOrigin (injected-file support). +def contextProviderFile = new File(srcDir, + 'org/jetbrains/kotlin/idea/codeinsight/api/applicable/ContextProvider.kt') +if (contextProviderFile.exists()) { + def text = contextProviderFile.text + text = text.replace('import org.jetbrains.kotlin.analysis.api.analyzeCopy\n', '') + text = text.replace('import org.jetbrains.kotlin.analysis.api.projectStructure.copyOrigin\n', '') + text = text.replace('import org.jetbrains.kotlin.idea.codeinsight.api.applicable.extensions.KaDanglingFileResolutionModeProvider\n', '') + text = text.replace( + '''@OptIn(KaExperimentalApi::class) +fun ContextProvider.getElementContext( + element: E, +): C? = if (element.containingFile.copyOrigin == null) analyze(element) { + prepareContext(element) +} else analyzeCopy( + element, + KaDanglingFileResolutionModeProvider.getDanglingFileResolutionMode(element) +) { + prepareContext(element) +}''', + '''@OptIn(KaExperimentalApi::class) +fun ContextProvider.getElementContext( + element: E, +): C? = analyze(element) { + prepareContext(element) +}''') + contextProviderFile.text = text + log.info("Patched ContextProvider.kt: dropped dangling-file support") +} + +// KotlinApplicableInspectionBase.kt: inline forbidAnalysis. +def applicableInspectionFile = new File(srcDir, + 'org/jetbrains/kotlin/idea/codeinsight/api/applicable/inspections/KotlinApplicableInspectionBase.kt') +if (applicableInspectionFile.exists()) { + def text = applicableInspectionFile.text + text = text.replace('import org.jetbrains.kotlin.analysis.api.permissions.forbidAnalysis\n', '') + text = text.replace('forbidAnalysis("isApplicableByPsi") {', 'run {') + text = text.replace('forbidAnalysis("getApplicabilityRanges") {', 'run {') + applicableInspectionFile.text = text + log.info("Patched KotlinApplicableInspectionBase.kt: inlined forbidAnalysis") +} + +// KotlinApplicableModCommandAction.kt: inline forbidAnalysis and widen isElementApplicable. +// `isElementApplicable` is `protected final` in the IDEA source; the NetBeans adapter layer +// lives in a different module and must call it to drive applicability, so it is made public. +def applicableModCommandFile = new File(srcDir, + 'org/jetbrains/kotlin/idea/codeinsight/api/applicable/intentions/KotlinApplicableModCommandAction.kt') +if (applicableModCommandFile.exists()) { + def text = applicableModCommandFile.text + text = text.replace('import org.jetbrains.kotlin.analysis.api.permissions.forbidAnalysis\n', '') + text = text.replace('forbidAnalysis("isApplicableByPsi") {', 'run {') + text = text.replace('forbidAnalysis("getApplicabilityRanges") {', 'run {') + text = text.replace('\n final override fun isElementApplicable(', '\n public final override fun isElementApplicable(') + applicableModCommandFile.text = text + log.info("Patched KotlinApplicableModCommandAction.kt: inlined forbidAnalysis, widened isElementApplicable") +} ]]> diff --git a/KotlinFormatter/pom.xml b/KotlinFormatter/pom.xml index 1fb0f873..6f399a52 100644 --- a/KotlinFormatter/pom.xml +++ b/KotlinFormatter/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.282-SNAPSHOT + 0.11.284-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-formatter diff --git a/KotlinHighlighting/pom.xml b/KotlinHighlighting/pom.xml index 12d4863e..e3b7b97c 100644 --- a/KotlinHighlighting/pom.xml +++ b/KotlinHighlighting/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.282-SNAPSHOT + 0.11.284-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-highlighting diff --git a/KotlinIcons/pom.xml b/KotlinIcons/pom.xml index f0c9d688..c25cecf3 100644 --- a/KotlinIcons/pom.xml +++ b/KotlinIcons/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.282-SNAPSHOT + 0.11.284-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-icons diff --git a/KotlinIntentionUtils/pom.xml b/KotlinIntentionUtils/pom.xml index 547aad73..be365f8a 100644 --- a/KotlinIntentionUtils/pom.xml +++ b/KotlinIntentionUtils/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.282-SNAPSHOT + 0.11.284-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-intention-utils diff --git a/KotlinRefactoring/pom.xml b/KotlinRefactoring/pom.xml index ee79aced..6faa1a81 100644 --- a/KotlinRefactoring/pom.xml +++ b/KotlinRefactoring/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.282-SNAPSHOT + 0.11.284-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-refactoring diff --git a/Nbm/pom.xml b/Nbm/pom.xml index e5975fd2..25b876a7 100644 --- a/Nbm/pom.xml +++ b/Nbm/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.282-SNAPSHOT + 0.11.284-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-nbm @@ -259,6 +259,10 @@ io.github.nbplugins netbeans-plugin-kotlin-refactoring + + io.github.nbplugins + netbeans-plugin-kotlin-code-insight-impl + com.jetbrains.intellij.platform analysis diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/KotlinHintsProvider.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/KotlinHintsProvider.kt index 4a3a2996..7633f84c 100644 --- a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/KotlinHintsProvider.kt +++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/KotlinHintsProvider.kt @@ -100,6 +100,7 @@ import io.github.nbplugins.kotlin.nbm.hints.intentions.KaChangeVisibilityToPubli import io.github.nbplugins.kotlin.nbm.hints.intentions.KaChangeVisibilityToPrivateIntention import io.github.nbplugins.kotlin.nbm.hints.intentions.KaChangeVisibilityToProtectedIntention import io.github.nbplugins.kotlin.nbm.hints.intentions.KaChangeVisibilityToInternalIntention +import io.github.nbplugins.kotlin.nbm.hints.intentions.KaStructuralConversionHints import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.hints.KotlinRuleContext import org.jetbrains.kotlin.hints.KotlinRule @@ -202,7 +203,9 @@ class KotlinHintsProvider : HintsProvider { KaChangeVisibilityToPublicIntention(doc, kaKtFile, psi), KaChangeVisibilityToPrivateIntention(doc, kaKtFile, psi), KaChangeVisibilityToProtectedIntention(doc, kaKtFile, psi), - KaChangeVisibilityToInternalIntention(doc, kaKtFile, psi) + KaChangeVisibilityToInternalIntention(doc, kaKtFile, psi), + // Group F8 — Structural conversions (compiled IDEA inspections/intentions) + *KaStructuralConversionHints.hints(doc, kaKtFile, psi).toTypedArray() ).filter { it.isApplicable(psi.textRange.startOffset) } } diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/fixes/KaModCommandFix.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/fixes/KaModCommandFix.kt index 0cafbc77..c204d613 100644 --- a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/fixes/KaModCommandFix.kt +++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/fixes/KaModCommandFix.kt @@ -4,11 +4,13 @@ package io.github.nbplugins.kotlin.nbm.hints.fixes import com.intellij.modcommand.ActionContext import com.intellij.modcommand.ModPsiUpdater +import com.intellij.modcommand.PsiBasedModCommandAction import com.intellij.modcommand.PsiUpdateModCommandAction import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import io.github.nbplugins.kotlin.nbm.diagnostics.KaDiagnosticError import io.github.nbplugins.kotlin.nbm.hints.atomicChange +import io.github.nbplugins.kotlin.nbm.hints.intentions.KaIdeaReflection import org.jetbrains.kotlin.hints.KotlinRule import org.jetbrains.kotlin.language.Priorities import org.jetbrains.kotlin.psi.KtFile @@ -74,34 +76,37 @@ class KaModCommandFix( TextRange.EMPTY_RANGE, element ) - syncMutation(kaKtFile, doc) { callProtectedInvoke(ctx, NoOpModPsiUpdater) } + syncMutation(kaKtFile, doc) { callInvoke(ideaFix, ctx, element, NoOpModPsiUpdater) } } - /** - * Calls `invoke(ActionContext, E, ModPsiUpdater)` on [ideaFix] via reflection. - * - * In era 253 the IntelliJ fixes extend [PsiUpdateModCommandAction] whose `invoke` is - * `protected abstract` — not directly callable from outside the hierarchy. Reflection - * is required. We walk the class hierarchy looking for the first non-bridge, non-synthetic - * `invoke` with three parameters; the concrete override in each fix subclass is found first. - */ - private fun callProtectedInvoke(ctx: ActionContext, updater: ModPsiUpdater) { - var cls: Class<*>? = ideaFix.javaClass - while (cls != null) { - val m = cls.declaredMethods.firstOrNull { m -> - m.name == "invoke" && m.parameterCount == 3 && !m.isSynthetic && !m.isBridge - } - if (m != null) { - m.isAccessible = true - m.invoke(ideaFix, ctx, element, updater) - return - } - cls = cls.superclass + companion object { + /** + * Invokes the `invoke(ActionContext, E, ModPsiUpdater)` method of [action] via reflection. + * + * In era 253 the IntelliJ fixes extend [PsiUpdateModCommandAction] whose `invoke` is + * `protected abstract` — not directly callable from outside the hierarchy, so + * [KaIdeaReflection] walks the class hierarchy for the first non-bridge, non-synthetic + * three-parameter `invoke`; the concrete override in each fix subclass is found first. + * + * The parameter is [PsiBasedModCommandAction] rather than [PsiUpdateModCommandAction] + * because the Kotlin plugin's own `KotlinPsiUpdateModCommandAction` — the base of the + * compiled IDEA intentions — derives directly from the former. + * + * @param action the action whose `invoke` should run + * @param ctx the action context to pass + * @param element the target PSI element + * @param updater the updater to pass + */ + @JvmStatic + fun callInvoke( + action: PsiBasedModCommandAction, + ctx: ActionContext, + element: E, + updater: ModPsiUpdater, + ) { + KaIdeaReflection.callOrFail(action, "invoke", 3, ctx, element, updater) } - error("Cannot find invoke(ActionContext, E, ModPsiUpdater) on ${ideaFix.javaClass}") - } - companion object { /** * Applies [mutate] as a PSI mutation and syncs the result to [doc]. * diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaInspectionHint.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaInspectionHint.kt new file mode 100644 index 00000000..ff00c181 --- /dev/null +++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaInspectionHint.kt @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright 2026 nbplugins contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *******************************************************************************/ +package io.github.nbplugins.kotlin.nbm.hints.intentions + +import com.intellij.modcommand.ModPsiUpdater +import com.intellij.openapi.project.Project +import io.github.nbplugins.kotlin.nbm.hints.KaApplicableIntention +import io.github.nbplugins.kotlin.nbm.hints.fixes.KaModCommandFix +import io.github.nbplugins.kotlin.nbm.hints.fixes.NoOpModPsiUpdater +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.getElementContext +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinApplicableInspectionBase +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinModCommandQuickFix +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtFile +import javax.swing.text.Document + +/** + * Drives an IDEA [KotlinApplicableInspectionBase] inspection compiled from + * `submodules/IntellijCommunity` as a NetBeans caret hint. + * + * IDEA inspections are normally run by the platform's inspection engine, which builds a + * `ProblemsHolder`, calls the inspection's visitor, and surfaces the resulting + * `ProblemDescriptor` and its quick fixes in the Alt+Enter menu. NetBeans has no such + * engine for Kotlin — hints are caret-driven — so this adapter runs the inspection's + * applicability pipeline directly, in the same order its own `visitTargetElement` does: + * + * 1. `isApplicableByPsi` — cheap PSI-only pre-filter. + * 2. `getApplicableRanges` — element-relative ranges; the caret must fall inside one. + * 3. `getElementContext` — the K2 Analysis API pass returning the inspection's context, + * or null when the inspection is not applicable. + * 4. `createQuickFix` — the fix to offer, reached by reflection because the inspection + * declares it `protected` (the same technique [KaModCommandFix] uses for `invoke`). + * + * Applying the fix calls `KotlinModCommandQuickFix.applyFix(Project, E, ModPsiUpdater)`, + * also `protected`, again by reflection. + * + * @param inspection the compiled IDEA inspection to adapt + * @param doc the Swing document for the file being edited + * @param kaKtFile K2-session-owned [KtFile] for this file + * @param psi PSI element at the caret position, belonging to [kaKtFile] + */ +class KaIdeaInspectionHint( + private val inspection: KotlinApplicableInspectionBase, + doc: Document, + kaKtFile: KtFile, + private val element: E, +) : KaApplicableIntention(doc, kaKtFile, element) { + + /** + * The fix resolved during the last [isApplicable] call. + * + * Resolution costs a K2 Analysis API pass, so it is cached rather than repeated in + * [implement]. NetBeans calls [isApplicable] before [implement] on the same instance. + */ + private var resolvedFix: KotlinModCommandQuickFix? = null + + /** + * Reports whether the IDEA inspection applies at [caretOffset]. + * + * The inspection's own K2 checks run inside [runCatching]: they are the only path in the + * hint provider that reaches a compiled IDEA inspection by reflection, so a failure here + * must not take down the whole caret popup. An inspection that cannot be evaluated is + * simply not offered. + */ + override fun isApplicable(caretOffset: Int): Boolean = runCatching { + if (!inspection.isApplicableByPsi(element)) return@runCatching false + + val relativeCaretOffset = caretOffset - element.textRange.startOffset + if (inspection.getApplicableRanges(element).none { it.containsOffset(relativeCaretOffset) }) { + return@runCatching false + } + + val fix = resolveQuickFix() ?: return@runCatching false + resolvedFix = fix + true + }.getOrDefault(false) + + override fun getDescription(): String = resolvedFix?.name ?: inspection.javaClass.simpleName + + override fun implement() { + val fix = resolvedFix ?: return + KaModCommandFix.syncMutation(kaKtFile, doc) { + @Suppress("UNCHECKED_CAST") + val updater = NoOpModPsiUpdater as ModPsiUpdater + KaIdeaReflection.call(fix, "applyFix", 3, element.project as Project, element, updater) + } + } + + /** + * Runs the inspection's context and quick-fix resolution. + * + * @return the resolved fix, or null when the inspection is not applicable here + */ + @OptIn(KaExperimentalApi::class) + private fun resolveQuickFix(): KotlinModCommandQuickFix? { + val context = inspection.getElementContext(element) ?: return null + @Suppress("UNCHECKED_CAST") + return KaIdeaReflection.call(inspection, "createQuickFix", 2, element, context) + as? KotlinModCommandQuickFix + } +} diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaIntentionHint.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaIntentionHint.kt new file mode 100644 index 00000000..bdd73c95 --- /dev/null +++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaIntentionHint.kt @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright 2026 nbplugins contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *******************************************************************************/ +package io.github.nbplugins.kotlin.nbm.hints.intentions + +import com.intellij.modcommand.ActionContext +import com.intellij.modcommand.Presentation +import com.intellij.openapi.util.TextRange +import io.github.nbplugins.kotlin.nbm.hints.KaApplicableIntention +import io.github.nbplugins.kotlin.nbm.hints.fixes.KaModCommandFix +import io.github.nbplugins.kotlin.nbm.hints.fixes.NoOpModPsiUpdater +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.intentions.KotlinApplicableModCommandAction +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtFile +import javax.swing.text.Document + +/** + * Drives an IDEA [KotlinApplicableModCommandAction] intention compiled from + * `submodules/IntellijCommunity` as a NetBeans caret hint. + * + * IDEA decides applicability in [KotlinApplicableModCommandAction.isElementApplicable], which + * needs an [ActionContext] carrying the absolute caret offset. This adapter builds that context + * and delegates, so the IDEA implementation — including its K2 Analysis API checks — runs + * unchanged. + * + * Applying the fix goes through [KaModCommandFix], which invokes the action's + * `invoke(ActionContext, E, ModPsiUpdater)` by reflection and syncs the mutated PSI back to + * the document. + * + * @param action the compiled IDEA intention to adapt + * @param doc the Swing document for the file being edited + * @param kaKtFile K2-session-owned [KtFile] for this file + * @param psi PSI element at the caret position, belonging to [kaKtFile] + */ +class KaIdeaIntentionHint( + private val action: KotlinApplicableModCommandAction, + doc: Document, + kaKtFile: KtFile, + private val element: E, +) : KaApplicableIntention(doc, kaKtFile, element) { + + /** + * Reports whether the IDEA action applies at [caretOffset]. + * + * The action's own K2 checks run inside [runCatching]: they are the only path in the hint + * provider that reaches a compiled IDEA action by reflection, so a failure here must not + * take down the whole caret popup. An action that cannot be evaluated is simply not offered. + */ + override fun isApplicable(caretOffset: Int): Boolean = runCatching { + action.isElementApplicable(element, actionContext(caretOffset)) + }.getOrDefault(false) + + /** + * Returns the action's user-visible name. + * + * [KotlinApplicableModCommandAction] builds it in `getPresentation`, which is `protected` + * and only reachable by reflection; when no presentation is produced the family name is + * the fallback, mirroring IDEA's own behaviour. + */ + override fun getDescription(): String { + val presentation = KaIdeaReflection.call( + action, "getPresentation", 2, actionContext(element.textRange.startOffset), element + ) as? Presentation + return presentation?.name() ?: action.familyName + } + + override fun implement() { + val ctx = actionContext(element.textRange.startOffset) + // KotlinPsiUpdateModCommandAction declares + // invoke(ActionContext, E, C, ModPsiUpdater) — four parameters, with the element + // context that isElementApplicable computed and discarded. It is resolved again here + // through the action's own protected getElementContext(ActionContext, E). + val elementContext = KaIdeaReflection.call(action, "getElementContext", 2, ctx, element) + KaModCommandFix.syncMutation(kaKtFile, doc) { + KaIdeaReflection.call(action, "invoke", 4, ctx, element, elementContext, NoOpModPsiUpdater) + } + } + + /** + * Builds the [ActionContext] IDEA expects for [caretOffset]. + * + * @param caretOffset absolute caret offset in the document + */ + private fun actionContext(caretOffset: Int) = ActionContext( + element.project, + kaKtFile, + caretOffset, + TextRange.EMPTY_RANGE, + element + ) +} diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflection.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflection.kt new file mode 100644 index 00000000..0b71d986 --- /dev/null +++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflection.kt @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright 2026 nbplugins contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *******************************************************************************/ +package io.github.nbplugins.kotlin.nbm.hints.intentions + +/** + * Reflection helper for reaching the members IDEA's inspection/intention API declares + * `protected`. + * + * The compiled IDEA classes are consumed from another module, so Kotlin's visibility rules + * hide their `protected` members — `getPresentation`, `createQuickFix`, `applyFix`. The + * NetBeans adapter must drive exactly those members to reuse the IDEA behaviour instead of + * reimplementing it. Looking the member up by name and arity across the class hierarchy + * also skips the compiler-generated bridge and synthetic methods that share the name. + */ +internal object KaIdeaReflection { + + /** + * Invokes the first declared method named [name] with [parameterCount] parameters found + * walking [target]'s class hierarchy from the most-derived class upwards. + * + * @param target receiver of the invocation + * @param name method name to look for + * @param parameterCount required number of parameters (bridges and synthetics excluded) + * @param args arguments to pass + * @return the method's return value, or null when no matching method exists + */ + fun call(target: Any, name: String, parameterCount: Int, vararg args: Any?): Any? = + find(target, name, parameterCount)?.let { method -> + method.isAccessible = true + method.invoke(target, *args) + } + + /** + * Invokes [name] like [call], but fails loudly when no matching method exists. + * + * Use this when a missing member means the adapter is broken rather than the action simply + * not applying. The return value is still nullable: a Kotlin `Unit` function compiles to a + * `void` method, whose [java.lang.reflect.Method.invoke] result is always null. + * + * @param target receiver of the invocation + * @param name method name to look for + * @param parameterCount required number of parameters + * @param args arguments to pass + * @return the method's return value + */ + fun callOrFail(target: Any, name: String, parameterCount: Int, vararg args: Any?): Any? { + val method = find(target, name, parameterCount) + ?: error("Cannot find $name/${parameterCount}args on ${target.javaClass}") + method.isAccessible = true + return method.invoke(target, *args) + } + + /** + * Finds the first declared method named [name] taking [parameterCount] parameters. + * + * @param target receiver whose class hierarchy is searched + * @param name method name to look for + * @param parameterCount required number of parameters + * @return the method, or null when the hierarchy declares none + */ + private fun find(target: Any, name: String, parameterCount: Int): java.lang.reflect.Method? { + var cls: Class<*>? = target.javaClass + while (cls != null) { + val method = cls.declaredMethods.firstOrNull { m -> + m.name == name && m.parameterCount == parameterCount && !m.isSynthetic && !m.isBridge + } + if (method != null) return method + cls = cls.superclass + } + return null + } +} diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaStructuralConversionHints.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaStructuralConversionHints.kt new file mode 100644 index 00000000..e627e42e --- /dev/null +++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaStructuralConversionHints.kt @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright 2026 nbplugins contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *******************************************************************************/ +package io.github.nbplugins.kotlin.nbm.hints.intentions + +import com.intellij.psi.PsiElement +import io.github.nbplugins.kotlin.nbm.hints.KaApplicableIntention +import org.jetbrains.kotlin.idea.codeInsight.inspections.shared.RemoveEmptyParenthesesFromLambdaCallInspection +import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.ReplaceContainsInspection +import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.ReplaceGetOrSetInspection +import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.expressions.ReplaceCallWithBinaryOperatorInspection +import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ReplaceCallWithUnaryOperatorIntention +import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ReplaceInvokeIntention +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtValueArgumentList +import javax.swing.text.Document + +/** + * Builds the F8 structural-conversion hints from the IDEA inspections and intentions + * compiled by the `KotlinCodeInsightImpl` module. + * + * Each IDEA action is typed on a specific PSI element, so the element the caret refers to + * must be looked up per conversion. These are the operator/function conversions: + * + * - `a.plus(b)` <-> `a + b` + * - `a.get(i)` / `a.set(i, v)` <-> `a[i]` / `a[i] = v` + * - `a.contains(b)` <-> `b in a` + * - `a.invoke(b)` <-> `a(b)` + * - `a.not()` <-> `!a` + * - `f() {}` <-> `f {}` + */ +object KaStructuralConversionHints { + + /** + * Returns the structural-conversion hints applicable at the element [psi]. + * + * @param doc the Swing document for the file being edited + * @param kaKtFile K2-session-owned [KtFile] for this file + * @param psi PSI element at the caret position, belonging to [kaKtFile] + * @return hints for every conversion whose target element encloses [psi] + */ + fun hints(doc: Document, kaKtFile: KtFile, psi: PsiElement): List = listOfNotNull( + psi.ancestorOfType()?.let { + KaIdeaIntentionHint(ReplaceInvokeIntention(), doc, kaKtFile, it) + }, + psi.ancestorOfType()?.let { + KaIdeaIntentionHint(ReplaceCallWithUnaryOperatorIntention(), doc, kaKtFile, it) + }, + psi.ancestorOfType()?.let { + KaIdeaInspectionHint(ReplaceCallWithBinaryOperatorInspection(), doc, kaKtFile, it) + }, + psi.ancestorOfType()?.let { + KaIdeaInspectionHint(ReplaceGetOrSetInspection(), doc, kaKtFile, it) + }, + psi.ancestorOfType()?.let { + KaIdeaInspectionHint(ReplaceContainsInspection(), doc, kaKtFile, it) + }, + // The caret may sit just after the `)` that closes the argument list, so the + // preceding leaf is searched too — matching IDEA, which anchors this inspection + // on the argument list itself. + (psi.ancestorOfType() ?: psi.previousLeafOfType())?.let { + KaIdeaInspectionHint(RemoveEmptyParenthesesFromLambdaCallInspection(), doc, kaKtFile, it) + }, + ) + + /** Walks up from [this] returning the first element of type [T], or null. */ + private inline fun PsiElement.ancestorOfType(): T? { + var current: PsiElement? = this + while (current != null) { + if (current is T) return current + current = current.parent + } + return null + } + + /** + * Walks up from the leaf preceding [this]. + * + * The `f() {}` -> `f {}` inspection is anchored on the [KtValueArgumentList], which the + * caret can sit just outside of. + */ + private fun PsiElement.previousLeafOfType(): KtValueArgumentList? { + val previous = com.intellij.psi.util.PsiTreeUtil.prevLeaf(this) ?: return null + return previous.ancestorOfType() + } +} diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflectionTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflectionTest.kt new file mode 100644 index 00000000..c836fd0f --- /dev/null +++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflectionTest.kt @@ -0,0 +1,126 @@ +/******************************************************************************* + * Copyright 2026 nbplugins contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *******************************************************************************/ +package io.github.nbplugins.kotlin.nbm.hints.intentions + +import org.netbeans.junit.NbTestCase + +/** + * Unit tests for [KaIdeaReflection]. + * + * The helper exists to reach `protected` members of the compiled IDEA inspection/intention + * classes, so the tests exercise the same class-hierarchy walk against a local hierarchy: + * a base declaration, a derived override, overloads distinguished by arity, and a + * compiler-generated bridge method that must be skipped. + */ +class KaIdeaReflectionTest : NbTestCase("KaIdeaReflectionTest") { + + /** A member declared only in the base class is found by walking up the hierarchy. */ + fun testCallFindsBaseClassMember() { + assertEquals("base", KaIdeaReflection.call(Derived(), "baseOnly", 0)) + } + + /** The most-derived override wins over the base declaration. */ + fun testCallPrefersMostDerivedOverride() { + assertEquals("derived", KaIdeaReflection.call(Derived(), "overridden", 0)) + } + + /** Overloads are selected by parameter count. */ + fun testCallSelectsByArity() { + assertEquals("one:7", KaIdeaReflection.call(Overloaded(), "describe", 1, 7)) + assertEquals("two:7:8", KaIdeaReflection.call(Overloaded(), "describe", 2, 7, 8)) + } + + /** Compiler-generated bridge methods are skipped, so the real override is invoked. */ + fun testCallSkipsBridgeMethod() { + assertEquals("boxed", KaIdeaReflection.call(StringBox(), "unwrap", 0)) + } + + /** A missing member returns null instead of throwing. */ + fun testCallReturnsNullWhenAbsent() { + assertNull(KaIdeaReflection.call(Derived(), "doesNotExist", 0)) + } + + /** A member whose parameter count does not match any overload returns null. */ + fun testCallReturnsNullOnArityMismatch() { + assertNull(KaIdeaReflection.call(Overloaded(), "describe", 3, 1, 2, 3)) + } + + /** A `protected` member is reachable, which is the whole point of the helper. */ + fun testCallReachesProtectedMember() { + assertEquals("protected", KaIdeaReflection.call(Derived(), "hidden", 0)) + } + + /** [KaIdeaReflection.callOrFail] returns the value of an existing member. */ + fun testCallOrFailReturnsValue() { + assertEquals("derived", KaIdeaReflection.callOrFail(Derived(), "overridden", 0)) + } + + /** [KaIdeaReflection.callOrFail] returns null for a `void` method, which is a success. */ + fun testCallOrFailReturnsNullForVoidMethod() { + assertNull(KaIdeaReflection.callOrFail(VoidMethod(), "run", 0)) + } + + /** [KaIdeaReflection.callOrFail] fails loudly when the member does not exist. */ + fun testCallOrFailThrowsWhenAbsent() { + try { + KaIdeaReflection.callOrFail(Derived(), "doesNotExist", 0) + fail("Expected an IllegalStateException for a missing member") + } catch (expected: IllegalStateException) { + assertTrue( + "Message should name the missing member", + expected.message!!.contains("doesNotExist") + ) + } + } + + /** Base class declaring the members the derived class partly overrides. */ + private open class Base { + protected fun hidden(): String = "protected" + + fun baseOnly(): String = "base" + + open fun overridden(): String = "base" + } + + /** Derived class overriding one member and inheriting the others. */ + private class Derived : Base() { + override fun overridden(): String = "derived" + } + + /** Class with same-named overloads of different arity. */ + private class Overloaded { + fun describe(a: Int): String = "one:$a" + + fun describe(a: Int, b: Int): String = "two:$a:$b" + } + + /** Class whose method returns `Unit`, compiled to a `void` method. */ + private class VoidMethod { + fun run() { + } + } + + /** Generic interface whose implementation gets a compiler-generated bridge method. */ + private interface Box { + fun unwrap(): T + } + + /** Implementation returning [String], producing a synthetic `unwrap(): Object` bridge. */ + private class StringBox : Box { + override fun unwrap(): String = "boxed" + } +} diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIntentionsTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIntentionsTest.kt index bf1545de..bc5f986a 100644 --- a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIntentionsTest.kt +++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIntentionsTest.kt @@ -16,7 +16,18 @@ *******************************************************************************/ package io.github.nbplugins.kotlin.nbm.hints.intentions +import com.intellij.psi.PsiElement import io.github.nbplugins.kotlin.nbm.hints.KaIntentionTestBase +import org.jetbrains.kotlin.idea.codeInsight.inspections.shared.RemoveEmptyParenthesesFromLambdaCallInspection +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinApplicableInspectionBase +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.intentions.KotlinApplicableModCommandAction +import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.ReplaceContainsInspection +import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.ReplaceGetOrSetInspection +import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.expressions.ReplaceCallWithBinaryOperatorInspection +import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ReplaceCallWithUnaryOperatorIntention +import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ReplaceInvokeIntention +import org.jetbrains.kotlin.psi.KtElement +import java.lang.reflect.ParameterizedType /** * Integration tests for K2 [io.github.nbplugins.kotlin.nbm.hints.KaApplicableIntention] subclasses. @@ -321,4 +332,142 @@ class KaIntentionsTest : KaIntentionTestBase("KaIntentions test", "intentions") fun testKaChangeVisibilityToInternal() = doTest("kaChangeVisibilityToInternal") { doc, kaKtFile, psi -> KaChangeVisibilityToInternalIntention(doc, kaKtFile, psi) } + + // ── F8 — Structural conversions (compiled IDEA sources) ───────────────── + // + // These adapt IDEA's own K2 inspections and intentions, compiled verbatim from + // submodules/IntellijCommunity by the KotlinCodeInsightImpl module. Unlike the + // hand-written intentions above, the element passed to the adapter must be the exact + // type the IDEA action targets, so it is found by walking up from the caret leaf. + + /** F8: `a.plus(b)` -> `a + b` (IDEA ReplaceCallWithBinaryOperatorInspection). */ + fun testKaReplaceCallWithBinaryOperator() = doTestInspection("kaReplaceCallWithBinaryOperator") { + ReplaceCallWithBinaryOperatorInspection() + } + + /** F8: `a.get(i)` -> `a[i]` (IDEA ReplaceGetOrSetInspection). */ + fun testKaReplaceGetOrSet() = doTestInspection("kaReplaceGetOrSet") { + ReplaceGetOrSetInspection() + } + + /** F8: `a.contains(b)` -> `b in a` (IDEA ReplaceContainsInspection). */ + fun testKaReplaceContains() = doTestInspection("kaReplaceContains") { + ReplaceContainsInspection() + } + + /** F8: `f() {}` -> `f {}` (IDEA RemoveEmptyParenthesesFromLambdaCallInspection). */ + fun testKaRemoveEmptyParenthesesFromLambdaCall() = + doTestInspection("kaRemoveEmptyParenthesesFromLambdaCall") { + RemoveEmptyParenthesesFromLambdaCallInspection() + } + + /** F8: `a.invoke(b)` -> `a(b)` (IDEA ReplaceInvokeIntention). */ + fun testKaReplaceInvoke() = doTestIntention("kaReplaceInvoke") { ReplaceInvokeIntention() } + + /** F8: `a.not()` -> `!a` (IDEA ReplaceCallWithUnaryOperatorIntention). */ + fun testKaReplaceCallWithUnaryOperator() = + doTestIntention("kaReplaceCallWithUnaryOperator") { ReplaceCallWithUnaryOperatorIntention() } + + /** + * Runs an IDEA [KotlinApplicableInspectionBase] test through [KaIdeaInspectionHint]. + * + * The caret leaf is walked up to the exact element type the inspection declares, because + * [KotlinApplicableInspectionBase] is typed on that element. + * + * @param fileName fixture name without extension + * @param factory creates the inspection under test + */ + private fun doTestInspection( + fileName: String, + factory: () -> KotlinApplicableInspectionBase, + ) = doTest(fileName) { doc, kaKtFile, psi -> + val inspection = factory() + @Suppress("UNCHECKED_CAST") + KaIdeaInspectionHint( + inspection, + doc, + kaKtFile, + psi.targetElement(factoryElementClass(inspection)) as E + ) + } + + /** + * Runs an IDEA [KotlinApplicableModCommandAction] test through [KaIdeaIntentionHint]. + * + * @param fileName fixture name without extension + * @param factory creates the intention under test + */ + private fun doTestIntention( + fileName: String, + factory: () -> KotlinApplicableModCommandAction, + ) = doTest(fileName) { doc, kaKtFile, psi -> + val intention = factory() + @Suppress("UNCHECKED_CAST") + KaIdeaIntentionHint( + intention, + doc, + kaKtFile, + psi.targetElement(factoryElementClass(intention)) as E + ) + } + + /** + * Returns the [KtElement] class an IDEA action targets. + * + * Both applicable-inspection and applicable-intention bases expose their element type + * through the generic supertype's type argument; that is read here so the test does not + * have to restate it per fixture. + */ + private fun factoryElementClass(action: Any): Class { + var cls: Class<*>? = action.javaClass + while (cls != null) { + val supertype = cls.genericSuperclass + if (supertype is ParameterizedType) { + val argument = supertype.actualTypeArguments.firstOrNull() + if (argument is Class<*> && KtElement::class.java.isAssignableFrom(argument)) { + @Suppress("UNCHECKED_CAST") + return argument as Class + } + } + cls = cls.superclass + } + error("Cannot determine target element class for ${action.javaClass}") + } + + /** Walks up from [this] until an element of [type] is found. */ + private fun PsiElement.ancestorOfType(type: Class<*>): KtElement { + var current: PsiElement? = this + while (current != null) { + if (type.isInstance(current)) { + return current as KtElement + } + current = current.parent + } + error("No ${type.simpleName} ancestor of ${text}") + } + + /** + * Finds the element of [type] the caret refers to, searching both up and sideways. + * + * Most fixtures put the caret inside the target element, so an ancestor walk suffices. + * The `removeEmptyParenthesesFromLambdaCall` fixture is the exception: its caret sits + * just after `)` — outside the [org.jetbrains.kotlin.psi.KtValueArgumentList] the + * inspection targets — so the preceding leaf is walked up to instead. + */ + private fun PsiElement.targetElement(type: Class<*>): KtElement { + ancestorOrNull(type)?.let { return it } + val previous = com.intellij.psi.util.PsiTreeUtil.prevLeaf(this) + return previous?.ancestorOrNull(type) + ?: error("No ${type.simpleName} near ${text}") + } + + /** Walks up from [this] returning the first element of [type], or null. */ + private fun PsiElement.ancestorOrNull(type: Class<*>): KtElement? { + var current: PsiElement? = this + while (current != null) { + if (type.isInstance(current)) return current as KtElement + current = current.parent + } + return null + } } diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/RefactoringCoverageBaselineTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/RefactoringCoverageBaselineTest.kt index 5a25cf63..57cb269a 100644 --- a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/RefactoringCoverageBaselineTest.kt +++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/RefactoringCoverageBaselineTest.kt @@ -39,7 +39,7 @@ class RefactoringCoverageBaselineTest : NbTestCase("RefactoringCoverageBaselineT val records = Files.readAllLines(matrix) .mapNotNull(::parseRecord) assertEquals("Every coverage ID must occur exactly once", records.size, records.map { it.id }.toSet().size) - assertEquals("F0 baseline row count", 24, records.size) + assertEquals("F0 baseline row count", 30, records.size) records.forEach { record -> assertTrue("Unknown status for ${record.id}: ${record.status}", record.status in STATUSES) @@ -54,9 +54,9 @@ class RefactoringCoverageBaselineTest : NbTestCase("RefactoringCoverageBaselineT } } - val totals = "**Baseline counts:** 0 complete, 24 partial, 0 absent, 24 total." + val totals = "**Baseline counts:** 0 complete, 30 partial, 0 absent, 30 total." assertTrue("Visible baseline totals must match the records", Files.readString(matrix).contains(totals)) - assertEquals(24, records.count { it.status == "partial" }) + assertEquals(30, records.count { it.status == "partial" }) assertEquals(0, records.count { it.status == "absent" }) } diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.after b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.after new file mode 100644 index 00000000..733cfee2 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.after @@ -0,0 +1,5 @@ +package intentions + +fun kaRemoveEmptyParenthesesFromLambdaCall() { + listOf(1).forEach { } +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.caret new file mode 100644 index 00000000..8b16f25e --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.caret @@ -0,0 +1,5 @@ +package intentions + +fun kaRemoveEmptyParenthesesFromLambdaCall() { + listOf(1).forEach() { } +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.kt new file mode 100644 index 00000000..626ffb3e --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.kt @@ -0,0 +1,5 @@ +package intentions + +fun kaRemoveEmptyParenthesesFromLambdaCall() { + listOf(1).forEach() { } +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.after b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.after new file mode 100644 index 00000000..b570d18e --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.after @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceCallWithBinaryOperator(): Int { + class Test + operator fun Test.plus(a: Int): Test = Test() + val test = Test() + return test + 1 +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.caret new file mode 100644 index 00000000..a714277a --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.caret @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceCallWithBinaryOperator(): Int { + class Test + operator fun Test.plus(a: Int): Test = Test() + val test = Test() + return test.plus(1) +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.kt new file mode 100644 index 00000000..8a91f238 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.kt @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceCallWithBinaryOperator(): Int { + class Test + operator fun Test.plus(a: Int): Test = Test() + val test = Test() + return test.plus(1) +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.after b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.after new file mode 100644 index 00000000..4467df26 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.after @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceCallWithUnaryOperator() { + class Test + operator fun Test.not(): Test = Test() + val test = Test() + !test +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.caret new file mode 100644 index 00000000..89db21d6 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.caret @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceCallWithUnaryOperator() { + class Test + operator fun Test.not(): Test = Test() + val test = Test() + test.not() +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.kt new file mode 100644 index 00000000..c14563a3 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.kt @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceCallWithUnaryOperator() { + class Test + operator fun Test.not(): Test = Test() + val test = Test() + test.not() +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.after b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.after new file mode 100644 index 00000000..43c5046b --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.after @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceContains(): Boolean { + class Test + operator fun Test.contains(a: Int): Boolean = true + val test = Test() + return 1 in test +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.caret new file mode 100644 index 00000000..37dbc556 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.caret @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceContains(): Boolean { + class Test + operator fun Test.contains(a: Int): Boolean = true + val test = Test() + return test.contains(1) +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.kt new file mode 100644 index 00000000..8d000edd --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.kt @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceContains(): Boolean { + class Test + operator fun Test.contains(a: Int): Boolean = true + val test = Test() + return test.contains(1) +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.after b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.after new file mode 100644 index 00000000..043086c2 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.after @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceGetOrSet(): Int { + class Test + operator fun Test.get(i: Int): Int = 0 + val test = Test() + return test[0] +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.caret new file mode 100644 index 00000000..cbce7b33 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.caret @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceGetOrSet(): Int { + class Test + operator fun Test.get(i: Int): Int = 0 + val test = Test() + return test.get(0) +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.kt new file mode 100644 index 00000000..b79513ee --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.kt @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceGetOrSet(): Int { + class Test + operator fun Test.get(i: Int): Int = 0 + val test = Test() + return test.get(0) +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.after b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.after new file mode 100644 index 00000000..c3f6ce21 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.after @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceInvoke() { + class Test + operator fun Test.invoke() {} + val test = Test() + test() +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.caret new file mode 100644 index 00000000..9d9d765a --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.caret @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceInvoke() { + class Test + operator fun Test.invoke() {} + val test = Test() + test.invoke() +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.kt new file mode 100644 index 00000000..d1f1feb7 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.kt @@ -0,0 +1,8 @@ +package intentions + +fun kaReplaceInvoke() { + class Test + operator fun Test.invoke() {} + val test = Test() + test.invoke() +} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.after b/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.after deleted file mode 100644 index 3f7e9bd5..00000000 --- a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.after +++ /dev/null @@ -1,4 +0,0 @@ -package intentions - -fun removeEmptyParenthesesFromLambdaCall(strs: Array) = strs.map {} - diff --git a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.caret b/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.caret deleted file mode 100644 index 26c907b2..00000000 --- a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.caret +++ /dev/null @@ -1,3 +0,0 @@ -package intentions - -fun removeEmptyParenthesesFromLambdaCall(strs: Array) = strs.map() {} diff --git a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.kt b/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.kt deleted file mode 100644 index a4d28441..00000000 --- a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.kt +++ /dev/null @@ -1,3 +0,0 @@ -package intentions - -fun removeEmptyParenthesesFromLambdaCall(strs: Array) = strs.map() {} diff --git a/docs/development-plan.md b/docs/development-plan.md index e96c70dd..80d4c455 100644 --- a/docs/development-plan.md +++ b/docs/development-plan.md @@ -1056,6 +1056,29 @@ Priority order: E1 → E2 → E3 → E4 → E5 → E6 → E7 → E8 → E9 → E - Supports immediately invoked Kotlin lambdas and anonymous functions. Non-invoked functions, Java/non-code/index-only references, and wider IDEA invocation/project-index cases remain unavailable. + - [x] **F8** — Structural conversions (operator/function conversions) + - Compiles the six IDEA K2 sources for the operator/function conversions into the new + `KotlinCodeInsightImpl` reactor module: `ReplaceCallWithBinaryOperatorInspection`, + `ReplaceGetOrSetInspection`, `ReplaceContainsInspection` (inspections-k2), + `ReplaceInvokeIntention`, `ReplaceCallWithUnaryOperatorIntention` (intentions-k2), and + `RemoveEmptyParenthesesFromLambdaCallInspection` (inspections-shared). The sources are copied + from `submodules/IntellijCommunity` by `maven-resources-plugin` and compiled unchanged; the + only patch is widening the three `internal class` inspections so the NetBeans layer can + construct them. + - `KotlinFixesImpl` was extended with the remaining `code-insight/api` applicable framework + (`KotlinApplicableInspectionBase`, `KotlinModCommandQuickFix`, `KotlinApplicableModCommandAction`, + `ApplicabilityRange`, `ApplicableRangesProvider`, `ContextProvider`) so those sources have + something to compile against. + - `KaIdeaIntentionHint` and `KaIdeaInspectionHint` drive the compiled actions by reflection + (their `getElementContext`/`createQuickFix`/`invoke`/`applyFix` are `protected`), reusing + `KaModCommandFix`'s `NoOpModPsiUpdater` and PSI→document sync. `KaStructuralConversionHints` + attaches each action to the PSI element the caret refers to, and + `KotlinHintsProvider.listOfK2Intentions` registers them alongside the hand-written hints. + - Covers `a.plus(b)` → `a + b`, `a.get(i)`/`a.set(i, v)` → `a[i]`/`a[i] = v`, `a.contains(b)` → + `b in a`, `a.invoke(b)` → `a(b)`, `a.not()`/`a.unaryPlus()` → `!a`/`+a`, and `f() {}` → `f {}`. + The reverse directions (`KaOperatorToFunctionIntention`, `KaConvertLambdaToReferenceIntention`) + stay hand-written: their IDEA counterparts drag in a much larger slice of `code-insight`. + - **E10** — J2K (Java→Kotlin): reimplement using `j2k/new` from `submodules/IntellijCommunity` or binary artifact once published; wire up stubbed `Java2KotlinConverter` (stubbed since D2) and re-enable `J2KTest` diff --git a/docs/refactoring-coverage.md b/docs/refactoring-coverage.md index 7718348e..ecc6073d 100644 --- a/docs/refactoring-coverage.md +++ b/docs/refactoring-coverage.md @@ -32,6 +32,12 @@ netbeans=; test=; milestone= + + + + + + ## Purpose and scope @@ -48,7 +54,7 @@ The corresponding record comments above are deliberately machine-readable. `Refa | **Absent** | IDEA exposes the Kotlin refactoring family but the NetBeans plugin has no corresponding command. | | **Not applicable** | IDEA-only integration which is not a Kotlin refactoring target for this plugin. No such rows are currently tracked. | -**Baseline counts:** 0 complete, 24 partial, 0 absent, 24 total. +**Baseline counts:** 0 complete, 30 partial, 0 absent, 30 total. ## Current NetBeans refactorings @@ -91,7 +97,31 @@ The corresponding record comments above are deliberately machine-readable. `Refa | **F5** | Inline Type Alias and Inline Anonymous Function. | | **F6** | Advanced Extract/Introduce and Extract Super parity. | | **F7** | Full Rename/Safe Delete filesystem, automatic-renamer and Java-bridge coverage. | -| **F8** | Structural conversions such as lambda/reference and operator/function conversions. | +| **F8** | Structural conversions such as lambda/reference and operator/function conversions. Delivered by compiling the IDEA K2 inspections/intentions for the operator/function conversions into the `KotlinCodeInsightImpl` reactor module and driving them from the caret hint provider; see *Structural conversions* below. | + +## Structural conversions (F8) + +IDEA implements the operator/function conversions as K2 **inspections** and **intentions**. The +`KotlinCodeInsightImpl` reactor module copies their sources out of `submodules/IntellijCommunity` +via `maven-resources-plugin` and compiles them unchanged (only `internal` → public visibility is +patched, so the NetBeans layer can construct them). `KaIdeaIntentionHint` / `KaIdeaInspectionHint` +in `Nbm` drive them through reflection, reusing the `KaModCommandFix` PSI→document sync, and +`KaStructuralConversionHints` attaches each action to the PSI element the caret refers to. + +| Conversion | IDEA K2 source | NetBeans adapter | Test | +|---|---|---|---| +| `a.plus(b)` → `a + b` | `code-insight/inspections-k2/.../expressions/ReplaceCallWithBinaryOperatorInspection.kt` | `KaIdeaInspectionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` | +| `a.get(i)` / `a.set(i, v)` → `a[i]` / `a[i] = v` | `code-insight/inspections-k2/.../ReplaceGetOrSetInspection.kt` | `KaIdeaInspectionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` | +| `a.contains(b)` → `b in a` | `code-insight/inspections-k2/.../ReplaceContainsInspection.kt` | `KaIdeaInspectionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` | +| `a.invoke(b)` → `a(b)` | `code-insight/intentions-k2/.../ReplaceInvokeIntention.kt` | `KaIdeaIntentionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` | +| `a.not()` / `a.unaryPlus()` → `!a` / `+a` | `code-insight/intentions-k2/.../ReplaceCallWithUnaryOperatorIntention.kt` | `KaIdeaIntentionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` | +| `f() {}` → `f {}` | `code-insight/inspections-shared/.../RemoveEmptyParenthesesFromLambdaCallInspection.kt` | `KaIdeaInspectionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` | + +The reverse directions (`a + b` → `a.plus(b)` as `KaOperatorToFunctionIntention`, and +lambda → callable reference as `KaConvertLambdaToReferenceIntention`) were already hand-written +before F8 and are **kept**: the IDEA counterparts (`OperatorToFunctionIntention`, +`ConvertLambdaToReferenceIntention`) pull in a much larger slice of `code-insight` than these six +conversions need, so replacing them is out of F8 scope. ## Representative compatibility baseline @@ -111,5 +141,6 @@ The existing NetBeans tests are the initial executable sample set. They intentio | Push Members Down into direct subclasses | `KaPushMembersDownComputerTest` | Build-wide Kotlin hierarchy baseline; advanced conflict scan remains incomplete | | Standalone K2 override/super-method bridge | `KotlinStandaloneHierarchySearchTest` | Build-wide Kotlin hierarchy baseline; Java and full IDEA-index coverage remain incomplete | | Introduce Type Alias on generic type text | `KaIntroduceTypeAliasTest` | Documented deviation: no generic type-parameter extraction | +| Operator/function structural conversions driven from a caret hint | `KaIntentionsTest` | Exact parity baseline for the six IDEA K2 conversions; the reverse `a + b` → `a.plus(b)` and lambda → reference directions remain hand-written | When a future port changes a row from Partial to Complete, it must add or strengthen the relevant compatibility fixture before changing this matrix status. diff --git a/pom.xml b/pom.xml index 10c838a2..159d8691 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.github.nbplugins netbeans-plugin-kotlin - 0.11.282-SNAPSHOT + 0.11.284-SNAPSHOT pom Netbeans Plugin Kotlin @@ -36,6 +36,7 @@ KotlinFixesImpl KotlinIcons KotlinRefactoring + KotlinCodeInsightImpl Nbm @@ -221,6 +222,11 @@ netbeans-plugin-kotlin-refactoring ${project.version} + + io.github.nbplugins + netbeans-plugin-kotlin-code-insight-impl + ${project.version} +