diff --git a/src/main/kotlin/insyncwithfoo/ryecharm/common/inspection/DependencyGroupNameInspection.kt b/src/main/kotlin/insyncwithfoo/ryecharm/common/inspection/DependencyGroupNameInspection.kt index 876d73b..6873622 100644 --- a/src/main/kotlin/insyncwithfoo/ryecharm/common/inspection/DependencyGroupNameInspection.kt +++ b/src/main/kotlin/insyncwithfoo/ryecharm/common/inspection/DependencyGroupNameInspection.kt @@ -1,11 +1,10 @@ package insyncwithfoo.ryecharm.common.inspection import com.intellij.codeInspection.LocalInspectionTool -import com.intellij.codeInspection.LocalInspectionToolSession import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.project.DumbAware -import com.intellij.openapi.util.Key +import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import insyncwithfoo.ryecharm.absoluteName import insyncwithfoo.ryecharm.isPyprojectToml @@ -14,7 +13,6 @@ import insyncwithfoo.ryecharm.keyValuePair import insyncwithfoo.ryecharm.message import insyncwithfoo.ryecharm.stringContent import org.toml.lang.psi.TomlArray -import org.toml.lang.psi.TomlFile import org.toml.lang.psi.TomlInlineTable import org.toml.lang.psi.TomlLiteral import org.toml.lang.psi.TomlTable @@ -22,29 +20,32 @@ import org.toml.lang.psi.TomlVisitor import org.toml.lang.psi.ext.name -private class DependencyGroupNameVisitor( - private val holder: ProblemsHolder, - private val session: LocalInspectionToolSession -) : TomlVisitor() { - - private var knownGroupNames: List? - get() = session.getUserData(KEY) - set(value) = session.putUserData(KEY, value) - - override fun visitTable(element: TomlTable) { - registerKnownGroupNames(element) - } - - private fun registerKnownGroupNames(table: TomlTable) { - val file = table.containingFile - - when { - file.virtualFile?.isPyprojectToml != true -> return - table.header.key?.name != "dependency-groups" -> return - } - - knownGroupNames = knownGroupNames ?: table.entries.mapNotNull { it.key.name } - } +private typealias DependencyGroupsTable = TomlTable +private typealias GroupName = String + + +// https://packaging.python.org/en/latest/specifications/name-normalization/#name-format +private val validGroupName = "(?i)^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$".toRegex() + + +private val GroupName.isValid: Boolean + get() = this.matches(validGroupName) + + +// https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization +private fun GroupName.normalize() = + this.replace("[-_.]+".toRegex(), "-").lowercase() + + +private val DependencyGroupsTable.groupNames: List + get() = entries.mapNotNull { it.key.name?.normalize() } + + +private val TomlTable.isDependencyGroups: Boolean + get() = header.key?.name == "dependency-groups" + + +private class DependencyGroupNameVisitor(private val holder: ProblemsHolder) : TomlVisitor() { override fun visitLiteral(element: TomlLiteral) { if (element.containingFile.virtualFile?.isPyprojectToml != true) { @@ -52,27 +53,39 @@ private class DependencyGroupNameVisitor( } val string = element.takeIf { it.isString } ?: return - val inlineTable = string.keyValuePair?.parent as? TomlInlineTable ?: return + val propertyPair = string.keyValuePair?.takeIf { it.key.name == "include-group" } ?: return + + val inlineTable = propertyPair.parent as? TomlInlineTable ?: return val array = inlineTable.parent as? TomlArray ?: return - val keyValuePair = array.keyValuePair ?: return + val arrayPropertyPair = array.keyValuePair ?: return + + if (!(arrayPropertyPair.key.absoluteName isChildOf "dependency-groups")) { + return + } + val dependencyGroupsTable = (arrayPropertyPair.parent as? TomlTable)?.takeIf { it.isDependencyGroups } + val registeredGroupNames = dependencyGroupsTable?.groupNames ?: emptyList() val groupName = string.stringContent ?: return - val knownGroupNames = this.knownGroupNames ?: return + val normalizedGroupName = groupName.normalize() when { - !(keyValuePair.key.absoluteName isChildOf "dependency-groups") -> return - groupName in knownGroupNames -> return + !groupName.isValid -> reportInvalidGroupName(element, groupName) + normalizedGroupName !in registeredGroupNames -> reportUnknownGroup(element, groupName) } - - val message = message("inspections.dependencyGroupNames.message", groupName) - val problemHighlightType = ProblemHighlightType.LIKE_UNKNOWN_SYMBOL + } + + private fun reportInvalidGroupName(element: PsiElement, groupName: String) { + val message = message("inspections.dependencyGroupNames.message.invalid", groupName) + val problemHighlightType = ProblemHighlightType.POSSIBLE_PROBLEM holder.registerProblem(element, message, problemHighlightType) } - companion object { - private const val KEY_NAME = "insyncwithfoo.ryecharm.common.inspection.DependencyGroupNameVisitor" - private val KEY = Key.create>(KEY_NAME) + private fun reportUnknownGroup(element: PsiElement, groupName: String) { + val message = message("inspections.dependencyGroupNames.message.unknown", groupName) + val problemHighlightType = ProblemHighlightType.LIKE_UNKNOWN_SYMBOL + + holder.registerProblem(element, message, problemHighlightType) } } @@ -82,12 +95,8 @@ internal class DependencyGroupNameInspection : LocalInspectionTool(), DumbAware override fun getShortName() = SHORT_NAME - override fun buildVisitor( - holder: ProblemsHolder, - isOnTheFly: Boolean, - session: LocalInspectionToolSession - ): PsiElementVisitor = - DependencyGroupNameVisitor(holder, session) + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor = + DependencyGroupNameVisitor(holder) companion object { const val SHORT_NAME = "insyncwithfoo.ryecharm.common.inspection.DependencyGroupNameInspection" diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 737612a..42760a5 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -142,7 +142,7 @@ suppressId="insyncwithfoo.ryecharm.common.inspection.DependencyGroupNameInspection" shortName="insyncwithfoo.ryecharm.common.inspection.DependencyGroupNameInspection" - language="Python" + language="TOML" groupName="RyeCharm" bundle="messages.ryecharm" diff --git a/src/main/resources/messages/ryecharm.properties b/src/main/resources/messages/ryecharm.properties index 390c12b..f8ee9af 100644 --- a/src/main/resources/messages/ryecharm.properties +++ b/src/main/resources/messages/ryecharm.properties @@ -221,7 +221,8 @@ intentions.uv.sync.familyName = Synchronize project inspections.ruff.displayName = Linting with Ruff inspections.dependencyGroupNames.displayName = Validate PEP 735 dependency group names -inspections.dependencyGroupNames.message = Unknown dependency group: {0} +inspections.dependencyGroupNames.message.invalid = Invalid dependency group name: {0} +inspections.dependencyGroupNames.message.unknown = Unknown dependency group: {0} inspections.uvLockEdit.message = uv.lock should not be edited manually. diff --git a/src/test/kotlin/insyncwithfoo/ryecharm/common/inspection/DependencyGroupNameInspectionTest.kt b/src/test/kotlin/insyncwithfoo/ryecharm/common/inspection/DependencyGroupNameInspectionTest.kt index cc88def..8700e73 100644 --- a/src/test/kotlin/insyncwithfoo/ryecharm/common/inspection/DependencyGroupNameInspectionTest.kt +++ b/src/test/kotlin/insyncwithfoo/ryecharm/common/inspection/DependencyGroupNameInspectionTest.kt @@ -12,6 +12,12 @@ internal class DependencyGroupNameInspectionTest : PlatformTestCase() { @Test fun `test invalid group name`() = doTest("invalidGroupName") + @Test + fun `test normalization`() = doTest("normalizedComparison") + + @Test + fun `test invalid key`() = doTest("invalidKey") + private fun doTest(subdrectory: String) = fileBasedTest("$subdrectory/pyproject.toml") { fixture.enableInspections(DependencyGroupNameInspection()) fixture.checkHighlighting() diff --git a/src/test/testData/common/inspections/DependencyGroupNameInspectionTest/invalidGroupName/pyproject.toml b/src/test/testData/common/inspections/DependencyGroupNameInspectionTest/invalidGroupName/pyproject.toml index be6bcb0..0878797 100644 --- a/src/test/testData/common/inspections/DependencyGroupNameInspectionTest/invalidGroupName/pyproject.toml +++ b/src/test/testData/common/inspections/DependencyGroupNameInspectionTest/invalidGroupName/pyproject.toml @@ -1,3 +1,2 @@ [dependency-groups] -foo.bar = ["ruff"] -baz = [{ include-group = "foo.bar" } ] +foo = [{ include-group = "b@r" }] diff --git a/src/test/testData/common/inspections/DependencyGroupNameInspectionTest/invalidKey/pyproject.toml b/src/test/testData/common/inspections/DependencyGroupNameInspectionTest/invalidKey/pyproject.toml new file mode 100644 index 0000000..57b621f --- /dev/null +++ b/src/test/testData/common/inspections/DependencyGroupNameInspectionTest/invalidKey/pyproject.toml @@ -0,0 +1,3 @@ +[dependency-groups] +foo = ["pyparsing"] +bar = [{ set-phasers-to = "b@r" }] diff --git a/src/test/testData/common/inspections/DependencyGroupNameInspectionTest/normalizedComparison/pyproject.toml b/src/test/testData/common/inspections/DependencyGroupNameInspectionTest/normalizedComparison/pyproject.toml new file mode 100644 index 0000000..4595db3 --- /dev/null +++ b/src/test/testData/common/inspections/DependencyGroupNameInspectionTest/normalizedComparison/pyproject.toml @@ -0,0 +1,3 @@ +[dependency-groups] +"foo---bar" = ["ruff"] +baz = [{ include-group = "foo_._bar" }]