diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index fef8ab2aac4e..b7b01a10c653 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -26126,6 +26126,20 @@ export function createTypeEvaluator( return boolVal === (intVal === 1); } + // `bytes` and `bytearray` are distinct built-in classes + // (neither subclasses the other), but `bytes.__eq__` and + // `bytearray.__eq__` cross-compare by content, so an + // expression like `b"abc" == bytearray(b"abc")` is True + // at runtime. Treat them as comparable so we don't emit + // a spurious `reportUnnecessaryComparison` diagnostic. + // The same applies when one side is a `Literal[bytes]`, + // because the literal is still a `bytes` instance. + const isBytesLike = (t: ClassType) => + ClassType.isBuiltIn(t, 'bytes') || ClassType.isBuiltIn(t, 'bytearray'); + if (isBytesLike(leftType) && isBytesLike(rightType)) { + return true; + } + return false; } } diff --git a/packages/pyright-internal/src/tests/samples/comparison3.py b/packages/pyright-internal/src/tests/samples/comparison3.py new file mode 100644 index 000000000000..d0146e2e54e6 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/comparison3.py @@ -0,0 +1,37 @@ +# This sample tests that `bytes` and `bytearray` are treated as +# comparable types for `==` / `!=`. Although they are distinct built-in +# classes (neither subclasses the other), their `__eq__` implementations +# cross-compare by content, so a comparison such as +# `b"abc" == bytearray(b"abc")` is `True` at runtime. See issue #11433. + + +def func1(b1: bytes, b2: bytearray) -> None: + if b1 == b2: + pass + + if b1 != b2: + pass + + if b2 == b1: + pass + + if b2 != b1: + pass + + +def func2(b: bytearray) -> None: + # Literal[bytes] on the RHS is still a `bytes` instance, so the same + # cross-class comparison rule applies. + if b == b"\x00\x00\x00\x00": + pass + + if b"\x00\x00\x00\x00" == b: + pass + + +def func3(b: bytes, s: str) -> None: + # bytes vs str remains a real "no overlap" — the special case must + # not over-relax. This should still generate an error when + # reportUnnecessaryComparison is enabled. + if b == s: + pass diff --git a/packages/pyright-internal/src/tests/typeEvaluator6.test.ts b/packages/pyright-internal/src/tests/typeEvaluator6.test.ts index f633c1a284d0..382e8b3e467f 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator6.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator6.test.ts @@ -638,6 +638,20 @@ test('Comparison2', () => { TestUtils.validateResults(analysisResults2, 18); }); +test('Comparison3', () => { + const configOptions = new ConfigOptions(Uri.empty()); + + const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['comparison3.py'], configOptions); + TestUtils.validateResults(analysisResults1, 0); + + // bytes/bytearray comparisons in func1 and func2 must NOT emit the + // reportUnnecessaryComparison diagnostic; only the bytes-vs-str case + // in func3 should — see issue #11433. + configOptions.diagnosticRuleSet.reportUnnecessaryComparison = 'error'; + const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['comparison3.py'], configOptions); + TestUtils.validateResults(analysisResults2, 1); +}); + test('EmptyContainers1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['emptyContainers1.py']); TestUtils.validateResults(analysisResults, 5);