Skip to content

Commit

Permalink
Make DependencyGroupNameInspection work
Browse files Browse the repository at this point in the history
  • Loading branch information
InSyncWithFoo committed Oct 15, 2024
1 parent 37212bc commit 84a6984
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,65 +13,79 @@ 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
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<String>?
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<String>
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) {
return
}

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<List<String>>(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)
}

}
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/messages/ryecharm.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
[dependency-groups]
foo.bar = ["ruff"]
baz = [{ include-group = <warning descr="Unknown dependency group: foo.bar">"foo.bar"</warning> } ]
foo = [{ include-group = <warning descr="Invalid dependency group name: b@r">"b@r"</warning> }]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[dependency-groups]
foo = ["pyparsing"]
bar = [{ set-phasers-to = "b@r" }]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[dependency-groups]
"foo---bar" = ["ruff"]
baz = [{ include-group = "foo_._bar" }]

0 comments on commit 84a6984

Please sign in to comment.