From c3df353a8da37d100d31c44997ffdb5ccaf3780c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 03:59:49 +0000 Subject: [PATCH] Implement fix for this type comparison false positive Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/checker.ts | 25 ++++-- .../baselines/reference/thisTypeComparison.js | 57 ++++++++++++++ .../reference/thisTypeComparison.symbols | 46 +++++++++++ .../reference/thisTypeComparison.types | 77 +++++++++++++++++++ .../compiler/thisTypeComparisonExtended.ts | 62 +++++++++++++++ 5 files changed, 262 insertions(+), 5 deletions(-) create mode 100644 tests/baselines/reference/thisTypeComparison.js create mode 100644 tests/baselines/reference/thisTypeComparison.symbols create mode 100644 tests/baselines/reference/thisTypeComparison.types create mode 100644 tests/cases/compiler/thisTypeComparisonExtended.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c5701087ebf..35972825d06 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22143,11 +22143,26 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { (source as NumberLiteralType).value === (target as NumberLiteralType).value ) return true; if (s & TypeFlags.BigIntLike && t & TypeFlags.BigInt) return true; - if (s & TypeFlags.BooleanLike && t & TypeFlags.Boolean) return true; - if (s & TypeFlags.ESSymbolLike && t & TypeFlags.ESSymbol) return true; - if ( - s & TypeFlags.Enum && t & TypeFlags.Enum && source.symbol.escapedName === target.symbol.escapedName && - isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter) + if (s & TypeFlags.BooleanLike && t & TypeFlags.Boolean) return true; + if (s & TypeFlags.ESSymbolLike && t & TypeFlags.ESSymbol) return true; + // For comparable relation, revert `this` type parameters back to their constrained class type + if (relation === comparableRelation) { + if (s & TypeFlags.TypeParameter && (source as TypeParameter).isThisType) { + const constraint = getConstraintOfTypeParameter(source as TypeParameter); + if (constraint && isTypeRelatedTo(constraint, target, relation)) { + return true; + } + } + if (t & TypeFlags.TypeParameter && (target as TypeParameter).isThisType) { + const constraint = getConstraintOfTypeParameter(target as TypeParameter); + if (constraint && isTypeRelatedTo(source, constraint, relation)) { + return true; + } + } + } + if ( + s & TypeFlags.Enum && t & TypeFlags.Enum && source.symbol.escapedName === target.symbol.escapedName && + isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter) ) return true; if (s & TypeFlags.EnumLiteral && t & TypeFlags.EnumLiteral) { if (s & TypeFlags.Union && t & TypeFlags.Union && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter)) return true; diff --git a/tests/baselines/reference/thisTypeComparison.js b/tests/baselines/reference/thisTypeComparison.js new file mode 100644 index 00000000000..67e5b96ad2b --- /dev/null +++ b/tests/baselines/reference/thisTypeComparison.js @@ -0,0 +1,57 @@ +//// [tests/cases/compiler/thisTypeComparison.ts] //// + +//// [thisTypeComparison.ts] +class AA { + do1() { + const b = dd.getB(); + if (this === b) { + console.log("this === b"); + } + } +} + +class BB extends AA { + getB(): BB { return this; } +} + +let dd = new BB(); +dd.do1(); + +//// [thisTypeComparison.js] +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var AA = /** @class */ (function () { + function AA() { + } + AA.prototype.do1 = function () { + var b = dd.getB(); + if (this === b) { + console.log("this === b"); + } + }; + return AA; +}()); +var BB = /** @class */ (function (_super) { + __extends(BB, _super); + function BB() { + return _super !== null && _super.apply(this, arguments) || this; + } + BB.prototype.getB = function () { return this; }; + return BB; +}(AA)); +var dd = new BB(); +dd.do1(); diff --git a/tests/baselines/reference/thisTypeComparison.symbols b/tests/baselines/reference/thisTypeComparison.symbols new file mode 100644 index 00000000000..22a976a3f35 --- /dev/null +++ b/tests/baselines/reference/thisTypeComparison.symbols @@ -0,0 +1,46 @@ +//// [tests/cases/compiler/thisTypeComparison.ts] //// + +=== thisTypeComparison.ts === +class AA { +>AA : Symbol(AA, Decl(thisTypeComparison.ts, 0, 0)) + + do1() { +>do1 : Symbol(AA.do1, Decl(thisTypeComparison.ts, 0, 10)) + + const b = dd.getB(); +>b : Symbol(b, Decl(thisTypeComparison.ts, 2, 13)) +>dd.getB : Symbol(BB.getB, Decl(thisTypeComparison.ts, 9, 21)) +>dd : Symbol(dd, Decl(thisTypeComparison.ts, 13, 3)) +>getB : Symbol(BB.getB, Decl(thisTypeComparison.ts, 9, 21)) + + if (this === b) { +>this : Symbol(AA, Decl(thisTypeComparison.ts, 0, 0)) +>b : Symbol(b, Decl(thisTypeComparison.ts, 2, 13)) + + console.log("this === b"); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) + } + } +} + +class BB extends AA { +>BB : Symbol(BB, Decl(thisTypeComparison.ts, 7, 1)) +>AA : Symbol(AA, Decl(thisTypeComparison.ts, 0, 0)) + + getB(): BB { return this; } +>getB : Symbol(BB.getB, Decl(thisTypeComparison.ts, 9, 21)) +>BB : Symbol(BB, Decl(thisTypeComparison.ts, 7, 1)) +>this : Symbol(BB, Decl(thisTypeComparison.ts, 7, 1)) +} + +let dd = new BB(); +>dd : Symbol(dd, Decl(thisTypeComparison.ts, 13, 3)) +>BB : Symbol(BB, Decl(thisTypeComparison.ts, 7, 1)) + +dd.do1(); +>dd.do1 : Symbol(AA.do1, Decl(thisTypeComparison.ts, 0, 10)) +>dd : Symbol(dd, Decl(thisTypeComparison.ts, 13, 3)) +>do1 : Symbol(AA.do1, Decl(thisTypeComparison.ts, 0, 10)) + diff --git a/tests/baselines/reference/thisTypeComparison.types b/tests/baselines/reference/thisTypeComparison.types new file mode 100644 index 00000000000..8c20f569cb0 --- /dev/null +++ b/tests/baselines/reference/thisTypeComparison.types @@ -0,0 +1,77 @@ +//// [tests/cases/compiler/thisTypeComparison.ts] //// + +=== thisTypeComparison.ts === +class AA { +>AA : AA +> : ^^ + + do1() { +>do1 : () => void +> : ^^^^^^^^^^ + + const b = dd.getB(); +>b : BB +> : ^^ +>dd.getB() : BB +> : ^^ +>dd.getB : () => BB +> : ^^^^^^ +>dd : BB +> : ^^ +>getB : () => BB +> : ^^^^^^ + + if (this === b) { +>this === b : boolean +> : ^^^^^^^ +>this : this +> : ^^^^ +>b : BB +> : ^^ + + console.log("this === b"); +>console.log("this === b") : void +> : ^^^^ +>console.log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>console : Console +> : ^^^^^^^ +>log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>"this === b" : "this === b" +> : ^^^^^^^^^^^^ + } + } +} + +class BB extends AA { +>BB : BB +> : ^^ +>AA : AA +> : ^^ + + getB(): BB { return this; } +>getB : () => BB +> : ^^^^^^ +>this : this +> : ^^^^ +} + +let dd = new BB(); +>dd : BB +> : ^^ +>new BB() : BB +> : ^^ +>BB : typeof BB +> : ^^^^^^^^^ + +dd.do1(); +>dd.do1() : void +> : ^^^^ +>dd.do1 : () => void +> : ^^^^^^^^^^ +>dd : BB +> : ^^ +>do1 : () => void +> : ^^^^^^^^^^ + diff --git a/tests/cases/compiler/thisTypeComparisonExtended.ts b/tests/cases/compiler/thisTypeComparisonExtended.ts new file mode 100644 index 00000000000..8d184501289 --- /dev/null +++ b/tests/cases/compiler/thisTypeComparisonExtended.ts @@ -0,0 +1,62 @@ +// @strict: true + +// Test 1: Original issue - this === subclass instance should work +class AA { + do1() { + const b = dd.getB(); + if (this === b) { // Should not error + console.log("this === b"); + } + } +} + +class BB extends AA { + getB(): BB { return this; } +} + +let dd = new BB(); +dd.do1(); + +// Test 2: this === unrelated class should still error +class CC { + value: number = 42; +} + +class DD { + test() { + const c = new CC(); + if (this === c) { // Should still error - no relationship + console.log("unrelated"); + } + } +} + +// Test 3: Multiple inheritance levels +class EE extends BB { + getE(): EE { return this; } +} + +class FF extends EE { + testMultiLevel() { + const e = new EE(); + if (this === e) { // Should not error - FF extends EE + console.log("multi-level inheritance"); + } + } +} + +// Test 4: Interface implementation +interface ITest { + getValue(): number; +} + +class GG implements ITest { + getValue() { return 42; } + + testInterface() { + const impl: ITest = new GG(); + if (this === impl) { // Should not error + console.log("interface implementation"); + } + } +} \ No newline at end of file