diff --git a/plugins/devkit/devkit-core/resources/inspectionDescriptions/PathAnnotationInspection.html b/plugins/devkit/devkit-core/resources/inspectionDescriptions/PathAnnotationInspection.html
new file mode 100644
index 000000000000..4122a4e47084
--- /dev/null
+++ b/plugins/devkit/devkit-core/resources/inspectionDescriptions/PathAnnotationInspection.html
@@ -0,0 +1,28 @@
+
+
+Reports incorrect usage of path annotations.
+
+
+ This inspection detects cases where a string annotated with one path annotation is used in a context that expects a string with a different path annotation.
+ It helps ensure proper usage of @MultiRoutingFileSystemPath and @NativePath annotations.
+
+
+The inspection highlights the following issues:
+
+ - When a string annotated with
@NativePath is used in a Path constructor or factory method
+ - When a string annotated with
@NativePath is passed to a method parameter annotated with @MultiRoutingFileSystemPath
+ - When a string annotated with
@MultiRoutingFileSystemPath is passed to a method parameter annotated with @NativePath
+ - When a string literal is used in a context that expects either annotation
+
+
+
+ Quick fixes are provided to add the appropriate annotation where needed.
+
+
+
+ This inspection helps prevent runtime errors that can occur when paths with different formats are used incorrectly.
+ The @MultiRoutingFileSystemPath annotation is used for paths that should work across different file systems,
+ while the @NativePath annotation is used for paths that are specific to the native file system.
+
+
+
diff --git a/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml b/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml
index a6eaee6f34a1..061e79b1ec9d 100644
--- a/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml
+++ b/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml
@@ -499,6 +499,13 @@
level="ERROR"
implementationClass="org.jetbrains.idea.devkit.inspections.PotentialDeadlockInServiceInitializationInspection"/>
+
+
()
+ val expectedInfo = getExpectedPathAnnotationInfo(expression, nonAnnotatedTargets)
+
+ if (expectedInfo is PathAnnotationInfo.MultiRouting) {
+ // Check if the string literal is used in a context that expects @MultiRoutingFileSystemPath
+ val fixes = mutableListOf()
+ for (target in nonAnnotatedTargets) {
+ if (target is PsiModifierListOwner) {
+ fixes.add(AddMultiRoutingAnnotationFix(target))
+ }
+ }
+ if (fixes.isNotEmpty()) {
+ holder.registerProblem(
+ sourcePsi,
+ DevKitBundle.message("inspections.message.multiroutingfilesystempath.expected"),
+ *fixes.toTypedArray()
+ )
+ }
+ }
+ else if (expectedInfo is PathAnnotationInfo.Native) {
+ // Check if the string literal is used in a context that expects @NativePath
+ val fixes = mutableListOf()
+ for (target in nonAnnotatedTargets) {
+ if (target is PsiModifierListOwner) {
+ fixes.add(AddNativePathAnnotationFix(target))
+ }
+ }
+ if (fixes.isNotEmpty()) {
+ holder.registerProblem(
+ sourcePsi,
+ DevKitBundle.message("inspections.message.nativepath.expected"),
+ *fixes.toTypedArray()
+ )
+ }
+ }
+ }
+
+ private fun getExpectedPathAnnotationInfo(
+ expression: UExpression,
+ nonAnnotatedTargets: MutableSet,
+ ): PathAnnotationInfo {
+ // Check if the expression is passed to a method that expects a specific path annotation
+ val parent = expression.uastParent
+ if (parent is UCallExpression) {
+ val method = parent.resolve() ?: return PathAnnotationInfo.Unspecified(null)
+ val index = parent.valueArguments.indexOf(expression)
+ if (index >= 0) {
+ val parameter = getParameterForArgument(method, index) ?: return PathAnnotationInfo.Unspecified(null)
+ val info = PathAnnotationInfo.forModifierListOwner(parameter)
+ if (info !is PathAnnotationInfo.Unspecified) {
+ return info
+ }
+ nonAnnotatedTargets.add(parameter)
+ }
+ }
+
+ // Check if the expression is assigned to a variable with a specific path annotation
+ if (parent is UVariable) {
+ val javaPsi = parent.javaPsi
+ if (javaPsi is PsiModifierListOwner) {
+ val info = PathAnnotationInfo.forModifierListOwner(javaPsi)
+ if (info !is PathAnnotationInfo.Unspecified) {
+ return info
+ }
+ nonAnnotatedTargets.add(javaPsi)
+ }
+ }
+
+ // Check if the expression is passed to a Path constructor or factory method
+ if (isPassedToMultiRoutingMethod(expression, nonAnnotatedTargets)) {
+ return PathAnnotationInfo.MultiRouting()
+ }
+
+ // Check if the expression is passed to a method that expects a native path
+ if (isPassedToNativePathMethod(expression, nonAnnotatedTargets)) {
+ return PathAnnotationInfo.Native()
+ }
+
+ return PathAnnotationInfo.Unspecified(null)
+ }
+
+ private fun isPathConstructorOrFactory(method: PsiElement): Boolean {
+ // Check if the method is a Path constructor or factory method like Path.of()
+ if (method is PsiModifierListOwner) {
+ val containingClass = (method as? com.intellij.psi.PsiMember)?.containingClass
+ if (containingClass != null) {
+ val qualifiedName = containingClass.qualifiedName
+ return qualifiedName == "java.nio.file.Path" || qualifiedName == "java.nio.file.Paths"
+ }
+ }
+ return false
+ }
+
+ private fun isPassedToMultiRoutingMethod(
+ expression: UExpression,
+ nonAnnotatedTargets: MutableSet,
+ ): Boolean {
+ // Check if the expression is passed to a Path constructor or factory method
+ val parent = expression.uastParent
+ if (parent is UCallExpression) {
+ val method = parent.resolve() ?: return false
+ if (isPathConstructorOrFactory(method)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun isPassedToNativePathMethod(
+ expression: UExpression,
+ nonAnnotatedTargets: MutableSet,
+ ): Boolean {
+ // Check if the expression is passed to a method that expects a native path
+ // This would be specific to your codebase, but could include methods like:
+ // - Docker container path methods
+ // - WSL path methods
+ // For now, we'll just return false as a placeholder
+ return false
+ }
+
+ private fun getParameterForArgument(method: PsiElement, index: Int): PsiModifierListOwner? {
+ if (method is com.intellij.psi.PsiMethod) {
+ val parameters = method.parameterList.parameters
+ if (index < parameters.size) {
+ return parameters[index]
+ }
+ }
+ return null
+ }
+ }
+
+ /**
+ * Contains information about path annotation status.
+ */
+ sealed class PathAnnotationInfo {
+ abstract fun getPathAnnotationStatus(): ThreeState
+
+ class MultiRouting(private val annotationCandidate: PsiModifierListOwner? = null) : PathAnnotationInfo() {
+ override fun getPathAnnotationStatus(): ThreeState = ThreeState.YES
+
+ fun getAnnotationCandidate(): PsiModifierListOwner? = annotationCandidate
+ }
+
+ class Native(private val annotationCandidate: PsiModifierListOwner? = null) : PathAnnotationInfo() {
+ override fun getPathAnnotationStatus(): ThreeState = ThreeState.YES
+
+ fun getAnnotationCandidate(): PsiModifierListOwner? = annotationCandidate
+ }
+
+ class Unspecified(private val annotationCandidate: PsiModifierListOwner?) : PathAnnotationInfo() {
+ override fun getPathAnnotationStatus(): ThreeState = ThreeState.UNSURE
+ fun getAnnotationCandidate(): PsiModifierListOwner? = annotationCandidate
+ }
+
+ companion object {
+ fun forExpression(expression: UExpression): PathAnnotationInfo {
+ // Check if the expression has a path annotation
+ val sourcePsi = expression.sourcePsi
+ if (sourcePsi != null && sourcePsi is PsiModifierListOwner) {
+ return forModifierListOwner(sourcePsi)
+ }
+
+ // Check if the expression is a reference to a variable with a path annotation
+ if (expression is UReferenceExpression) {
+ val resolved = expression.resolve()
+ if (resolved is PsiModifierListOwner) {
+ return forModifierListOwner(resolved)
+ }
+ }
+
+ return Unspecified(null)
+ }
+
+ fun forModifierListOwner(owner: PsiModifierListOwner): PathAnnotationInfo {
+ // Check if the owner has a path annotation
+ if (AnnotationUtil.isAnnotated(owner, MultiRoutingFileSystemPath::class.java.name, AnnotationUtil.CHECK_TYPE)) {
+ return MultiRouting(owner)
+ }
+ if (AnnotationUtil.isAnnotated(owner, NativePath::class.java.name, AnnotationUtil.CHECK_TYPE)) {
+ return Native(owner)
+ }
+ return Unspecified(owner)
+ }
+ }
+ }
+
+ /**
+ * Quick fix to add @MultiRoutingFileSystemPath annotation.
+ */
+ private class AddMultiRoutingAnnotationFix(private val target: PsiModifierListOwner?) : LocalQuickFix {
+ override fun getFamilyName(): String = DevKitBundle.message("inspections.intention.family.name.add.multiroutingfilesystempath.annotation")
+
+ override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
+ if (target != null) {
+ val annotationOwner = target.modifierList
+ if (annotationOwner != null) {
+ AddAnnotationPsiFix.addPhysicalAnnotationIfAbsent(
+ MultiRoutingFileSystemPath::class.java.name,
+ emptyArray(),
+ annotationOwner
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Quick fix to add @NativePath annotation.
+ */
+ private class AddNativePathAnnotationFix(private val target: PsiModifierListOwner?) : LocalQuickFix {
+ override fun getFamilyName(): String = DevKitBundle.message("inspections.intention.family.name.add.nativepath.annotation")
+
+ override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
+ if (target != null) {
+ val annotationOwner = target.modifierList
+ if (annotationOwner != null) {
+ AddAnnotationPsiFix.addPhysicalAnnotationIfAbsent(
+ NativePath::class.java.name,
+ emptyArray(),
+ annotationOwner
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/PathAnnotationInspectionTestBase.kt b/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/PathAnnotationInspectionTestBase.kt
new file mode 100644
index 000000000000..ae5c4e2f9d2d
--- /dev/null
+++ b/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/PathAnnotationInspectionTestBase.kt
@@ -0,0 +1,94 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.idea.devkit.inspections
+
+import com.intellij.pom.java.LanguageLevel
+import com.intellij.testFramework.IdeaTestUtil
+import com.intellij.testFramework.IndexingTestUtil
+import com.intellij.testFramework.LightProjectDescriptor
+import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
+import org.intellij.lang.annotations.Language
+import org.jetbrains.idea.devkit.inspections.path.PathAnnotationInspection
+
+abstract class PathAnnotationInspectionTestBase : LightJavaCodeInsightFixtureTestCase() {
+
+ override fun getProjectDescriptor(): LightProjectDescriptor = JAVA_17
+
+ protected abstract fun getFileExtension(): String
+
+ override fun setUp() {
+ super.setUp()
+ addPlatformClasses()
+ IdeaTestUtil.setProjectLanguageLevel(project, LanguageLevel.JDK_17)
+ IndexingTestUtil.waitUntilIndexesAreReady(project)
+ myFixture.enableInspections(PathAnnotationInspection())
+ }
+
+ private fun addPlatformClasses() {
+ // Add the path annotations
+ myFixture.addClass(
+ """
+ package com.intellij.platform.eel.annotations;
+
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.RetentionPolicy;
+ import java.lang.annotation.Target;
+ import static java.lang.annotation.ElementType.*;
+
+ /**
+ * This annotation should be applied to strings that could be directly used to construct java.nio.file.Path instances.
+ * These strings are either local to the IDE process or have prefix pointing to the specific environment.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({FIELD, LOCAL_VARIABLE, PARAMETER, METHOD, TYPE_USE})
+ public @interface MultiRoutingFileSystemPath {}
+ """.trimIndent()
+ )
+
+ myFixture.addClass(
+ """
+ package com.intellij.platform.eel.annotations;
+
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.RetentionPolicy;
+ import java.lang.annotation.Target;
+ import static java.lang.annotation.ElementType.*;
+
+ /**
+ * This is the path within the specific environment.
+ * For example, for a path in WSL it would be a Unix path within the WSL machine,
+ * and for a path in a Docker container it would be a path within this Docker container.
+ *
+ * It should not be directly used in the java.nio.file.Path ctor and auxiliary methods like java.nio.file.Path.of.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({FIELD, LOCAL_VARIABLE, PARAMETER, METHOD, TYPE_USE})
+ public @interface NativePath {}
+ """.trimIndent()
+ )
+
+ // Add the NativeContext annotation
+ myFixture.addClass(
+ """
+ package com.intellij.platform.eel.annotations;
+
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.RetentionPolicy;
+ import java.lang.annotation.Target;
+ import static java.lang.annotation.ElementType.*;
+
+ /**
+ * @see NativePath
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({FIELD, LOCAL_VARIABLE, PARAMETER, METHOD, TYPE_USE})
+ public @interface NativeContext {}
+ """.trimIndent()
+ )
+ }
+
+ protected open fun doTest(@Language("JAVA") code: String) {
+ val filePath = getTestName(false) + '.' + getFileExtension()
+ myFixture.configureByText(filePath, code.trimIndent())
+ myFixture.testHighlighting(filePath)
+ }
+}
diff --git a/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/path/PathAnnotationInspectionJavaTest.kt b/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/path/PathAnnotationInspectionJavaTest.kt
new file mode 100644
index 000000000000..7faa5666f3db
--- /dev/null
+++ b/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/path/PathAnnotationInspectionJavaTest.kt
@@ -0,0 +1,52 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.idea.devkit.inspections.path
+
+import org.jetbrains.idea.devkit.inspections.PathAnnotationInspectionTestBase
+
+class PathAnnotationInspectionJavaTest : PathAnnotationInspectionTestBase() {
+ override fun getFileExtension(): String = "java"
+
+ fun testNativePathInPathOf() {
+ doTest("""
+ import com.intellij.platform.eel.annotations.NativePath;
+ import java.nio.file.Path;
+
+ public class NativePathInPathOf {
+ public void testMethod() {
+ @NativePath String nativePath = "/usr/local/bin";
+ // This should be highlighted as an error because @NativePath strings should not be used directly in Path.of()
+ Path path = Path.of(nativePath);
+ }
+ }
+ """.trimIndent())
+ }
+
+ fun testMultiRoutingPathInNativeContext() {
+ doTest("""
+ import com.intellij.platform.eel.annotations.MultiRoutingFileSystemPath;
+ import com.intellij.platform.eel.annotations.NativePath;
+ import com.intellij.platform.eel.annotations.NativeContext;
+
+ public class MultiRoutingPathInNativeContext {
+ public void testMethod() {
+ @MultiRoutingFileSystemPath String multiRoutingPath = "/home/user/documents";
+
+ // This method expects a @NativePath string
+ processNativePath(multiRoutingPath);
+ }
+
+ public void processNativePath(@NativePath String path) {
+ // Process the native path
+ System.out.println("Processing native path: " + path);
+ }
+
+ @NativeContext
+ public void nativeContextMethod() {
+ @MultiRoutingFileSystemPath String multiRoutingPath = "/home/user/documents";
+ // Using a @MultiRoutingFileSystemPath string in a @NativeContext method
+ String processedPath = multiRoutingPath + "/file.txt";
+ }
+ }
+ """.trimIndent())
+ }
+}
\ No newline at end of file