/python/python-psi-impl/src/com/jetbrains/python/inspections/PyNamedTupleInspection.kt

https://github.com/JetBrains/intellij-community · Kotlin · 169 lines · 142 code · 26 blank · 1 comment · 33 complexity · f3c10fc9855838b3b3c16be6c475dccc MD5 · raw file

  1. // Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
  2. package com.jetbrains.python.inspections
  3. import com.intellij.codeInspection.LocalInspectionToolSession
  4. import com.intellij.codeInspection.ProblemHighlightType
  5. import com.intellij.codeInspection.ProblemsHolder
  6. import com.intellij.psi.PsiElement
  7. import com.intellij.psi.PsiElementVisitor
  8. import com.intellij.psi.ResolveState
  9. import com.intellij.psi.scope.PsiScopeProcessor
  10. import com.jetbrains.python.codeInsight.stdlib.PyNamedTupleTypeProvider
  11. import com.jetbrains.python.psi.LanguageLevel
  12. import com.jetbrains.python.psi.PyClass
  13. import com.jetbrains.python.psi.PyTargetExpression
  14. import com.jetbrains.python.psi.types.TypeEvalContext
  15. import java.util.*
  16. class PyNamedTupleInspection : PyInspection() {
  17. companion object {
  18. fun inspectFieldsOrder(cls: PyClass,
  19. classFieldsFilter: (PyClass) -> Boolean,
  20. checkInheritedOrder: Boolean,
  21. context: TypeEvalContext,
  22. callback: (PsiElement, String, ProblemHighlightType) -> Unit,
  23. fieldsFilter: (PyTargetExpression) -> Boolean = { true },
  24. hasAssignedValue: (PyTargetExpression) -> Boolean = PyTargetExpression::hasAssignedValue) {
  25. val fieldsProcessor = if (classFieldsFilter(cls)) processFields(
  26. cls, fieldsFilter, hasAssignedValue)
  27. else null
  28. if (fieldsProcessor?.lastFieldWithoutDefaultValue == null && !checkInheritedOrder) return
  29. val ancestors = cls.getAncestorClasses(context)
  30. val ancestorsFields = ancestors.map {
  31. if (!classFieldsFilter(it)) {
  32. Ancestor.FILTERED
  33. }
  34. else {
  35. val processor = processFields(it,
  36. fieldsFilter,
  37. hasAssignedValue)
  38. if (processor.fieldsWithDefaultValue.isNotEmpty()) {
  39. Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE
  40. }
  41. else if (processor.lastFieldWithoutDefaultValue != null) {
  42. Ancestor.HAS_NOT_FIELD_WITH_DEFAULT_VALUE
  43. }
  44. else {
  45. Ancestor.FILTERED
  46. }
  47. }
  48. }
  49. if (checkInheritedOrder) {
  50. var seenAncestorHavingFieldWithDefaultValue: PyClass? = null
  51. for (ancestorAndFields in ancestors.zip(ancestorsFields).asReversed()) {
  52. if (ancestorAndFields.second == Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE) seenAncestorHavingFieldWithDefaultValue = ancestorAndFields.first
  53. else if (ancestorAndFields.second == Ancestor.HAS_NOT_FIELD_WITH_DEFAULT_VALUE && seenAncestorHavingFieldWithDefaultValue != null) {
  54. callback(
  55. cls.superClassExpressionList!!,
  56. "Inherited non-default argument(s) defined in ${ancestorAndFields.first.name} follows " +
  57. "inherited default argument defined in ${seenAncestorHavingFieldWithDefaultValue.name}",
  58. ProblemHighlightType.GENERIC_ERROR
  59. )
  60. break
  61. }
  62. }
  63. }
  64. val lastFieldWithoutDefaultValue = fieldsProcessor?.lastFieldWithoutDefaultValue
  65. if (lastFieldWithoutDefaultValue != null) {
  66. if (ancestorsFields.contains(
  67. Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE)) {
  68. cls.nameIdentifier?.let { name ->
  69. val ancestorsNames = ancestors
  70. .asSequence()
  71. .zip(ancestorsFields.asSequence())
  72. .filter { it.second == Ancestor.HAS_FIELD_WITH_DEFAULT_VALUE }
  73. .joinToString { "'${it.first.name}'" }
  74. callback(name,
  75. "Non-default argument(s) follows default argument(s) defined in $ancestorsNames",
  76. ProblemHighlightType.GENERIC_ERROR)
  77. }
  78. }
  79. fieldsProcessor.fieldsWithDefaultValue.headSet(lastFieldWithoutDefaultValue).forEach {
  80. callback(it,
  81. "Fields with a default value must come after any fields without a default.",
  82. ProblemHighlightType.GENERIC_ERROR)
  83. }
  84. }
  85. }
  86. private fun processFields(cls: PyClass,
  87. filter: (PyTargetExpression) -> Boolean,
  88. hasAssignedValue: (PyTargetExpression) -> Boolean): LocalFieldsProcessor {
  89. val fieldsProcessor = LocalFieldsProcessor(filter,
  90. hasAssignedValue)
  91. cls.processClassLevelDeclarations(fieldsProcessor)
  92. return fieldsProcessor
  93. }
  94. private enum class Ancestor {
  95. FILTERED, HAS_FIELD_WITH_DEFAULT_VALUE, HAS_NOT_FIELD_WITH_DEFAULT_VALUE
  96. }
  97. }
  98. override fun buildVisitor(holder: ProblemsHolder,
  99. isOnTheFly: Boolean,
  100. session: LocalInspectionToolSession): PsiElementVisitor = Visitor(
  101. holder,PyInspectionVisitor.getContext(session))
  102. private class Visitor(holder: ProblemsHolder, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
  103. override fun visitPyClass(node: PyClass) {
  104. super.visitPyClass(node)
  105. if (LanguageLevel.forElement(node).isAtLeast(LanguageLevel.PYTHON36) &&
  106. PyNamedTupleTypeProvider.isTypingNamedTupleDirectInheritor(node, myTypeEvalContext)) {
  107. inspectFieldsOrder(node, { it == node }, false,
  108. myTypeEvalContext,
  109. this::registerProblem)
  110. }
  111. }
  112. }
  113. private class LocalFieldsProcessor(private val filter: (PyTargetExpression) -> Boolean,
  114. private val hasAssignedValue: (PyTargetExpression) -> Boolean) : PsiScopeProcessor {
  115. val lastFieldWithoutDefaultValue: PyTargetExpression?
  116. get() = lastFieldWithoutDefaultValueBox.result
  117. val fieldsWithDefaultValue: TreeSet<PyTargetExpression>
  118. private val lastFieldWithoutDefaultValueBox: MaxBy<PyTargetExpression>
  119. init {
  120. val offsetComparator = compareBy(PyTargetExpression::getTextOffset)
  121. lastFieldWithoutDefaultValueBox = MaxBy(offsetComparator)
  122. fieldsWithDefaultValue = TreeSet(offsetComparator)
  123. }
  124. override fun execute(element: PsiElement, state: ResolveState): Boolean {
  125. if (element is PyTargetExpression && filter(element)) {
  126. when {
  127. hasAssignedValue(element) -> fieldsWithDefaultValue.add(element)
  128. else -> lastFieldWithoutDefaultValueBox.apply(element)
  129. }
  130. }
  131. return true
  132. }
  133. }
  134. private class MaxBy<T>(private val comparator: Comparator<T>) {
  135. var result: T? = null
  136. private set
  137. fun apply(t: T) {
  138. if (result == null || comparator.compare(result, t) < 0) {
  139. result = t
  140. }
  141. }
  142. }
  143. }