PY-34617 Take into account sys.version_info checks when analyzing Python files

Support and, or, <=, > operators in version checks.

GitOrigin-RevId: 5006e88b0f7935d0bf0841dfd5fad5c371e8ff12
This commit is contained in:
Petr
2024-08-21 16:52:47 +02:00
committed by intellij-monorepo-bot
parent 0b8f4cc1e8
commit 79dc479c63
18 changed files with 420 additions and 254 deletions

View File

@@ -16,9 +16,12 @@ import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.types.PyClassTypeImpl;
import com.jetbrains.python.psi.types.TypeEvalContext;
import com.jetbrains.python.pyi.PyiUtil;
import junit.framework.TestCase;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
public abstract class PyCommonResolveTest extends PyCommonResolveTestCase {
@Override
@@ -1920,71 +1923,182 @@ public abstract class PyCommonResolveTest extends PyCommonResolveTestCase {
}
// PY-34617
public void testFileAttributeMatchingVersionCheck() {
myFixture.copyDirectoryToProject("resolve/FileAttributeUnderVersionCheck", "");
runWithLanguageLevel(LanguageLevel.PYTHON310, () -> {
myFixture.configureByText(
PythonFileType.INSTANCE,
"""
import mod
mod.foo
<ref>"""
);
final PsiElement element = PyCommonResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertResolveResult(element, PyTargetExpression.class, "foo", "mod.py");
});
assertFilesNotParsed();
public void testModuleAttributeUnderVersionCheck() {
String decl = """
import sys
if True:
if sys.version_info >= (3,):
if sys.version_info >= (3, 10) and sys.version_info < (3, 12):
foo = 23
if sys.version_info < (3, 11) and (sys.version_info < (3, 5) or sys.version_info > (3, 7)):
buz = 23
else:
bar = -1
""";
String foo = decl + """
foo
<ref>""";
Consumer<PsiElement> fooTargetExpr = e -> assertResolveResult(e, PyTargetExpression.class, "foo", null);
String buz = decl + """
buz
<ref>""";
Consumer<PsiElement> buzTargetExpr = e -> assertResolveResult(e, PyTargetExpression.class, "buz", null);
String bar = decl + """
bar
<ref>""";
Consumer<PsiElement> barTargetExpr = e -> assertResolveResult(e, PyTargetExpression.class, "bar", null);
assertResolvedElement(LanguageLevel.PYTHON310, foo, fooTargetExpr);
assertResolvedElement(LanguageLevel.PYTHON310, buz, buzTargetExpr);
assertResolvedElement(LanguageLevel.PYTHON310, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON312, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON312, buz, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON312, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON38, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON38, buz, buzTargetExpr);
assertResolvedElement(LanguageLevel.PYTHON38, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON37, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON37, buz, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON37, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON34, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON34, buz, buzTargetExpr);
assertResolvedElement(LanguageLevel.PYTHON34, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, buz, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, bar, barTargetExpr);
}
// PY-34617
public void testFileAttributeNotMatchingVersionCheck() {
myFixture.copyDirectoryToProject("resolve/FileAttributeUnderVersionCheck", "");
runWithLanguageLevel(LanguageLevel.PYTHON310, () -> {
myFixture.configureByText(
PythonFileType.INSTANCE,
"""
import mod
mod.bar
<ref>"""
);
final PsiElement element = PyCommonResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertNull(element);
});
assertFilesNotParsed();
public void testModuleAttributeUnderVersionCheckMultifile() {
myFixture.copyDirectoryToProject("resolve/ModuleAttributeUnderVersionCheck", "");
String foo = """
import mod
mod.foo
<ref>""";
Consumer<PsiElement> fooTargetExpr = e -> assertResolveResult(e, PyTargetExpression.class, "foo", "mod.py");
String buz = """
import mod
mod.buz
<ref>""";
Consumer<PsiElement> buzTargetExpr = e -> assertResolveResult(e, PyTargetExpression.class, "buz", "mod.py");
String bar = """
import mod
mod.bar
<ref>""";
Consumer<PsiElement> barTargetExpr = e -> assertResolveResult(e, PyTargetExpression.class, "bar", "mod.py");
assertResolvedElement(LanguageLevel.PYTHON310, foo, fooTargetExpr);
assertResolvedElement(LanguageLevel.PYTHON310, buz, buzTargetExpr);
assertResolvedElement(LanguageLevel.PYTHON310, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON312, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON312, buz, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON312, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON38, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON38, buz, buzTargetExpr);
assertResolvedElement(LanguageLevel.PYTHON38, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON37, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON37, buz, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON37, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON34, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON34, buz, buzTargetExpr);
assertResolvedElement(LanguageLevel.PYTHON34, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, buz, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, bar, barTargetExpr);
}
// PY-34617
public void testClassAttributeMatchingVersionCheck() {
public void testClassAttributeUnderVersionCheck() {
String classDecl = """
import sys
if sys.version_info < (4,):
class MyClass:
if sys.version_info >= (3,):
def foo(self):
pass
elif sys.version_info < (2, 5):
def bar(self):
pass
else:
def buz(self):
pass
""";
String foo = classDecl + """
MyClass().foo()
<ref>""";
String bar = classDecl + """
MyClass().bar()
<ref>""";
String buz = classDecl + """
MyClass().buz()
<ref>""";
assertResolvedElement(LanguageLevel.PYTHON310, foo, e -> assertResolveResult(e, PyFunction.class, "foo", null));
assertResolvedElement(LanguageLevel.PYTHON310, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON310, buz, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON24, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON24, bar, e -> assertResolveResult(e, PyFunction.class, "bar", null));
assertResolvedElement(LanguageLevel.PYTHON24, buz, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, buz, e -> assertResolveResult(e, PyFunction.class, "buz", null));
}
// PY-34617
public void testClassAttributeUnderVersionCheckMultifile() {
myFixture.copyDirectoryToProject("resolve/ClassAttributeUnderVersionCheck", "");
runWithLanguageLevel(LanguageLevel.PYTHON27, () -> {
myFixture.configureByText(
PythonFileType.INSTANCE,
"""
from mod import MyClass
m = MyClass()
m.buz()
<ref>"""
);
final PsiElement element = PyCommonResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertResolveResult(element, PyFunction.class, "buz", "mod.py");
});
assertFilesNotParsed();
String foo = """
from mod import MyClass
m = MyClass()
m.foo()
<ref>""";
String bar = """
from mod import MyClass
m = MyClass()
m.bar()
<ref>""";
String buz = """
from mod import MyClass
m = MyClass()
m.buz()
<ref>""";
assertResolvedElement(LanguageLevel.PYTHON310, foo, e -> assertResolveResult(e, PyFunction.class, "foo", "mod.py"));
assertResolvedElement(LanguageLevel.PYTHON310, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON310, buz, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON24, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON24, bar, e -> assertResolveResult(e, PyFunction.class, "bar", "mod.py"));
assertResolvedElement(LanguageLevel.PYTHON24, buz, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, foo, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, bar, TestCase::assertNull);
assertResolvedElement(LanguageLevel.PYTHON27, buz, e -> assertResolveResult(e, PyFunction.class, "buz", "mod.py"));
}
// PY-34617
public void testClassAttributeNotMatchingVersionCheck() {
myFixture.copyDirectoryToProject("resolve/ClassAttributeUnderVersionCheck", "");
runWithLanguageLevel(LanguageLevel.PYTHON27, () -> {
myFixture.configureByText(
PythonFileType.INSTANCE,
"""
from mod import MyClass
m = MyClass()
m.foo()
<ref>"""
);
final PsiElement element = PyCommonResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertNull(element);
private void assertResolvedElement(@NotNull LanguageLevel languageLevel, @NotNull String text, @NotNull Consumer<PsiElement> assertion) {
runWithLanguageLevel(languageLevel, () -> {
myFixture.configureByText(PythonFileType.INSTANCE, text);
PsiElement element = PyCommonResolveTestCase.findReferenceByMarker(myFixture.getFile()).resolve();
assertion.accept(element);
});
assertFilesNotParsed();
}

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.psi.impl
import com.intellij.openapi.util.Version
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.psi.PyIfStatement
import com.jetbrains.python.psi.PyRecursiveElementVisitor
@@ -10,20 +11,22 @@ import org.jetbrains.annotations.ApiStatus
* @see [Version and Platform Checks](https://typing.readthedocs.io/en/latest/source/stubs.html.version-and-platform-checks)
*/
@ApiStatus.Internal
open class PyVersionAwareElementVisitor(private val languageLevel: LanguageLevel?) : PyRecursiveElementVisitor() {
open class PyVersionAwareElementVisitor(languageLevel: LanguageLevel?) : PyRecursiveElementVisitor() {
private val version = languageLevel?.let { Version(it.majorVersion, it.minorVersion, 0) }
override fun visitPyIfStatement(node: PyIfStatement) {
if (languageLevel == null) {
if (version == null) {
super.visitPyIfStatement(node)
return
}
val ifParts = sequenceOf(node.getIfPart()) + node.elifParts.asSequence()
val ifParts = sequenceOf(node.ifPart) + node.elifParts.asSequence()
for (ifPart in ifParts) {
val versionCheck = PyVersionCheck.fromCondition(ifPart)
if (versionCheck == null) {
val versions = ifPart.condition?.let(PyVersionCheck::convertToVersionRanges)
if (versions == null) {
super.visitPyIfStatement(node)
return
}
if (versionCheck.matches(languageLevel)) {
if (versions.contains(version)) {
ifPart.statementList.accept(this)
return
}

View File

@@ -1,85 +1,88 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.psi.impl
import com.google.common.collect.ImmutableRangeSet
import com.google.common.collect.Range
import com.intellij.openapi.util.Version
import com.intellij.psi.util.QualifiedName
import com.jetbrains.python.PyTokenTypes
import com.jetbrains.python.ast.*
import com.jetbrains.python.ast.impl.PyPsiUtilsCore
import com.jetbrains.python.psi.LanguageLevel
import org.jetbrains.annotations.ApiStatus
import java.math.BigInteger
@ApiStatus.Internal
data class PyVersionCheck(val version: Version, val isLessThan: Boolean) {
fun matches(languageLevel: LanguageLevel): Boolean {
return isLessThan == languageLevel.isLessThan(version)
}
companion object {
/**
* Extracts the Python version comparison from {@code ifPart}'s condition if it's a version check as specified in
* <a href="https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks">Version and Platform Checks</a> E.g.
* <pre>{@code
* if sys.version_info >= (3,):
* ...
* }</pre>
* @return A {@link VersionCheck} instance if {@code ifPart} is a (valid) version check, or {@code null} otherwise.
*/
@JvmStatic
fun fromCondition(ifPart: PyAstIfPart): PyVersionCheck? {
val binaryExpr = PyPsiUtilsCore.flattenParens(ifPart.condition)
if (binaryExpr !is PyAstBinaryExpression) return null
val lhsRefExpr = PyPsiUtilsCore.flattenParens(binaryExpr.leftExpression)
if (lhsRefExpr !is PyAstReferenceExpression) return null
if (SYS_VERSION_INFO_QUALIFIED_NAME != lhsRefExpr.asQualifiedName()) return null
val versionTuple = PyPsiUtilsCore.flattenParens(binaryExpr.rightExpression)
if (versionTuple !is PyAstTupleExpression<*>) return null
val version = evaluateVersion(versionTuple)
if (version == null) return null
val operator = binaryExpr.getOperator()
if (operator !== PyTokenTypes.LT && operator !== PyTokenTypes.GE) return null
return PyVersionCheck(version, operator === PyTokenTypes.LT)
}
private val SYS_VERSION_INFO_QUALIFIED_NAME = QualifiedName.fromDottedString("sys.version_info")
private fun evaluateVersion(versionTuple: PyAstTupleExpression<*>): Version? {
val elements = versionTuple.elements
if (elements.size != 1 && elements.size != 2) {
return null
object PyVersionCheck {
/**
* @return Version ranges if {@code expression} is a version check, {@code null} otherwise
*
* @see <a href="https://peps.python.org/pep-0484/#version-and-platform-checking">Version and Platform Checks</a>
*/
@JvmStatic
fun convertToVersionRanges(expression: PyAstExpression): ImmutableRangeSet<Version>? {
val binaryExpr = PyPsiUtilsCore.flattenParens(expression) as? PyAstBinaryExpression ?: return null
when (val operator = binaryExpr.operator) {
PyTokenTypes.AND_KEYWORD, PyTokenTypes.OR_KEYWORD -> {
val rhs = binaryExpr.rightExpression ?: return null
val ranges1 = convertToVersionRanges(binaryExpr.leftExpression) ?: return null
val ranges2 = convertToVersionRanges(rhs) ?: return null
return if (operator === PyTokenTypes.AND_KEYWORD)
ranges1.intersection(ranges2)
else
ranges1.union(ranges2)
}
val major = evaluateNumber(elements[0])
if (major == null) {
return null
PyTokenTypes.LT, PyTokenTypes.GT, PyTokenTypes.LE, PyTokenTypes.GE -> {
val refExpr = PyPsiUtilsCore.flattenParens(binaryExpr.leftExpression) as? PyAstReferenceExpression ?: return null
if (SYS_VERSION_INFO_QUALIFIED_NAME != refExpr.asQualifiedName()) return null
val tuple = PyPsiUtilsCore.flattenParens(binaryExpr.rightExpression) as? PyAstTupleExpression<*> ?: return null
val version = evaluateVersion(tuple) ?: return null
val range = when (operator) {
PyTokenTypes.LT -> Range.lessThan(version)
PyTokenTypes.GT -> Range.greaterThan(version)
PyTokenTypes.LE -> Range.atMost(version)
PyTokenTypes.GE -> Range.atLeast(version)
else -> throw IllegalStateException()
}
return ImmutableRangeSet.of(range)
}
if (elements.size == 1) {
return Version(major, 0, 0)
}
val minor = evaluateNumber(elements[1])
if (minor == null) {
return null
}
return Version(major, minor, 0)
}
private fun evaluateNumber(expression: PyAstExpression?): Int? {
if (expression !is PyAstNumericLiteralExpression) return null
if (!expression.isIntegerLiteral) return null
val value = expression.bigIntegerValue
val intValue = value.toInt()
return if (BigInteger.valueOf(intValue.toLong()) == value) intValue else null
else -> return null
}
}
}
fun LanguageLevel.isLessThan(version: Version): Boolean {
return version.compareTo(majorVersion, minorVersion) > 0
}
private val SYS_VERSION_INFO_QUALIFIED_NAME = QualifiedName.fromDottedString("sys.version_info")
private fun evaluateVersion(versionTuple: PyAstTupleExpression<*>): Version? {
val elements = versionTuple.elements
if (elements.size != 1 && elements.size != 2) {
return null
}
val major = evaluateNumber(elements[0])
if (major == null) {
return null
}
if (elements.size == 1) {
return Version(major, 0, 0)
}
val minor = evaluateNumber(elements[1])
if (minor == null) {
return null
}
return Version(major, minor, 0)
}
private fun evaluateNumber(expression: PyAstExpression?): Int? {
if (expression !is PyAstNumericLiteralExpression) return null
if (!expression.isIntegerLiteral) return null
val value = expression.bigIntegerValue
val intValue = value.toInt()
return if (BigInteger.valueOf(intValue.toLong()) == value) intValue else null
}
}

View File

@@ -1,10 +1,9 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.psi.stubs
import com.google.common.collect.RangeSet
import com.intellij.openapi.util.Version
interface PyVersionSpecificStub {
val versionRange: PyVersionRange
val versions: RangeSet<Version>
}
data class PyVersionRange(val lowInclusive: Version?, val highExclusive: Version?)

View File

@@ -1,7 +1,9 @@
// Copyright 2000-2018 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.
package com.jetbrains.python.psi.impl.stubs;
import com.google.common.collect.RangeSet;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.Version;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.stubs.*;
@@ -55,7 +57,7 @@ public class PyClassElementType extends PyStubElementType<PyClassStub, PyClass>
PyPsiUtils.strValue(psi.getDocStringExpression()),
psi.getDeprecationMessage(),
getStubElementType(),
PyVersionSpecificStubBaseKt.evaluateVersionRangeForElement(psi),
PyVersionSpecificStubBaseKt.evaluateVersionsForElement(psi),
createCustomStub(psi));
}
@@ -136,7 +138,7 @@ public class PyClassElementType extends PyStubElementType<PyClassStub, PyClass>
dataStream.writeUTFFast(docString != null ? docString : "");
dataStream.writeName(pyClassStub.getDeprecationMessage());
PyVersionSpecificStubBaseKt.serializeVersionRange(pyClassStub.getVersionRange(), dataStream);
PyVersionSpecificStubBaseKt.serializeVersions(pyClassStub.getVersions(), dataStream);
serializeCustomStub(pyClassStub.getCustomStub(PyCustomClassStub.class), dataStream);
}
@@ -168,12 +170,12 @@ public class PyClassElementType extends PyStubElementType<PyClassStub, PyClass>
final String deprecationMessage = dataStream.readNameString();
final PyVersionRange versionRange = PyVersionSpecificStubBaseKt.deserializeVersionRange(dataStream);
final RangeSet<Version> versions = PyVersionSpecificStubBaseKt.deserializeVersions(dataStream);
final PyCustomClassStub customStub = deserializeCustomStub(dataStream);
return new PyClassStubImpl(name, parentStub, superClasses, baseClassesText, metaClass, slots, docString, deprecationMessage,
getStubElementType(), versionRange, customStub);
getStubElementType(), versions, customStub);
}
@Override

View File

@@ -1,13 +1,14 @@
// Copyright 2000-2018 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.
package com.jetbrains.python.psi.impl.stubs;
import com.google.common.collect.RangeSet;
import com.intellij.openapi.util.Version;
import com.intellij.psi.stubs.IStubElementType;
import com.intellij.psi.stubs.StubElement;
import com.intellij.psi.util.QualifiedName;
import com.intellij.util.ObjectUtils;
import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.stubs.PyClassStub;
import com.jetbrains.python.psi.stubs.PyVersionRange;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -49,9 +50,9 @@ public class PyClassStubImpl extends PyVersionSpecificStubBase<PyClass> implemen
@Nullable String docString,
@Nullable String deprecationMessage,
@NotNull IStubElementType stubElementType,
@NotNull PyVersionRange versionRange,
@NotNull RangeSet<Version> versions,
@Nullable PyCustomClassStub customStub) {
super(parentStub, stubElementType, versionRange);
super(parentStub, stubElementType, versions);
myName = name;
mySuperClasses = superClasses;
mySuperClassesText = superClassesText;

View File

@@ -15,7 +15,9 @@
*/
package com.jetbrains.python.psi.impl.stubs;
import com.google.common.collect.RangeSet;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.Version;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.stubs.*;
@@ -60,7 +62,7 @@ public class PyFunctionElementType extends PyStubElementType<PyFunctionStub, PyF
final PyStringLiteralExpression docStringExpression = function.getDocStringExpression();
final String typeComment = function.getTypeCommentAnnotation();
final String annotationContent = function.getAnnotationValue();
final PyVersionRange versionRange = PyVersionSpecificStubBaseKt.evaluateVersionRangeForElement(psi);
final RangeSet<Version> versions = PyVersionSpecificStubBaseKt.evaluateVersionsForElement(psi);
return new PyFunctionStubImpl(psi.getName(),
PyPsiUtils.strValue(docStringExpression),
message,
@@ -71,7 +73,7 @@ public class PyFunctionElementType extends PyStubElementType<PyFunctionStub, PyF
annotationContent,
parentStub,
getStubElementType(),
versionRange);
versions);
}
@Override
@@ -84,7 +86,7 @@ public class PyFunctionElementType extends PyStubElementType<PyFunctionStub, PyF
dataStream.writeBoolean(stub.onlyRaisesNotImplementedError());
dataStream.writeName(stub.getTypeComment());
dataStream.writeName(stub.getAnnotation());
PyVersionSpecificStubBaseKt.serializeVersionRange(stub.getVersionRange(), dataStream);
PyVersionSpecificStubBaseKt.serializeVersions(stub.getVersions(), dataStream);
}
@Override
@@ -98,7 +100,7 @@ public class PyFunctionElementType extends PyStubElementType<PyFunctionStub, PyF
final boolean onlyRaisesNotImplementedError = dataStream.readBoolean();
String typeComment = dataStream.readNameString();
String annotationContent = dataStream.readNameString();
PyVersionRange versionRange = PyVersionSpecificStubBaseKt.deserializeVersionRange(dataStream);
RangeSet<Version> versions = PyVersionSpecificStubBaseKt.deserializeVersions(dataStream);
return new PyFunctionStubImpl(name,
StringUtil.nullize(docString),
deprecationMessage,
@@ -109,7 +111,7 @@ public class PyFunctionElementType extends PyStubElementType<PyFunctionStub, PyF
annotationContent,
parentStub,
getStubElementType(),
versionRange);
versions);
}
@Override

View File

@@ -1,11 +1,12 @@
// 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.
package com.jetbrains.python.psi.impl.stubs;
import com.google.common.collect.RangeSet;
import com.intellij.openapi.util.Version;
import com.intellij.psi.stubs.IStubElementType;
import com.intellij.psi.stubs.StubElement;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.psi.stubs.PyFunctionStub;
import com.jetbrains.python.psi.stubs.PyVersionRange;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -29,8 +30,8 @@ public class PyFunctionStubImpl extends PyVersionSpecificStubBase<PyFunction> im
@Nullable String annotation,
final StubElement parent,
@NotNull IStubElementType stubElementType,
@NotNull PyVersionRange versionRange) {
super(parent, stubElementType, versionRange);
@NotNull RangeSet<Version> versions) {
super(parent, stubElementType, versions);
myName = name;
myDocString = docString;
myDeprecationMessage = deprecationMessage;

View File

@@ -1,8 +1,10 @@
// Copyright 2000-2018 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.
package com.jetbrains.python.psi.impl.stubs;
import com.google.common.collect.RangeSet;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.Version;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.stubs.IndexSink;
@@ -55,12 +57,12 @@ public class PyTargetExpressionElementType extends PyStubElementType<PyTargetExp
final String docString = DocStringUtil.getDocStringValue(psi);
final String typeComment = psi.getTypeCommentAnnotation();
final String annotation = psi.getAnnotationValue();
final PyVersionRange versionRange = PyVersionSpecificStubBaseKt.evaluateVersionRangeForElement(psi);
final RangeSet<Version> versions = PyVersionSpecificStubBaseKt.evaluateVersionsForElement(psi);
CustomTargetExpressionStub customStub = createCustomStub(psi);
if (customStub != null) {
return new PyTargetExpressionStubImpl(name, docString, typeComment, annotation, psi.hasAssignedValue(), customStub, parentStub,
versionRange);
versions);
}
PyTargetExpressionStub.InitializerType initializerType = PyTargetExpressionStub.InitializerType.Other;
@@ -78,7 +80,7 @@ public class PyTargetExpressionElementType extends PyStubElementType<PyTargetExp
}
}
return new PyTargetExpressionStubImpl(name, docString, initializerType, initializer, psi.isQualified(), typeComment, annotation,
psi.hasAssignedValue(), parentStub, versionRange);
psi.hasAssignedValue(), parentStub, versions);
}
@Override
@@ -90,7 +92,7 @@ public class PyTargetExpressionElementType extends PyStubElementType<PyTargetExp
stream.writeName(stub.getTypeComment());
stream.writeName(stub.getAnnotation());
stream.writeBoolean(stub.hasAssignedValue());
PyVersionSpecificStubBaseKt.serializeVersionRange(stub.getVersionRange(), stream);
PyVersionSpecificStubBaseKt.serializeVersions(stub.getVersions(), stream);
final CustomTargetExpressionStub customStub = stub.getCustomStub(CustomTargetExpressionStub.class);
if (customStub != null) {
serializeCustomStub(customStub, stream);
@@ -113,15 +115,15 @@ public class PyTargetExpressionElementType extends PyStubElementType<PyTargetExp
String typeComment = stream.readNameString();
String annotation = stream.readNameString();
final boolean hasAssignedValue = stream.readBoolean();
PyVersionRange versionRange = PyVersionSpecificStubBaseKt.deserializeVersionRange(stream);
RangeSet<Version> versions = PyVersionSpecificStubBaseKt.deserializeVersions(stream);
if (initializerType == PyTargetExpressionStub.InitializerType.Custom) {
CustomTargetExpressionStub stub = deserializeCustomStub(stream);
return new PyTargetExpressionStubImpl(name, docString, typeComment, annotation, hasAssignedValue, stub, parentStub, versionRange);
return new PyTargetExpressionStubImpl(name, docString, typeComment, annotation, hasAssignedValue, stub, parentStub, versions);
}
QualifiedName initializer = QualifiedName.deserialize(stream);
boolean isQualified = stream.readBoolean();
return new PyTargetExpressionStubImpl(name, docString, initializerType, initializer, isQualified, typeComment, annotation,
hasAssignedValue, parentStub, versionRange);
hasAssignedValue, parentStub, versions);
}
@Override

View File

@@ -15,6 +15,8 @@
*/
package com.jetbrains.python.psi.impl.stubs;
import com.google.common.collect.RangeSet;
import com.intellij.openapi.util.Version;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.stubs.StubElement;
import com.intellij.psi.util.QualifiedName;
@@ -22,7 +24,6 @@ import com.intellij.util.ObjectUtils;
import com.jetbrains.python.PyStubElementTypes;
import com.jetbrains.python.psi.PyTargetExpression;
import com.jetbrains.python.psi.stubs.PyTargetExpressionStub;
import com.jetbrains.python.psi.stubs.PyVersionRange;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -46,8 +47,8 @@ public class PyTargetExpressionStubImpl extends PyVersionSpecificStubBase<PyTarg
boolean hasAssignedValue,
CustomTargetExpressionStub customStub,
StubElement parent,
@NotNull PyVersionRange versionRange) {
super(parent, PyStubElementTypes.TARGET_EXPRESSION, versionRange);
@NotNull RangeSet<Version> versions) {
super(parent, PyStubElementTypes.TARGET_EXPRESSION, versions);
myName = name;
myTypeComment = typeComment;
myAnnotation = annotation;
@@ -68,8 +69,8 @@ public class PyTargetExpressionStubImpl extends PyVersionSpecificStubBase<PyTarg
@Nullable String annotation,
boolean hasAssignedValue,
final StubElement parentStub,
@NotNull PyVersionRange versionRange) {
super(parentStub, PyStubElementTypes.TARGET_EXPRESSION, versionRange);
@NotNull RangeSet<Version> versions) {
super(parentStub, PyStubElementTypes.TARGET_EXPRESSION, versions);
myName = name;
myTypeComment = typeComment;
myAnnotation = annotation;

View File

@@ -1,6 +1,8 @@
package com.jetbrains.python.psi.impl.stubs;
import com.google.common.collect.RangeSet;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.Version;
import com.intellij.psi.PsiElement;
import com.intellij.psi.stubs.IStubElementType;
import com.intellij.psi.stubs.StubElement;
@@ -11,7 +13,6 @@ import com.jetbrains.python.psi.PyStubElementType;
import com.jetbrains.python.psi.PyTypeAliasStatement;
import com.jetbrains.python.psi.impl.PyTypeAliasStatementImpl;
import com.jetbrains.python.psi.stubs.PyTypeAliasStatementStub;
import com.jetbrains.python.psi.stubs.PyVersionRange;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
@@ -37,14 +38,14 @@ public class PyTypeAliasStatementElementType extends PyStubElementType<PyTypeAli
@NotNull
public PyTypeAliasStatementStub createStub(@NotNull PyTypeAliasStatement psi, StubElement<? extends PsiElement> parentStub) {
return new PyTypeAliasStatementStubImpl(psi.getName(), (psi.getTypeExpression() != null ? psi.getTypeExpression().getText() : null),
parentStub, getStubElementType(), PyVersionSpecificStubBaseKt.evaluateVersionRangeForElement(psi));
parentStub, getStubElementType(), PyVersionSpecificStubBaseKt.evaluateVersionsForElement(psi));
}
@Override
public void serialize(@NotNull PyTypeAliasStatementStub stub, @NotNull StubOutputStream dataStream) throws IOException {
dataStream.writeName(stub.getName());
dataStream.writeName(stub.getTypeExpressionText());
PyVersionSpecificStubBaseKt.serializeVersionRange(stub.getVersionRange(), dataStream);
PyVersionSpecificStubBaseKt.serializeVersions(stub.getVersions(), dataStream);
}
@Override
@@ -52,9 +53,9 @@ public class PyTypeAliasStatementElementType extends PyStubElementType<PyTypeAli
public PyTypeAliasStatementStub deserialize(@NotNull StubInputStream dataStream, StubElement parentStub) throws IOException {
String name = dataStream.readNameString();
String typeExpressionText = dataStream.readNameString();
PyVersionRange versionRange = PyVersionSpecificStubBaseKt.deserializeVersionRange(dataStream);
RangeSet<Version> versions = PyVersionSpecificStubBaseKt.deserializeVersions(dataStream);
return new PyTypeAliasStatementStubImpl(name, typeExpressionText, parentStub, getStubElementType(), versionRange);
return new PyTypeAliasStatementStubImpl(name, typeExpressionText, parentStub, getStubElementType(), versions);
}
@NotNull

View File

@@ -1,10 +1,11 @@
package com.jetbrains.python.psi.impl.stubs;
import com.google.common.collect.RangeSet;
import com.intellij.openapi.util.Version;
import com.intellij.psi.stubs.IStubElementType;
import com.intellij.psi.stubs.StubElement;
import com.jetbrains.python.psi.PyTypeAliasStatement;
import com.jetbrains.python.psi.stubs.PyTypeAliasStatementStub;
import com.jetbrains.python.psi.stubs.PyVersionRange;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -17,8 +18,8 @@ public class PyTypeAliasStatementStubImpl extends PyVersionSpecificStubBase<PyTy
@Nullable String typeExpressionText,
@Nullable StubElement parent,
@NotNull IStubElementType stubElementType,
@NotNull PyVersionRange versionRange) {
super(parent, stubElementType, versionRange);
@NotNull RangeSet<Version> versions) {
super(parent, stubElementType, versions);
myName = name;
myTypeExpressionText = typeExpressionText;
}

View File

@@ -1,116 +1,141 @@
package com.jetbrains.python.psi.impl.stubs
import com.google.common.collect.BoundType
import com.google.common.collect.ImmutableRangeSet
import com.google.common.collect.Range
import com.google.common.collect.RangeSet
import com.google.common.collect.TreeRangeSet
import com.intellij.openapi.util.Version
import com.intellij.psi.PsiElement
import com.intellij.psi.stubs.*
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.psi.PyElsePart
import com.jetbrains.python.psi.PyIfPart
import com.jetbrains.python.psi.PyIfStatement
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyVersionCheck
import com.jetbrains.python.psi.impl.isLessThan
import com.jetbrains.python.psi.stubs.PyVersionRange
import com.jetbrains.python.psi.stubs.PyVersionSpecificStub
internal abstract class PyVersionSpecificStubBase<T : PsiElement>(
parent: StubElement<*>?,
elementType: IStubElementType<*, *>?,
override val versionRange: PyVersionRange,
override val versions: RangeSet<Version>,
) : StubBase<T>(parent, elementType), PyVersionSpecificStub
internal fun getChildrenStubs(stub: StubElement<*>, languageLevel: LanguageLevel): Iterable<StubElement<*>> {
val version = Version(languageLevel.majorVersion, languageLevel.minorVersion, 0)
return stub.childrenStubs.asSequence()
.filter { it !is PyVersionSpecificStub || it.versionRange.contains(languageLevel) }
.filter { it !is PyVersionSpecificStub || it.versions.contains(version) }
.asIterable()
}
private fun PyVersionRange.contains(languageLevel: LanguageLevel): Boolean {
val low = lowInclusive
val high = highExclusive
return (low == null || !languageLevel.isLessThan(low)) &&
(high == null || languageLevel.isLessThan(high))
}
internal fun evaluateVersionRangeForElement(element: PsiElement): PyVersionRange {
internal fun evaluateVersionsForElement(element: PsiElement): ImmutableRangeSet<Version> {
return CachedValuesManager.getCachedValue(element) {
val parent = element.parent
var range: PyVersionRange
var result: ImmutableRangeSet<Version>
if (parent == null) {
range = PyVersionRange(null, null)
result = ImmutableRangeSet.of(Range.all())
}
else {
range = evaluateVersionRangeForElement(parent)
result = evaluateVersionsForElement(parent)
if (parent is PyIfPart || parent is PyElsePart) {
val grandParent = parent.parent
if (grandParent is PyIfStatement) {
range = evaluateRange(range, grandParent, parent)
if (grandParent is PyIfStatement && element === (parent as PyStatementPart).statementList) {
val versions = evaluateVersionRangeForIfStatementPart(grandParent, parent)
if (versions != null) {
result = result.intersection(versions)
}
}
}
}
CachedValueProvider.Result.create(range, element)
CachedValueProvider.Result.create(result, element)
}
}
private fun evaluateRange(
initialRange: PyVersionRange,
ifStatement: PyIfStatement,
ifStatementPart: PsiElement,
): PyVersionRange {
val versionChecks = mutableListOf<PyVersionCheck>()
private fun evaluateVersionRangeForIfStatementPart(ifStatement: PyIfStatement, ifStatementPart: PsiElement): RangeSet<Version>? {
assert(ifStatementPart is PyIfPart || ifStatementPart is PyElsePart)
val result = if (ifStatementPart is PyIfPart) {
val versionRanges = ifStatementPart.condition?.let(PyVersionCheck::convertToVersionRanges) ?: return null
TreeRangeSet.create(versionRanges)
}
else {
TreeRangeSet.create(listOf(Range.all<Version>()))
}
val ifParts = sequenceOf(ifStatement.ifPart) + ifStatement.elifParts.asSequence()
for (ifPart in ifParts.takeWhile { it !== ifStatementPart }) {
val versionCheck = PyVersionCheck.fromCondition(ifPart) ?: return initialRange
versionChecks.add(PyVersionCheck(versionCheck.version, !versionCheck.isLessThan))
val versionRanges = ifPart.condition?.let(PyVersionCheck::convertToVersionRanges) ?: return null
result.removeAll(versionRanges)
}
if (ifStatementPart is PyIfPart) {
val versionCheck = PyVersionCheck.fromCondition(ifStatementPart) ?: return initialRange
versionChecks.add(versionCheck)
}
return versionChecks.fold(initialRange, ::clampRange)
return result
}
private fun clampRange(versionRange: PyVersionRange, versionCheck: PyVersionCheck): PyVersionRange {
return if (versionCheck.isLessThan)
PyVersionRange(versionRange.lowInclusive, min(versionRange.highExclusive, versionCheck.version))
internal fun serializeVersions(versions: RangeSet<Version>, outputStream: StubOutputStream) {
val ranges = versions.asRanges()
outputStream.writeVarInt(ranges.size)
for (range in ranges) {
serializeRange(range, outputStream)
}
}
private fun serializeRange(range: Range<Version>, outputStream: StubOutputStream) {
val low = if (range.hasLowerBound()) Endpoint(range.lowerEndpoint(), range.lowerBoundType()) else null
val high = if (range.hasUpperBound()) Endpoint(range.upperEndpoint(), range.upperBoundType()) else null
serializeEndpoint(low, outputStream)
serializeEndpoint(high, outputStream)
}
private fun serializeEndpoint(endpoint: Endpoint?, outputStream: StubOutputStream) {
if (endpoint == null) {
outputStream.writeByte(EndpointType.UNBOUND)
}
else {
val endpointType = when (endpoint.boundType) {
BoundType.OPEN -> EndpointType.OPEN
BoundType.CLOSED -> EndpointType.CLOSED
}
outputStream.writeByte(endpointType)
outputStream.writeVarInt(endpoint.version.major)
outputStream.writeVarInt(endpoint.version.minor)
}
}
internal fun deserializeVersions(stream: StubInputStream): RangeSet<Version> {
val size = stream.readVarInt()
val builder = ImmutableRangeSet.builder<Version>()
repeat(size) {
builder.add(deserializeRange(stream))
}
return builder.build()
}
private fun deserializeRange(stream: StubInputStream): Range<Version> {
val low = deserializeEndpoint(stream)
val high = deserializeEndpoint(stream)
return if (low != null && high != null)
Range.range(low.version, low.boundType, high.version, high.boundType)
else if (high != null)
Range.upTo(high.version, high.boundType)
else if (low != null)
Range.downTo(low.version, low.boundType)
else
PyVersionRange(max(versionRange.lowInclusive, versionCheck.version), versionRange.highExclusive)
Range.all()
}
private fun min(a: Version?, b: Version): Version {
return if (a == null) b else minOf(a, b)
}
private fun max(a: Version?, b: Version): Version {
return if (a == null) b else maxOf(a, b)
}
internal fun serializeVersionRange(versionRange: PyVersionRange, outputStream: StubOutputStream) {
serializeVersion(versionRange.lowInclusive, outputStream)
serializeVersion(versionRange.highExclusive, outputStream)
}
private fun serializeVersion(version: Version?, outputStream: StubOutputStream) {
outputStream.writeBoolean(version != null)
if (version != null) {
outputStream.writeVarInt(version.major)
outputStream.writeVarInt(version.minor)
private fun deserializeEndpoint(stream: StubInputStream): Endpoint? {
val endpointType = stream.readByte().toInt()
val boundType = when (endpointType) {
EndpointType.UNBOUND -> return null
EndpointType.OPEN -> BoundType.OPEN
EndpointType.CLOSED -> BoundType.CLOSED
else -> throw IllegalArgumentException()
}
val major = stream.readVarInt()
val minor = stream.readVarInt()
return Endpoint(Version(major, minor, 0), boundType)
}
internal fun deserializeVersionRange(stream: StubInputStream): PyVersionRange {
val lowInclusive = deserializeVersion(stream)
val highExclusive = deserializeVersion(stream)
return PyVersionRange(lowInclusive, highExclusive)
}
private data class Endpoint(val version: Version, val boundType: BoundType)
private fun deserializeVersion(stream: StubInputStream): Version? {
val isNotNull = stream.readBoolean()
if (isNotNull) {
val major = stream.readVarInt()
val minor = stream.readVarInt()
return Version(major, minor, 0)
}
return null
private object EndpointType {
const val UNBOUND = 0
const val OPEN = 1
const val CLOSED = 2
}

View File

@@ -5,7 +5,7 @@ if sys.version_info < (4,):
if sys.version_info >= (3,):
def foo(self):
pass
elif sys.version_info <= (2, 5):
elif sys.version_info < (2, 5):
def bar(self):
pass
else:

View File

@@ -1,8 +0,0 @@
import sys
if True:
if sys.version_info >= (3,):
if sys.version_info < (3, 12):
foo = 23
else:
bar = -1

View File

@@ -0,0 +1,10 @@
import sys
if True:
if sys.version_info >= (3,):
if sys.version_info >= (3, 10) and sys.version_info < (3, 12):
foo = 23
if sys.version_info < (3, 11) and (sys.version_info < (3, 5) or sys.version_info > (3, 7)):
buz = 23
else:
bar = -1

View File

@@ -20,3 +20,6 @@ if condition1:
s = "x"
else:
i = 1
if (sys.version_info > (2, 1) and ((sys.version_info <= (2, 2) or sys.version_info > (3, )))):
qux = ""

View File

@@ -1,6 +1,9 @@
// Copyright 2000-2021 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.
package com.jetbrains.python;
import com.google.common.collect.ImmutableRangeSet;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.intellij.lang.FileASTNode;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Document;
@@ -144,11 +147,14 @@ public class PyStubsTest extends PyTestCase {
.withChildren(
element(PyClassStub.class, versionRange("2.5", "3.0"))
.withChildren(
element(PyFunctionStub.class, versionRange("3.11", "3.0")),
element(PyFunctionStub.class, ImmutableRangeSet.of()),
element(PyTargetExpressionStub.class, versionRange("2.5", "3.0"))
),
element(PyTargetExpressionStub.class, versionRange("3.12", "3.0"))
)
element(PyTargetExpressionStub.class, ImmutableRangeSet.of())
),
element(PyTargetExpressionStub.class,
ImmutableRangeSet.unionOf(List.of(Range.openClosed(Version.parseVersion("2.1"), Version.parseVersion("2.2")),
Range.greaterThan(Version.parseVersion("3.0")))))
)
.test(file.getStub());
}
@@ -1263,19 +1269,19 @@ public class PyStubsTest extends PyTestCase {
}
private static <T extends PyVersionSpecificStub> @NotNull StubElementValidator element(@NotNull Class<T> clazz,
@NotNull PyVersionRange versionRange) {
@NotNull RangeSet<Version> versions) {
return stub -> {
assertInstanceOf(stub, clazz);
assertEquals(versionRange, clazz.cast(stub).getVersionRange());
assertEquals(versions, clazz.cast(stub).getVersions());
};
}
private static @NotNull PyVersionRange versionLessThan(@NotNull String version) {
return new PyVersionRange(null, Version.parseVersion(version));
private static @NotNull RangeSet<Version> versionLessThan(@NotNull String version) {
return ImmutableRangeSet.of(Range.lessThan(Version.parseVersion(version)));
}
private static @NotNull PyVersionRange versionRange(@NotNull String lowInclusive, @NotNull String highExclusive) {
return new PyVersionRange(Version.parseVersion(lowInclusive), Version.parseVersion(highExclusive));
private static @NotNull RangeSet<Version> versionRange(@NotNull String lowInclusive, @NotNull String highExclusive) {
return ImmutableRangeSet.of(Range.closedOpen(Version.parseVersion(lowInclusive), Version.parseVersion(highExclusive)));
}
private interface StubElementValidator {