From 61b2625379b7dfdfdbaf9e53d7fd51dd7cd3e579 Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Sat, 29 Nov 2025 19:04:53 +0000 Subject: [PATCH] JS-619 Fix S3403 false positive for indexed access types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule was incorrectly raising issues when comparing indexed access types (e.g., T[K] where T extends Record) with string literals. These types represent unknown values at compile time and can legitimately be compared with any type using strict equality operators. Added check to allow comparisons involving unknown types or indexed access types. When type information is unknown, prefer not raising an issue. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/jsts/src/rules/S3403/rule.ts | 44 +++++++++++++++++++++- packages/jsts/src/rules/S3403/unit.test.ts | 13 +++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/jsts/src/rules/S3403/rule.ts b/packages/jsts/src/rules/S3403/rule.ts index 214f9e4e5b1..14256ff1bd8 100644 --- a/packages/jsts/src/rules/S3403/rule.ts +++ b/packages/jsts/src/rules/S3403/rule.ts @@ -18,6 +18,7 @@ import type { Rule } from 'eslint'; import type estree from 'estree'; +import * as ts from 'typescript'; import { generateMeta, getTypeFromTreeNode, @@ -35,10 +36,49 @@ export const rule: Rule.RuleModule = { return {}; } + function hasUnknownOrIndexedAccessType(type: ts.Type, checker: ts.TypeChecker): boolean { + // Check if this type is 'unknown' (JS-619) + if (type.flags & ts.TypeFlags.Unknown) { + return true; + } + + // Check for IndexedAccess type flag (JS-619) + // T[K] where T extends Record should be allowed to compare with any type + // since the actual value type is unknown at compile time + if (type.flags & ts.TypeFlags.IndexedAccess) { + return true; + } + + // Check union types - any constituent type with unknown or indexed access should allow comparison + if (type.isUnion()) { + return type.types.some(t => hasUnknownOrIndexedAccessType(t, checker)); + } + + // Check intersection types + if (type.isIntersection()) { + return type.types.some(t => hasUnknownOrIndexedAccessType(t, checker)); + } + + return false; + } + function isComparableTo(lhs: estree.Node, rhs: estree.Node) { const checker = services.program.getTypeChecker(); - const lhsType = checker.getBaseTypeOfLiteralType(getTypeFromTreeNode(lhs, services)); - const rhsType = checker.getBaseTypeOfLiteralType(getTypeFromTreeNode(rhs, services)); + const lhsOriginalType = getTypeFromTreeNode(lhs, services); + const rhsOriginalType = getTypeFromTreeNode(rhs, services); + const lhsType = checker.getBaseTypeOfLiteralType(lhsOriginalType); + const rhsType = checker.getBaseTypeOfLiteralType(rhsOriginalType); + + // Allow comparison when type information is unknown (JS-619) + // Comparing 'unknown' type with any other type is valid in TypeScript + // Check if either type is or contains 'unknown' or is an indexed access type + if ( + hasUnknownOrIndexedAccessType(lhsOriginalType, checker) || + hasUnknownOrIndexedAccessType(rhsOriginalType, checker) + ) { + return true; + } + // @ts-ignore private API return ( checker.isTypeAssignableTo(lhsType, rhsType) || checker.isTypeAssignableTo(rhsType, lhsType) diff --git a/packages/jsts/src/rules/S3403/unit.test.ts b/packages/jsts/src/rules/S3403/unit.test.ts index 26213063b94..aac222b96d5 100644 --- a/packages/jsts/src/rules/S3403/unit.test.ts +++ b/packages/jsts/src/rules/S3403/unit.test.ts @@ -158,6 +158,19 @@ describe('S3403', () => { symbols.filter(symbol => symbol !== foo); `, }, + { + code: ` + // False positive scenario: comparison with unknown type from generic Record + // This should NOT raise an issue because unknown can be compared with string + class Foo> { + constructor(private readonly foo: T) {} + + reproduction(key: Key) { + return this.foo[key] === 'foo'; + } + } + `, + }, ], invalid: [ {