[python] PY-83776: Python list can't be sorted due to "Comparison method violates its general contract!".

`com.intellij.python.community.services.shared.PythonInfoWithUiComparator` sorts by `PythonInfoComparator` and then by `UiComparator`.

It used to multiply first result to `10` trying to shift it one digit left, but `compare` contract doesn't guarantee anything except the sign. It could be `-233` for certain tool names (i.e: different lengths, different characters with a huge distance between their codes).

We now only check contract


Merge-request: IJ-MR-186470
Merged-by: Ilya Kazakevich <ilya.kazakevich@jetbrains.com>

GitOrigin-RevId: 89e9b3de2bd0e4b46d1c213f3b262dd698462275
This commit is contained in:
Ilya Kazakevich
2025-12-19 15:58:25 +00:00
committed by intellij-monorepo-bot
parent 1e71902271
commit 5d4fd17a0f
2 changed files with 41 additions and 2 deletions
@@ -32,7 +32,11 @@ class PythonInfoWithUiComparator<T> : Comparator<T> where T : PythonInfoHolder,
if (logger.isTraceEnabled) {
logger.trace("full ${o1.string()} vs ${o2.string()}")
}
return PythonInfoComparator.compare(o1, o2) * 10 + UiComparator.compare(o1, o2)
val infoResult = PythonInfoComparator.compare(o1, o2)
if (infoResult != 0) {
return infoResult
}
return UiComparator.compare(o1, o2)
}
}
@@ -9,6 +9,7 @@ import com.jetbrains.python.PythonInfo
import com.jetbrains.python.psi.LanguageLevel
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import java.util.*
@@ -41,9 +42,43 @@ class ComparatorsTest {
MockInfo(PythonInfo(LanguageLevel.PYTHON27))
))
}
/**
* Check [Comparator.compare] contract for [PythonInfoWithUiComparator]
*/
@Test
fun testCompareContract() {
val longName = " ".repeat(100)
val data = mutableListOf<MockInfo>()
for (lang in LanguageLevel.entries) {
for (free in arrayOf(true, false)) {
for (title in arrayOf(null, "uv", "homebrew", " ", "ÿ", longName)) {
val ui = if (title == null) null else PyToolUIInfo(title)
data.add(MockInfo(PythonInfo(lang, free), ui))
}
}
}
for (x in data) {
Assertions.assertEquals(0, x.compareTo(x), "Reflexivity broken for $x")
for (y in data) {
Assertions.assertEquals(-(y.compareTo(x)), x.compareTo(y), "Antisymmetry broken for $x, $y")
for (z in data) {
if (x > y && y > z) {
Assertions.assertTrue(x > z, "Transitivity broken for $x, $y, $z")
}
}
}
}
}
}
private data class MockInfo(
override val pythonInfo: PythonInfo,
override val ui: PyToolUIInfo? = null,
) : PythonInfoHolder, UiHolder
) : PythonInfoHolder, UiHolder, Comparable<MockInfo> {
override fun compareTo(other: MockInfo): Int = COMPARATOR.compare(this, other)
}
private val COMPARATOR = PythonInfoWithUiComparator<MockInfo>()